黄琪勇 1 год назад
Родитель
Сommit
7cade5ade6

+ 6 - 0
src/page-instrument/header-top/modeView.tsx

@@ -13,6 +13,12 @@ import state from "/src/state";
 import { studentQueryUserInfo } from "../api";
 import { usePageVisibility } from "@vant/use";
 
+
+/* todo */
+/*
+   打击乐和节奏练习 模式可能不是3个 到时候根据字段来判断
+ */
+
 export default defineComponent({
    name: "modeView",
    setup() {

+ 9 - 55
src/page-instrument/view-detail/index.module.less

@@ -53,7 +53,7 @@
 
 
 :global {
-    #cursorImg-0, #cursor-copy {
+    #cursorImg-0 {
         width: 2PX !important;
         min-height: 58PX;
         height: 58PX;
@@ -67,24 +67,21 @@
     }
 
     .staff {
-        #cursorImg-0, #cursor-copy {
+        #cursorImg-0 {
             width: 14Px;
             transform: translateX(11Px);
         }
-        #cursor-copy {
-            transform: translate(0, -34%);
-        }
     }
 
     .jianpuTone {
-        #cursorImg-0, #cursor-copy {
+        #cursorImg-0 {
             width: 18Px;
             transform: translateX(6.3Px) !important;
         }
     }
 
     .eyeProtection {
-        #cursorImg-0, #cursor-copy {
+        #cursorImg-0 {
             background-color: rgb(255, 159, 88);
         }
     }
@@ -96,7 +93,7 @@
 
 .xiaomi {
     :global {
-        #cursorImg-0, #cursor-copy {
+        #cursorImg-0 {
             height: 58PX !important;
             min-height: auto !important;
         }
@@ -105,21 +102,21 @@
 
 .PC {
     :global {
-        #cursorImg-0, #cursor-copy {
+        #cursorImg-0 {
             margin-top: -18PX;
             min-height: 94Px;
             border-radius: 10Px;
         }
 
         .staff {
-            #cursorImg-0, #cursor-copy {
+            #cursorImg-0 {
                 width: 35Px;
                 transform: translateX(21Px) !important;
             }
         }
 
         .jianpuTone {
-            #cursorImg-0, #cursor-copy {
+            #cursorImg-0 {
                 width: 29Px;
                 transform: translateX(13Px) !important;
             }
@@ -139,7 +136,6 @@
                 justify-content: center;
             }
         }
-       
     }
 
     .headHeight.headHide {
@@ -162,7 +158,7 @@
         #osmdCanvasPage1 {
             padding-bottom: 0 !important;
         }
-        #cursorImg-0, #cursor-copy {
+        #cursorImg-0 {
             opacity: 0 !important;
         }
     }
@@ -176,48 +172,6 @@
     opacity: 0;
 }
 
