@applemusic-like-lyrics/core 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1731,6 +1731,10 @@ var BackgroundRender = class BackgroundRender {
1731
1731
  //#region src/styles/lyric-player.module.css
1732
1732
  var lyric_player_module_default = {
1733
1733
  "active": "FmKaba_active",
1734
+ "bgWrapper": "FmKaba_bgWrapper",
1735
+ "bgWrapperActive": "FmKaba_bgWrapperActive",
1736
+ "bgWrapperHidden": "FmKaba_bgWrapperHidden",
1737
+ "bgWrapperTop": "FmKaba_bgWrapperTop",
1734
1738
  "bottomLine": "FmKaba_bottomLine",
1735
1739
  "dirty": "FmKaba_dirty",
1736
1740
  "disableSpring": "FmKaba_disableSpring",
@@ -1743,8 +1747,10 @@ var lyric_player_module_default = {
1743
1747
  "lyricBgLine": "FmKaba_lyricBgLine",
1744
1748
  "lyricDuetLine": "FmKaba_lyricDuetLine",
1745
1749
  "lyricLine": "FmKaba_lyricLine",
1750
+ "lyricLineWrapper": "FmKaba_lyricLineWrapper",
1746
1751
  "lyricMainLine": "FmKaba_lyricMainLine",
1747
1752
  "lyricSubLine": "FmKaba_lyricSubLine",
1753
+ "playing": "FmKaba_playing",
1748
1754
  "romanWord": "FmKaba_romanWord",
1749
1755
  "rubyWord": "FmKaba_rubyWord",
1750
1756
  "tmpDisableTransition": "FmKaba_tmpDisableTransition",
@@ -1845,27 +1851,31 @@ function cleanUnintentionalOverlaps(lines) {
1845
1851
  * 尝试让歌词提前最多 600ms 开始,如果有重叠则尝试最多提前 400ms 或上一行时长的 30%
1846
1852
  */
1847
1853
  function tryAdvanceStartTime(lines) {
1848
- for (let i = lines.length - 1; i >= 0; i--) {
1854
+ const defaultAdvanceAmount = 600;
1855
+ const fallbackAdvanceAmount = 400;
1856
+ const fallbackAdvanceRatio = .3;
1857
+ let prevLineStartTime = 0;
1858
+ let prevLineEndTime = 0;
1859
+ let prevMainGroupStartTime = 0;
1860
+ let prevMainGroupEndTime = 0;
1861
+ let hasPrevLine = false;
1862
+ for (let i = 0; i < lines.length; i++) {
1849
1863
  const line = lines[i];
1850
1864
  if (line.isBG) continue;
1851
- let prevLine = null;
1852
- if (i > 0) {
1853
- let prevIdx = i - 1;
1854
- if (lines[prevIdx].isBG) prevIdx--;
1855
- if (prevIdx >= 0) prevLine = lines[prevIdx];
1856
- }
1865
+ const originalStartTime = line.startTime;
1866
+ const originalEndTime = line.endTime;
1857
1867
  let targetAdvanceAmount = 0;
1858
1868
  let safeBoundary = 0;
1859
- if (prevLine) if (line.startTime >= prevLine.endTime) {
1860
- targetAdvanceAmount = 600;
1861
- safeBoundary = prevLine.endTime;
1869
+ if (hasPrevLine) if (originalStartTime >= prevLineEndTime) {
1870
+ targetAdvanceAmount = defaultAdvanceAmount;
1871
+ safeBoundary = prevMainGroupEndTime;
1862
1872
  } else {
1863
- targetAdvanceAmount = 400;
1864
- const prevDuration = prevLine.endTime - prevLine.startTime;
1865
- safeBoundary = prevLine.startTime + prevDuration * .3;
1873
+ targetAdvanceAmount = fallbackAdvanceAmount;
1874
+ const prevDuration = prevLineEndTime - prevLineStartTime;
1875
+ safeBoundary = prevLineStartTime + prevDuration * fallbackAdvanceRatio;
1866
1876
  }
1867
1877
  else {
1868
- targetAdvanceAmount = 600;
1878
+ targetAdvanceAmount = defaultAdvanceAmount;
1869
1879
  safeBoundary = 0;
1870
1880
  }
1871
1881
  const targetTime = line.startTime - targetAdvanceAmount;
@@ -1873,6 +1883,20 @@ function tryAdvanceStartTime(lines) {
1873
1883
  if (newStartTime < line.startTime) line.startTime = newStartTime;
1874
1884
  const nextLine = lines[i + 1];
1875
1885
  if (nextLine?.isBG) nextLine.startTime = line.startTime;
1886
+ if (hasPrevLine) if (originalStartTime < prevMainGroupEndTime && originalEndTime > prevMainGroupStartTime) {
1887
+ prevMainGroupStartTime = Math.min(prevMainGroupStartTime, originalStartTime);
1888
+ prevMainGroupEndTime = Math.max(prevMainGroupEndTime, originalEndTime);
1889
+ } else {
1890
+ prevMainGroupStartTime = originalStartTime;
1891
+ prevMainGroupEndTime = originalEndTime;
1892
+ }
1893
+ else {
1894
+ prevMainGroupStartTime = originalStartTime;
1895
+ prevMainGroupEndTime = originalEndTime;
1896
+ }
1897
+ prevLineStartTime = originalStartTime;
1898
+ prevLineEndTime = originalEndTime;
1899
+ hasPrevLine = true;
1876
1900
  }
1877
1901
  }
1878
1902
  /**
@@ -2272,19 +2296,19 @@ const LayoutAlignAnchor = {
2272
2296
  function computeCurrentInterlude(input) {
2273
2297
  const currentTime = input.currentTime + 20;
2274
2298
  const currentIndex = input.scrollToIndex;
2275
- const lines = input.processedLines;
2299
+ const groups = input.currentGroups;
2276
2300
  const checkGap = (k) => {
2277
- if (k < -1 || k >= lines.length - 1) return void 0;
2278
- const prevLine = k === -1 ? null : lines[k];
2279
- const nextLine = lines[k + 1];
2280
- const gapStart = prevLine ? prevLine.endTime : 0;
2281
- const gapEnd = Math.max(gapStart, nextLine.startTime - 250);
2282
- if (gapEnd - gapStart < 4e3) return;
2301
+ if (k < -1 || k >= groups.length - 1) return void 0;
2302
+ const prevGroup = k === -1 ? null : groups[k];
2303
+ const nextGroup = groups[k + 1];
2304
+ const gapStart = prevGroup ? prevGroup.endTime : 0;
2305
+ const gapEnd = Math.max(gapStart, nextGroup.startTime - 250);
2306
+ if (gapEnd - gapStart < 4e3) return void 0;
2283
2307
  if (gapEnd > currentTime && gapStart < currentTime) return {
2284
2308
  startTime: Math.max(gapStart, currentTime),
2285
2309
  endTime: gapEnd,
2286
2310
  anchorLineIndex: k,
2287
- isNextDuet: nextLine.isDuet
2311
+ isNextDuet: nextGroup.mainLine.getLine().isDuet
2288
2312
  };
2289
2313
  };
2290
2314
  return checkGap(currentIndex - 1) || checkGap(currentIndex) || checkGap(currentIndex + 1);
@@ -2297,8 +2321,8 @@ function computeCurrentInterlude(input) {
2297
2321
  * - 普通播放时根据相邻歌词的时间间隔动态调整 stiffness / damping
2298
2322
  */
2299
2323
  function computeLinePosYSpringParams(input) {
2300
- const { enabled, processedLines, scrollToIndex, isSeeking, isInterludeActive } = input;
2301
- if (!enabled || processedLines.length === 0) return { shouldUpdate: false };
2324
+ const { enabled, currentGroups, scrollToIndex, isSeeking, isInterludeActive } = input;
2325
+ if (!enabled || currentGroups.length === 0) return { shouldUpdate: false };
2302
2326
  if (isSeeking || isInterludeActive) return {
2303
2327
  shouldUpdate: true,
2304
2328
  params: {
@@ -2306,10 +2330,10 @@ function computeLinePosYSpringParams(input) {
2306
2330
  damping: 15
2307
2331
  }
2308
2332
  };
2309
- const currentLine = processedLines[scrollToIndex];
2310
- const prevLine = processedLines[scrollToIndex - 1];
2311
- if (!currentLine || !prevLine) return { shouldUpdate: false };
2312
- const interval = currentLine.startTime - (prevLine.words[0]?.startTime ?? prevLine.startTime);
2333
+ const currentGroup = currentGroups[scrollToIndex];
2334
+ const prevGroup = currentGroups[scrollToIndex - 1];
2335
+ if (!currentGroup || !prevGroup) return { shouldUpdate: false };
2336
+ const interval = currentGroup.startTime - prevGroup.startTime;
2313
2337
  const MIN_INTERVAL = 100;
2314
2338
  const MAX_INTERVAL = 800;
2315
2339
  const clampedInterval = clamp(interval, MIN_INTERVAL, MAX_INTERVAL);
@@ -2327,38 +2351,33 @@ function computeLinePosYSpringParams(input) {
2327
2351
  };
2328
2352
  }
2329
2353
  /**
2330
- * 计算单行歌词在当前布局中的视觉呈现参数。
2354
+ * 计算一组歌词在当前布局中的视觉呈现参数。
2331
2355
  *
2332
2356
  * 根据播放状态、缓冲状态、布局模式与间奏信息,
2333
- * 生成一行歌词最终应使用的 opacity、scale、blur 和 render mode。
2357
+ * 生成一组歌词最终应使用的活跃状态、不透明度与模糊值。
2334
2358
  */
2335
- function computeLinePresentation(input) {
2336
- const { line, lineIndex, scrollToIndex, latestIndex, hasBuffered, hidePassedLines, isPlaying, isNonDynamic, enableScale, enableBlur, isUserScrolling, isCompact, interlude } = input;
2337
- const isActive = hasBuffered || lineIndex >= scrollToIndex && lineIndex < latestIndex;
2359
+ function computeGroupPresentation(input) {
2360
+ const { groupIndex, scrollToIndex, latestIndex, hasBuffered, hidePassedLines, isPlaying, isNonDynamic, enableBlur, isUserScrolling, isCompact, interlude } = input;
2361
+ const isActive = hasBuffered || groupIndex >= scrollToIndex && groupIndex < latestIndex;
2338
2362
  const blurLevel = computeLineBlur({
2339
2363
  enableBlur,
2340
2364
  isUserScrolling,
2341
2365
  isActive,
2342
- itemIndex: lineIndex,
2366
+ itemIndex: groupIndex,
2343
2367
  scrollToIndex,
2344
2368
  latestIndex,
2345
2369
  isCompact
2346
2370
  });
2347
2371
  let targetOpacity;
2348
- if (hidePassedLines) if (lineIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2372
+ if (hidePassedLines) if (groupIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2349
2373
  else if (hasBuffered) targetOpacity = .85;
2350
2374
  else targetOpacity = isNonDynamic ? .2 : 1;
2351
2375
  else if (hasBuffered) targetOpacity = .85;
2352
2376
  else targetOpacity = isNonDynamic ? .2 : 1;
2353
- const SCALE_ASPECT = enableScale ? 97 : 100;
2354
- let targetScale = 100;
2355
- if (!isActive && isPlaying) targetScale = line.isBG ? 75 : SCALE_ASPECT;
2356
2377
  return {
2357
2378
  isActive,
2358
2379
  targetOpacity,
2359
- targetScale,
2360
- blurLevel,
2361
- renderMode: isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID
2380
+ blurLevel
2362
2381
  };
2363
2382
  }
2364
2383
  /**
@@ -2514,50 +2533,29 @@ const eqSet = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x))
2514
2533
  * - 根据新的热行状态和已有的缓冲行状态,计算出应移除的缓冲行 ID
2515
2534
  */
2516
2535
  function computePlayerTimeState(input) {
2517
- const { time, processedLines, timelineState: { hotLines, bufferedLines } } = input;
2518
- const nextHotLines = new Set(hotLines);
2536
+ const { time, currentGroups, timelineState: { hotGroups, bufferedGroups } } = input;
2537
+ const nextHotGroups = new Set(hotGroups);
2519
2538
  const addedIds = /* @__PURE__ */ new Set();
2520
2539
  const removedHotIds = /* @__PURE__ */ new Set();
2521
2540
  const removedBufferedIds = /* @__PURE__ */ new Set();
2522
- for (const lastHotId of hotLines) {
2523
- const line = processedLines[lastHotId];
2524
- if (!line) {
2525
- nextHotLines.delete(lastHotId);
2526
- removedHotIds.add(lastHotId);
2527
- continue;
2528
- }
2529
- if (line.isBG) continue;
2530
- const nextLine = processedLines[lastHotId + 1];
2531
- if (nextLine?.isBG) {
2532
- const nextMainLine = processedLines[lastHotId + 2];
2533
- const startTime = Math.min(line.startTime, nextLine.startTime);
2534
- const endTime = Math.min(Math.max(line.endTime, nextMainLine?.startTime ?? Number.MAX_VALUE), Math.max(line.endTime, nextLine.endTime));
2535
- if (time < startTime || endTime <= time) {
2536
- nextHotLines.delete(lastHotId);
2537
- removedHotIds.add(lastHotId);
2538
- nextHotLines.delete(lastHotId + 1);
2539
- removedHotIds.add(lastHotId + 1);
2540
- }
2541
- } else if (time < line.startTime || line.endTime <= time) {
2542
- nextHotLines.delete(lastHotId);
2541
+ for (const lastHotId of hotGroups) {
2542
+ const group = currentGroups[lastHotId];
2543
+ if (!group || time < group.startTime || group.endTime <= time) {
2544
+ nextHotGroups.delete(lastHotId);
2543
2545
  removedHotIds.add(lastHotId);
2544
2546
  }
2545
2547
  }
2546
- for (let id = 0; id < processedLines.length; id++) {
2547
- const line = processedLines[id];
2548
- if (!line || line.isBG) continue;
2549
- if (line.startTime <= time && line.endTime > time && !nextHotLines.has(id)) {
2550
- nextHotLines.add(id);
2548
+ for (let id = 0; id < currentGroups.length; id++) {
2549
+ const group = currentGroups[id];
2550
+ if (!group) continue;
2551
+ if (group.startTime <= time && group.endTime > time && !nextHotGroups.has(id)) {
2552
+ nextHotGroups.add(id);
2551
2553
  addedIds.add(id);
2552
- if (processedLines[id + 1]?.isBG) {
2553
- nextHotLines.add(id + 1);
2554
- addedIds.add(id + 1);
2555
- }
2556
2554
  }
2557
2555
  }
2558
- for (const id of bufferedLines) if (!nextHotLines.has(id)) removedBufferedIds.add(id);
2556
+ for (const id of bufferedGroups) if (!nextHotGroups.has(id)) removedBufferedIds.add(id);
2559
2557
  return {
2560
- nextHotLines,
2558
+ nextHotGroups,
2561
2559
  addedIds,
2562
2560
  removedHotIds,
2563
2561
  removedBufferedIds
@@ -2569,10 +2567,10 @@ function computePlayerTimeState(input) {
2569
2567
  * 若当前仍存在缓冲行,则优先对齐到最靠前的缓冲行;
2570
2568
  * 否则对齐到第一条开始时间不小于当前时间的歌词行。
2571
2569
  */
2572
- function pickScrollToIndexForSeek(time, processedLines, bufferedLines) {
2573
- if (bufferedLines.size > 0) return Math.min(...bufferedLines);
2574
- const foundIndex = processedLines.findIndex((line) => line.startTime >= time);
2575
- return foundIndex === -1 ? processedLines.length : foundIndex;
2570
+ function pickScrollToIndexForSeek(time, currentGroups, bufferedGroups) {
2571
+ if (bufferedGroups.size > 0) return Math.min(...bufferedGroups);
2572
+ const foundIndex = currentGroups.findIndex((group) => group.startTime >= time);
2573
+ return foundIndex === -1 ? currentGroups.length : foundIndex;
2576
2574
  }
2577
2575
  /**
2578
2576
  * 提交时间线状态转移的纯函数。
@@ -2582,45 +2580,45 @@ function pickScrollToIndexForSeek(time, processedLines, bufferedLines) {
2582
2580
  * 是否需要重置用户滚动状态、是否需要触发布局。
2583
2581
  */
2584
2582
  function commitPlayerTimeState(input) {
2585
- const { timelineState, time, processedLines, hasBottomContent, stateResult } = input;
2583
+ const { timelineState, time, currentGroups, hasBottomContent, stateResult } = input;
2586
2584
  const { addedIds, removedHotIds, removedBufferedIds } = stateResult;
2587
2585
  const { isSeeking } = timelineState;
2588
2586
  timelineState.currentTime = time;
2589
- timelineState.hotLines = stateResult.nextHotLines;
2587
+ timelineState.hotGroups = stateResult.nextHotGroups;
2590
2588
  let shouldLayout = false;
2591
2589
  let shouldResetScroll = false;
2592
- const linesToEnable = [];
2593
- const linesToDisable = /* @__PURE__ */ new Set();
2590
+ const groupsToEnable = [];
2591
+ const groupsToDisable = /* @__PURE__ */ new Set();
2594
2592
  if (isSeeking) {
2595
- timelineState.bufferedLines = new Set([...timelineState.hotLines]);
2596
- timelineState.scrollToIndex = pickScrollToIndexForSeek(time, processedLines, timelineState.bufferedLines);
2597
- for (const id of removedHotIds) linesToDisable.add(id);
2598
- for (const id of timelineState.hotLines) linesToEnable.push(id);
2599
- for (const id of removedBufferedIds) linesToDisable.add(id);
2593
+ timelineState.bufferedGroups = new Set([...timelineState.hotGroups]);
2594
+ timelineState.scrollToIndex = pickScrollToIndexForSeek(time, currentGroups, timelineState.bufferedGroups);
2595
+ for (const id of removedHotIds) groupsToDisable.add(id);
2596
+ for (const id of timelineState.hotGroups) groupsToEnable.push(id);
2597
+ for (const id of removedBufferedIds) groupsToDisable.add(id);
2600
2598
  shouldResetScroll = true;
2601
2599
  shouldLayout = true;
2602
2600
  } else if (addedIds.size > 0) {
2603
2601
  for (const id of addedIds) {
2604
- timelineState.bufferedLines.add(id);
2605
- linesToEnable.push(id);
2602
+ timelineState.bufferedGroups.add(id);
2603
+ groupsToEnable.push(id);
2606
2604
  }
2607
2605
  for (const id of removedBufferedIds) {
2608
- timelineState.bufferedLines.delete(id);
2609
- linesToDisable.add(id);
2606
+ timelineState.bufferedGroups.delete(id);
2607
+ groupsToDisable.add(id);
2610
2608
  }
2611
- if (timelineState.bufferedLines.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedLines);
2609
+ if (timelineState.bufferedGroups.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedGroups);
2612
2610
  shouldLayout = true;
2613
- } else if (removedBufferedIds.size > 0 && eqSet(removedBufferedIds, timelineState.bufferedLines)) {
2614
- for (const id of timelineState.bufferedLines) {
2615
- if (timelineState.hotLines.has(id)) continue;
2616
- timelineState.bufferedLines.delete(id);
2617
- linesToDisable.add(id);
2611
+ } else if (removedBufferedIds.size > 0 && eqSet(removedBufferedIds, timelineState.bufferedGroups)) {
2612
+ for (const id of timelineState.bufferedGroups) {
2613
+ if (timelineState.hotGroups.has(id)) continue;
2614
+ timelineState.bufferedGroups.delete(id);
2615
+ groupsToDisable.add(id);
2618
2616
  }
2619
2617
  shouldLayout = true;
2620
2618
  }
2621
- if (timelineState.bufferedLines.size === 0 && processedLines.length > 0) {
2622
- if (time >= processedLines[processedLines.length - 1].endTime) {
2623
- const targetIndex = hasBottomContent ? processedLines.length : processedLines.length - 1;
2619
+ if (timelineState.bufferedGroups.size === 0 && currentGroups.length > 0) {
2620
+ if (time >= currentGroups[currentGroups.length - 1].endTime) {
2621
+ const targetIndex = hasBottomContent ? currentGroups.length : currentGroups.length - 1;
2624
2622
  if (timelineState.scrollToIndex !== targetIndex) {
2625
2623
  timelineState.scrollToIndex = targetIndex;
2626
2624
  shouldLayout = true;
@@ -2631,8 +2629,8 @@ function commitPlayerTimeState(input) {
2631
2629
  return {
2632
2630
  shouldLayout,
2633
2631
  shouldResetScroll,
2634
- linesToEnable,
2635
- linesToDisable: [...linesToDisable]
2632
+ groupsToEnable,
2633
+ groupsToDisable: [...groupsToDisable]
2636
2634
  };
2637
2635
  }
2638
2636
  //#endregion
@@ -2647,17 +2645,15 @@ var LyricPlayerBase = class extends EventTarget {
2647
2645
  timelineState = {
2648
2646
  currentTime: 0,
2649
2647
  lastCurrentTime: 0,
2650
- hotLines: /* @__PURE__ */ new Set(),
2651
- bufferedLines: /* @__PURE__ */ new Set(),
2648
+ hotGroups: /* @__PURE__ */ new Set(),
2649
+ bufferedGroups: /* @__PURE__ */ new Set(),
2652
2650
  scrollToIndex: 0,
2653
2651
  isSeeking: false,
2654
2652
  isPlaying: true,
2655
2653
  initialLayoutFinished: false
2656
2654
  };
2657
2655
  /** @internal */
2658
- lyricLinesSize = /* @__PURE__ */ new WeakMap();
2659
- /** @internal */
2660
- lyricLineElementMap = /* @__PURE__ */ new WeakMap();
2656
+ lyricGroupElementMap = /* @__PURE__ */ new WeakMap();
2661
2657
  currentLyricLines = [];
2662
2658
  processedLines = [];
2663
2659
  lyricLinesIndexes = /* @__PURE__ */ new WeakMap();
@@ -2689,10 +2685,13 @@ var LyricPlayerBase = class extends EventTarget {
2689
2685
  isScrolled: false,
2690
2686
  isUserScrolling: false
2691
2687
  };
2692
- currentLyricLineObjects = [];
2688
+ currentLyricGroups = [];
2689
+ lyricGroupSize = /* @__PURE__ */ new WeakMap();
2693
2690
  size = [0, 0];
2694
2691
  isPageVisible = true;
2695
2692
  optimizeOptions = {};
2693
+ /** 是否强制让背景人声行始终后置(即始终在主歌词下方显示,不前置背景人声) */
2694
+ alwaysPostpositionBackground = false;
2696
2695
  posXSpringParams = {
2697
2696
  mass: 1,
2698
2697
  damping: 10,
@@ -2742,13 +2741,13 @@ var LyricPlayerBase = class extends EventTarget {
2742
2741
  shouldRelayout = true;
2743
2742
  }
2744
2743
  } else {
2745
- const lineObj = this.lyricLineElementMap.get(entry.target);
2746
- if (lineObj) {
2744
+ const groupObj = this.lyricGroupElementMap.get(entry.target);
2745
+ if (groupObj) {
2747
2746
  const newSize = [entry.target.clientWidth, entry.target.clientHeight];
2748
- const oldSize = this.lyricLinesSize.get(lineObj) ?? [0, 0];
2747
+ const oldSize = this.lyricGroupSize.get(groupObj) ?? [0, 0];
2749
2748
  if (newSize[0] !== oldSize[0] || newSize[1] !== oldSize[1]) {
2750
- this.lyricLinesSize.set(lineObj, newSize);
2751
- lineObj.onLineSizeChange(newSize);
2749
+ this.lyricGroupSize.set(groupObj, newSize);
2750
+ groupObj.onLineSizeChange(newSize);
2752
2751
  shouldRelayout = true;
2753
2752
  }
2754
2753
  }
@@ -2874,7 +2873,7 @@ var LyricPlayerBase = class extends EventTarget {
2874
2873
  }
2875
2874
  }
2876
2875
  rebuildLyricLines() {
2877
- for (const lineObj of this.currentLyricLineObjects) lineObj.rebuildElement();
2876
+ for (const group of this.currentLyricGroups) group.rebuildAllLines();
2878
2877
  }
2879
2878
  /**
2880
2879
  * 根据当前配置处理不雅用语单词
@@ -2976,11 +2975,11 @@ var LyricPlayerBase = class extends EventTarget {
2976
2975
  break;
2977
2976
  }
2978
2977
  this.hasDuetLine = this.processedLines.some((line) => line.isDuet);
2979
- for (const line of this.currentLyricLineObjects) line.dispose();
2978
+ for (const group of this.currentLyricGroups) group.dispose();
2979
+ this.currentLyricGroups = [];
2980
2980
  this.interludeDots.setInterlude(void 0);
2981
- this.timelineState.hotLines.clear();
2982
- this.timelineState.bufferedLines.clear();
2983
- this.setCurrentTime(0, true);
2981
+ this.timelineState.hotGroups.clear();
2982
+ this.timelineState.bufferedGroups.clear();
2984
2983
  if (process.env.NODE_ENV !== "production") console.log("歌词处理完成", this);
2985
2984
  }
2986
2985
  /**
@@ -3006,19 +3005,19 @@ var LyricPlayerBase = class extends EventTarget {
3006
3005
  if (!timelineState.initialLayoutFinished && !timelineState.isSeeking) return;
3007
3006
  const stateResult = computePlayerTimeState({
3008
3007
  time,
3009
- processedLines: this.processedLines,
3008
+ currentGroups: this.currentLyricGroups,
3010
3009
  timelineState
3011
3010
  });
3012
3011
  const hasBottomContent = this.bottomLine.getElement().innerHTML.trim().length > 0;
3013
3012
  const commitResult = commitPlayerTimeState({
3014
3013
  timelineState,
3015
3014
  time,
3016
- processedLines: this.processedLines,
3015
+ currentGroups: this.currentLyricGroups,
3017
3016
  hasBottomContent,
3018
3017
  stateResult
3019
3018
  });
3020
- for (const id of commitResult.linesToDisable) this.currentLyricLineObjects[id]?.disable();
3021
- for (const id of commitResult.linesToEnable) this.currentLyricLineObjects[id]?.enable();
3019
+ for (const id of commitResult.groupsToDisable) this.currentLyricGroups[id]?.disable();
3020
+ for (const id of commitResult.groupsToEnable) this.currentLyricGroups[id]?.enable();
3022
3021
  if (commitResult.shouldResetScroll) this.resetScroll();
3023
3022
  if (commitResult.shouldLayout) this.calcLayout();
3024
3023
  }
@@ -3043,14 +3042,14 @@ var LyricPlayerBase = class extends EventTarget {
3043
3042
  const interlude = computeCurrentInterlude({
3044
3043
  currentTime: this.timelineState.currentTime,
3045
3044
  scrollToIndex: this.timelineState.scrollToIndex,
3046
- processedLines: this.processedLines
3045
+ currentGroups: this.currentLyricGroups
3047
3046
  });
3048
3047
  const isInterludeActive = !!interlude;
3049
3048
  if (this.layoutState.targetAlignIndex !== this.timelineState.scrollToIndex || this.layoutState.lastInterludeState !== isInterludeActive) {
3050
3049
  this.layoutState.lastInterludeState = isInterludeActive;
3051
3050
  const springParams = computeLinePosYSpringParams({
3052
3051
  enabled: this.getEnableSpring(),
3053
- processedLines: this.processedLines,
3052
+ currentGroups: this.currentLyricGroups,
3054
3053
  scrollToIndex: this.timelineState.scrollToIndex,
3055
3054
  isSeeking: this.timelineState.isSeeking,
3056
3055
  isInterludeActive
@@ -3068,17 +3067,15 @@ var LyricPlayerBase = class extends EventTarget {
3068
3067
  if (interlude.anchorLineIndex !== -1) curPos -= totalInterludeHeight;
3069
3068
  }
3070
3069
  const LINE_HEIGHT_FALLBACK = this.size[1] / 5;
3071
- const scrollOffset = this.currentLyricLineObjects.slice(0, targetAlignIndex).reduce((acc, el) => acc + (el.getLine().isBG && this.timelineState.isPlaying ? 0 : this.lyricLinesSize.get(el)?.[1] ?? LINE_HEIGHT_FALLBACK), 0);
3070
+ const scrollOffset = this.currentLyricGroups.slice(0, targetAlignIndex).reduce((acc, group) => acc + (this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK), 0);
3072
3071
  this.scrollState.scrollBoundary.minOffset = -scrollOffset;
3073
3072
  curPos -= scrollOffset;
3074
3073
  curPos += this.size[1] * this.layoutState.alignPosition;
3075
- const curLine = this.currentLyricLineObjects[targetAlignIndex];
3074
+ const curGroup = this.currentLyricGroups[targetAlignIndex];
3076
3075
  this.layoutState.targetAlignIndex = targetAlignIndex;
3077
- const isBottomFocused = targetAlignIndex === this.currentLyricLineObjects.length;
3076
+ const isBottomFocused = targetAlignIndex === this.currentLyricGroups.length;
3078
3077
  this.bottomLine.setFocused(isBottomFocused);
3079
- let targetLineHeight = 0;
3080
- if (curLine) targetLineHeight = this.lyricLinesSize.get(curLine)?.[1] ?? LINE_HEIGHT_FALLBACK;
3081
- else if (isBottomFocused) targetLineHeight = this.bottomLine.lineSize[1];
3078
+ const targetLineHeight = curGroup ? this.lyricGroupSize.get(curGroup)?.[1] ?? LINE_HEIGHT_FALLBACK : isBottomFocused ? this.bottomLine.lineSize[1] : 0;
3082
3079
  if (targetLineHeight > 0) switch (this.layoutState.alignAnchor) {
3083
3080
  case LayoutAlignAnchor.Bottom:
3084
3081
  curPos -= targetLineHeight;
@@ -3088,13 +3085,12 @@ var LyricPlayerBase = class extends EventTarget {
3088
3085
  break;
3089
3086
  case LayoutAlignAnchor.Top: break;
3090
3087
  }
3091
- const latestIndex = Math.max(...this.timelineState.bufferedLines);
3088
+ const latestIndex = Math.max(...this.timelineState.bufferedGroups);
3092
3089
  let delay = 0;
3093
3090
  let baseDelay = sync ? 0 : .05;
3094
3091
  let setDots = false;
3095
- this.currentLyricLineObjects.forEach((lineObj, i) => {
3096
- const hasBuffered = this.timelineState.bufferedLines.has(i);
3097
- const line = lineObj.getLine();
3092
+ this.currentLyricGroups.forEach((group, i) => {
3093
+ const hasBuffered = this.timelineState.bufferedGroups.has(i);
3098
3094
  const shouldShowDots = interlude && i === interlude.anchorLineIndex + 1;
3099
3095
  if (!setDots && shouldShowDots) {
3100
3096
  setDots = true;
@@ -3106,31 +3102,28 @@ var LyricPlayerBase = class extends EventTarget {
3106
3102
  curPos += this.layoutState.interludeDotsSize[1];
3107
3103
  curPos += dotMargin;
3108
3104
  }
3109
- const presentation = computeLinePresentation({
3110
- line,
3111
- lineIndex: i,
3105
+ const presentation = computeGroupPresentation({
3106
+ groupIndex: i,
3112
3107
  scrollToIndex: this.timelineState.scrollToIndex,
3113
3108
  latestIndex,
3114
3109
  hasBuffered,
3115
3110
  hidePassedLines: this.hidePassedLines,
3116
3111
  isPlaying: this.timelineState.isPlaying,
3117
3112
  isNonDynamic: this.isNonDynamic,
3118
- enableScale: this.enableScale,
3119
3113
  enableBlur: this.enableBlur,
3120
3114
  isUserScrolling: this.scrollState.isUserScrolling,
3121
3115
  isCompact: window.innerWidth <= 1024,
3122
3116
  interlude
3123
3117
  });
3124
- lineObj.setTransform(curPos, presentation.targetScale, presentation.targetOpacity, presentation.blurLevel, force, delay, presentation.renderMode);
3125
- if (line.isBG && (presentation.isActive || !this.timelineState.isPlaying)) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
3126
- else if (!line.isBG) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
3118
+ group.setTransform(curPos, force, delay, presentation.isActive, presentation.targetOpacity, presentation.blurLevel);
3119
+ curPos += this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK;
3127
3120
  if (curPos >= 0 && !this.timelineState.isSeeking) {
3128
- if (!line.isBG) delay += baseDelay;
3121
+ delay += baseDelay;
3129
3122
  if (i >= this.timelineState.scrollToIndex) baseDelay /= 1.05;
3130
3123
  }
3131
3124
  });
3132
3125
  this.scrollState.scrollBoundary.maxOffset = curPos + this.scrollState.scrollOffset - this.size[1] / 2;
3133
- const bottomIndex = this.currentLyricLineObjects.length;
3126
+ const bottomIndex = this.currentLyricGroups.length;
3134
3127
  const finalBottomBlur = computeLineBlur({
3135
3128
  enableBlur: this.enableBlur,
3136
3129
  isUserScrolling: this.scrollState.isUserScrolling,
@@ -3160,7 +3153,10 @@ var LyricPlayerBase = class extends EventTarget {
3160
3153
  ...params
3161
3154
  };
3162
3155
  this.bottomLine.lineTransforms.posY.updateParams(this.posYSpringParams);
3163
- for (const line of this.currentLyricLineObjects) line.lineTransforms.posY.updateParams(this.posYSpringParams);
3156
+ for (const group of this.currentLyricGroups) {
3157
+ group.posY.updateParams(this.posYSpringParams);
3158
+ group.bgSlideY.updateParams(this.posYSpringParams);
3159
+ }
3164
3160
  }
3165
3161
  /**
3166
3162
  * 设置所有歌词行在​缩放大小上的弹簧属性,包括重量、弹力和阻力。
@@ -3176,8 +3172,10 @@ var LyricPlayerBase = class extends EventTarget {
3176
3172
  ...this.scaleForBGSpringParams,
3177
3173
  ...params
3178
3174
  };
3179
- for (const lineObj of this.currentLyricLineObjects) if (lineObj.getLine().isBG) lineObj.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
3180
- else lineObj.lineTransforms.scale.updateParams(this.scaleSpringParams);
3175
+ for (const group of this.currentLyricGroups) {
3176
+ group.mainLine.lineTransforms.scale.updateParams(this.scaleSpringParams);
3177
+ group.bgLine?.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
3178
+ }
3181
3179
  }
3182
3180
  /**
3183
3181
  * 暂停部分效果演出,目前会暂停播放间奏点的动画,且将背景歌词显示出来
@@ -3249,6 +3247,23 @@ var LyricPlayerBase = class extends EventTarget {
3249
3247
  getCurrentTime() {
3250
3248
  return this.timelineState.currentTime;
3251
3249
  }
3250
+ /**
3251
+ * 设置是否让背景人声行始终后置显示
3252
+ *
3253
+ * 默认情况下,如果背景歌词开始时间早于主歌词,会在主歌词上方展示;
3254
+ * 如果设置为 `true`,则无论时间顺序如何,背景歌词都会始终在主歌词下方展示
3255
+ * @param enable 是否启用始终后置
3256
+ */
3257
+ setAlwaysPostpositionBackground(enable) {
3258
+ if (this.alwaysPostpositionBackground === enable) return;
3259
+ this.alwaysPostpositionBackground = enable;
3260
+ this.rebuildLyricLines();
3261
+ this.calcLayout();
3262
+ }
3263
+ /** 获取当前是否设置了让背景人声行始终后置显示 */
3264
+ getAlwaysPostpositionBackground() {
3265
+ return this.alwaysPostpositionBackground;
3266
+ }
3252
3267
  getElement() {
3253
3268
  return this.element;
3254
3269
  }
@@ -3259,6 +3274,191 @@ var LyricPlayerBase = class extends EventTarget {
3259
3274
  }
3260
3275
  };
3261
3276
  //#endregion
3277
+ //#region src/lyric-player/base/group.ts
3278
+ var LyricLineGroupBase = class {
3279
+ posY = new Spring(0);
3280
+ bgSlideY = new Spring(-80);
3281
+ top = 0;
3282
+ delay = 0;
3283
+ isActive = false;
3284
+ opacity = 1;
3285
+ blur = 0;
3286
+ isBgFirst = false;
3287
+ constructor(mainLine, bgLine) {
3288
+ this.mainLine = mainLine;
3289
+ this.bgLine = bgLine;
3290
+ }
3291
+ get startTime() {
3292
+ return this.mainLine.getLine().startTime;
3293
+ }
3294
+ get endTime() {
3295
+ return this.mainLine.getLine().endTime;
3296
+ }
3297
+ onLineSizeChange(size) {
3298
+ this.mainLine.onLineSizeChange(size);
3299
+ this.bgLine?.onLineSizeChange(size);
3300
+ }
3301
+ setTransform(top, force, delay, isActive, opacity, blur) {
3302
+ this.top = top;
3303
+ this.delay = delay;
3304
+ this.isActive = isActive;
3305
+ this.opacity = opacity;
3306
+ this.blur = blur;
3307
+ this.setLineTransformations(force, delay);
3308
+ const enableSpring = this.lyricPlayer.getEnableSpring();
3309
+ const hiddenSlideY = (this.lyricPlayer.getAlwaysPostpositionBackground() ? false : this.isBgFirst) ? 80 : -80;
3310
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3311
+ const targetBgSlideY = isActive || !isPlaying ? 0 : hiddenSlideY;
3312
+ if (force || !enableSpring) {
3313
+ this.posY.setPosition(top);
3314
+ this.bgSlideY.setPosition(targetBgSlideY);
3315
+ this.renderStyles();
3316
+ } else {
3317
+ this.posY.setTargetPosition(top, delay);
3318
+ this.bgSlideY.setTargetPosition(targetBgSlideY, delay);
3319
+ }
3320
+ }
3321
+ setLineTransformations(force, delay) {
3322
+ const enableScale = this.lyricPlayer.getEnableScale();
3323
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3324
+ const renderMode = this.isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID;
3325
+ const SCALE_ASPECT = enableScale ? 97 : 100;
3326
+ let mainScale = 100;
3327
+ if (!this.isActive && isPlaying) mainScale = SCALE_ASPECT;
3328
+ this.mainLine.setTransform(mainScale, 1, 0, force, delay, renderMode);
3329
+ let bgScale = 100;
3330
+ if (!this.isActive && isPlaying) bgScale = 75;
3331
+ this.bgLine?.setTransform(bgScale, 1, 0, force, delay, renderMode);
3332
+ }
3333
+ update(delta) {
3334
+ if (this.lyricPlayer.getEnableSpring()) {
3335
+ this.posY.update(delta);
3336
+ this.bgSlideY.update(delta);
3337
+ this.renderStyles();
3338
+ }
3339
+ this.mainLine.update(delta);
3340
+ this.bgLine?.update(delta);
3341
+ }
3342
+ rebuildAllLines() {
3343
+ this.mainLine.rebuildElement();
3344
+ this.bgLine?.rebuildElement();
3345
+ }
3346
+ enable(time, shouldPlay) {
3347
+ this.mainLine.enable(time, shouldPlay);
3348
+ this.bgLine?.enable(time, shouldPlay);
3349
+ }
3350
+ disable() {
3351
+ this.mainLine.disable();
3352
+ this.bgLine?.disable();
3353
+ }
3354
+ dispose() {
3355
+ this.mainLine.dispose();
3356
+ this.bgLine?.dispose();
3357
+ }
3358
+ };
3359
+ //#endregion
3360
+ //#region src/lyric-player/dom/lyric-group.ts
3361
+ var LyricLineGroup = class extends LyricLineGroupBase {
3362
+ element;
3363
+ bgWrapper;
3364
+ lastIsActive;
3365
+ constructor(lyricPlayer, mainLine) {
3366
+ super(mainLine);
3367
+ this.lyricPlayer = lyricPlayer;
3368
+ this.element = document.createElement("div");
3369
+ this.element.className = lyric_player_module_default.lyricLineWrapper;
3370
+ this.element.appendChild(mainLine.getElement());
3371
+ this.posY.setPosition(window.innerHeight * 2);
3372
+ lyricPlayer.resizeObserver.observe(this.element);
3373
+ }
3374
+ get isInSight() {
3375
+ const t = this.posY.getCurrentPosition();
3376
+ let h = this.lyricPlayer.lyricGroupSize?.get(this)?.[1];
3377
+ if (h === void 0 || h === 0) h = this.element.clientHeight || 0;
3378
+ const pb = this.lyricPlayer.size[1];
3379
+ const ov = this.lyricPlayer.getOverscanPx();
3380
+ return !(t > pb + h + ov || t < -h - ov);
3381
+ }
3382
+ show() {
3383
+ if (!this.element.parentElement) {
3384
+ const playerEl = this.lyricPlayer.getElement();
3385
+ const groups = this.lyricPlayer.currentLyricGroups;
3386
+ const myIndex = groups.indexOf(this);
3387
+ let referenceNode = null;
3388
+ if (myIndex !== -1) {
3389
+ for (let i = myIndex + 1; i < groups.length; i++) if (groups[i].element.parentElement === playerEl) {
3390
+ referenceNode = groups[i].element;
3391
+ break;
3392
+ }
3393
+ }
3394
+ playerEl.insertBefore(this.element, referenceNode);
3395
+ this.lyricPlayer.resizeObserver.observe(this.element);
3396
+ }
3397
+ this.mainLine.show();
3398
+ this.bgLine?.show();
3399
+ }
3400
+ hide() {
3401
+ if (this.element.parentElement) {
3402
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3403
+ this.element.remove();
3404
+ this.mainLine.teardownContent();
3405
+ this.bgLine?.teardownContent();
3406
+ }
3407
+ }
3408
+ update(delta) {
3409
+ if (this.isInSight) this.show();
3410
+ else this.hide();
3411
+ super.update(delta);
3412
+ }
3413
+ addBgLine(bgLine) {
3414
+ if (this.bgLine) this.bgLine.dispose();
3415
+ if (this.bgWrapper) this.bgWrapper.remove();
3416
+ this.bgLine = bgLine;
3417
+ const bgStartTime = bgLine.getLine().words[0]?.startTime ?? bgLine.getLine().startTime;
3418
+ const mainStartTime = this.mainLine.getLine().words[0]?.startTime ?? this.mainLine.getLine().startTime;
3419
+ this.isBgFirst = bgStartTime < mainStartTime;
3420
+ if (this.mainLine.getLine().isDuet) bgLine.getElement().classList.add(lyric_player_module_default.lyricDuetLine);
3421
+ this.bgWrapper = document.createElement("div");
3422
+ this.bgWrapper.className = lyric_player_module_default.bgWrapper;
3423
+ this.bgWrapper.appendChild(bgLine.getElement());
3424
+ if (!this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst) {
3425
+ this.bgWrapper.classList.add(lyric_player_module_default.bgWrapperTop);
3426
+ this.element.insertBefore(this.bgWrapper, this.mainLine.getElement());
3427
+ this.bgSlideY.setPosition(80);
3428
+ } else this.element.appendChild(this.bgWrapper);
3429
+ }
3430
+ renderStyles() {
3431
+ const y = this.posY.getCurrentPosition().toFixed(1);
3432
+ this.element.style.transform = `translateY(${y}px)`;
3433
+ this.element.style.opacity = this.opacity.toString();
3434
+ this.element.style.filter = `blur(${Math.min(5, this.blur)}px)`;
3435
+ if (!this.lyricPlayer.getEnableSpring()) this.element.style.transitionDelay = `${this.delay}ms`;
3436
+ if (this.bgWrapper) {
3437
+ if (this.lastIsActive !== this.isActive) {
3438
+ this.lastIsActive = this.isActive;
3439
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperActive, this.isActive);
3440
+ }
3441
+ const slideY = this.bgSlideY.getCurrentPosition();
3442
+ const slideYStr = slideY.toFixed(1);
3443
+ const activeProgress = clamp01(1 - Math.abs(slideY) / 80);
3444
+ const scaleStr = (.8 + activeProgress * .2).toFixed(3);
3445
+ this.bgWrapper.style.transform = `translateY(${slideYStr}%) scale(${scaleStr})`;
3446
+ const shouldBgFirst = !this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst;
3447
+ if (shouldBgFirst) {
3448
+ const currentMarginTop = -(this.bgWrapper.clientHeight || 0) * (1 - activeProgress);
3449
+ this.bgWrapper.style.marginTop = `${currentMarginTop.toFixed(1)}px`;
3450
+ } else this.bgWrapper.style.marginTop = "";
3451
+ const isHidden = slideYStr === (shouldBgFirst ? "80.0" : "-80.0") && !this.isActive;
3452
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperHidden, isHidden);
3453
+ }
3454
+ }
3455
+ dispose() {
3456
+ super.dispose();
3457
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3458
+ this.element.remove();
3459
+ }
3460
+ };
3461
+ //#endregion
3262
3462
  //#region src/utils/is-cjk.ts
3263
3463
  const isCJK = (char) => {
3264
3464
  return /^[\p{Unified_Ideograph}\u0800-\u9FFC]+$/u.test(char);
@@ -3275,10 +3475,7 @@ var LyricLineBase = class extends EventTarget {
3275
3475
  blur = 0;
3276
3476
  opacity = 1;
3277
3477
  delay = 0;
3278
- lineTransforms = {
3279
- posY: new Spring(0),
3280
- scale: new Spring(100)
3281
- };
3478
+ lineTransforms = { scale: new Spring(100) };
3282
3479
  /**
3283
3480
  * 用于 CJK 词语边界检测的分词器
3284
3481
  */
@@ -3288,9 +3485,7 @@ var LyricLineBase = class extends EventTarget {
3288
3485
  * 用于正确处理 emoji、复合字符等
3289
3486
  */
3290
3487
  static graphemeSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
3291
- onLineSizeChange(_size) {}
3292
- setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3293
- this.top = top;
3488
+ setTransform(scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3294
3489
  this.scale = scale;
3295
3490
  this.opacity = opacity;
3296
3491
  this.blur = blur;
@@ -3768,12 +3963,9 @@ var LyricLineEl = class extends LyricLineBase {
3768
3963
  super();
3769
3964
  this.lyricPlayer = lyricPlayer;
3770
3965
  this.lyricLine = lyricLine;
3771
- this._prevParentEl = lyricPlayer.getElement();
3772
- lyricPlayer.resizeObserver.observe(this.element);
3773
3966
  this.element.setAttribute("class", lyric_player_module_default.lyricLine);
3774
3967
  if (this.lyricLine.isBG) this.element.classList.add(lyric_player_module_default.lyricBgLine);
3775
3968
  if (this.lyricLine.isDuet) this.element.classList.add(lyric_player_module_default.lyricDuetLine);
3776
- this.lineTransforms.posY.setPosition(window.innerHeight * 2);
3777
3969
  this.element.appendChild(document.createElement("div"));
3778
3970
  this.element.appendChild(document.createElement("div"));
3779
3971
  this.element.appendChild(document.createElement("div"));
@@ -3905,34 +4097,18 @@ var LyricLineEl = class extends LyricLineBase {
3905
4097
  getLine() {
3906
4098
  return this.lyricLine;
3907
4099
  }
3908
- _prevParentEl;
3909
4100
  lastStyle = "";
3910
4101
  show() {
3911
- if (!this.element.parentElement) {
3912
- this._prevParentEl.appendChild(this.element);
3913
- this.lyricPlayer.resizeObserver.observe(this.element);
3914
- }
3915
4102
  if (!this.built) {
3916
4103
  this.rebuildElement();
3917
4104
  this.built = true;
3918
4105
  this.updateMaskImageSync();
3919
4106
  }
3920
- this.rebuildStyle();
3921
- }
3922
- hide() {
3923
- if (this.element.parentElement) {
3924
- this._prevParentEl.removeChild(this.element);
3925
- this.lyricPlayer.resizeObserver.unobserve(this.element);
3926
- }
3927
- if (this.built) {
3928
- this.disposeElements();
3929
- this.built = false;
3930
- }
3931
4107
  }
3932
4108
  rebuildStyle() {
3933
4109
  let style = "";
3934
- style += `transform:translateY(${this.lineTransforms.posY.getCurrentPosition().toFixed(1)}px) scale(${(this.lineTransforms.scale.getCurrentPosition() / 100).toFixed(4)});`;
3935
- if (!this.lyricPlayer.getEnableSpring() && this.isInSight) style += `transition-delay:${this.delay}ms;`;
4110
+ style += `transform: scale(${(this.lineTransforms.scale.getCurrentPosition() / 100).toFixed(4)});`;
4111
+ if (!this.lyricPlayer.getEnableSpring()) style += `transition-delay:${this.delay}ms;`;
3936
4112
  style += `filter:blur(${Math.min(5, this.blur)}px);`;
3937
4113
  if (style !== this.lastStyle) {
3938
4114
  this.lastStyle = style;
@@ -4375,25 +4551,19 @@ var LyricLineEl = class extends LyricLineBase {
4375
4551
  this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
4376
4552
  this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
4377
4553
  }
4378
- setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4379
- super.setTransform(top, scale, opacity, blur, force, delay);
4554
+ setTransform(scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4555
+ super.setTransform(scale, opacity, blur, force, delay);
4380
4556
  this.renderMode = mode;
4381
- const beforeInSight = this.isInSight;
4382
4557
  const enableSpring = this.lyricPlayer.getEnableSpring();
4383
- this.top = top;
4558
+ this.top = 0;
4384
4559
  this.scale = scale;
4385
4560
  this.delay = delay * 1e3 | 0;
4386
4561
  const main = this.element.children[0];
4387
4562
  main.style.opacity = `${opacity}`;
4388
4563
  if (force || !enableSpring) {
4389
4564
  this.blur = Math.min(32, blur);
4390
- this.lineTransforms.posY.setPosition(top);
4391
4565
  this.lineTransforms.scale.setPosition(scale);
4392
- if (!enableSpring) {
4393
- const afterInSight = this.isInSight;
4394
- if (beforeInSight || afterInSight) this.show();
4395
- else this.hide();
4396
- } else this.rebuildStyle();
4566
+ this.rebuildStyle();
4397
4567
  const currentScale = this.lineTransforms.scale.getCurrentPosition();
4398
4568
  this.updateMaskAlphaTargets(currentScale / 100);
4399
4569
  this.currentBrightAlpha = this.targetBrightAlpha;
@@ -4401,21 +4571,18 @@ var LyricLineEl = class extends LyricLineBase {
4401
4571
  this.element.style.setProperty("--bright-mask-alpha", String(this.currentBrightAlpha));
4402
4572
  this.element.style.setProperty("--dark-mask-alpha", String(this.currentDarkAlpha));
4403
4573
  } else {
4404
- this.lineTransforms.posY.setTargetPosition(top, delay);
4405
4574
  this.lineTransforms.scale.setTargetPosition(scale);
4406
4575
  if (this.blur !== Math.min(5, blur)) {
4407
4576
  this.blur = Math.min(5, blur);
4408
- const roundedBlur = blur.toFixed(3);
4409
- this.element.style.filter = `blur(${roundedBlur}px)`;
4577
+ this.element.style.filter = `blur(${blur.toFixed(3)}px)`;
4410
4578
  }
4411
4579
  }
4412
4580
  }
4413
4581
  update(delta = 0) {
4414
4582
  if (!this.lyricPlayer.getEnableSpring()) return;
4415
- this.lineTransforms.posY.update(delta);
4416
4583
  this.lineTransforms.scale.update(delta);
4417
- if (this.isInSight) this.show();
4418
- else this.hide();
4584
+ this.rebuildStyle();
4585
+ if (!this.built) return;
4419
4586
  const currentScale = this.lineTransforms.scale.getCurrentPosition() / 100;
4420
4587
  this.updateMaskAlphaTargets(currentScale);
4421
4588
  this.applyAlphaToDom(delta);
@@ -4423,13 +4590,11 @@ var LyricLineEl = class extends LyricLineBase {
4423
4590
  _getDebugTargetPos() {
4424
4591
  return `[位移: ${this.top}; 缩放: ${this.scale}; 延时: ${this.delay}]`;
4425
4592
  }
4426
- get isInSight() {
4427
- const t = this.lineTransforms.posY.getCurrentPosition();
4428
- const h = this.lyricPlayer.lyricLinesSize.get(this)?.[1] ?? 0;
4429
- const b = t + h;
4430
- const pb = this.lyricPlayer.size[1];
4431
- const ov = this.lyricPlayer.getOverscanPx();
4432
- return !(t > pb + h + ov || b < -h - ov);
4593
+ teardownContent() {
4594
+ if (this.built) {
4595
+ this.disposeElements();
4596
+ this.built = false;
4597
+ }
4433
4598
  }
4434
4599
  disposeElements() {
4435
4600
  this.balancer?.reset();
@@ -4477,7 +4642,7 @@ var LyricLineMouseEvent = class extends MouseEvent {
4477
4642
  * 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
4478
4643
  */
4479
4644
  var DomLyricPlayer = class extends LyricPlayerBase {
4480
- currentLyricLineObjects = [];
4645
+ currentLyricGroups = [];
4481
4646
  onResize() {
4482
4647
  const computedStyles = getComputedStyle(this.element);
4483
4648
  this._baseFontSize = Number.parseFloat(computedStyles.fontSize);
@@ -4514,7 +4679,10 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4514
4679
  rebuildStyle() {}
4515
4680
  setWordFadeWidth(value = .5) {
4516
4681
  super.setWordFadeWidth(value);
4517
- for (const el of this.currentLyricLineObjects) el.updateMaskImageSync();
4682
+ for (const group of this.currentLyricGroups) {
4683
+ group.mainLine.updateMaskImageSync();
4684
+ group.bgLine?.updateMaskImageSync();
4685
+ }
4518
4686
  }
4519
4687
  /**
4520
4688
  * 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
@@ -4526,36 +4694,52 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4526
4694
  if (this.hasDuetLine) this.element.classList.add(lyric_player_module_default.hasDuetLine);
4527
4695
  else this.element.classList.remove(lyric_player_module_default.hasDuetLine);
4528
4696
  if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${initialTime}`);
4529
- for (const line of this.currentLyricLineObjects) {
4530
- line.removeMouseEventListener("click", this.onLineClickedHandler);
4531
- line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
4532
- line.dispose();
4697
+ for (const group of this.currentLyricGroups) {
4698
+ const linesToDispose = [group.mainLine, group.bgLine].filter((line) => !!line);
4699
+ for (const line of linesToDispose) {
4700
+ line.removeMouseEventListener("click", this.onLineClickedHandler);
4701
+ line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
4702
+ }
4703
+ group.dispose();
4533
4704
  }
4534
- this.currentLyricLineObjects = this.processedLines.map((line, i) => {
4705
+ this.currentLyricGroups = [];
4706
+ let currentGroup = null;
4707
+ for (let i = 0; i < this.processedLines.length; i++) {
4708
+ const line = this.processedLines[i];
4535
4709
  const lineEl = new LyricLineEl(this, line);
4536
4710
  lineEl.addMouseEventListener("click", this.onLineClickedHandler);
4537
4711
  lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
4538
4712
  this.lyricLinesIndexes.set(lineEl, i);
4539
- this.lyricLineElementMap.set(lineEl.getElement(), lineEl);
4540
- return lineEl;
4541
- });
4713
+ if (!line.isBG || !currentGroup) {
4714
+ currentGroup = new LyricLineGroup(this, lineEl);
4715
+ this.currentLyricGroups.push(currentGroup);
4716
+ this.lyricGroupElementMap.set(currentGroup.element, currentGroup);
4717
+ } else currentGroup.addBgLine(lineEl);
4718
+ }
4542
4719
  this.setLinePosXSpringParams({});
4543
4720
  this.setLinePosYSpringParams({});
4544
4721
  this.setLineScaleSpringParams({});
4722
+ this.setCurrentTime(initialTime, true);
4545
4723
  this.calcLayout(true);
4546
4724
  this.update(0);
4547
4725
  }
4548
4726
  pause() {
4549
4727
  super.pause();
4550
- this.element.classList.remove("playing");
4728
+ this.element.classList.remove(lyric_player_module_default.playing);
4551
4729
  this.interludeDots.pause();
4552
- for (const line of this.currentLyricLineObjects) line.pause();
4730
+ for (const group of this.currentLyricGroups) {
4731
+ group.mainLine.pause();
4732
+ group.bgLine?.pause();
4733
+ }
4553
4734
  }
4554
4735
  resume() {
4555
4736
  super.resume();
4556
- this.element.classList.add("playing");
4737
+ this.element.classList.add(lyric_player_module_default.playing);
4557
4738
  this.interludeDots.resume();
4558
- for (const line of this.currentLyricLineObjects) line.resume();
4739
+ for (const group of this.currentLyricGroups) {
4740
+ group.mainLine.resume();
4741
+ group.bgLine?.resume();
4742
+ }
4559
4743
  }
4560
4744
  update(delta = 0) {
4561
4745
  if (!this.timelineState.initialLayoutFinished) return;
@@ -4563,12 +4747,12 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4563
4747
  if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${this.timelineState.currentTime}`);
4564
4748
  if (!this.isPageVisible) return;
4565
4749
  const deltaS = delta / 1e3;
4566
- for (const line of this.currentLyricLineObjects) line.update(deltaS);
4750
+ for (const group of this.currentLyricGroups) group.update(deltaS);
4567
4751
  }
4568
4752
  dispose() {
4569
4753
  super.dispose();
4570
4754
  this.element.remove();
4571
- for (const el of this.currentLyricLineObjects) el.dispose();
4755
+ for (const group of this.currentLyricGroups) group.dispose();
4572
4756
  this.bottomLine.dispose();
4573
4757
  this.interludeDots.dispose();
4574
4758
  }