@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.
@@ -1706,6 +1706,10 @@ var BackgroundRender = class BackgroundRender {
1706
1706
  //#region src/styles/lyric-player.module.css
1707
1707
  var lyric_player_module_default = {
1708
1708
  "active": "FmKaba_active",
1709
+ "bgWrapper": "FmKaba_bgWrapper",
1710
+ "bgWrapperActive": "FmKaba_bgWrapperActive",
1711
+ "bgWrapperHidden": "FmKaba_bgWrapperHidden",
1712
+ "bgWrapperTop": "FmKaba_bgWrapperTop",
1709
1713
  "bottomLine": "FmKaba_bottomLine",
1710
1714
  "dirty": "FmKaba_dirty",
1711
1715
  "disableSpring": "FmKaba_disableSpring",
@@ -1718,8 +1722,10 @@ var lyric_player_module_default = {
1718
1722
  "lyricBgLine": "FmKaba_lyricBgLine",
1719
1723
  "lyricDuetLine": "FmKaba_lyricDuetLine",
1720
1724
  "lyricLine": "FmKaba_lyricLine",
1725
+ "lyricLineWrapper": "FmKaba_lyricLineWrapper",
1721
1726
  "lyricMainLine": "FmKaba_lyricMainLine",
1722
1727
  "lyricSubLine": "FmKaba_lyricSubLine",
1728
+ "playing": "FmKaba_playing",
1723
1729
  "romanWord": "FmKaba_romanWord",
1724
1730
  "rubyWord": "FmKaba_rubyWord",
1725
1731
  "tmpDisableTransition": "FmKaba_tmpDisableTransition",
@@ -1820,27 +1826,31 @@ function cleanUnintentionalOverlaps(lines) {
1820
1826
  * 尝试让歌词提前最多 600ms 开始,如果有重叠则尝试最多提前 400ms 或上一行时长的 30%
1821
1827
  */
1822
1828
  function tryAdvanceStartTime(lines) {
1823
- for (let i = lines.length - 1; i >= 0; i--) {
1829
+ const defaultAdvanceAmount = 600;
1830
+ const fallbackAdvanceAmount = 400;
1831
+ const fallbackAdvanceRatio = .3;
1832
+ let prevLineStartTime = 0;
1833
+ let prevLineEndTime = 0;
1834
+ let prevMainGroupStartTime = 0;
1835
+ let prevMainGroupEndTime = 0;
1836
+ let hasPrevLine = false;
1837
+ for (let i = 0; i < lines.length; i++) {
1824
1838
  const line = lines[i];
1825
1839
  if (line.isBG) continue;
1826
- let prevLine = null;
1827
- if (i > 0) {
1828
- let prevIdx = i - 1;
1829
- if (lines[prevIdx].isBG) prevIdx--;
1830
- if (prevIdx >= 0) prevLine = lines[prevIdx];
1831
- }
1840
+ const originalStartTime = line.startTime;
1841
+ const originalEndTime = line.endTime;
1832
1842
  let targetAdvanceAmount = 0;
1833
1843
  let safeBoundary = 0;
1834
- if (prevLine) if (line.startTime >= prevLine.endTime) {
1835
- targetAdvanceAmount = 600;
1836
- safeBoundary = prevLine.endTime;
1844
+ if (hasPrevLine) if (originalStartTime >= prevLineEndTime) {
1845
+ targetAdvanceAmount = defaultAdvanceAmount;
1846
+ safeBoundary = prevMainGroupEndTime;
1837
1847
  } else {
1838
- targetAdvanceAmount = 400;
1839
- const prevDuration = prevLine.endTime - prevLine.startTime;
1840
- safeBoundary = prevLine.startTime + prevDuration * .3;
1848
+ targetAdvanceAmount = fallbackAdvanceAmount;
1849
+ const prevDuration = prevLineEndTime - prevLineStartTime;
1850
+ safeBoundary = prevLineStartTime + prevDuration * fallbackAdvanceRatio;
1841
1851
  }
1842
1852
  else {
1843
- targetAdvanceAmount = 600;
1853
+ targetAdvanceAmount = defaultAdvanceAmount;
1844
1854
  safeBoundary = 0;
1845
1855
  }
1846
1856
  const targetTime = line.startTime - targetAdvanceAmount;
@@ -1848,6 +1858,20 @@ function tryAdvanceStartTime(lines) {
1848
1858
  if (newStartTime < line.startTime) line.startTime = newStartTime;
1849
1859
  const nextLine = lines[i + 1];
1850
1860
  if (nextLine?.isBG) nextLine.startTime = line.startTime;
1861
+ if (hasPrevLine) if (originalStartTime < prevMainGroupEndTime && originalEndTime > prevMainGroupStartTime) {
1862
+ prevMainGroupStartTime = Math.min(prevMainGroupStartTime, originalStartTime);
1863
+ prevMainGroupEndTime = Math.max(prevMainGroupEndTime, originalEndTime);
1864
+ } else {
1865
+ prevMainGroupStartTime = originalStartTime;
1866
+ prevMainGroupEndTime = originalEndTime;
1867
+ }
1868
+ else {
1869
+ prevMainGroupStartTime = originalStartTime;
1870
+ prevMainGroupEndTime = originalEndTime;
1871
+ }
1872
+ prevLineStartTime = originalStartTime;
1873
+ prevLineEndTime = originalEndTime;
1874
+ hasPrevLine = true;
1851
1875
  }
1852
1876
  }
1853
1877
  /**
@@ -2247,19 +2271,19 @@ const LayoutAlignAnchor = {
2247
2271
  function computeCurrentInterlude(input) {
2248
2272
  const currentTime = input.currentTime + 20;
2249
2273
  const currentIndex = input.scrollToIndex;
2250
- const lines = input.processedLines;
2274
+ const groups = input.currentGroups;
2251
2275
  const checkGap = (k) => {
2252
- if (k < -1 || k >= lines.length - 1) return void 0;
2253
- const prevLine = k === -1 ? null : lines[k];
2254
- const nextLine = lines[k + 1];
2255
- const gapStart = prevLine ? prevLine.endTime : 0;
2256
- const gapEnd = Math.max(gapStart, nextLine.startTime - 250);
2257
- if (gapEnd - gapStart < 4e3) return;
2276
+ if (k < -1 || k >= groups.length - 1) return void 0;
2277
+ const prevGroup = k === -1 ? null : groups[k];
2278
+ const nextGroup = groups[k + 1];
2279
+ const gapStart = prevGroup ? prevGroup.endTime : 0;
2280
+ const gapEnd = Math.max(gapStart, nextGroup.startTime - 250);
2281
+ if (gapEnd - gapStart < 4e3) return void 0;
2258
2282
  if (gapEnd > currentTime && gapStart < currentTime) return {
2259
2283
  startTime: Math.max(gapStart, currentTime),
2260
2284
  endTime: gapEnd,
2261
2285
  anchorLineIndex: k,
2262
- isNextDuet: nextLine.isDuet
2286
+ isNextDuet: nextGroup.mainLine.getLine().isDuet
2263
2287
  };
2264
2288
  };
2265
2289
  return checkGap(currentIndex - 1) || checkGap(currentIndex) || checkGap(currentIndex + 1);
@@ -2272,8 +2296,8 @@ function computeCurrentInterlude(input) {
2272
2296
  * - 普通播放时根据相邻歌词的时间间隔动态调整 stiffness / damping
2273
2297
  */
2274
2298
  function computeLinePosYSpringParams(input) {
2275
- const { enabled, processedLines, scrollToIndex, isSeeking, isInterludeActive } = input;
2276
- if (!enabled || processedLines.length === 0) return { shouldUpdate: false };
2299
+ const { enabled, currentGroups, scrollToIndex, isSeeking, isInterludeActive } = input;
2300
+ if (!enabled || currentGroups.length === 0) return { shouldUpdate: false };
2277
2301
  if (isSeeking || isInterludeActive) return {
2278
2302
  shouldUpdate: true,
2279
2303
  params: {
@@ -2281,10 +2305,10 @@ function computeLinePosYSpringParams(input) {
2281
2305
  damping: 15
2282
2306
  }
2283
2307
  };
2284
- const currentLine = processedLines[scrollToIndex];
2285
- const prevLine = processedLines[scrollToIndex - 1];
2286
- if (!currentLine || !prevLine) return { shouldUpdate: false };
2287
- const interval = currentLine.startTime - (prevLine.words[0]?.startTime ?? prevLine.startTime);
2308
+ const currentGroup = currentGroups[scrollToIndex];
2309
+ const prevGroup = currentGroups[scrollToIndex - 1];
2310
+ if (!currentGroup || !prevGroup) return { shouldUpdate: false };
2311
+ const interval = currentGroup.startTime - prevGroup.startTime;
2288
2312
  const MIN_INTERVAL = 100;
2289
2313
  const MAX_INTERVAL = 800;
2290
2314
  const clampedInterval = clamp(interval, MIN_INTERVAL, MAX_INTERVAL);
@@ -2302,38 +2326,33 @@ function computeLinePosYSpringParams(input) {
2302
2326
  };
2303
2327
  }
2304
2328
  /**
2305
- * 计算单行歌词在当前布局中的视觉呈现参数。
2329
+ * 计算一组歌词在当前布局中的视觉呈现参数。
2306
2330
  *
2307
2331
  * 根据播放状态、缓冲状态、布局模式与间奏信息,
2308
- * 生成一行歌词最终应使用的 opacity、scale、blur 和 render mode。
2332
+ * 生成一组歌词最终应使用的活跃状态、不透明度与模糊值。
2309
2333
  */
2310
- function computeLinePresentation(input) {
2311
- const { line, lineIndex, scrollToIndex, latestIndex, hasBuffered, hidePassedLines, isPlaying, isNonDynamic, enableScale, enableBlur, isUserScrolling, isCompact, interlude } = input;
2312
- const isActive = hasBuffered || lineIndex >= scrollToIndex && lineIndex < latestIndex;
2334
+ function computeGroupPresentation(input) {
2335
+ const { groupIndex, scrollToIndex, latestIndex, hasBuffered, hidePassedLines, isPlaying, isNonDynamic, enableBlur, isUserScrolling, isCompact, interlude } = input;
2336
+ const isActive = hasBuffered || groupIndex >= scrollToIndex && groupIndex < latestIndex;
2313
2337
  const blurLevel = computeLineBlur({
2314
2338
  enableBlur,
2315
2339
  isUserScrolling,
2316
2340
  isActive,
2317
- itemIndex: lineIndex,
2341
+ itemIndex: groupIndex,
2318
2342
  scrollToIndex,
2319
2343
  latestIndex,
2320
2344
  isCompact
2321
2345
  });
2322
2346
  let targetOpacity;
2323
- if (hidePassedLines) if (lineIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2347
+ if (hidePassedLines) if (groupIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2324
2348
  else if (hasBuffered) targetOpacity = .85;
2325
2349
  else targetOpacity = isNonDynamic ? .2 : 1;
2326
2350
  else if (hasBuffered) targetOpacity = .85;
2327
2351
  else targetOpacity = isNonDynamic ? .2 : 1;
2328
- const SCALE_ASPECT = enableScale ? 97 : 100;
2329
- let targetScale = 100;
2330
- if (!isActive && isPlaying) targetScale = line.isBG ? 75 : SCALE_ASPECT;
2331
2352
  return {
2332
2353
  isActive,
2333
2354
  targetOpacity,
2334
- targetScale,
2335
- blurLevel,
2336
- renderMode: isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID
2355
+ blurLevel
2337
2356
  };
2338
2357
  }
2339
2358
  /**
@@ -2489,50 +2508,29 @@ const eqSet = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x))
2489
2508
  * - 根据新的热行状态和已有的缓冲行状态,计算出应移除的缓冲行 ID
2490
2509
  */
2491
2510
  function computePlayerTimeState(input) {
2492
- const { time, processedLines, timelineState: { hotLines, bufferedLines } } = input;
2493
- const nextHotLines = new Set(hotLines);
2511
+ const { time, currentGroups, timelineState: { hotGroups, bufferedGroups } } = input;
2512
+ const nextHotGroups = new Set(hotGroups);
2494
2513
  const addedIds = /* @__PURE__ */ new Set();
2495
2514
  const removedHotIds = /* @__PURE__ */ new Set();
2496
2515
  const removedBufferedIds = /* @__PURE__ */ new Set();
2497
- for (const lastHotId of hotLines) {
2498
- const line = processedLines[lastHotId];
2499
- if (!line) {
2500
- nextHotLines.delete(lastHotId);
2501
- removedHotIds.add(lastHotId);
2502
- continue;
2503
- }
2504
- if (line.isBG) continue;
2505
- const nextLine = processedLines[lastHotId + 1];
2506
- if (nextLine?.isBG) {
2507
- const nextMainLine = processedLines[lastHotId + 2];
2508
- const startTime = Math.min(line.startTime, nextLine.startTime);
2509
- const endTime = Math.min(Math.max(line.endTime, nextMainLine?.startTime ?? Number.MAX_VALUE), Math.max(line.endTime, nextLine.endTime));
2510
- if (time < startTime || endTime <= time) {
2511
- nextHotLines.delete(lastHotId);
2512
- removedHotIds.add(lastHotId);
2513
- nextHotLines.delete(lastHotId + 1);
2514
- removedHotIds.add(lastHotId + 1);
2515
- }
2516
- } else if (time < line.startTime || line.endTime <= time) {
2517
- nextHotLines.delete(lastHotId);
2516
+ for (const lastHotId of hotGroups) {
2517
+ const group = currentGroups[lastHotId];
2518
+ if (!group || time < group.startTime || group.endTime <= time) {
2519
+ nextHotGroups.delete(lastHotId);
2518
2520
  removedHotIds.add(lastHotId);
2519
2521
  }
2520
2522
  }
2521
- for (let id = 0; id < processedLines.length; id++) {
2522
- const line = processedLines[id];
2523
- if (!line || line.isBG) continue;
2524
- if (line.startTime <= time && line.endTime > time && !nextHotLines.has(id)) {
2525
- nextHotLines.add(id);
2523
+ for (let id = 0; id < currentGroups.length; id++) {
2524
+ const group = currentGroups[id];
2525
+ if (!group) continue;
2526
+ if (group.startTime <= time && group.endTime > time && !nextHotGroups.has(id)) {
2527
+ nextHotGroups.add(id);
2526
2528
  addedIds.add(id);
2527
- if (processedLines[id + 1]?.isBG) {
2528
- nextHotLines.add(id + 1);
2529
- addedIds.add(id + 1);
2530
- }
2531
2529
  }
2532
2530
  }
2533
- for (const id of bufferedLines) if (!nextHotLines.has(id)) removedBufferedIds.add(id);
2531
+ for (const id of bufferedGroups) if (!nextHotGroups.has(id)) removedBufferedIds.add(id);
2534
2532
  return {
2535
- nextHotLines,
2533
+ nextHotGroups,
2536
2534
  addedIds,
2537
2535
  removedHotIds,
2538
2536
  removedBufferedIds
@@ -2544,10 +2542,10 @@ function computePlayerTimeState(input) {
2544
2542
  * 若当前仍存在缓冲行,则优先对齐到最靠前的缓冲行;
2545
2543
  * 否则对齐到第一条开始时间不小于当前时间的歌词行。
2546
2544
  */
2547
- function pickScrollToIndexForSeek(time, processedLines, bufferedLines) {
2548
- if (bufferedLines.size > 0) return Math.min(...bufferedLines);
2549
- const foundIndex = processedLines.findIndex((line) => line.startTime >= time);
2550
- return foundIndex === -1 ? processedLines.length : foundIndex;
2545
+ function pickScrollToIndexForSeek(time, currentGroups, bufferedGroups) {
2546
+ if (bufferedGroups.size > 0) return Math.min(...bufferedGroups);
2547
+ const foundIndex = currentGroups.findIndex((group) => group.startTime >= time);
2548
+ return foundIndex === -1 ? currentGroups.length : foundIndex;
2551
2549
  }
2552
2550
  /**
2553
2551
  * 提交时间线状态转移的纯函数。
@@ -2557,45 +2555,45 @@ function pickScrollToIndexForSeek(time, processedLines, bufferedLines) {
2557
2555
  * 是否需要重置用户滚动状态、是否需要触发布局。
2558
2556
  */
2559
2557
  function commitPlayerTimeState(input) {
2560
- const { timelineState, time, processedLines, hasBottomContent, stateResult } = input;
2558
+ const { timelineState, time, currentGroups, hasBottomContent, stateResult } = input;
2561
2559
  const { addedIds, removedHotIds, removedBufferedIds } = stateResult;
2562
2560
  const { isSeeking } = timelineState;
2563
2561
  timelineState.currentTime = time;
2564
- timelineState.hotLines = stateResult.nextHotLines;
2562
+ timelineState.hotGroups = stateResult.nextHotGroups;
2565
2563
  let shouldLayout = false;
2566
2564
  let shouldResetScroll = false;
2567
- const linesToEnable = [];
2568
- const linesToDisable = /* @__PURE__ */ new Set();
2565
+ const groupsToEnable = [];
2566
+ const groupsToDisable = /* @__PURE__ */ new Set();
2569
2567
  if (isSeeking) {
2570
- timelineState.bufferedLines = new Set([...timelineState.hotLines]);
2571
- timelineState.scrollToIndex = pickScrollToIndexForSeek(time, processedLines, timelineState.bufferedLines);
2572
- for (const id of removedHotIds) linesToDisable.add(id);
2573
- for (const id of timelineState.hotLines) linesToEnable.push(id);
2574
- for (const id of removedBufferedIds) linesToDisable.add(id);
2568
+ timelineState.bufferedGroups = new Set([...timelineState.hotGroups]);
2569
+ timelineState.scrollToIndex = pickScrollToIndexForSeek(time, currentGroups, timelineState.bufferedGroups);
2570
+ for (const id of removedHotIds) groupsToDisable.add(id);
2571
+ for (const id of timelineState.hotGroups) groupsToEnable.push(id);
2572
+ for (const id of removedBufferedIds) groupsToDisable.add(id);
2575
2573
  shouldResetScroll = true;
2576
2574
  shouldLayout = true;
2577
2575
  } else if (addedIds.size > 0) {
2578
2576
  for (const id of addedIds) {
2579
- timelineState.bufferedLines.add(id);
2580
- linesToEnable.push(id);
2577
+ timelineState.bufferedGroups.add(id);
2578
+ groupsToEnable.push(id);
2581
2579
  }
2582
2580
  for (const id of removedBufferedIds) {
2583
- timelineState.bufferedLines.delete(id);
2584
- linesToDisable.add(id);
2581
+ timelineState.bufferedGroups.delete(id);
2582
+ groupsToDisable.add(id);
2585
2583
  }
2586
- if (timelineState.bufferedLines.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedLines);
2584
+ if (timelineState.bufferedGroups.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedGroups);
2587
2585
  shouldLayout = true;
2588
- } else if (removedBufferedIds.size > 0 && eqSet(removedBufferedIds, timelineState.bufferedLines)) {
2589
- for (const id of timelineState.bufferedLines) {
2590
- if (timelineState.hotLines.has(id)) continue;
2591
- timelineState.bufferedLines.delete(id);
2592
- linesToDisable.add(id);
2586
+ } else if (removedBufferedIds.size > 0 && eqSet(removedBufferedIds, timelineState.bufferedGroups)) {
2587
+ for (const id of timelineState.bufferedGroups) {
2588
+ if (timelineState.hotGroups.has(id)) continue;
2589
+ timelineState.bufferedGroups.delete(id);
2590
+ groupsToDisable.add(id);
2593
2591
  }
2594
2592
  shouldLayout = true;
2595
2593
  }
2596
- if (timelineState.bufferedLines.size === 0 && processedLines.length > 0) {
2597
- if (time >= processedLines[processedLines.length - 1].endTime) {
2598
- const targetIndex = hasBottomContent ? processedLines.length : processedLines.length - 1;
2594
+ if (timelineState.bufferedGroups.size === 0 && currentGroups.length > 0) {
2595
+ if (time >= currentGroups[currentGroups.length - 1].endTime) {
2596
+ const targetIndex = hasBottomContent ? currentGroups.length : currentGroups.length - 1;
2599
2597
  if (timelineState.scrollToIndex !== targetIndex) {
2600
2598
  timelineState.scrollToIndex = targetIndex;
2601
2599
  shouldLayout = true;
@@ -2606,8 +2604,8 @@ function commitPlayerTimeState(input) {
2606
2604
  return {
2607
2605
  shouldLayout,
2608
2606
  shouldResetScroll,
2609
- linesToEnable,
2610
- linesToDisable: [...linesToDisable]
2607
+ groupsToEnable,
2608
+ groupsToDisable: [...groupsToDisable]
2611
2609
  };
2612
2610
  }
2613
2611
  //#endregion
@@ -2622,17 +2620,15 @@ var LyricPlayerBase = class extends EventTarget {
2622
2620
  timelineState = {
2623
2621
  currentTime: 0,
2624
2622
  lastCurrentTime: 0,
2625
- hotLines: /* @__PURE__ */ new Set(),
2626
- bufferedLines: /* @__PURE__ */ new Set(),
2623
+ hotGroups: /* @__PURE__ */ new Set(),
2624
+ bufferedGroups: /* @__PURE__ */ new Set(),
2627
2625
  scrollToIndex: 0,
2628
2626
  isSeeking: false,
2629
2627
  isPlaying: true,
2630
2628
  initialLayoutFinished: false
2631
2629
  };
2632
2630
  /** @internal */
2633
- lyricLinesSize = /* @__PURE__ */ new WeakMap();
2634
- /** @internal */
2635
- lyricLineElementMap = /* @__PURE__ */ new WeakMap();
2631
+ lyricGroupElementMap = /* @__PURE__ */ new WeakMap();
2636
2632
  currentLyricLines = [];
2637
2633
  processedLines = [];
2638
2634
  lyricLinesIndexes = /* @__PURE__ */ new WeakMap();
@@ -2664,10 +2660,13 @@ var LyricPlayerBase = class extends EventTarget {
2664
2660
  isScrolled: false,
2665
2661
  isUserScrolling: false
2666
2662
  };
2667
- currentLyricLineObjects = [];
2663
+ currentLyricGroups = [];
2664
+ lyricGroupSize = /* @__PURE__ */ new WeakMap();
2668
2665
  size = [0, 0];
2669
2666
  isPageVisible = true;
2670
2667
  optimizeOptions = {};
2668
+ /** 是否强制让背景人声行始终后置(即始终在主歌词下方显示,不前置背景人声) */
2669
+ alwaysPostpositionBackground = false;
2671
2670
  posXSpringParams = {
2672
2671
  mass: 1,
2673
2672
  damping: 10,
@@ -2717,13 +2716,13 @@ var LyricPlayerBase = class extends EventTarget {
2717
2716
  shouldRelayout = true;
2718
2717
  }
2719
2718
  } else {
2720
- const lineObj = this.lyricLineElementMap.get(entry.target);
2721
- if (lineObj) {
2719
+ const groupObj = this.lyricGroupElementMap.get(entry.target);
2720
+ if (groupObj) {
2722
2721
  const newSize = [entry.target.clientWidth, entry.target.clientHeight];
2723
- const oldSize = this.lyricLinesSize.get(lineObj) ?? [0, 0];
2722
+ const oldSize = this.lyricGroupSize.get(groupObj) ?? [0, 0];
2724
2723
  if (newSize[0] !== oldSize[0] || newSize[1] !== oldSize[1]) {
2725
- this.lyricLinesSize.set(lineObj, newSize);
2726
- lineObj.onLineSizeChange(newSize);
2724
+ this.lyricGroupSize.set(groupObj, newSize);
2725
+ groupObj.onLineSizeChange(newSize);
2727
2726
  shouldRelayout = true;
2728
2727
  }
2729
2728
  }
@@ -2849,7 +2848,7 @@ var LyricPlayerBase = class extends EventTarget {
2849
2848
  }
2850
2849
  }
2851
2850
  rebuildLyricLines() {
2852
- for (const lineObj of this.currentLyricLineObjects) lineObj.rebuildElement();
2851
+ for (const group of this.currentLyricGroups) group.rebuildAllLines();
2853
2852
  }
2854
2853
  /**
2855
2854
  * 根据当前配置处理不雅用语单词
@@ -2951,11 +2950,11 @@ var LyricPlayerBase = class extends EventTarget {
2951
2950
  break;
2952
2951
  }
2953
2952
  this.hasDuetLine = this.processedLines.some((line) => line.isDuet);
2954
- for (const line of this.currentLyricLineObjects) line.dispose();
2953
+ for (const group of this.currentLyricGroups) group.dispose();
2954
+ this.currentLyricGroups = [];
2955
2955
  this.interludeDots.setInterlude(void 0);
2956
- this.timelineState.hotLines.clear();
2957
- this.timelineState.bufferedLines.clear();
2958
- this.setCurrentTime(0, true);
2956
+ this.timelineState.hotGroups.clear();
2957
+ this.timelineState.bufferedGroups.clear();
2959
2958
  if (process.env.NODE_ENV !== "production") console.log("歌词处理完成", this);
2960
2959
  }
2961
2960
  /**
@@ -2981,19 +2980,19 @@ var LyricPlayerBase = class extends EventTarget {
2981
2980
  if (!timelineState.initialLayoutFinished && !timelineState.isSeeking) return;
2982
2981
  const stateResult = computePlayerTimeState({
2983
2982
  time,
2984
- processedLines: this.processedLines,
2983
+ currentGroups: this.currentLyricGroups,
2985
2984
  timelineState
2986
2985
  });
2987
2986
  const hasBottomContent = this.bottomLine.getElement().innerHTML.trim().length > 0;
2988
2987
  const commitResult = commitPlayerTimeState({
2989
2988
  timelineState,
2990
2989
  time,
2991
- processedLines: this.processedLines,
2990
+ currentGroups: this.currentLyricGroups,
2992
2991
  hasBottomContent,
2993
2992
  stateResult
2994
2993
  });
2995
- for (const id of commitResult.linesToDisable) this.currentLyricLineObjects[id]?.disable();
2996
- for (const id of commitResult.linesToEnable) this.currentLyricLineObjects[id]?.enable();
2994
+ for (const id of commitResult.groupsToDisable) this.currentLyricGroups[id]?.disable();
2995
+ for (const id of commitResult.groupsToEnable) this.currentLyricGroups[id]?.enable();
2997
2996
  if (commitResult.shouldResetScroll) this.resetScroll();
2998
2997
  if (commitResult.shouldLayout) this.calcLayout();
2999
2998
  }
@@ -3018,14 +3017,14 @@ var LyricPlayerBase = class extends EventTarget {
3018
3017
  const interlude = computeCurrentInterlude({
3019
3018
  currentTime: this.timelineState.currentTime,
3020
3019
  scrollToIndex: this.timelineState.scrollToIndex,
3021
- processedLines: this.processedLines
3020
+ currentGroups: this.currentLyricGroups
3022
3021
  });
3023
3022
  const isInterludeActive = !!interlude;
3024
3023
  if (this.layoutState.targetAlignIndex !== this.timelineState.scrollToIndex || this.layoutState.lastInterludeState !== isInterludeActive) {
3025
3024
  this.layoutState.lastInterludeState = isInterludeActive;
3026
3025
  const springParams = computeLinePosYSpringParams({
3027
3026
  enabled: this.getEnableSpring(),
3028
- processedLines: this.processedLines,
3027
+ currentGroups: this.currentLyricGroups,
3029
3028
  scrollToIndex: this.timelineState.scrollToIndex,
3030
3029
  isSeeking: this.timelineState.isSeeking,
3031
3030
  isInterludeActive
@@ -3043,17 +3042,15 @@ var LyricPlayerBase = class extends EventTarget {
3043
3042
  if (interlude.anchorLineIndex !== -1) curPos -= totalInterludeHeight;
3044
3043
  }
3045
3044
  const LINE_HEIGHT_FALLBACK = this.size[1] / 5;
3046
- 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);
3045
+ const scrollOffset = this.currentLyricGroups.slice(0, targetAlignIndex).reduce((acc, group) => acc + (this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK), 0);
3047
3046
  this.scrollState.scrollBoundary.minOffset = -scrollOffset;
3048
3047
  curPos -= scrollOffset;
3049
3048
  curPos += this.size[1] * this.layoutState.alignPosition;
3050
- const curLine = this.currentLyricLineObjects[targetAlignIndex];
3049
+ const curGroup = this.currentLyricGroups[targetAlignIndex];
3051
3050
  this.layoutState.targetAlignIndex = targetAlignIndex;
3052
- const isBottomFocused = targetAlignIndex === this.currentLyricLineObjects.length;
3051
+ const isBottomFocused = targetAlignIndex === this.currentLyricGroups.length;
3053
3052
  this.bottomLine.setFocused(isBottomFocused);
3054
- let targetLineHeight = 0;
3055
- if (curLine) targetLineHeight = this.lyricLinesSize.get(curLine)?.[1] ?? LINE_HEIGHT_FALLBACK;
3056
- else if (isBottomFocused) targetLineHeight = this.bottomLine.lineSize[1];
3053
+ const targetLineHeight = curGroup ? this.lyricGroupSize.get(curGroup)?.[1] ?? LINE_HEIGHT_FALLBACK : isBottomFocused ? this.bottomLine.lineSize[1] : 0;
3057
3054
  if (targetLineHeight > 0) switch (this.layoutState.alignAnchor) {
3058
3055
  case LayoutAlignAnchor.Bottom:
3059
3056
  curPos -= targetLineHeight;
@@ -3063,13 +3060,12 @@ var LyricPlayerBase = class extends EventTarget {
3063
3060
  break;
3064
3061
  case LayoutAlignAnchor.Top: break;
3065
3062
  }
3066
- const latestIndex = Math.max(...this.timelineState.bufferedLines);
3063
+ const latestIndex = Math.max(...this.timelineState.bufferedGroups);
3067
3064
  let delay = 0;
3068
3065
  let baseDelay = sync ? 0 : .05;
3069
3066
  let setDots = false;
3070
- this.currentLyricLineObjects.forEach((lineObj, i) => {
3071
- const hasBuffered = this.timelineState.bufferedLines.has(i);
3072
- const line = lineObj.getLine();
3067
+ this.currentLyricGroups.forEach((group, i) => {
3068
+ const hasBuffered = this.timelineState.bufferedGroups.has(i);
3073
3069
  const shouldShowDots = interlude && i === interlude.anchorLineIndex + 1;
3074
3070
  if (!setDots && shouldShowDots) {
3075
3071
  setDots = true;
@@ -3081,31 +3077,28 @@ var LyricPlayerBase = class extends EventTarget {
3081
3077
  curPos += this.layoutState.interludeDotsSize[1];
3082
3078
  curPos += dotMargin;
3083
3079
  }
3084
- const presentation = computeLinePresentation({
3085
- line,
3086
- lineIndex: i,
3080
+ const presentation = computeGroupPresentation({
3081
+ groupIndex: i,
3087
3082
  scrollToIndex: this.timelineState.scrollToIndex,
3088
3083
  latestIndex,
3089
3084
  hasBuffered,
3090
3085
  hidePassedLines: this.hidePassedLines,
3091
3086
  isPlaying: this.timelineState.isPlaying,
3092
3087
  isNonDynamic: this.isNonDynamic,
3093
- enableScale: this.enableScale,
3094
3088
  enableBlur: this.enableBlur,
3095
3089
  isUserScrolling: this.scrollState.isUserScrolling,
3096
3090
  isCompact: window.innerWidth <= 1024,
3097
3091
  interlude
3098
3092
  });
3099
- lineObj.setTransform(curPos, presentation.targetScale, presentation.targetOpacity, presentation.blurLevel, force, delay, presentation.renderMode);
3100
- if (line.isBG && (presentation.isActive || !this.timelineState.isPlaying)) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
3101
- else if (!line.isBG) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
3093
+ group.setTransform(curPos, force, delay, presentation.isActive, presentation.targetOpacity, presentation.blurLevel);
3094
+ curPos += this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK;
3102
3095
  if (curPos >= 0 && !this.timelineState.isSeeking) {
3103
- if (!line.isBG) delay += baseDelay;
3096
+ delay += baseDelay;
3104
3097
  if (i >= this.timelineState.scrollToIndex) baseDelay /= 1.05;
3105
3098
  }
3106
3099
  });
3107
3100
  this.scrollState.scrollBoundary.maxOffset = curPos + this.scrollState.scrollOffset - this.size[1] / 2;
3108
- const bottomIndex = this.currentLyricLineObjects.length;
3101
+ const bottomIndex = this.currentLyricGroups.length;
3109
3102
  const finalBottomBlur = computeLineBlur({
3110
3103
  enableBlur: this.enableBlur,
3111
3104
  isUserScrolling: this.scrollState.isUserScrolling,
@@ -3135,7 +3128,10 @@ var LyricPlayerBase = class extends EventTarget {
3135
3128
  ...params
3136
3129
  };
3137
3130
  this.bottomLine.lineTransforms.posY.updateParams(this.posYSpringParams);
3138
- for (const line of this.currentLyricLineObjects) line.lineTransforms.posY.updateParams(this.posYSpringParams);
3131
+ for (const group of this.currentLyricGroups) {
3132
+ group.posY.updateParams(this.posYSpringParams);
3133
+ group.bgSlideY.updateParams(this.posYSpringParams);
3134
+ }
3139
3135
  }
3140
3136
  /**
3141
3137
  * 设置所有歌词行在​缩放大小上的弹簧属性,包括重量、弹力和阻力。
@@ -3151,8 +3147,10 @@ var LyricPlayerBase = class extends EventTarget {
3151
3147
  ...this.scaleForBGSpringParams,
3152
3148
  ...params
3153
3149
  };
3154
- for (const lineObj of this.currentLyricLineObjects) if (lineObj.getLine().isBG) lineObj.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
3155
- else lineObj.lineTransforms.scale.updateParams(this.scaleSpringParams);
3150
+ for (const group of this.currentLyricGroups) {
3151
+ group.mainLine.lineTransforms.scale.updateParams(this.scaleSpringParams);
3152
+ group.bgLine?.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
3153
+ }
3156
3154
  }
3157
3155
  /**
3158
3156
  * 暂停部分效果演出,目前会暂停播放间奏点的动画,且将背景歌词显示出来
@@ -3224,6 +3222,23 @@ var LyricPlayerBase = class extends EventTarget {
3224
3222
  getCurrentTime() {
3225
3223
  return this.timelineState.currentTime;
3226
3224
  }
3225
+ /**
3226
+ * 设置是否让背景人声行始终后置显示
3227
+ *
3228
+ * 默认情况下,如果背景歌词开始时间早于主歌词,会在主歌词上方展示;
3229
+ * 如果设置为 `true`,则无论时间顺序如何,背景歌词都会始终在主歌词下方展示
3230
+ * @param enable 是否启用始终后置
3231
+ */
3232
+ setAlwaysPostpositionBackground(enable) {
3233
+ if (this.alwaysPostpositionBackground === enable) return;
3234
+ this.alwaysPostpositionBackground = enable;
3235
+ this.rebuildLyricLines();
3236
+ this.calcLayout();
3237
+ }
3238
+ /** 获取当前是否设置了让背景人声行始终后置显示 */
3239
+ getAlwaysPostpositionBackground() {
3240
+ return this.alwaysPostpositionBackground;
3241
+ }
3227
3242
  getElement() {
3228
3243
  return this.element;
3229
3244
  }
@@ -3234,6 +3249,191 @@ var LyricPlayerBase = class extends EventTarget {
3234
3249
  }
3235
3250
  };
3236
3251
  //#endregion
3252
+ //#region src/lyric-player/base/group.ts
3253
+ var LyricLineGroupBase = class {
3254
+ posY = new Spring(0);
3255
+ bgSlideY = new Spring(-80);
3256
+ top = 0;
3257
+ delay = 0;
3258
+ isActive = false;
3259
+ opacity = 1;
3260
+ blur = 0;
3261
+ isBgFirst = false;
3262
+ constructor(mainLine, bgLine) {
3263
+ this.mainLine = mainLine;
3264
+ this.bgLine = bgLine;
3265
+ }
3266
+ get startTime() {
3267
+ return this.mainLine.getLine().startTime;
3268
+ }
3269
+ get endTime() {
3270
+ return this.mainLine.getLine().endTime;
3271
+ }
3272
+ onLineSizeChange(size) {
3273
+ this.mainLine.onLineSizeChange(size);
3274
+ this.bgLine?.onLineSizeChange(size);
3275
+ }
3276
+ setTransform(top, force, delay, isActive, opacity, blur) {
3277
+ this.top = top;
3278
+ this.delay = delay;
3279
+ this.isActive = isActive;
3280
+ this.opacity = opacity;
3281
+ this.blur = blur;
3282
+ this.setLineTransformations(force, delay);
3283
+ const enableSpring = this.lyricPlayer.getEnableSpring();
3284
+ const hiddenSlideY = (this.lyricPlayer.getAlwaysPostpositionBackground() ? false : this.isBgFirst) ? 80 : -80;
3285
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3286
+ const targetBgSlideY = isActive || !isPlaying ? 0 : hiddenSlideY;
3287
+ if (force || !enableSpring) {
3288
+ this.posY.setPosition(top);
3289
+ this.bgSlideY.setPosition(targetBgSlideY);
3290
+ this.renderStyles();
3291
+ } else {
3292
+ this.posY.setTargetPosition(top, delay);
3293
+ this.bgSlideY.setTargetPosition(targetBgSlideY, delay);
3294
+ }
3295
+ }
3296
+ setLineTransformations(force, delay) {
3297
+ const enableScale = this.lyricPlayer.getEnableScale();
3298
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3299
+ const renderMode = this.isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID;
3300
+ const SCALE_ASPECT = enableScale ? 97 : 100;
3301
+ let mainScale = 100;
3302
+ if (!this.isActive && isPlaying) mainScale = SCALE_ASPECT;
3303
+ this.mainLine.setTransform(mainScale, 1, 0, force, delay, renderMode);
3304
+ let bgScale = 100;
3305
+ if (!this.isActive && isPlaying) bgScale = 75;
3306
+ this.bgLine?.setTransform(bgScale, 1, 0, force, delay, renderMode);
3307
+ }
3308
+ update(delta) {
3309
+ if (this.lyricPlayer.getEnableSpring()) {
3310
+ this.posY.update(delta);
3311
+ this.bgSlideY.update(delta);
3312
+ this.renderStyles();
3313
+ }
3314
+ this.mainLine.update(delta);
3315
+ this.bgLine?.update(delta);
3316
+ }
3317
+ rebuildAllLines() {
3318
+ this.mainLine.rebuildElement();
3319
+ this.bgLine?.rebuildElement();
3320
+ }
3321
+ enable(time, shouldPlay) {
3322
+ this.mainLine.enable(time, shouldPlay);
3323
+ this.bgLine?.enable(time, shouldPlay);
3324
+ }
3325
+ disable() {
3326
+ this.mainLine.disable();
3327
+ this.bgLine?.disable();
3328
+ }
3329
+ dispose() {
3330
+ this.mainLine.dispose();
3331
+ this.bgLine?.dispose();
3332
+ }
3333
+ };
3334
+ //#endregion
3335
+ //#region src/lyric-player/dom/lyric-group.ts
3336
+ var LyricLineGroup = class extends LyricLineGroupBase {
3337
+ element;
3338
+ bgWrapper;
3339
+ lastIsActive;
3340
+ constructor(lyricPlayer, mainLine) {
3341
+ super(mainLine);
3342
+ this.lyricPlayer = lyricPlayer;
3343
+ this.element = document.createElement("div");
3344
+ this.element.className = lyric_player_module_default.lyricLineWrapper;
3345
+ this.element.appendChild(mainLine.getElement());
3346
+ this.posY.setPosition(window.innerHeight * 2);
3347
+ lyricPlayer.resizeObserver.observe(this.element);
3348
+ }
3349
+ get isInSight() {
3350
+ const t = this.posY.getCurrentPosition();
3351
+ let h = this.lyricPlayer.lyricGroupSize?.get(this)?.[1];
3352
+ if (h === void 0 || h === 0) h = this.element.clientHeight || 0;
3353
+ const pb = this.lyricPlayer.size[1];
3354
+ const ov = this.lyricPlayer.getOverscanPx();
3355
+ return !(t > pb + h + ov || t < -h - ov);
3356
+ }
3357
+ show() {
3358
+ if (!this.element.parentElement) {
3359
+ const playerEl = this.lyricPlayer.getElement();
3360
+ const groups = this.lyricPlayer.currentLyricGroups;
3361
+ const myIndex = groups.indexOf(this);
3362
+ let referenceNode = null;
3363
+ if (myIndex !== -1) {
3364
+ for (let i = myIndex + 1; i < groups.length; i++) if (groups[i].element.parentElement === playerEl) {
3365
+ referenceNode = groups[i].element;
3366
+ break;
3367
+ }
3368
+ }
3369
+ playerEl.insertBefore(this.element, referenceNode);
3370
+ this.lyricPlayer.resizeObserver.observe(this.element);
3371
+ }
3372
+ this.mainLine.show();
3373
+ this.bgLine?.show();
3374
+ }
3375
+ hide() {
3376
+ if (this.element.parentElement) {
3377
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3378
+ this.element.remove();
3379
+ this.mainLine.teardownContent();
3380
+ this.bgLine?.teardownContent();
3381
+ }
3382
+ }
3383
+ update(delta) {
3384
+ if (this.isInSight) this.show();
3385
+ else this.hide();
3386
+ super.update(delta);
3387
+ }
3388
+ addBgLine(bgLine) {
3389
+ if (this.bgLine) this.bgLine.dispose();
3390
+ if (this.bgWrapper) this.bgWrapper.remove();
3391
+ this.bgLine = bgLine;
3392
+ const bgStartTime = bgLine.getLine().words[0]?.startTime ?? bgLine.getLine().startTime;
3393
+ const mainStartTime = this.mainLine.getLine().words[0]?.startTime ?? this.mainLine.getLine().startTime;
3394
+ this.isBgFirst = bgStartTime < mainStartTime;
3395
+ if (this.mainLine.getLine().isDuet) bgLine.getElement().classList.add(lyric_player_module_default.lyricDuetLine);
3396
+ this.bgWrapper = document.createElement("div");
3397
+ this.bgWrapper.className = lyric_player_module_default.bgWrapper;
3398
+ this.bgWrapper.appendChild(bgLine.getElement());
3399
+ if (!this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst) {
3400
+ this.bgWrapper.classList.add(lyric_player_module_default.bgWrapperTop);
3401
+ this.element.insertBefore(this.bgWrapper, this.mainLine.getElement());
3402
+ this.bgSlideY.setPosition(80);
3403
+ } else this.element.appendChild(this.bgWrapper);
3404
+ }
3405
+ renderStyles() {
3406
+ const y = this.posY.getCurrentPosition().toFixed(1);
3407
+ this.element.style.transform = `translateY(${y}px)`;
3408
+ this.element.style.opacity = this.opacity.toString();
3409
+ this.element.style.filter = `blur(${Math.min(5, this.blur)}px)`;
3410
+ if (!this.lyricPlayer.getEnableSpring()) this.element.style.transitionDelay = `${this.delay}ms`;
3411
+ if (this.bgWrapper) {
3412
+ if (this.lastIsActive !== this.isActive) {
3413
+ this.lastIsActive = this.isActive;
3414
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperActive, this.isActive);
3415
+ }
3416
+ const slideY = this.bgSlideY.getCurrentPosition();
3417
+ const slideYStr = slideY.toFixed(1);
3418
+ const activeProgress = clamp01(1 - Math.abs(slideY) / 80);
3419
+ const scaleStr = (.8 + activeProgress * .2).toFixed(3);
3420
+ this.bgWrapper.style.transform = `translateY(${slideYStr}%) scale(${scaleStr})`;
3421
+ const shouldBgFirst = !this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst;
3422
+ if (shouldBgFirst) {
3423
+ const currentMarginTop = -(this.bgWrapper.clientHeight || 0) * (1 - activeProgress);
3424
+ this.bgWrapper.style.marginTop = `${currentMarginTop.toFixed(1)}px`;
3425
+ } else this.bgWrapper.style.marginTop = "";
3426
+ const isHidden = slideYStr === (shouldBgFirst ? "80.0" : "-80.0") && !this.isActive;
3427
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperHidden, isHidden);
3428
+ }
3429
+ }
3430
+ dispose() {
3431
+ super.dispose();
3432
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3433
+ this.element.remove();
3434
+ }
3435
+ };
3436
+ //#endregion
3237
3437
  //#region src/utils/is-cjk.ts
3238
3438
  const isCJK = (char) => {
3239
3439
  return /^[\p{Unified_Ideograph}\u0800-\u9FFC]+$/u.test(char);
@@ -3250,10 +3450,7 @@ var LyricLineBase = class extends EventTarget {
3250
3450
  blur = 0;
3251
3451
  opacity = 1;
3252
3452
  delay = 0;
3253
- lineTransforms = {
3254
- posY: new Spring(0),
3255
- scale: new Spring(100)
3256
- };
3453
+ lineTransforms = { scale: new Spring(100) };
3257
3454
  /**
3258
3455
  * 用于 CJK 词语边界检测的分词器
3259
3456
  */
@@ -3263,9 +3460,7 @@ var LyricLineBase = class extends EventTarget {
3263
3460
  * 用于正确处理 emoji、复合字符等
3264
3461
  */
3265
3462
  static graphemeSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
3266
- onLineSizeChange(_size) {}
3267
- setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3268
- this.top = top;
3463
+ setTransform(scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3269
3464
  this.scale = scale;
3270
3465
  this.opacity = opacity;
3271
3466
  this.blur = blur;
@@ -3743,12 +3938,9 @@ var LyricLineEl = class extends LyricLineBase {
3743
3938
  super();
3744
3939
  this.lyricPlayer = lyricPlayer;
3745
3940
  this.lyricLine = lyricLine;
3746
- this._prevParentEl = lyricPlayer.getElement();
3747
- lyricPlayer.resizeObserver.observe(this.element);
3748
3941
  this.element.setAttribute("class", lyric_player_module_default.lyricLine);
3749
3942
  if (this.lyricLine.isBG) this.element.classList.add(lyric_player_module_default.lyricBgLine);
3750
3943
  if (this.lyricLine.isDuet) this.element.classList.add(lyric_player_module_default.lyricDuetLine);
3751
- this.lineTransforms.posY.setPosition(window.innerHeight * 2);
3752
3944
  this.element.appendChild(document.createElement("div"));
3753
3945
  this.element.appendChild(document.createElement("div"));
3754
3946
  this.element.appendChild(document.createElement("div"));
@@ -3880,34 +4072,18 @@ var LyricLineEl = class extends LyricLineBase {
3880
4072
  getLine() {
3881
4073
  return this.lyricLine;
3882
4074
  }
3883
- _prevParentEl;
3884
4075
  lastStyle = "";
3885
4076
  show() {
3886
- if (!this.element.parentElement) {
3887
- this._prevParentEl.appendChild(this.element);
3888
- this.lyricPlayer.resizeObserver.observe(this.element);
3889
- }
3890
4077
  if (!this.built) {
3891
4078
  this.rebuildElement();
3892
4079
  this.built = true;
3893
4080
  this.updateMaskImageSync();
3894
4081
  }
3895
- this.rebuildStyle();
3896
- }
3897
- hide() {
3898
- if (this.element.parentElement) {
3899
- this._prevParentEl.removeChild(this.element);
3900
- this.lyricPlayer.resizeObserver.unobserve(this.element);
3901
- }
3902
- if (this.built) {
3903
- this.disposeElements();
3904
- this.built = false;
3905
- }
3906
4082
  }
3907
4083
  rebuildStyle() {
3908
4084
  let style = "";
3909
- style += `transform:translateY(${this.lineTransforms.posY.getCurrentPosition().toFixed(1)}px) scale(${(this.lineTransforms.scale.getCurrentPosition() / 100).toFixed(4)});`;
3910
- if (!this.lyricPlayer.getEnableSpring() && this.isInSight) style += `transition-delay:${this.delay}ms;`;
4085
+ style += `transform: scale(${(this.lineTransforms.scale.getCurrentPosition() / 100).toFixed(4)});`;
4086
+ if (!this.lyricPlayer.getEnableSpring()) style += `transition-delay:${this.delay}ms;`;
3911
4087
  style += `filter:blur(${Math.min(5, this.blur)}px);`;
3912
4088
  if (style !== this.lastStyle) {
3913
4089
  this.lastStyle = style;
@@ -4350,25 +4526,19 @@ var LyricLineEl = class extends LyricLineBase {
4350
4526
  this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
4351
4527
  this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
4352
4528
  }
4353
- setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4354
- super.setTransform(top, scale, opacity, blur, force, delay);
4529
+ setTransform(scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4530
+ super.setTransform(scale, opacity, blur, force, delay);
4355
4531
  this.renderMode = mode;
4356
- const beforeInSight = this.isInSight;
4357
4532
  const enableSpring = this.lyricPlayer.getEnableSpring();
4358
- this.top = top;
4533
+ this.top = 0;
4359
4534
  this.scale = scale;
4360
4535
  this.delay = delay * 1e3 | 0;
4361
4536
  const main = this.element.children[0];
4362
4537
  main.style.opacity = `${opacity}`;
4363
4538
  if (force || !enableSpring) {
4364
4539
  this.blur = Math.min(32, blur);
4365
- this.lineTransforms.posY.setPosition(top);
4366
4540
  this.lineTransforms.scale.setPosition(scale);
4367
- if (!enableSpring) {
4368
- const afterInSight = this.isInSight;
4369
- if (beforeInSight || afterInSight) this.show();
4370
- else this.hide();
4371
- } else this.rebuildStyle();
4541
+ this.rebuildStyle();
4372
4542
  const currentScale = this.lineTransforms.scale.getCurrentPosition();
4373
4543
  this.updateMaskAlphaTargets(currentScale / 100);
4374
4544
  this.currentBrightAlpha = this.targetBrightAlpha;
@@ -4376,21 +4546,18 @@ var LyricLineEl = class extends LyricLineBase {
4376
4546
  this.element.style.setProperty("--bright-mask-alpha", String(this.currentBrightAlpha));
4377
4547
  this.element.style.setProperty("--dark-mask-alpha", String(this.currentDarkAlpha));
4378
4548
  } else {
4379
- this.lineTransforms.posY.setTargetPosition(top, delay);
4380
4549
  this.lineTransforms.scale.setTargetPosition(scale);
4381
4550
  if (this.blur !== Math.min(5, blur)) {
4382
4551
  this.blur = Math.min(5, blur);
4383
- const roundedBlur = blur.toFixed(3);
4384
- this.element.style.filter = `blur(${roundedBlur}px)`;
4552
+ this.element.style.filter = `blur(${blur.toFixed(3)}px)`;
4385
4553
  }
4386
4554
  }
4387
4555
  }
4388
4556
  update(delta = 0) {
4389
4557
  if (!this.lyricPlayer.getEnableSpring()) return;
4390
- this.lineTransforms.posY.update(delta);
4391
4558
  this.lineTransforms.scale.update(delta);
4392
- if (this.isInSight) this.show();
4393
- else this.hide();
4559
+ this.rebuildStyle();
4560
+ if (!this.built) return;
4394
4561
  const currentScale = this.lineTransforms.scale.getCurrentPosition() / 100;
4395
4562
  this.updateMaskAlphaTargets(currentScale);
4396
4563
  this.applyAlphaToDom(delta);
@@ -4398,13 +4565,11 @@ var LyricLineEl = class extends LyricLineBase {
4398
4565
  _getDebugTargetPos() {
4399
4566
  return `[位移: ${this.top}; 缩放: ${this.scale}; 延时: ${this.delay}]`;
4400
4567
  }
4401
- get isInSight() {
4402
- const t = this.lineTransforms.posY.getCurrentPosition();
4403
- const h = this.lyricPlayer.lyricLinesSize.get(this)?.[1] ?? 0;
4404
- const b = t + h;
4405
- const pb = this.lyricPlayer.size[1];
4406
- const ov = this.lyricPlayer.getOverscanPx();
4407
- return !(t > pb + h + ov || b < -h - ov);
4568
+ teardownContent() {
4569
+ if (this.built) {
4570
+ this.disposeElements();
4571
+ this.built = false;
4572
+ }
4408
4573
  }
4409
4574
  disposeElements() {
4410
4575
  this.balancer?.reset();
@@ -4452,7 +4617,7 @@ var LyricLineMouseEvent = class extends MouseEvent {
4452
4617
  * 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
4453
4618
  */
4454
4619
  var DomLyricPlayer = class extends LyricPlayerBase {
4455
- currentLyricLineObjects = [];
4620
+ currentLyricGroups = [];
4456
4621
  onResize() {
4457
4622
  const computedStyles = getComputedStyle(this.element);
4458
4623
  this._baseFontSize = Number.parseFloat(computedStyles.fontSize);
@@ -4489,7 +4654,10 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4489
4654
  rebuildStyle() {}
4490
4655
  setWordFadeWidth(value = .5) {
4491
4656
  super.setWordFadeWidth(value);
4492
- for (const el of this.currentLyricLineObjects) el.updateMaskImageSync();
4657
+ for (const group of this.currentLyricGroups) {
4658
+ group.mainLine.updateMaskImageSync();
4659
+ group.bgLine?.updateMaskImageSync();
4660
+ }
4493
4661
  }
4494
4662
  /**
4495
4663
  * 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
@@ -4501,36 +4669,52 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4501
4669
  if (this.hasDuetLine) this.element.classList.add(lyric_player_module_default.hasDuetLine);
4502
4670
  else this.element.classList.remove(lyric_player_module_default.hasDuetLine);
4503
4671
  if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${initialTime}`);
4504
- for (const line of this.currentLyricLineObjects) {
4505
- line.removeMouseEventListener("click", this.onLineClickedHandler);
4506
- line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
4507
- line.dispose();
4672
+ for (const group of this.currentLyricGroups) {
4673
+ const linesToDispose = [group.mainLine, group.bgLine].filter((line) => !!line);
4674
+ for (const line of linesToDispose) {
4675
+ line.removeMouseEventListener("click", this.onLineClickedHandler);
4676
+ line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
4677
+ }
4678
+ group.dispose();
4508
4679
  }
4509
- this.currentLyricLineObjects = this.processedLines.map((line, i) => {
4680
+ this.currentLyricGroups = [];
4681
+ let currentGroup = null;
4682
+ for (let i = 0; i < this.processedLines.length; i++) {
4683
+ const line = this.processedLines[i];
4510
4684
  const lineEl = new LyricLineEl(this, line);
4511
4685
  lineEl.addMouseEventListener("click", this.onLineClickedHandler);
4512
4686
  lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
4513
4687
  this.lyricLinesIndexes.set(lineEl, i);
4514
- this.lyricLineElementMap.set(lineEl.getElement(), lineEl);
4515
- return lineEl;
4516
- });
4688
+ if (!line.isBG || !currentGroup) {
4689
+ currentGroup = new LyricLineGroup(this, lineEl);
4690
+ this.currentLyricGroups.push(currentGroup);
4691
+ this.lyricGroupElementMap.set(currentGroup.element, currentGroup);
4692
+ } else currentGroup.addBgLine(lineEl);
4693
+ }
4517
4694
  this.setLinePosXSpringParams({});
4518
4695
  this.setLinePosYSpringParams({});
4519
4696
  this.setLineScaleSpringParams({});
4697
+ this.setCurrentTime(initialTime, true);
4520
4698
  this.calcLayout(true);
4521
4699
  this.update(0);
4522
4700
  }
4523
4701
  pause() {
4524
4702
  super.pause();
4525
- this.element.classList.remove("playing");
4703
+ this.element.classList.remove(lyric_player_module_default.playing);
4526
4704
  this.interludeDots.pause();
4527
- for (const line of this.currentLyricLineObjects) line.pause();
4705
+ for (const group of this.currentLyricGroups) {
4706
+ group.mainLine.pause();
4707
+ group.bgLine?.pause();
4708
+ }
4528
4709
  }
4529
4710
  resume() {
4530
4711
  super.resume();
4531
- this.element.classList.add("playing");
4712
+ this.element.classList.add(lyric_player_module_default.playing);
4532
4713
  this.interludeDots.resume();
4533
- for (const line of this.currentLyricLineObjects) line.resume();
4714
+ for (const group of this.currentLyricGroups) {
4715
+ group.mainLine.resume();
4716
+ group.bgLine?.resume();
4717
+ }
4534
4718
  }
4535
4719
  update(delta = 0) {
4536
4720
  if (!this.timelineState.initialLayoutFinished) return;
@@ -4538,12 +4722,12 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4538
4722
  if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${this.timelineState.currentTime}`);
4539
4723
  if (!this.isPageVisible) return;
4540
4724
  const deltaS = delta / 1e3;
4541
- for (const line of this.currentLyricLineObjects) line.update(deltaS);
4725
+ for (const group of this.currentLyricGroups) group.update(deltaS);
4542
4726
  }
4543
4727
  dispose() {
4544
4728
  super.dispose();
4545
4729
  this.element.remove();
4546
- for (const el of this.currentLyricLineObjects) el.dispose();
4730
+ for (const group of this.currentLyricGroups) group.dispose();
4547
4731
  this.bottomLine.dispose();
4548
4732
  this.interludeDots.dispose();
4549
4733
  }