Browse Source

feat: 一行谱功能修改

TIANYONG 1 year ago
parent
commit
fda9c08260

+ 1 - 1
osmd-extended

@@ -1 +1 @@
-Subproject commit 6967750d4de04e03f3abb9753b4bca0cbaae83a9
+Subproject commit 83b5517686f1364c5ac18dc64597d46c355c714c

+ 1 - 28
src/helpers/formateMusic.ts

@@ -822,7 +822,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 	// 是否是变速的曲子
 	const hasVaryingSpeed = _notes.some((item: any) => item.measuresTempoInBPM !== _notes[0].measuresTempoInBPM)
 	console.log('变速曲子',hasVaryingSpeed)
-	let voicesBBox: any = null;
+	// let voicesBBox: any = null;
 	for (let { note, iterator, currentTime, isDouble, isMutileSubject } of _notes) {
 		if (note) {
 			if (preMeasureNumber != note?.sourceMeasure?.MeasureNumberXML) {
@@ -1054,32 +1054,6 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			// console.log(note.tie)
 			// console.log('👀看看endtime', duration, relaEndtime, fixtime, i)
 			// console.log('频率',note?.pitch?.frequency,i)
-			
-			/**
-			 * 兼容合并休止小节没有音符的情况,将合并的小节宽度等分,音符位置就在等分的位置
-			 */
-			let bbox: any = null;
-			if (svgElement?.attrs.id) {
-				bbox = document.getElementById(`vf-${svgElement?.attrs?.id}`)?.getBoundingClientRect();
-			} else {
-				// @ts-ignore
-				let currentVoicesBBox: any = document.getElementById(`${stave?.attrs?.id}`)?.nextSibling?.getBoundingClientRect();
-				if (!currentVoicesBBox && multipleRestMeasures <= totalMultipleRestMeasures) {
-					currentVoicesBBox = voicesBBox;
-				}
-				const ratioWidth = currentVoicesBBox?.width / (totalMultipleRestMeasures + 1) || 0;
-				bbox = currentVoicesBBox ? {
-					bottom: currentVoicesBBox.bottom,
-					height: 30,
-					left: currentVoicesBBox.left + ratioWidth * multipleRestMeasures,
-					right: currentVoicesBBox.right,
-					top: currentVoicesBBox.top,
-					width: 1,
-					x: currentVoicesBBox.x + ratioWidth * multipleRestMeasures,
-					y: currentVoicesBBox.y
-				} : null;
-				voicesBBox = currentVoicesBBox;
-			}
 
 			const nodeDetail = {
 				isStaccato: note.voiceEntry.isStaccato(),
@@ -1127,7 +1101,6 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				totalMultipleRestMeasures, // 当前小节总的合并小节数
 				measureSpeed,  // 小节速度
 				maxNoteNum: note.maxNoteNum, // 当前小节音符最多的分轨的音符数量
-				bbox,
 			};
 			nodeDetail.realKey = formatRealKey(note.halfTone - fixedKey * 12, nodeDetail);
 			nodeDetail.duration = nodeDetail.endtime - nodeDetail.time;

+ 33 - 3
src/page-instrument/view-detail/index.module.less

@@ -62,7 +62,7 @@
             transform: translateX(11Px);
         }
         #cursor-copy {
-            transform: translate(11Px, -23%);
+            transform: translate(0, -34%);
         }
     }
 
@@ -174,6 +174,36 @@
         .staveBox {
             display: none !important;
         }
+        .cursorAnimate {
+            animation: cnimate 1s ease-in-out infinite;
+        }
+        .leftNoteBg {
+            position: relative;
+            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;
+            }
+        }        
     }
-
-}
+}

+ 5 - 1
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 } from "/src/state";
+import state, { EnumMusicRenderType, evaluatCreateMusicPlayer, handleSetSpeed, IAudioState, IPlatform, isRhythmicExercises, resetPlaybackToStart, togglePlay, getMusicDetail, calculateDistance, createFixedCursor, addNoteBBox } from "/src/state";
 import { browser, setGlobalData } from "../../utils";
 import AudioList from "../../view/audio-list";
 import MusicScore, { resetMusicScore } from "../../view/music-score";