-.singleLineDetail {
-    :global {
-        #cursorImg-0 {
-            display: none;
-        }
-        .staveBox {
-            display: none !important;
-        }
-        .cursorAnimate {
-            animation: cnimate 1s ease-in-out infinite;
-        }
-        .leftNoteBg {
-            position: sticky;
-            background: rgba(0, 0, 0, 0.3);
-            ::before {
-                content: "";
-                position: absolute;
-                left: 0;
-                top: 0;
-                width: 200px;
-                height: 100px;
-                background: rgba(0,0,0,0.4);
-                z-index: 999;
-                display: block;
-            }
-        }
-        #cursor-copy {
-            &::after {
-                content: "";
-                position: sticky;
-                left: 0;
-                top: 0;
-                width: 200px;
-                height: 200px;
-                background: rgba(0,0,0,0.4);
-                z-index: 999;
-                display: block;
-            }
-        }        
-    }
-}
-
 @keyframes headerDown {
     100% {
         transform: translateY(0%);

+ 6 - 7
src/page-instrument/view-detail/index.tsx

@@ -2,7 +2,7 @@ import { Popup, Skeleton } from "vant";
 import { computed, defineComponent, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, Transition, watch, watchEffect, defineAsyncComponent } from "vue";
 import { formateTimes } from "../../helpers/formateMusic";
 import Metronome, { metronomeData } from "../../helpers/metronome";
-import state, { EnumMusicRenderType, evaluatCreateMusicPlayer, handleSetSpeed, IAudioState, IPlatform, isRhythmicExercises, resetPlaybackToStart, togglePlay, getMusicDetail, calculateDistance, createFixedCursor, addNoteBBox } from "/src/state";
+import state, { EnumMusicRenderType, evaluatCreateMusicPlayer, handleSetSpeed, IAudioState, IPlatform, isRhythmicExercises, resetPlaybackToStart, togglePlay, getMusicDetail, addNoteBBox } from "/src/state";
 import { browser, setGlobalData } from "../../utils";
 import AudioList from "../../view/audio-list";
 import MusicScore, { resetMusicScore } from "../../view/music-score";
@@ -33,6 +33,7 @@ import { usePageVisibility } from "@vant/use";
 import { initMidi } from "/src/helpers/midiPlay"
 import TheAudio from "/src/components/the-audio"
 import tickWav from "/src/assets/tick.mp3";
+import { initSmoothAnimation } from "./smoothAnimation"
 
 const DelayCheck = defineAsyncComponent(() =>
   import('/src/page-instrument/evaluat-model/delay-check')
@@ -136,8 +137,7 @@ export default defineComponent({
       if (state.isPreView) {
         state.zoom = 0.65
       }
-      state.isSingleLine = query.isSingleLine; // 一行谱模式
-      state.moveType = query.moveType == '2' ? 'uniform' : 'smooth'; // 一行谱平移模式
+      state.isSingleLine = query.isSingleLine === "true" ? true : false; // 一行谱模式
       // Promise.all([sysMusicScoreAccompanimentQueryPage(id)]).then((values) => {
       //   getMusicInfo(values[0]);
       // });
@@ -172,9 +172,8 @@ export default defineComponent({
       if (state.isSingleLine) {
         // 音符添加位置信息bbox
         addNoteBBox(state.times);
-        // 一行谱,创建固定的音符指针
-        createFixedCursor();
-        calculateDistance();
+        // 一行谱创建 动画
+        initSmoothAnimation();
       }
       // state.times = resetFrequency(state.times);
       state.times = setNoteHalfTone(state.times);
@@ -393,7 +392,7 @@ export default defineComponent({
     }) 
     return () => (
       <div
-        class={[styles.detail, modeClass.value, state.setting.eyeProtection && "eyeProtection", (state.platform === IPlatform.PC && state.zoom > 0.8) && styles.PC, state.isPreView && styles.preViewDetail, state.isSingleLine && styles.singleLineDetail]}
+        class={[styles.detail, modeClass.value, state.setting.eyeProtection && "eyeProtection", (state.platform === IPlatform.PC && state.zoom > 0.8) && styles.PC, state.isPreView && styles.preViewDetail]}
         style={{
           paddingLeft: detailData.paddingLeft,
           background: state.setting.camera ? `rgba(${state.setting.eyeProtection ? "253,244,229" : "255,255,255"} ,${state.setting.cameraOpacity / 100}) !important` : "",

BIN
src/page-instrument/view-detail/smoothAnimation/bird.png


+ 20 - 0
src/page-instrument/view-detail/smoothAnimation/index.less

@@ -0,0 +1,20 @@
+#musicAndSelection.singleLineMusicBox{
+    .smoothAnimationBox{
+        position: relative;
+        .smoothBot{
+            position: absolute;
+            background: url("./bird.png") no-repeat;
+            background-size: 100% 100%;
+            width: 40Px;
+            height: 50Px;
+            left: 0;
+            top: 0;
+        }
+    }
+    #cursorImg-0 {
+        display: none;
+    }
+    .staveBox {
+        display: none !important;
+    }
+}

+ 439 - 0
src/page-instrument/view-detail/smoothAnimation/index.ts

@@ -0,0 +1,439 @@
+/**
+ * 一行谱动画
+ */
+import { watch, ref, Ref } from "vue"
+import { getAudioCurrentTime } from "/src/view/audio-list"
+import state from "/src/state"
+import "./index.less"
+
+type pointsPosType = { x: number; y: number; MeasureNumberXML: number }[]
+type smoothAnimationType = {
+   isShow: Ref<boolean>
+   canvasDom: null | HTMLCanvasElement
+   canvasCtx: null | undefined | CanvasRenderingContext2D
+   canvasDomWith: number
+   canvasDomHeight: number
+   smoothAnimationBoxDom: null | HTMLElement
+   smoothBotDom: null | HTMLElement
+   osmdCanvasPageDom: null | HTMLElement
+   osdmScrollDom: null | HTMLElement
+   pointsPos: pointsPosType
+   translateXNum: number
+   aveSpeed: number
+   clientWidth: number
+}
+
+const _numberOfSegments = 58 // 中间切割线的个数
+
+export const smoothAnimationState = {
+   isShow: ref(false), // 是否显示
+   canvasDom: null,
+   canvasCtx: null,
+   canvasDomWith: 0,
+   canvasDomHeight: 160,
+   smoothAnimationBoxDom: null,
+   smoothBotDom: null,
+   osmdCanvasPageDom: null,
+   osdmScrollDom: null,
+   pointsPos: [], // 计算之后的点坐标数组
+   translateXNum: 0, // 当前谱面的translateX的距离   谱面的位置信息 由translateX和scrollLeft的偏移一起决定
+   aveSpeed: 0, // 谱面的一帧的平均速度
+   clientWidth: 0 // 屏幕宽度
+} as smoothAnimationType
+
+// 监听显示与隐藏
+watch(smoothAnimationState.isShow, () => {
+   if (smoothAnimationState.isShow.value) {
+      smoothAnimationState.smoothAnimationBoxDom?.classList.remove("smoothAnimationBoxHide")
+      moveSmoothAnimation(moveState.progress, moveState.activeIndex)
+   } else {
+      smoothAnimationState.smoothAnimationBoxDom?.classList.add("smoothAnimationBoxHide")
+   }
+})
+
+/**
+ * 初始化
+ */
+export function initSmoothAnimation() {
+   // 创建dom
+   createSmoothAnimation()
+   // 初始化动画数据
+   const batePos = getPointsPosByBatePos()
+   console.log(batePos)
+   smoothAnimationState.pointsPos = createSmoothCurvePoints(batePos, undefined, undefined, _numberOfSegments)
+   // 谱面的平均速度(因为可能有反复的情况所以实际距离要加上反复的距离)
+   const canvasDomPath = batePos.reduce((path, item, index, arr) => {
+      if (index !== 0) {
+         if (Math.abs(item.MeasureNumberXML - arr[index - 1].MeasureNumberXML) <= 1) {
+            path += item.x - arr[index - 1].x
+         }
+      }
+      return path
+   }, 0)
+   smoothAnimationState.aveSpeed = (canvasDomPath / (state.times[state.times.length - 1].time - state.times[0].time) / 1000) * 16.67
+   // 当前屏幕的宽度
+   calcClientWidth()
+   document.addEventListener("resize", calcClientWidth)
+   smoothAnimationState.isShow.value = true
+   console.log(smoothAnimationState, 777777)
+}
+
+/**
+ * 销毁
+ */
+export function destroySmoothAnimation() {
+   document.removeEventListener("resize", calcClientWidth)
+   smoothAnimationState.smoothAnimationBoxDom?.remove()
+   Object.assign(smoothAnimationState, {
+      canvasDom: null,
+      canvasCtx: null,
+      canvasDomWith: 0,
+      canvasDomHeight: 160,
+      smoothAnimationBoxDom: null,
+      smoothBotDom: null,
+      osmdCanvasPageDom: null,
+      osdmScrollDom: null,
+      pointsPos: [],
+      translateXNum: 0,
+      aveSpeed: 0,
+      clientWidth: 0
+   })
+}
+
+/**
+ * 根据播放时间进度移动处理
+ */
+export function moveSmoothAnimationByPlayTime() {
+   const currentTime = getAudioCurrentTime()
+   if (currentTime <= state.fixtime) return
+   if (currentTime > state.times.last()?.time) return
+   // 当休止小节,可能当前音符在谱面上没有实际的音符(没有bbox),所以往后找谱面上有的音符
+   let nextIndex = state.activeNoteIndex + 1
+   let nextBBox = state.times[nextIndex]?.bbox
+   while (!nextBBox && nextIndex < state.times.length) {
+      nextIndex += 1
+      nextBBox = state.times[nextIndex]?.bbox
+   }
+   // 当前的音符和下一个音符之间的时值
+   const noteDuration = state.times[nextIndex].time - state.times[state.activeNoteIndex]?.time
+   // 当前时值在该区间的占比
+   const playProgress = (currentTime - state.times[state.activeNoteIndex]?.time) / noteDuration
+   moveSmoothAnimation(playProgress, state.activeNoteIndex)
+}
+
+/**
+ * 移动处理
+ * progress  当前音符到下一个音符的距离百分比
+ * activeIndex 当前
+ */
+const moveState = {
+   oldIndex: -1, // 上一次index
+   progress: 0,
+   activeIndex: 0
+}
+export function moveSmoothAnimation(progress: number, activeIndex: number) {
+   moveState.progress = progress
+   moveState.activeIndex = activeIndex
+   // if (!smoothAnimationState.isShow.value) {
+   //    return
+   // }
+   // 计算 下一个音符index 在pointsPos 中的距离
+   const nextPointsIndex = (activeIndex + 1) * (_numberOfSegments + 1) - 1
+   // 百分比转为当前的index 距离个数
+   const progressCalcIndex = Math.round(progress * _numberOfSegments)
+   // // 当前的index
+   let nowIndex = nextPointsIndex - _numberOfSegments + progressCalcIndex
+   // 当前计算的位置和上一次值一样时候不运行
+   if (moveState.oldIndex === nowIndex) {
+      return
+   }
+   moveState.oldIndex = nowIndex
+   const nowPointsPos = smoothAnimationState.pointsPos[nowIndex]
+   smoothAnimationState.canvasCtx?.clearRect(0, 0, smoothAnimationState.canvasDomWith, smoothAnimationState.canvasDomHeight)
+   // 移动
+   smoothAnimationMove(
+      {
+         x: nowPointsPos.x - 20,
+         y: nowPointsPos.y - 25
+      },
+      smoothAnimationState.pointsPos,
+      smoothAnimationState.pointsPos.slice(0, nowIndex)
+   )
+   move_osmd(nowPointsPos)
+}
+
+/**
+ * 谱面移动逻辑
+ */
+function move_osmd(nowPointsPos: pointsPosType[0]) {
+   const speed = smoothAnimationState.aveSpeed * (state.speed / 60)
+   const clientWidth = smoothAnimationState.clientWidth
+   const clientMidWidth = smoothAnimationState.clientWidth / 2
+   const { left, right, width } = smoothAnimationState.smoothBotDom!.getBoundingClientRect()
+   const midBotNum = left + width / 2
+   // 分阶段移动
+   if (right > clientWidth) {
+      // 移动超过屏幕时候
+      smoothAnimationState.translateXNum = 0
+      smoothAnimationState.osdmScrollDom!.scrollLeft = nowPointsPos.x - clientWidth * 0.9
+   } else if (left < 0) {
+      // 移动小于屏幕时候
+      smoothAnimationState.translateXNum = 0
+      smoothAnimationState.osdmScrollDom!.scrollLeft = nowPointsPos.x - clientWidth * 0.1
+   } else if (midBotNum > clientMidWidth - clientWidth * 0.3 && midBotNum <= clientMidWidth - clientWidth * 0.25) {
+      smoothAnimationState.translateXNum += speed * 0.5
+   } else if (midBotNum > clientMidWidth - clientWidth * 0.25 && midBotNum <= clientMidWidth - clientWidth * 0.2) {
+      smoothAnimationState.translateXNum += speed * 0.6
+   } else if (midBotNum > clientMidWidth - clientWidth * 0.2 && midBotNum <= clientMidWidth - clientWidth * 0.15) {
+      smoothAnimationState.translateXNum += speed * 0.7
+   } else if (midBotNum > clientMidWidth - clientWidth * 0.15 && midBotNum <= clientMidWidth - clientWidth * 0.1) {
+      smoothAnimationState.translateXNum += speed * 0.8
+   } else if (midBotNum > clientMidWidth - clientWidth * 0.1 && midBotNum <= clientMidWidth - clientWidth * 0.05) {
+      smoothAnimationState.translateXNum += speed * 0.9
+   } else if (midBotNum > clientMidWidth - clientWidth * 0.05 && midBotNum <= clientMidWidth) {
+      smoothAnimationState.translateXNum += speed
+   } else if (midBotNum > clientMidWidth && midBotNum <= clientMidWidth + clientWidth * 0.05) {
+      smoothAnimationState.translateXNum += speed * 1.2
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.05 && midBotNum <= clientMidWidth + clientWidth * 0.1) {
+      smoothAnimationState.translateXNum += speed * 1.4
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.1 && midBotNum <= clientMidWidth + clientWidth * 0.15) {
+      smoothAnimationState.translateXNum += speed * 1.7
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.15 && midBotNum <= clientMidWidth + clientWidth * 0.2) {
+      smoothAnimationState.translateXNum += speed * 2
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.2 && midBotNum <= clientMidWidth + clientWidth * 0.25) {
+      smoothAnimationState.translateXNum += speed * 2.4
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.25 && midBotNum <= clientMidWidth + clientWidth * 0.3) {
+      smoothAnimationState.translateXNum += speed * 2.8
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.3 && midBotNum <= clientMidWidth + clientWidth * 0.35) {
+      smoothAnimationState.translateXNum += speed * 3.3
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.35 && midBotNum <= clientMidWidth + clientWidth * 0.4) {
+      smoothAnimationState.translateXNum += speed * 3.8
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.4 && midBotNum <= clientMidWidth + clientWidth * 0.45) {
+      smoothAnimationState.translateXNum += speed * 4.4
+   } else if (midBotNum > clientMidWidth + clientWidth * 0.45 && midBotNum <= clientMidWidth + clientWidth * 0.5) {
+      smoothAnimationState.translateXNum += speed * 5
+   }
+   smoothAnimationState.osmdCanvasPageDom!.style.transform = `translateX(-${smoothAnimationState.translateXNum}px)`
+}
+
+/**
+ * 进度条和块移动方法
+ */
+function smoothAnimationMove(pos: { x: number; y: number }, pointsPos: pointsPosType, progresspointsPos?: pointsPosType) {
+   smoothAnimationState.smoothBotDom && (smoothAnimationState.smoothBotDom.style.transform = `translate(${pos.x}px, ${pos.y}px)`)
+   smoothAnimationState.canvasCtx && drawSmoothCurve(smoothAnimationState.canvasCtx, pointsPos, progresspointsPos)
+}
+/**
+ * 计算屏幕宽度
+ */
+function calcClientWidth() {
+   smoothAnimationState.clientWidth = document.body.clientWidth
+}
+/**
+ * 创建dom
+ */
+function createSmoothAnimation() {
+   // osdmScrollDom
+   const osdmScrollDom = document.querySelector("#musicAndSelection") as HTMLElement
+   smoothAnimationState.osdmScrollDom = osdmScrollDom
+   // osmdCanvasPage
+   const osmdCanvasPageDom = document.querySelector("#osmdCanvasPage1") as HTMLElement
+   smoothAnimationState.osmdCanvasPageDom = osmdCanvasPageDom
+   // box
+   const smoothAnimationBoxDom = document.createElement("div")
+   smoothAnimationBoxDom.className = "smoothAnimationBox"
+   smoothAnimationState.smoothAnimationBoxDom = smoothAnimationBoxDom
+   //canvas
+   const smoothCanvasDom = document.createElement("canvas")
+   smoothCanvasDom.className = "smoothCanvas"
+   smoothAnimationState.canvasDom = smoothCanvasDom
+   smoothAnimationState.canvasDomWith = osmdCanvasPageDom?.offsetWidth | 0
+   smoothCanvasDom.width = smoothAnimationState.canvasDomWith
+   smoothCanvasDom.height = smoothAnimationState.canvasDomHeight
+   smoothAnimationState.canvasCtx = smoothCanvasDom.getContext("2d")
+   // bot
+   const smoothBotDom = document.createElement("div")
+   smoothBotDom.className = "smoothBot"
+   smoothAnimationState.smoothBotDom = smoothBotDom
+   smoothAnimationBoxDom.appendChild(smoothCanvasDom)
+   smoothAnimationBoxDom.appendChild(smoothBotDom)
+   // 添加到 osmdCanvasPage1
+   osmdCanvasPageDom?.insertBefore(smoothAnimationBoxDom, osmdCanvasPageDom.firstChild)
+}
+
+/**
+ * 根据音符获取坐标
+ */
+function getPointsPosByBatePos(): pointsPosType {
+   let totalAvInde = 0
+   // 取平均值
+   const totalAv =
+      state.times.reduce((total, item) => {
+         if (item.frequency !== -1) {
+            // -1 为休止符
+            total += item.frequency
+            totalAvInde++
+         }
+         return total
+      }, 0) / totalAvInde
+   const pointsPos = state.times.reduce((posArr: any[], item) => {
+      // 当休止小节,可能当前音符在谱面上没有实际的音符(没有bbox),所以往后找谱面上有的音符
+      if (item.bbox) {
+         posArr.push({
+            MeasureNumberXML: item.MeasureNumberXML,
+            x: item.bbox.x,
+            y:
+               ((((item.frequency === -1 ? totalAv : item.frequency) - totalAv) / totalAv) * smoothAnimationState.canvasDomHeight) / 2 +
+               smoothAnimationState.canvasDomHeight / 2 // cavans 高度为160 所以基准为80
+         })
+      }
+      return posArr
+   }, [])
+   return pointsPos
+}
+
+/**
+ * 使用传入的曲线的顶点坐标创建平滑曲线的顶点。
+ * @param  {Array}   points  曲线顶点坐标数组,
+ * @param  {Float}   tension 密集程度,默认为 0.5
+ * @param  {Boolean} closed  是否创建闭合曲线,默认为 false
+ * @param  {Int}     numberOfSegments 平滑曲线 2 个顶点间的线段数,默认为 20
+ * @return {Array}   平滑曲线的顶点坐标数组
+ */
+function createSmoothCurvePoints(pointsPos: pointsPosType, tension?: number, closed?: boolean, numberOfSegments?: number) {
+   if (pointsPos.length <= 2) {
+      return pointsPos
+   }
+   tension = tension ? tension : 0.5
+   closed = closed ? true : false
+   numberOfSegments = numberOfSegments ? numberOfSegments : 20
+   let ps = pointsPos.slice(0),
+      result = [],
+      x,
+      y,
+      t1x,
+      t2x,
+      t1y,
+      t2y,
+      c1,
+      c2,
+      c3,
+      c4,
+      st,
+      t,
+      i
+   if (closed) {
+      ps.unshift(pointsPos[pointsPos.length - 1])
+      ps.unshift(pointsPos[pointsPos.length - 1])
+      ps.push(pointsPos[0])
+   } else {
+      ps.unshift(pointsPos[0])
+      ps.push(pointsPos[pointsPos.length - 1])
+   }
+   for (i = 1; i < ps.length - 2; i += 1) {
+      //console.log(ps[i + 1].MeasureNumberXML, ps[i - 1].MeasureNumberXML, ps[i + 2].MeasureNumberXML, ps[i].MeasureNumberXML)
+      t1x = (ps[i + 1].x - ps[i - 1].x) * tension
+      t2x = (ps[i + 2].x - ps[i].x) * tension
+      t1y = (ps[i + 1].y - ps[i - 1].y) * tension
+      t2y = (ps[i + 2].y - ps[i].y) * tension
+      // 当中途出现反复 刚开始反复时候 53 52 22 52  (22)中途值会变小 这里强行拉大 防止算法平均值出现很大偏差
+      if (ps[i + 1].MeasureNumberXML - ps[i + 2].MeasureNumberXML > 1) {
+         const nowNumberXML = ps[i + 1].MeasureNumberXML + 1
+         //在当前值的情况下 向前一位
+         let index = ps.findIndex(item => {
+            return nowNumberXML === item.MeasureNumberXML
+         })
+         // 查询不到index时候取当前值
+         index === -1 && (index = i + 1)
+         t2x = (ps[index].x - ps[i].x) * tension
+      }
+      // 当中途出现反复 结束反复时候 22 53 22 22  (53)中途值会变大 这里强行缩小 防止算法平均值出现很大偏差
+      if (ps[i - 1].MeasureNumberXML - ps[i].MeasureNumberXML > 1) {
+         //在当前值的情况下 向后一位
+         const nowNumberXML = ps[i].MeasureNumberXML - 1
+         let index = ps.findIndex((item, index) => {
+            return nowNumberXML === item.MeasureNumberXML && nowNumberXML !== ps[index + 1]?.MeasureNumberXML
+         })
+         // 查询不到index时候取当前值
+         index === -1 && (index = i)
+         t1x = (ps[i + 1].x - ps[index].x) * tension
+      }
+      // 当中途出现跳房子 刚开始跳房子时候 35 35 54 35  (54)中途值会变大 这里强行缩小 防止算法平均值出现很大偏差
+      if (ps[i + 1].MeasureNumberXML - ps[i + 2].MeasureNumberXML < -1) {
+         const nowNumberXML = ps[i + 1].MeasureNumberXML + 1
+         //在当前值的情况下 向前一位
+         let index = ps.findIndex(item => {
+            return nowNumberXML === item.MeasureNumberXML
+         })
+         // 查询不到index时候取当前值
+         index === -1 && (index = i + 1)
+         t2x = (ps[index].x - ps[i].x) * tension
+      }
+      // 当中途出现跳房子 结束跳房子时候 54 35 54 54  (35)中途值会变小 这里强行拉大 防止算法平均值出现很大偏差
+      if (ps[i - 1].MeasureNumberXML - ps[i].MeasureNumberXML < -1) {
+         const nowNumberXML = ps[i].MeasureNumberXML - 1
+         let index = ps.findIndex((item, index) => {
+            return nowNumberXML === item.MeasureNumberXML && nowNumberXML !== ps[index + 1]?.MeasureNumberXML
+         })
+         // 查询不到index时候取当前值
+         index === -1 && (index = i)
+         t1x = (ps[i + 1].x - ps[index].x) * tension
+      }
+      const nowMeasureNumberXML = pointsPos[i - 1].MeasureNumberXML
+      const nextMeasureNumberXML = pointsPos[i].MeasureNumberXML
+      for (t = 0; t <= numberOfSegments; t++) {
+         // 小于1时候是反复   大于1是跳房子  不画曲线  停留
+         if (nextMeasureNumberXML - nowMeasureNumberXML < 0 || nextMeasureNumberXML - nowMeasureNumberXML > 1) {
+            //console.log(x, y)
+            result.push({
+               x: x as number,
+               y: y as number,
+               MeasureNumberXML: nowMeasureNumberXML
+            })
+            continue
+         }
+         st = t / numberOfSegments
+         c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1
+         c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2)
+         c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st
+         c4 = Math.pow(st, 3) - Math.pow(st, 2)
+         x = c1 * ps[i].x + c2 * ps[i + 1].x + c3 * t1x + c4 * t2x
+         y = c1 * ps[i].y + c2 * ps[i + 1].y + c3 * t1y + c4 * t2y
+         //console.log(x, y)
+         result.push({
+            x,
+            y,
+            MeasureNumberXML: t === numberOfSegments ? nextMeasureNumberXML : nowMeasureNumberXML
+         })
+      }
+   }
+   return result
+}
+/**
+ * 根据坐标划线
+ */
+function drawSmoothCurve(context: CanvasRenderingContext2D, pointsPos: pointsPosType, progresspointsPos?: pointsPosType) {
+   context.lineWidth = 4
+   context.lineJoin = "round" // 优化锯齿
+   context.strokeStyle = "rgba(255,255,255,0.6)"
+   drawLines(context, pointsPos)
+   if (progresspointsPos?.length) {
+      context.strokeStyle = "#FFC121"
+      drawLines(context, progresspointsPos)
+   }
+}
+function drawLines(context: CanvasRenderingContext2D, points: pointsPosType) {
+   context.beginPath()
+   context.moveTo(points[0].x, points[0].y)
+   for (let i = 1; i < points.length - 1; i++) {
+      if (Math.abs(points[i].MeasureNumberXML - points[i - 1].MeasureNumberXML) > 1) {
+         // 取消反复和跳房子连线
+         context.stroke()
+         context.beginPath()
+         context.moveTo(points[i + 1].x, points[i + 1].y)
+         continue
+      }
+      context.lineTo(points[i].x, points[i].y)
+   }
+   context.stroke()
+}

+ 7 - 126
src/state.ts

@@ -15,6 +15,7 @@ import { getMusicSheetDetail } from "./utils/baseApi"
 import { getQuery } from "/src/utils/queryString";
 import { followData } from "/src/view/follow-practice/index"
 import { changeSongSourceByBate } from "/src/view/audio-list"
+import { moveSmoothAnimation, smoothAnimationState, moveSmoothAnimationByPlayTime} from "/src/page-instrument/view-detail/smoothAnimation"
 
 const query: any = getQuery();
 
@@ -455,19 +456,8 @@ const state = reactive({
   audioDone: false,
   /** 节拍文件是否加载成功 */
   audioBetaDone: false,
-  /** 谱面svgdom节点 */
-  osmdSvgDom: null as any,
-  /** 滚动容器dom */
-  osdmScrollDom: null as any,
-  /** 光标dom */
-  cursorDom: null as any,
-  fistNoteLeft: 0,
   /** 是否为单行谱渲染模式 */
   isSingleLine: false,
-  /** 首尾音符的间距 */
-  noteDistance: 0,
-  /** 一行谱运动模式,平滑移动、匀速移动 */
-  moveType: "smooth" as "smooth" | "uniform",
   /** 是否是evxml */
   isEvxml: false,
   noTimes: [] as any,
@@ -636,11 +626,7 @@ const handlePlaying = () => {
   metronomeData.metro?.sound(currentTime);
   // 一行谱,需要滚动小节
   if (state.isSingleLine) {
-    if (state.moveType === 'smooth') {
-      smoothMoveSvgDom();
-    } else {
-      uniformMoveSvgDom();
-    }
+    moveSmoothAnimationByPlayTime()
   }
 
 };
@@ -1442,66 +1428,14 @@ export const addNoteBBox = (list: any[]) => {
       } : null;
       voicesBBox = currentVoicesBBox;
     }
+    //  todo  连续修止小节bug
     note.bbox = bbox;
   }
 
 }
 
-// 一行谱模式,创建固定的光标
-export const createFixedCursor = () => {
-  if (!state.isSingleLine) return;
-  const svg: any = document.getElementById("osmdSvgPage1");
-  state.osmdSvgDom = svg;
-  const scrollDom = document.getElementById("musicAndSelection");
-  const cursorDom = document.getElementById("cursorImg-0");
-  state.fistNoteLeft = cursorDom?.getBoundingClientRect()?.left || 0;
-  state.osdmScrollDom = scrollDom;
-  state.cursorDom = cursorDom;
-  let copyCursor: any = cursorDom?.cloneNode(true);
-  if (copyCursor) {
-    copyCursor.setAttribute('id', 'cursor-copy');
-    copyCursor.style.position = 'sticky';
-    copyCursor.style.zIndex = '2';
-    // if (!state.times[0]?.id) {
-    //   copyCursor.style.left = state.times[0]?.bbox?.x + state.times[0]?.bbox?.width / 3 + 'px';
-    // }
-    copyCursor.style.left = state.times[0]?.bbox?.x + state.times[0]?.bbox?.width / 2 - 1 + 'px';
-    copyCursor.style.height = parseFloat(copyCursor.style.height) * 3 + 'px';
-    copyCursor.style.opacity = state.moveType === 'uniform' ? 0 : 1;
-    // copyCursor.style.background = 'red';
-    copyCursor && scrollDom?.appendChild(copyCursor);
-
-    // 创建左侧背景dom
-    // @ts-ignore
-    const firstMeasureBBox: any = document.querySelector('.vf-measure')?.getBBox();
-    const leftDom = document.createElement("div");
-    leftDom.style.width = state.times[0]?.bbox?.x - firstMeasureBBox?.x * state.zoom + 'px';
-    leftDom.style.height = firstMeasureBBox?.height * state.zoom + 'px';
-    leftDom.style.left = firstMeasureBBox?.x * state.zoom + 'px';
-    leftDom.style.transform = 'translateY(20px)';
-    leftDom.classList.add('leftNoteBg');
-    // scrollDom?.appendChild(leftDom);
-  }
-}
-
-/** 计算首尾音符的间距 */
-export const calculateDistance = () => {
-  const firstNoteBBox = state.times[0]?.bbox;
-  const lastNoteBBox = state.times.last()?.bbox;
-  if (firstNoteBBox && lastNoteBBox) {
-    const noteDistance = lastNoteBBox.x - firstNoteBBox.x + lastNoteBBox.width / 2 - firstNoteBBox.width / 2 - 1;
-    console.log('首尾间距', noteDistance)
-    state.noteDistance = noteDistance || 0;
-  }
-}
-
 /** 跳动svgdom */
 export const moveSvgDom = (skipNote?: boolean) => {
-  // const cursorLeft = state.cursorDom?.getBoundingClientRect()?.left || 0;
-  // const leftValue = parseFloat(state.cursorDom.style.left) - 47 - 20;
-  // console.log(cursorLeft,leftValue,'光标位置')
-  // state.osmdSvgDom.style.transform = `translateX(${-leftValue}px)`;
-  // state.osdmScrollDom.scrollLeft = leftValue
   // console.log('当前音符',state.activeNoteIndex)
   state.times.forEach((item: any, idx: number) => {
     const svgEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}`)
@@ -1514,7 +1448,6 @@ export const moveSvgDom = (skipNote?: boolean) => {
       stemEl?.classList.remove('noteActive')
     }
   })
-  // document.getElementById('cursor-copy')?.classList.add('cursorAnimate');
 
   /**
    * 计算需要移动的距离
@@ -1522,64 +1455,12 @@ export const moveSvgDom = (skipNote?: boolean) => {
    */
   if (skipNote) {
     const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + state.times[state.activeNoteIndex].bbox?.width / 2 - state.times[0].bbox?.width / 2;
-    state.osdmScrollDom.scrollTo({
+    // 点击 清空translateXNum
+    smoothAnimationState.translateXNum = 0
+    moveSmoothAnimation(0, state.activeNoteIndex)
+    smoothAnimationState.osdmScrollDom!.scrollTo({
       left: distance,
       behavior: "smooth",
     });
   }
-}
-
-/** 平滑移动svgdom */
-export const smoothMoveSvgDom = () => {
-  const currentTime = getAudioCurrentTime();
-  const matchNoteIdx = state.times.findIndex((item: any) => Math.abs(item.time - currentTime) * 1000 < 100)
-  // if (matchNoteIdx >= 0) {
-  //   console.log('匹配',matchNoteIdx,currentTime)
-  // }
-
-  if (currentTime <= state.fixtime) return;
-  if (currentTime > state.times.last()?.time) return;
-  // console.log('跳转音符',currentTime)
-  const currentBBox = state.times[state.activeNoteIndex]?.bbox;
-  let nextIndex = state.activeNoteIndex + 1;
-  let nextBBox = state.times[nextIndex]?.bbox;
-  while (!nextBBox && nextIndex < state.times.length) {
-    nextIndex += 1;
-    nextBBox = state.times[nextIndex]?.bbox;
-  }
-  // 下一个音符和当前播放音符之间的间距
-  let noteDistance = nextBBox?.x - state.times[state.activeNoteIndex].bbox?.x + nextBBox?.width / 4 - state.times[state.activeNoteIndex].bbox?.width / 4 || 0
-  if (noteDistance) {
-    // 当前的音符和下一个音符之间的时值
-    const noteDuration = state.times[nextIndex].time - state.times[state.activeNoteIndex]?.time;
-    // 当前时值在该区间的占比
-    const playProgress = (currentTime - state.times[state.activeNoteIndex]?.time) / noteDuration;
-    // 如果当前播放的音符是休止小节的,实际没有音符
-    // if (!state.times[state.activeNoteIndex]?.id && state.times[state.activeNoteIndex]?.multipleRestMeasures && state.times[state.activeNoteIndex+1].id) {
-    //   noteDistance = noteDistance - state.times[state.activeNoteIndex]?.bbox?.svgBodyLeft;
-    // }
-    const distance = noteDistance * playProgress;
-
-    // 上一个音符和第一个音符的间距
-    let preDistance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + state.times[state.activeNoteIndex].bbox?.width / 4;
-
-    // console.log(state.activeNoteIndex,'滑动', distance, preDistance,  state.osdmScrollDom.scrollLeft, noteDistance,  nextIndex, currentTime, noteDuration )
-    // console.log('当前音符',state.activeNoteIndex,'距离',noteDistance,'比例',playProgress,'上一个距离',preDistance,'时值',currentTime, noteDuration)
-    //console.log('滑动','音符:',state.activeNoteIndex,'小节:', state.activeMeasureIndex)
-    state.osdmScrollDom.scrollLeft = distance + preDistance;
-  } else {
-    const playProgress = (currentTime - state.times[0]?.time) / state.times.last()?.time
-    const distance = state.noteDistance * playProgress;
-    state.osdmScrollDom.scrollLeft = distance;
-  }
-}
-
-// 匀速平移
-export const uniformMoveSvgDom = () => {
-  const currentTime = getAudioCurrentTime();
-  if (currentTime <= state.fixtime) return;
-  if (currentTime > state.times.last()?.time) return;
-  const playProgress = (currentTime - state.fixtime) / state.times.last()?.time;
-  const distance = playProgress * state.noteDistance || 0;
-  state.osdmScrollDom.scrollLeft = distance;
 }

+ 3 - 2
src/view/music-score/index.tsx

@@ -114,8 +114,8 @@ export default defineComponent({
 				
 			});
 			// osmd.EngravingRules.CompactMode = true // 紧凑模式
-			osmd.EngravingRules.PageRightMargin = state.isSingleLine ? (window.innerWidth+200)/10 : 2;
-			osmd.EngravingRules.FixedMeasureWidth = state.isSingleLine ? true : false; // 是否固定小节的宽度(小节同一宽度渲染)
+			// osmd.EngravingRules.PageRightMargin = state.isSingleLine ? (window.innerWidth+200)/10 : 2;
+			// osmd.EngravingRules.FixedMeasureWidth = state.isSingleLine ? true : false; // 是否固定小节的宽度(小节同一宽度渲染)
 			osmd.EngravingRules.PageTopMargin = state.platform === IPlatform.PC ? 0 : 1; // 老师端顶部间距
 			osmd.EngravingRules.PageTopMarginNarrow = 3;
 			osmd.EngravingRules.PageLeftMargin = 2;
@@ -179,6 +179,7 @@ export default defineComponent({
 				class={[
 					isInTheGradualRange.value && styles.inGradualRange,
 					state.musicRenderType == EnumMusicRenderType.staff ? "staff" : "jianpuTone",
+					state.isSingleLine && "singleLineMusicBox"
 				]}
 			>
 				{props.showSelection && musicData.showSelection && !state.isPreView && !state.isEvaluatReport && <Selection />}

+ 0 - 6
src/view/selection/index.module.less

@@ -21,12 +21,6 @@
     background-color: var(--active-stave-box) !important;
 }
 
-.singleLineSelection {
-    .staveBox {
-        opacity: 0;
-    }
-}
-
 .leftStaveBox {
     background-color: var(--active-stave-box);
 

+ 2 - 3
src/view/selection/index.tsx

@@ -208,13 +208,13 @@ export default defineComponent({
 								}
 								return styles.rightStaveBox;
 							}
-							return styles.staveBox;
+							return styles.staveBox + " staveBox";  // 加上固定css 一行谱可以隐藏
 						}
 					}
 				} else {
 					if (state.activeMeasureIndex == item.MeasureNumberXML && !state.isReport) {
 						item.staveBox.height = selectData.measureHeight + 'px';
-						return styles.staveBox;
+						return styles.staveBox + " staveBox"; // 加上固定css 一行谱可以隐藏
 					}
 				}
 			};
@@ -232,7 +232,6 @@ export default defineComponent({
 				id="selectionBox"
 				class={[
 					styles.selectionContainer,
-					!state.sectionStatus && state.isSingleLine ? styles.singleLineSelection : ''
 				]}
 				onClick={(e: Event) => e.stopPropagation()}
 			>