|
|
@@ -8,7 +8,7 @@ import { IFingering, mappingVoicePart, subjectFingering, matchVoicePart } from "
|
|
|
import { handleStartTick, closeTick } from "./view/tick";
|
|
|
import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentTime, setAudioPlaybackRate, audioData } from "./view/audio-list";
|
|
|
import { toggleFollow } from "./view/follow-practice";
|
|
|
-import { browser, setStorageSpeed, setGlobalData, checkDecimal } from "./utils";
|
|
|
+import { browser, setStorageSpeed, setGlobalData, checkDecimal, getNumbers } from "./utils";
|
|
|
import { api_cloudGetMediaStatus, api_createMusicPlayer, api_cloudChangeSpeed, api_cloudSuspend, api_cloudSetCurrentTime, api_cloudDestroy } from "./helpers/communication";
|
|
|
import { verifyCanRepeat, getDuration, xmlAddPartName } from "./helpers/formateMusic";
|
|
|
import { getMusicSheetDetail, getInstrumentCode } from "./utils/baseApi"
|
|
|
@@ -380,6 +380,10 @@ const state = reactive({
|
|
|
headTopHeight: 0,
|
|
|
/** 是否是来源于缓存的xml */
|
|
|
xmlFromStore: false,
|
|
|
+ /** 是否是自定义的note时值 */
|
|
|
+ isCustomNoteTime: false,
|
|
|
+ /** 中英文歌词是否需要同时高亮 */
|
|
|
+ isTogetherHeighLight: false,
|
|
|
});
|
|
|
const browserInfo = browser();
|
|
|
let offset_duration = 0;
|
|
|
@@ -1125,6 +1129,7 @@ export const hanldeDirectSelection = (list: any[]) => {
|
|
|
}, 0);
|
|
|
};
|
|
|
let offsetTop = 0, musicScrollTop = 0;
|
|
|
+let lastLineTop = 0; // 记录上一次的行位置
|
|
|
/**
|
|
|
* 窗口内滚动到音符的区域
|
|
|
* @param isScroll 可选: 强制滚动到顶部, 默认: false
|
|
|
@@ -1138,10 +1143,15 @@ export const scrollViewNote = (resetTop?: boolean) => {
|
|
|
}
|
|
|
if (state.activeNoteIndex <= 1 || resetTop) {
|
|
|
offsetTop = 0;
|
|
|
+ lastLineTop = 0; // 重置行位置记录
|
|
|
}
|
|
|
const domId = "vf" + noteId;
|
|
|
// 合并休止小节没有音符,取小节的位置,否则取音符指针位置
|
|
|
- const cursorElement: any = !noteId && state.times[state.activeNoteIndex]?.totalMultipleRestMeasures ? document.querySelector(`.measureIndex_${state.activeMeasureIndex}`) : document.querySelector(`[data-vf=${domId}]`)?.parentElement;
|
|
|
+ let cursorElement: any = !noteId && state.times[state.activeNoteIndex]?.totalMultipleRestMeasures ? document.querySelector(`.measureIndex_${state.activeMeasureIndex}`) : document.querySelector(`[data-vf=${domId}]`)?.parentElement;
|
|
|
+ // 如果是简谱
|
|
|
+ if (state.musicRenderType === EnumMusicRenderType.firstTone || state.musicRenderType === EnumMusicRenderType.fixedTone) {
|
|
|
+ // cursorElement = cursorElement.querySelector('.node-dot')
|
|
|
+ }
|
|
|
const musicAndSelection = document.getElementById(state.scrollContainer)!;
|
|
|
if (!state.headTopHeight) {
|
|
|
state.headTopHeight = document.querySelector('.headHeight')?.getBoundingClientRect()?.height || 100;
|
|
|
@@ -1149,16 +1159,63 @@ export const scrollViewNote = (resetTop?: boolean) => {
|
|
|
if (!cursorElement || !musicAndSelection) {
|
|
|
return
|
|
|
}
|
|
|
- // offsetTop = musicAndSelection.scrollTop || offsetTop;
|
|
|
- const noteCenterOffsetTop = cursorElement ? cursorElement?.offsetTop + (cursorElement?.offsetHeight/2) : 0;
|
|
|
- // console.log('滑动',offsetTop, noteCenterOffsetTop)
|
|
|
+
|
|
|
+ // 获取当前音符的位置信息
|
|
|
+ const rect = cursorElement.getBoundingClientRect();
|
|
|
+ const rawOffsetTop = cursorElement.offsetTop;
|
|
|
+ const currentNote = state.times[state.activeNoteIndex];
|
|
|
+ const scrollTarget = state.musicRenderType === EnumMusicRenderType.staff ? rawOffsetTop : rect.height/2 + rawOffsetTop;
|
|
|
+
|
|
|
+ // 同一行内的音符,offsetTop 可能因为音高不同而有 60-80px 的差异
|
|
|
+ // 但真正换行时,位置变化通常会超过 100px(向上翻页)或 150px(向下换行)
|
|
|
+ // 所以直接使用 rawOffsetTop 的实际差异来判断,而不是用固定行高分组
|
|
|
+
|
|
|
+ let currentTop = rawOffsetTop;
|
|
|
+ let isLineChanged = false;
|
|
|
+
|
|
|
+ // 计算与上次位置的差异
|
|
|
+ const positionDiff = currentTop - lastLineTop;
|
|
|
+
|
|
|
+ // 换行判断阈值:
|
|
|
+ // - 向下换行(新行在下方):位置增加超过 100px
|
|
|
+ // - 向上翻页(新行在上方):位置减少超过 100px
|
|
|
+ // - 同一行内移动:位置变化在 -100px 到 +100px 之间
|
|
|
+ const lineChangeThreshold = 100; // 真正换行的阈值
|
|
|
+
|
|
|
+ if (Math.abs(positionDiff) > lineChangeThreshold) {
|
|
|
+ // 位置变化超过阈值,认为是换行
|
|
|
+ isLineChanged = true;
|
|
|
+ } else {
|
|
|
+ // 位置变化较小,认为是同一行内移动
|
|
|
+ isLineChanged = false;
|
|
|
+ // 同一行内,使用上次的 lastLineTop 作为基准,避免累积误差
|
|
|
+ currentTop = lastLineTop;
|
|
|
+ }
|
|
|
+
|
|
|
+ // console.log('滚动检测', {
|
|
|
+ // currentTop,
|
|
|
+ // lastLineTop,
|
|
|
+ // isLineChanged,
|
|
|
+ // positionDiff,
|
|
|
+ // absDiff: Math.abs(positionDiff),
|
|
|
+ // noteIndex: state.activeNoteIndex,
|
|
|
+ // rawOffsetTop,
|
|
|
+ // scrollTarget,
|
|
|
+ // MeasureNumberXML: currentNote?.MeasureNumberXML
|
|
|
+ // })
|
|
|
+
|
|
|
if (Math.abs(musicAndSelection?.scrollTop - musicScrollTop) > 30) {
|
|
|
// 手动滑动谱面,重新播放需要滚动到对应位置
|
|
|
+ lastLineTop = currentTop; // 更新行位置记录
|
|
|
} else {
|
|
|
- if (offsetTop === cursorElement.offsetTop || Math.abs(offsetTop - cursorElement.offsetTop) < 30) return;
|
|
|
+ // 如果没有换行,则不执行滚动,直接返回
|
|
|
+ if (!isLineChanged) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
- // offsetTop = noteCenterOffsetTop;
|
|
|
- offsetTop = cursorElement.offsetTop;
|
|
|
+ // 换行了,更新 offsetTop 和行位置记录
|
|
|
+ lastLineTop = currentTop;
|
|
|
+ offsetTop = scrollTarget; // 使用计算好的滚动目标值
|
|
|
const animateType = browser().android ? "instant" : "smooth"
|
|
|
if (offsetTop > (state.headTopHeight + 30)) {
|
|
|
musicScrollTop = (offsetTop - state.headTopHeight - 30) * state.musicZoom
|
|
|
@@ -1937,12 +1994,21 @@ export const fillWordColor = () => {
|
|
|
})
|
|
|
const currentLyrics: SVGAElement[] = Array.from(document.querySelectorAll(`.lyric${currentNote?.noteId}`));
|
|
|
currentLyrics.forEach((lyric, index) => {
|
|
|
- const lyricIndex = lyric.getAttribute('lyricIndex');
|
|
|
+ let lyricIndex = lyric.getAttribute('lyricIndex');
|
|
|
// bug:#10942,如果需要反复唱的小节,只有一遍歌词,反复唱的时候,歌词都需要高亮
|
|
|
const onlyOneLyric = currentNote.measures?.every((item: any) => item?.formatLyricsEntries?.length <= 1);
|
|
|
- if ((index === currentNote.repeatIdx && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx != index && !onlyOneLyric && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx > 0 && currentNote.formatLyricsEntries?.length === 1 && onlyOneLyric)) {
|
|
|
- lyric?.classList.add('lyricActive')
|
|
|
- }
|
|
|
+
|
|
|
+ // 中英文歌词同时高亮
|
|
|
+ if (state.isTogetherHeighLight) {
|
|
|
+ let multiIdxs = getNumbers(currentNote.repeatIdx+1)
|
|
|
+ if ((index === currentNote.repeatIdx && multiIdxs.includes(Number(lyricIndex)) ) || (currentNote.repeatIdx != index && !onlyOneLyric && multiIdxs.includes(Number(lyricIndex)) ) || (currentNote.repeatIdx > 0 && currentNote.formatLyricsEntries?.length === 1 && onlyOneLyric)) {
|
|
|
+ lyric?.classList.add('lyricActive')
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if ((index === currentNote.repeatIdx && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx != index && !onlyOneLyric && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx > 0 && currentNote.formatLyricsEntries?.length === 1 && onlyOneLyric)) {
|
|
|
+ lyric?.classList.add('lyricActive')
|
|
|
+ }
|
|
|
+ }
|
|
|
// bug: #11189,兼容处理需要唱4遍,但是只打了2遍歌词的情况,1、3唱一样的歌词,2、4唱一样的歌词
|
|
|
if ( currentNote.formatLyricsEntries.length == 2 && currentNote.repeatIdx >= 2 && index === (currentNote.repeatIdx - 2) ) {
|
|
|
lyric?.classList.add('lyricActive')
|