@@ -162,6 +162,10 @@ export default defineComponent({
       setCustomGradual();
 			setCustomNoteRealValue();
       state.times = formateTimes(osmd);
+      // 音符添加位置信息bbox
+      addNoteBBox(state.times);
+      // 一行谱,创建固定的音符指针
+      createFixedCursor();
       calculateDistance();
       // state.times = resetFrequency(state.times);
       state.times = setNoteHalfTone(state.times);

+ 115 - 10
src/state.ts

@@ -1224,6 +1224,97 @@ export const followBeatPaly = () => {
   });
 };
 
+// 音符添加bbox
+export const addNoteBBox = (list: any[]) => {
+  let voicesBBox: any = null;
+  for (let i = 0; i < list.length; i++) {
+    const note = list[i];
+    const { svgElement, multipleRestMeasures, totalMultipleRestMeasures, stave } = note;
+    /**
+     * 兼容合并休止小节没有音符的情况,将合并的小节宽度等分,音符位置就在等分的位置
+     */
+    let bbox: any = null;
+    if (svgElement?.attrs.id) {
+      // @ts-ignore
+      bbox = document.getElementById(`vf-${svgElement?.attrs?.id}`)?.getBBox();
+      bbox = {
+        x: bbox.x * state.zoom,
+        y: bbox.y * state.zoom,
+        width: bbox.width * state.zoom,
+        height: bbox.height * state.zoom,
+      }
+    } else {
+      // @ts-ignore
+      let currentVoicesBBox: any = document.getElementById(`${stave?.attrs?.id}`)?.nextSibling?.getBBox();
+      const svgBodyBBox: any = document.getElementById('musicAndSelection')?.getBoundingClientRect();
+      if (!currentVoicesBBox && multipleRestMeasures <= totalMultipleRestMeasures) {
+        currentVoicesBBox = voicesBBox;
+      }
+      let nextIndex = i + 1;
+      while (!list[nextIndex].id && nextIndex < list.length) {
+        nextIndex += 1;
+      }
+      // 休止小节的开头和下一个音符之间的间距
+      let multipleWidth: any = currentVoicesBBox?.width * state.zoom;
+      if (list[nextIndex].id) {
+        // @ts-ignore
+        multipleWidth = document.getElementById(`${list[nextIndex]?.stave?.attrs?.id}`)?.getBBox()?.x * state.zoom - currentVoicesBBox?.x * state.zoom;
+      }
+      const ratioWidth = multipleWidth / totalMultipleRestMeasures || 0;
+      bbox = currentVoicesBBox ? {
+        bottom: currentVoicesBBox.bottom,
+        height: 30,
+        left: currentVoicesBBox.x * state.zoom + ratioWidth * (multipleRestMeasures - 1),
+        right: currentVoicesBBox.y,
+        top: currentVoicesBBox.top,
+        width: 1,
+        x: currentVoicesBBox.x * state.zoom + ratioWidth * (multipleRestMeasures - 1),
+        y: currentVoicesBBox.y,
+        svgBodyLeft: svgBodyBBox?.x,
+      } : null;
+      voicesBBox = currentVoicesBBox;
+    }
+    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.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.top = '20px';
+    leftDom.classList.add('leftNoteBg');
+    scrollDom?.appendChild(leftDom);
+  }
+}
+
 /** 计算首尾音符的间距 */
 export const calculateDistance = () => {
   const firstNoteRect = document.getElementById(`vf-${state.times[0]?.svgElement?.attrs?.id}`)?.getBoundingClientRect();
@@ -1237,15 +1328,16 @@ export const calculateDistance = () => {
 
 /** 跳动svgdom */
 export const moveSvgDom = () => {
-  const cursorLeft = state.cursorDom?.getBoundingClientRect()?.left || 0;
-  const leftValue = parseFloat(state.cursorDom.style.left) - 47 - 20;
+  // 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}`)
     const stemEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}-stem`)
-    if (item.i === state.activeNoteIndex && item.svgElement) {
+    if ((item.i === state.activeNoteIndex || item.id === state.times[state.activeNoteIndex].id) && item.svgElement) {
       svgEl?.classList.add('noteActive')
       stemEl?.classList.add('noteActive')
     } else {
@@ -1253,9 +1345,15 @@ export const moveSvgDom = () => {
       stemEl?.classList.remove('noteActive')
     }
   })
+  // document.getElementById('cursor-copy')?.classList.add('cursorAnimate');
 
+  /**
+   * 计算需要移动的距离
+   * 当前选中的音符和第一个音符之间的间距
+   */
+  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({
-    left: leftValue,
+    left: distance,
     behavior: "smooth",
   });
 }
@@ -1263,9 +1361,10 @@ export const moveSvgDom = () => {
 /** 平滑移动svgdom */
 export const smoothMoveSvgDom = () => {
   const currentTime = getAudioCurrentTime();
-  const matchNoteIdx = state.times.findIndex((item: any) => {
-    Math.abs(item.time - currentTime) * 1000 < 100
-  })
+  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; 
@@ -1277,18 +1376,24 @@ export const smoothMoveSvgDom = () => {
     nextIndex += 1;
     nextBBox = state.times[nextIndex]?.bbox;
   }
-  // 下一个音符和第一个音符之间的间距
-  const noteDistance = nextBBox?.x - state.times[state.activeNoteIndex].bbox?.x + nextBBox?.width / 4 - state.times[state.activeNoteIndex].bbox?.width / 4 || 0
+  // 下一个音符和当前播放音符之间的间距
+  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('滑动', distance, preDistance,  state.osdmScrollDom.scrollLeft, noteDistance, state.activeNoteIndex, nextIndex, currentTime )
+
+    // 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 {

+ 48 - 0
src/style.css

@@ -122,4 +122,52 @@ body{
   animation-duration       : 1.5s;
   animation-name           : guideKeyframes;
   animation-iteration-count: infinite;
+}
+
+@keyframes cnimate{
+  0%{
+      opacity: 0;
+  }
+  50%{
+      opacity: 0.5;
+  }
+  100%{
+      opacity: 1;
+  }
+}
+
+@keyframes noteAnimate{
+  0%{
+      scale: 0.4;
+  }
+  10%{
+    scale: 0.6;
+  }
+  20%{
+    scale: 0.8;
+  }
+  30%{
+    scale: 1;
+  }
+  40%{
+    scale: 1.2;
+  }
+  50%{
+      opacity: 1.4;
+  }
+  60%{
+      opacity: 1.6;
+  }
+  70%{
+    opacity: 1.4;
+  }
+  80%{
+    opacity: 1.2;
+  }  
+  90%{
+      opacity: 1.1;
+  }  
+  100%{
+      opacity: 1;
+  }
 }

+ 5 - 2
src/view/music-score/index.module.less

@@ -33,9 +33,12 @@
         path {
             fill: #FF2B29;
             stroke: #FF2B29;
-            // transform: scale(1.2);
-            // transition: all 0.3s;
         }
+        transform-box: fill-box;
+        transform-origin: center;
+        animation: noteAnimate 0.3s linear;
+        // transform: scale(2);
+        // transition: all 0.3s;
     }
 }
 .inGradualRange{

+ 1 - 20
src/view/music-score/index.tsx

@@ -89,25 +89,7 @@ export default defineComponent({
 				state.gradual = getGradualLengthByXml(xml);
 			}
 		};
-		// 一行谱模式,创建固定的光标
-		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';
-				// copyCursor.style.background = 'red';
-				copyCursor && scrollDom?.appendChild(copyCursor);
-			}
-		}
+
 		const init = async () => {
 			const container = document.getElementById("musicAndSelection");
 			if (!container || !musicData.score) return;
@@ -147,7 +129,6 @@ export default defineComponent({
 			await osmd.load(musicData.score);
 			osmd.zoom = state.zoom;
 			osmd.render();
-			createFixedCursor();
 			// console.log("🚀 ~ osmd:", osmd)
 			emit("rendered", osmd);
 			resetFormate();

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

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

+ 4 - 1
src/view/selection/index.tsx

@@ -231,7 +231,10 @@ export default defineComponent({
 		return () => (
 			<div
 				id="selectionBox"
-				class={styles.selectionContainer}
+				class={[
+					styles.selectionContainer,
+					!state.sectionStatus && state.isSingleLine ? styles.singleLineSelection : ''
+				]}
 				onClick={(e: Event) => e.stopPropagation()}
 			>
 				{selectData.staves.map((item: any) => {