@applemusic-like-lyrics/core 0.5.0 → 0.5.2

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.
@@ -39,6 +39,7 @@ function clamp1(x) {
39
39
  return Math.max(1, x);
40
40
  }
41
41
  var BaseRenderer = class extends AbstractBaseRenderer {
42
+ canvas;
42
43
  observer;
43
44
  flowSpeed = 1;
44
45
  currerntRenderScale = .75;
@@ -563,10 +564,10 @@ function generateControlPoints(width, height, variationFraction = randomRange(.4
563
564
  }
564
565
  //#endregion
565
566
  //#region \0raw:/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/core/src/bg-render/mesh-renderer/mesh.frag.glsl
566
- var mesh_frag_default = "precision highp float;\r\n\r\nvarying vec3 v_color;\r\nvarying vec2 v_uv;\r\nuniform sampler2D u_texture;\r\nuniform float u_time;\r\nuniform float u_volume;\r\nuniform float u_alpha;\r\n\r\n// 预计算常量\r\nconst float INV_255 = 1.0 / 255.0;\r\nconst float HALF_INV_255 = 0.5 / 255.0;\r\nconst float GRADIENT_NOISE_A = 52.9829189;\r\nconst vec2 GRADIENT_NOISE_B = vec2(0.06711056, 0.00583715);\r\n\r\n/* Gradient noise from Jorge Jimenez's presentation: */\r\n/* http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare */\r\nfloat gradientNoise(in vec2 uv) {\r\n return fract(GRADIENT_NOISE_A * fract(dot(uv, GRADIENT_NOISE_B)));\r\n}\r\n\r\n// 优化的旋转函数,避免重复计算sin/cos\r\nvec2 rot(vec2 v, float angle) {\r\n float s = sin(angle);\r\n float c = cos(angle);\r\n return vec2(c * v.x - s * v.y, s * v.x + c * v.y);\r\n}\r\n\r\nvoid main() {\r\n // 合并计算以减少指令数\r\n float volumeEffect = u_volume * 2.0;\r\n float timeVolume = u_time + u_volume;\r\n \r\n float dither = INV_255 * gradientNoise(gl_FragCoord.xy) - HALF_INV_255;\r\n vec2 centeredUV = v_uv - vec2(0.2);\r\n vec2 rotatedUV = rot(centeredUV, timeVolume * 2.0);\r\n vec2 finalUV = rotatedUV * max(0.001, 1.0 - volumeEffect) + vec2(0.5);\r\n \r\n vec4 result = texture2D(u_texture, finalUV);\r\n \r\n float alphaVolumeFactor = u_alpha * max(0.5, 1.0 - u_volume * 0.5);\r\n result.rgb *= v_color * alphaVolumeFactor;\r\n result.a *= alphaVolumeFactor;\r\n \r\n result.rgb += vec3(dither);\r\n \r\n float dist = distance(v_uv, vec2(0.5));\r\n float vignette = smoothstep(0.8, 0.3, dist);\r\n float mask = 0.6 + vignette * 0.4;\r\n result.rgb *= mask;\r\n \r\n gl_FragColor = result;\r\n}\r\n";
567
+ var mesh_frag_default = "precision mediump float;\n\nvarying vec3 v_color;\nvarying vec2 v_uv;\nuniform sampler2D u_texture;\nuniform float u_volume;\nuniform float u_alpha;\nuniform float u_sinAngle;\nuniform float u_cosAngle;\n\n// 预计算常量\nconst float INV_255 = 1.0 / 255.0;\nconst float HALF_INV_255 = 0.5 / 255.0;\nconst float GRADIENT_NOISE_A = 52.9829189;\nconst vec2 GRADIENT_NOISE_B = vec2(0.06711056, 0.00583715);\n\nfloat gradientNoise(in vec2 uv) {\n return fract(GRADIENT_NOISE_A * fract(dot(uv, GRADIENT_NOISE_B)));\n}\n\nvoid main() {\n float volumeEffect = u_volume * 2.0;\n\n float dither = INV_255 * gradientNoise(gl_FragCoord.xy) - HALF_INV_255;\n\n vec2 centeredUV = v_uv - vec2(0.2);\n\n vec2 rotatedUV = vec2(\n u_cosAngle * centeredUV.x - u_sinAngle * centeredUV.y,\n u_sinAngle * centeredUV.x + u_cosAngle * centeredUV.y\n );\n\n vec2 finalUV = rotatedUV * max(0.001, 1.0 - volumeEffect) + vec2(0.5);\n \n vec4 result = texture2D(u_texture, finalUV);\n \n float alphaVolumeFactor = u_alpha * max(0.5, 1.0 - u_volume * 0.5);\n result.rgb *= v_color * alphaVolumeFactor;\n result.a *= alphaVolumeFactor;\n \n result.rgb += vec3(dither);\n \n float dist = distance(v_uv, vec2(0.5));\n float vignette = smoothstep(0.8, 0.3, dist);\n float mask = 0.6 + vignette * 0.4;\n result.rgb *= mask;\n \n gl_FragColor = result;\n}\n";
567
568
  //#endregion
568
569
  //#region \0raw:/home/runner/work/applemusic-like-lyrics/applemusic-like-lyrics/packages/core/src/bg-render/mesh-renderer/mesh.vert.glsl
569
- var mesh_vert_default = "precision highp float;\n\nattribute vec2 a_pos;\nattribute vec3 a_color;\nattribute vec2 a_uv;\nvarying vec3 v_color;\nvarying vec2 v_uv;\n\nuniform float u_aspect;\n\nvoid main() {\n v_color = a_color;\n v_uv = a_uv;\n vec2 pos = a_pos;\n if (u_aspect > 1.0) {\n pos.y *= u_aspect;\n } else {\n pos.x /= u_aspect;\n }\n gl_Position = vec4(pos, 0.0, 1.0);\n}\n";
570
+ var mesh_vert_default = "precision mediump float;\n\nattribute vec2 a_pos;\nattribute vec3 a_color;\nattribute vec2 a_uv;\nvarying vec3 v_color;\nvarying vec2 v_uv;\n\nuniform float u_aspect;\n\nvoid main() {\n v_color = a_color;\n v_uv = a_uv;\n vec2 pos = a_pos;\n if (u_aspect > 1.0) {\n pos.y *= u_aspect;\n } else {\n pos.x /= u_aspect;\n }\n gl_Position = vec4(pos, 0.0, 1.0);\n}\n";
570
571
  //#endregion
571
572
  //#region src/bg-render/mesh-renderer/index.ts
572
573
  /**
@@ -597,6 +598,7 @@ function easeInOutSine(x) {
597
598
  return -(Math.cos(Math.PI * x) - 1) / 2;
598
599
  }
599
600
  var GLProgram = class {
601
+ label;
600
602
  gl;
601
603
  program;
602
604
  vertexShader;
@@ -678,6 +680,10 @@ var GLProgram = class {
678
680
  }
679
681
  };
680
682
  var Mesh = class {
683
+ gl;
684
+ attrPos;
685
+ attrColor;
686
+ attrUV;
681
687
  vertexWidth = 0;
682
688
  vertexHeight = 0;
683
689
  vertexBuffer;
@@ -1100,6 +1106,7 @@ var BHPMesh = class extends Mesh {
1100
1106
  }
1101
1107
  };
1102
1108
  var GLTexture = class {
1109
+ gl;
1103
1110
  tex;
1104
1111
  constructor(gl, albumImageData) {
1105
1112
  this.gl = gl;
@@ -1265,11 +1272,14 @@ var MeshGradientRenderer = class extends BaseRenderer {
1265
1272
  gl.clear(gl.COLOR_BUFFER_BIT);
1266
1273
  this.mainProgram.use();
1267
1274
  gl.activeTexture(gl.TEXTURE0);
1268
- this.mainProgram.setUniform1f("u_time", tickTime / 1e4);
1275
+ const uTime = tickTime / 1e4;
1269
1276
  this.mainProgram.setUniform1f("u_aspect", this.manualControl ? 1 : this.canvas.width / this.canvas.height);
1270
1277
  this.mainProgram.setUniform1i("u_texture", 0);
1271
1278
  this.mainProgram.setUniform1f("u_volume", this.volume);
1272
1279
  this.mainProgram.setUniform1f("u_alpha", 1);
1280
+ const angle = (uTime + this.volume) * 2;
1281
+ this.mainProgram.setUniform1f("u_sinAngle", Math.sin(angle));
1282
+ this.mainProgram.setUniform1f("u_cosAngle", Math.cos(angle));
1273
1283
  state.texture.bind();
1274
1284
  state.mesh.bind();
1275
1285
  state.mesh.draw();
@@ -1507,6 +1517,7 @@ var TimedContainer = class extends _pixi_display.Container {
1507
1517
  time = 0;
1508
1518
  };
1509
1519
  var PixiRenderer = class extends BaseRenderer {
1520
+ canvas;
1510
1521
  app;
1511
1522
  curContainer;
1512
1523
  staticMode = false;
@@ -1731,8 +1742,11 @@ var BackgroundRender = class BackgroundRender {
1731
1742
  //#region src/styles/lyric-player.module.css
1732
1743
  var lyric_player_module_default = {
1733
1744
  "active": "FmKaba_active",
1745
+ "bgWrapper": "FmKaba_bgWrapper",
1746
+ "bgWrapperActive": "FmKaba_bgWrapperActive",
1747
+ "bgWrapperHidden": "FmKaba_bgWrapperHidden",
1748
+ "bgWrapperTop": "FmKaba_bgWrapperTop",
1734
1749
  "bottomLine": "FmKaba_bottomLine",
1735
- "dirty": "FmKaba_dirty",
1736
1750
  "disableSpring": "FmKaba_disableSpring",
1737
1751
  "duet": "FmKaba_duet",
1738
1752
  "emphasize": "FmKaba_emphasize",
@@ -1743,8 +1757,10 @@ var lyric_player_module_default = {
1743
1757
  "lyricBgLine": "FmKaba_lyricBgLine",
1744
1758
  "lyricDuetLine": "FmKaba_lyricDuetLine",
1745
1759
  "lyricLine": "FmKaba_lyricLine",
1760
+ "lyricLineWrapper": "FmKaba_lyricLineWrapper",
1746
1761
  "lyricMainLine": "FmKaba_lyricMainLine",
1747
1762
  "lyricSubLine": "FmKaba_lyricSubLine",
1763
+ "playing": "FmKaba_playing",
1748
1764
  "romanWord": "FmKaba_romanWord",
1749
1765
  "rubyWord": "FmKaba_rubyWord",
1750
1766
  "tmpDisableTransition": "FmKaba_tmpDisableTransition",
@@ -1845,27 +1861,31 @@ function cleanUnintentionalOverlaps(lines) {
1845
1861
  * 尝试让歌词提前最多 600ms 开始,如果有重叠则尝试最多提前 400ms 或上一行时长的 30%
1846
1862
  */
1847
1863
  function tryAdvanceStartTime(lines) {
1848
- for (let i = lines.length - 1; i >= 0; i--) {
1864
+ const defaultAdvanceAmount = 600;
1865
+ const fallbackAdvanceAmount = 400;
1866
+ const fallbackAdvanceRatio = .3;
1867
+ let prevLineStartTime = 0;
1868
+ let prevLineEndTime = 0;
1869
+ let prevMainGroupStartTime = 0;
1870
+ let prevMainGroupEndTime = 0;
1871
+ let hasPrevLine = false;
1872
+ for (let i = 0; i < lines.length; i++) {
1849
1873
  const line = lines[i];
1850
1874
  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
- }
1875
+ const originalStartTime = line.startTime;
1876
+ const originalEndTime = line.endTime;
1857
1877
  let targetAdvanceAmount = 0;
1858
1878
  let safeBoundary = 0;
1859
- if (prevLine) if (line.startTime >= prevLine.endTime) {
1860
- targetAdvanceAmount = 600;
1861
- safeBoundary = prevLine.endTime;
1879
+ if (hasPrevLine) if (originalStartTime >= prevLineEndTime) {
1880
+ targetAdvanceAmount = defaultAdvanceAmount;
1881
+ safeBoundary = prevMainGroupEndTime;
1862
1882
  } else {
1863
- targetAdvanceAmount = 400;
1864
- const prevDuration = prevLine.endTime - prevLine.startTime;
1865
- safeBoundary = prevLine.startTime + prevDuration * .3;
1883
+ targetAdvanceAmount = fallbackAdvanceAmount;
1884
+ const prevDuration = prevLineEndTime - prevLineStartTime;
1885
+ safeBoundary = prevLineStartTime + prevDuration * fallbackAdvanceRatio;
1866
1886
  }
1867
1887
  else {
1868
- targetAdvanceAmount = 600;
1888
+ targetAdvanceAmount = defaultAdvanceAmount;
1869
1889
  safeBoundary = 0;
1870
1890
  }
1871
1891
  const targetTime = line.startTime - targetAdvanceAmount;
@@ -1873,6 +1893,20 @@ function tryAdvanceStartTime(lines) {
1873
1893
  if (newStartTime < line.startTime) line.startTime = newStartTime;
1874
1894
  const nextLine = lines[i + 1];
1875
1895
  if (nextLine?.isBG) nextLine.startTime = line.startTime;
1896
+ if (hasPrevLine) if (originalStartTime < prevMainGroupEndTime && originalEndTime > prevMainGroupStartTime) {
1897
+ prevMainGroupStartTime = Math.min(prevMainGroupStartTime, originalStartTime);
1898
+ prevMainGroupEndTime = Math.max(prevMainGroupEndTime, originalEndTime);
1899
+ } else {
1900
+ prevMainGroupStartTime = originalStartTime;
1901
+ prevMainGroupEndTime = originalEndTime;
1902
+ }
1903
+ else {
1904
+ prevMainGroupStartTime = originalStartTime;
1905
+ prevMainGroupEndTime = originalEndTime;
1906
+ }
1907
+ prevLineStartTime = originalStartTime;
1908
+ prevLineEndTime = originalEndTime;
1909
+ hasPrevLine = true;
1876
1910
  }
1877
1911
  }
1878
1912
  /**
@@ -2150,6 +2184,7 @@ function solveSpring(from, velocity, to, delay = 0, params) {
2150
2184
  //#endregion
2151
2185
  //#region src/lyric-player/base/bottom-line.ts
2152
2186
  var BottomLineEl = class {
2187
+ lyricPlayer;
2153
2188
  element = document.createElement("div");
2154
2189
  left = 0;
2155
2190
  top = 0;
@@ -2272,19 +2307,19 @@ const LayoutAlignAnchor = {
2272
2307
  function computeCurrentInterlude(input) {
2273
2308
  const currentTime = input.currentTime + 20;
2274
2309
  const currentIndex = input.scrollToIndex;
2275
- const lines = input.processedLines;
2310
+ const groups = input.currentGroups;
2276
2311
  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;
2312
+ if (k < -1 || k >= groups.length - 1) return void 0;
2313
+ const prevGroup = k === -1 ? null : groups[k];
2314
+ const nextGroup = groups[k + 1];
2315
+ const gapStart = prevGroup ? prevGroup.endTime : 0;
2316
+ const gapEnd = Math.max(gapStart, nextGroup.startTime - 250);
2317
+ if (gapEnd - gapStart < 4e3) return void 0;
2283
2318
  if (gapEnd > currentTime && gapStart < currentTime) return {
2284
2319
  startTime: Math.max(gapStart, currentTime),
2285
2320
  endTime: gapEnd,
2286
2321
  anchorLineIndex: k,
2287
- isNextDuet: nextLine.isDuet
2322
+ isNextDuet: nextGroup.mainLine.getLine().isDuet
2288
2323
  };
2289
2324
  };
2290
2325
  return checkGap(currentIndex - 1) || checkGap(currentIndex) || checkGap(currentIndex + 1);
@@ -2297,8 +2332,8 @@ function computeCurrentInterlude(input) {
2297
2332
  * - 普通播放时根据相邻歌词的时间间隔动态调整 stiffness / damping
2298
2333
  */
2299
2334
  function computeLinePosYSpringParams(input) {
2300
- const { enabled, processedLines, scrollToIndex, isSeeking, isInterludeActive } = input;
2301
- if (!enabled || processedLines.length === 0) return { shouldUpdate: false };
2335
+ const { enabled, currentGroups, scrollToIndex, isSeeking, isInterludeActive } = input;
2336
+ if (!enabled || currentGroups.length === 0) return { shouldUpdate: false };
2302
2337
  if (isSeeking || isInterludeActive) return {
2303
2338
  shouldUpdate: true,
2304
2339
  params: {
@@ -2306,10 +2341,10 @@ function computeLinePosYSpringParams(input) {
2306
2341
  damping: 15
2307
2342
  }
2308
2343
  };
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);
2344
+ const currentGroup = currentGroups[scrollToIndex];
2345
+ const prevGroup = currentGroups[scrollToIndex - 1];
2346
+ if (!currentGroup || !prevGroup) return { shouldUpdate: false };
2347
+ const interval = currentGroup.startTime - prevGroup.startTime;
2313
2348
  const MIN_INTERVAL = 100;
2314
2349
  const MAX_INTERVAL = 800;
2315
2350
  const clampedInterval = clamp(interval, MIN_INTERVAL, MAX_INTERVAL);
@@ -2327,38 +2362,33 @@ function computeLinePosYSpringParams(input) {
2327
2362
  };
2328
2363
  }
2329
2364
  /**
2330
- * 计算单行歌词在当前布局中的视觉呈现参数。
2365
+ * 计算一组歌词在当前布局中的视觉呈现参数。
2331
2366
  *
2332
2367
  * 根据播放状态、缓冲状态、布局模式与间奏信息,
2333
- * 生成一行歌词最终应使用的 opacity、scale、blur 和 render mode。
2368
+ * 生成一组歌词最终应使用的活跃状态、不透明度与模糊值。
2334
2369
  */
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;
2370
+ function computeGroupPresentation(input) {
2371
+ const { groupIndex, scrollToIndex, latestIndex, hasBuffered, hidePassedLines, isPlaying, isNonDynamic, enableBlur, isUserScrolling, isCompact, interlude } = input;
2372
+ const isActive = hasBuffered || groupIndex >= scrollToIndex && groupIndex < latestIndex;
2338
2373
  const blurLevel = computeLineBlur({
2339
2374
  enableBlur,
2340
2375
  isUserScrolling,
2341
2376
  isActive,
2342
- itemIndex: lineIndex,
2377
+ itemIndex: groupIndex,
2343
2378
  scrollToIndex,
2344
2379
  latestIndex,
2345
2380
  isCompact
2346
2381
  });
2347
2382
  let targetOpacity;
2348
- if (hidePassedLines) if (lineIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2383
+ if (hidePassedLines) if (groupIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2349
2384
  else if (hasBuffered) targetOpacity = .85;
2350
2385
  else targetOpacity = isNonDynamic ? .2 : 1;
2351
2386
  else if (hasBuffered) targetOpacity = .85;
2352
2387
  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
2388
  return {
2357
2389
  isActive,
2358
2390
  targetOpacity,
2359
- targetScale,
2360
- blurLevel,
2361
- renderMode: isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID
2391
+ blurLevel
2362
2392
  };
2363
2393
  }
2364
2394
  /**
@@ -2514,50 +2544,29 @@ const eqSet = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x))
2514
2544
  * - 根据新的热行状态和已有的缓冲行状态,计算出应移除的缓冲行 ID
2515
2545
  */
2516
2546
  function computePlayerTimeState(input) {
2517
- const { time, processedLines, timelineState: { hotLines, bufferedLines } } = input;
2518
- const nextHotLines = new Set(hotLines);
2547
+ const { time, currentGroups, timelineState: { hotGroups, bufferedGroups } } = input;
2548
+ const nextHotGroups = new Set(hotGroups);
2519
2549
  const addedIds = /* @__PURE__ */ new Set();
2520
2550
  const removedHotIds = /* @__PURE__ */ new Set();
2521
2551
  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);
2552
+ for (const lastHotId of hotGroups) {
2553
+ const group = currentGroups[lastHotId];
2554
+ if (!group || time < group.startTime || group.endTime <= time) {
2555
+ nextHotGroups.delete(lastHotId);
2543
2556
  removedHotIds.add(lastHotId);
2544
2557
  }
2545
2558
  }
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);
2559
+ for (let id = 0; id < currentGroups.length; id++) {
2560
+ const group = currentGroups[id];
2561
+ if (!group) continue;
2562
+ if (group.startTime <= time && group.endTime > time && !nextHotGroups.has(id)) {
2563
+ nextHotGroups.add(id);
2551
2564
  addedIds.add(id);
2552
- if (processedLines[id + 1]?.isBG) {
2553
- nextHotLines.add(id + 1);
2554
- addedIds.add(id + 1);
2555
- }
2556
2565
  }
2557
2566
  }
2558
- for (const id of bufferedLines) if (!nextHotLines.has(id)) removedBufferedIds.add(id);
2567
+ for (const id of bufferedGroups) if (!nextHotGroups.has(id)) removedBufferedIds.add(id);
2559
2568
  return {
2560
- nextHotLines,
2569
+ nextHotGroups,
2561
2570
  addedIds,
2562
2571
  removedHotIds,
2563
2572
  removedBufferedIds
@@ -2569,10 +2578,10 @@ function computePlayerTimeState(input) {
2569
2578
  * 若当前仍存在缓冲行,则优先对齐到最靠前的缓冲行;
2570
2579
  * 否则对齐到第一条开始时间不小于当前时间的歌词行。
2571
2580
  */
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;
2581
+ function pickScrollToIndexForSeek(time, currentGroups, bufferedGroups) {
2582
+ if (bufferedGroups.size > 0) return Math.min(...bufferedGroups);
2583
+ const foundIndex = currentGroups.findIndex((group) => group.startTime >= time);
2584
+ return foundIndex === -1 ? currentGroups.length : foundIndex;
2576
2585
  }
2577
2586
  /**
2578
2587
  * 提交时间线状态转移的纯函数。
@@ -2582,45 +2591,45 @@ function pickScrollToIndexForSeek(time, processedLines, bufferedLines) {
2582
2591
  * 是否需要重置用户滚动状态、是否需要触发布局。
2583
2592
  */
2584
2593
  function commitPlayerTimeState(input) {
2585
- const { timelineState, time, processedLines, hasBottomContent, stateResult } = input;
2594
+ const { timelineState, time, currentGroups, hasBottomContent, stateResult } = input;
2586
2595
  const { addedIds, removedHotIds, removedBufferedIds } = stateResult;
2587
2596
  const { isSeeking } = timelineState;
2588
2597
  timelineState.currentTime = time;
2589
- timelineState.hotLines = stateResult.nextHotLines;
2598
+ timelineState.hotGroups = stateResult.nextHotGroups;
2590
2599
  let shouldLayout = false;
2591
2600
  let shouldResetScroll = false;
2592
- const linesToEnable = [];
2593
- const linesToDisable = /* @__PURE__ */ new Set();
2601
+ const groupsToEnable = [];
2602
+ const groupsToDisable = /* @__PURE__ */ new Set();
2594
2603
  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);
2604
+ timelineState.bufferedGroups = new Set([...timelineState.hotGroups]);
2605
+ timelineState.scrollToIndex = pickScrollToIndexForSeek(time, currentGroups, timelineState.bufferedGroups);
2606
+ for (const id of removedHotIds) groupsToDisable.add(id);
2607
+ for (const id of timelineState.hotGroups) groupsToEnable.push(id);
2608
+ for (const id of removedBufferedIds) groupsToDisable.add(id);
2600
2609
  shouldResetScroll = true;
2601
2610
  shouldLayout = true;
2602
2611
  } else if (addedIds.size > 0) {
2603
2612
  for (const id of addedIds) {
2604
- timelineState.bufferedLines.add(id);
2605
- linesToEnable.push(id);
2613
+ timelineState.bufferedGroups.add(id);
2614
+ groupsToEnable.push(id);
2606
2615
  }
2607
2616
  for (const id of removedBufferedIds) {
2608
- timelineState.bufferedLines.delete(id);
2609
- linesToDisable.add(id);
2617
+ timelineState.bufferedGroups.delete(id);
2618
+ groupsToDisable.add(id);
2610
2619
  }
2611
- if (timelineState.bufferedLines.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedLines);
2620
+ if (timelineState.bufferedGroups.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedGroups);
2612
2621
  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);
2622
+ } else if (removedBufferedIds.size > 0 && eqSet(removedBufferedIds, timelineState.bufferedGroups)) {
2623
+ for (const id of timelineState.bufferedGroups) {
2624
+ if (timelineState.hotGroups.has(id)) continue;
2625
+ timelineState.bufferedGroups.delete(id);
2626
+ groupsToDisable.add(id);
2618
2627
  }
2619
2628
  shouldLayout = true;
2620
2629
  }
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;
2630
+ if (timelineState.bufferedGroups.size === 0 && currentGroups.length > 0) {
2631
+ if (time >= currentGroups[currentGroups.length - 1].endTime) {
2632
+ const targetIndex = hasBottomContent ? currentGroups.length : currentGroups.length - 1;
2624
2633
  if (timelineState.scrollToIndex !== targetIndex) {
2625
2634
  timelineState.scrollToIndex = targetIndex;
2626
2635
  shouldLayout = true;
@@ -2631,8 +2640,8 @@ function commitPlayerTimeState(input) {
2631
2640
  return {
2632
2641
  shouldLayout,
2633
2642
  shouldResetScroll,
2634
- linesToEnable,
2635
- linesToDisable: [...linesToDisable]
2643
+ groupsToEnable,
2644
+ groupsToDisable: [...groupsToDisable]
2636
2645
  };
2637
2646
  }
2638
2647
  //#endregion
@@ -2647,17 +2656,15 @@ var LyricPlayerBase = class extends EventTarget {
2647
2656
  timelineState = {
2648
2657
  currentTime: 0,
2649
2658
  lastCurrentTime: 0,
2650
- hotLines: /* @__PURE__ */ new Set(),
2651
- bufferedLines: /* @__PURE__ */ new Set(),
2659
+ hotGroups: /* @__PURE__ */ new Set(),
2660
+ bufferedGroups: /* @__PURE__ */ new Set(),
2652
2661
  scrollToIndex: 0,
2653
2662
  isSeeking: false,
2654
2663
  isPlaying: true,
2655
2664
  initialLayoutFinished: false
2656
2665
  };
2657
2666
  /** @internal */
2658
- lyricLinesSize = /* @__PURE__ */ new WeakMap();
2659
- /** @internal */
2660
- lyricLineElementMap = /* @__PURE__ */ new WeakMap();
2667
+ lyricGroupElementMap = /* @__PURE__ */ new WeakMap();
2661
2668
  currentLyricLines = [];
2662
2669
  processedLines = [];
2663
2670
  lyricLinesIndexes = /* @__PURE__ */ new WeakMap();
@@ -2689,10 +2696,13 @@ var LyricPlayerBase = class extends EventTarget {
2689
2696
  isScrolled: false,
2690
2697
  isUserScrolling: false
2691
2698
  };
2692
- currentLyricLineObjects = [];
2699
+ currentLyricGroups = [];
2700
+ lyricGroupSize = /* @__PURE__ */ new WeakMap();
2693
2701
  size = [0, 0];
2694
2702
  isPageVisible = true;
2695
2703
  optimizeOptions = {};
2704
+ /** 是否强制让背景人声行始终后置(即始终在主歌词下方显示,不前置背景人声) */
2705
+ alwaysPostpositionBackground = false;
2696
2706
  posXSpringParams = {
2697
2707
  mass: 1,
2698
2708
  damping: 10,
@@ -2742,13 +2752,13 @@ var LyricPlayerBase = class extends EventTarget {
2742
2752
  shouldRelayout = true;
2743
2753
  }
2744
2754
  } else {
2745
- const lineObj = this.lyricLineElementMap.get(entry.target);
2746
- if (lineObj) {
2755
+ const groupObj = this.lyricGroupElementMap.get(entry.target);
2756
+ if (groupObj) {
2747
2757
  const newSize = [entry.target.clientWidth, entry.target.clientHeight];
2748
- const oldSize = this.lyricLinesSize.get(lineObj) ?? [0, 0];
2758
+ const oldSize = this.lyricGroupSize.get(groupObj) ?? [0, 0];
2749
2759
  if (newSize[0] !== oldSize[0] || newSize[1] !== oldSize[1]) {
2750
- this.lyricLinesSize.set(lineObj, newSize);
2751
- lineObj.onLineSizeChange(newSize);
2760
+ this.lyricGroupSize.set(groupObj, newSize);
2761
+ groupObj.onLineSizeChange(newSize);
2752
2762
  shouldRelayout = true;
2753
2763
  }
2754
2764
  }
@@ -2874,7 +2884,7 @@ var LyricPlayerBase = class extends EventTarget {
2874
2884
  }
2875
2885
  }
2876
2886
  rebuildLyricLines() {
2877
- for (const lineObj of this.currentLyricLineObjects) lineObj.rebuildElement();
2887
+ for (const group of this.currentLyricGroups) group.rebuildAllLines();
2878
2888
  }
2879
2889
  /**
2880
2890
  * 根据当前配置处理不雅用语单词
@@ -2976,11 +2986,11 @@ var LyricPlayerBase = class extends EventTarget {
2976
2986
  break;
2977
2987
  }
2978
2988
  this.hasDuetLine = this.processedLines.some((line) => line.isDuet);
2979
- for (const line of this.currentLyricLineObjects) line.dispose();
2989
+ for (const group of this.currentLyricGroups) group.dispose();
2990
+ this.currentLyricGroups = [];
2980
2991
  this.interludeDots.setInterlude(void 0);
2981
- this.timelineState.hotLines.clear();
2982
- this.timelineState.bufferedLines.clear();
2983
- this.setCurrentTime(0, true);
2992
+ this.timelineState.hotGroups.clear();
2993
+ this.timelineState.bufferedGroups.clear();
2984
2994
  if (process.env.NODE_ENV !== "production") console.log("歌词处理完成", this);
2985
2995
  }
2986
2996
  /**
@@ -3006,19 +3016,19 @@ var LyricPlayerBase = class extends EventTarget {
3006
3016
  if (!timelineState.initialLayoutFinished && !timelineState.isSeeking) return;
3007
3017
  const stateResult = computePlayerTimeState({
3008
3018
  time,
3009
- processedLines: this.processedLines,
3019
+ currentGroups: this.currentLyricGroups,
3010
3020
  timelineState
3011
3021
  });
3012
3022
  const hasBottomContent = this.bottomLine.getElement().innerHTML.trim().length > 0;
3013
3023
  const commitResult = commitPlayerTimeState({
3014
3024
  timelineState,
3015
3025
  time,
3016
- processedLines: this.processedLines,
3026
+ currentGroups: this.currentLyricGroups,
3017
3027
  hasBottomContent,
3018
3028
  stateResult
3019
3029
  });
3020
- for (const id of commitResult.linesToDisable) this.currentLyricLineObjects[id]?.disable();
3021
- for (const id of commitResult.linesToEnable) this.currentLyricLineObjects[id]?.enable();
3030
+ for (const id of commitResult.groupsToDisable) this.currentLyricGroups[id]?.disable();
3031
+ for (const id of commitResult.groupsToEnable) this.currentLyricGroups[id]?.enable();
3022
3032
  if (commitResult.shouldResetScroll) this.resetScroll();
3023
3033
  if (commitResult.shouldLayout) this.calcLayout();
3024
3034
  }
@@ -3043,14 +3053,14 @@ var LyricPlayerBase = class extends EventTarget {
3043
3053
  const interlude = computeCurrentInterlude({
3044
3054
  currentTime: this.timelineState.currentTime,
3045
3055
  scrollToIndex: this.timelineState.scrollToIndex,
3046
- processedLines: this.processedLines
3056
+ currentGroups: this.currentLyricGroups
3047
3057
  });
3048
3058
  const isInterludeActive = !!interlude;
3049
3059
  if (this.layoutState.targetAlignIndex !== this.timelineState.scrollToIndex || this.layoutState.lastInterludeState !== isInterludeActive) {
3050
3060
  this.layoutState.lastInterludeState = isInterludeActive;
3051
3061
  const springParams = computeLinePosYSpringParams({
3052
3062
  enabled: this.getEnableSpring(),
3053
- processedLines: this.processedLines,
3063
+ currentGroups: this.currentLyricGroups,
3054
3064
  scrollToIndex: this.timelineState.scrollToIndex,
3055
3065
  isSeeking: this.timelineState.isSeeking,
3056
3066
  isInterludeActive
@@ -3068,17 +3078,15 @@ var LyricPlayerBase = class extends EventTarget {
3068
3078
  if (interlude.anchorLineIndex !== -1) curPos -= totalInterludeHeight;
3069
3079
  }
3070
3080
  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);
3081
+ const scrollOffset = this.currentLyricGroups.slice(0, targetAlignIndex).reduce((acc, group) => acc + (this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK), 0);
3072
3082
  this.scrollState.scrollBoundary.minOffset = -scrollOffset;
3073
3083
  curPos -= scrollOffset;
3074
3084
  curPos += this.size[1] * this.layoutState.alignPosition;
3075
- const curLine = this.currentLyricLineObjects[targetAlignIndex];
3085
+ const curGroup = this.currentLyricGroups[targetAlignIndex];
3076
3086
  this.layoutState.targetAlignIndex = targetAlignIndex;
3077
- const isBottomFocused = targetAlignIndex === this.currentLyricLineObjects.length;
3087
+ const isBottomFocused = targetAlignIndex === this.currentLyricGroups.length;
3078
3088
  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];
3089
+ const targetLineHeight = curGroup ? this.lyricGroupSize.get(curGroup)?.[1] ?? LINE_HEIGHT_FALLBACK : isBottomFocused ? this.bottomLine.lineSize[1] : 0;
3082
3090
  if (targetLineHeight > 0) switch (this.layoutState.alignAnchor) {
3083
3091
  case LayoutAlignAnchor.Bottom:
3084
3092
  curPos -= targetLineHeight;
@@ -3088,13 +3096,12 @@ var LyricPlayerBase = class extends EventTarget {
3088
3096
  break;
3089
3097
  case LayoutAlignAnchor.Top: break;
3090
3098
  }
3091
- const latestIndex = Math.max(...this.timelineState.bufferedLines);
3099
+ const latestIndex = Math.max(...this.timelineState.bufferedGroups);
3092
3100
  let delay = 0;
3093
3101
  let baseDelay = sync ? 0 : .05;
3094
3102
  let setDots = false;
3095
- this.currentLyricLineObjects.forEach((lineObj, i) => {
3096
- const hasBuffered = this.timelineState.bufferedLines.has(i);
3097
- const line = lineObj.getLine();
3103
+ this.currentLyricGroups.forEach((group, i) => {
3104
+ const hasBuffered = this.timelineState.bufferedGroups.has(i);
3098
3105
  const shouldShowDots = interlude && i === interlude.anchorLineIndex + 1;
3099
3106
  if (!setDots && shouldShowDots) {
3100
3107
  setDots = true;
@@ -3106,31 +3113,28 @@ var LyricPlayerBase = class extends EventTarget {
3106
3113
  curPos += this.layoutState.interludeDotsSize[1];
3107
3114
  curPos += dotMargin;
3108
3115
  }
3109
- const presentation = computeLinePresentation({
3110
- line,
3111
- lineIndex: i,
3116
+ const presentation = computeGroupPresentation({
3117
+ groupIndex: i,
3112
3118
  scrollToIndex: this.timelineState.scrollToIndex,
3113
3119
  latestIndex,
3114
3120
  hasBuffered,
3115
3121
  hidePassedLines: this.hidePassedLines,
3116
3122
  isPlaying: this.timelineState.isPlaying,
3117
3123
  isNonDynamic: this.isNonDynamic,
3118
- enableScale: this.enableScale,
3119
3124
  enableBlur: this.enableBlur,
3120
3125
  isUserScrolling: this.scrollState.isUserScrolling,
3121
3126
  isCompact: window.innerWidth <= 1024,
3122
3127
  interlude
3123
3128
  });
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;
3129
+ group.setTransform(curPos, force, delay, presentation.isActive, presentation.targetOpacity, presentation.blurLevel);
3130
+ curPos += this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK;
3127
3131
  if (curPos >= 0 && !this.timelineState.isSeeking) {
3128
- if (!line.isBG) delay += baseDelay;
3132
+ delay += baseDelay;
3129
3133
  if (i >= this.timelineState.scrollToIndex) baseDelay /= 1.05;
3130
3134
  }
3131
3135
  });
3132
3136
  this.scrollState.scrollBoundary.maxOffset = curPos + this.scrollState.scrollOffset - this.size[1] / 2;
3133
- const bottomIndex = this.currentLyricLineObjects.length;
3137
+ const bottomIndex = this.currentLyricGroups.length;
3134
3138
  const finalBottomBlur = computeLineBlur({
3135
3139
  enableBlur: this.enableBlur,
3136
3140
  isUserScrolling: this.scrollState.isUserScrolling,
@@ -3160,7 +3164,10 @@ var LyricPlayerBase = class extends EventTarget {
3160
3164
  ...params
3161
3165
  };
3162
3166
  this.bottomLine.lineTransforms.posY.updateParams(this.posYSpringParams);
3163
- for (const line of this.currentLyricLineObjects) line.lineTransforms.posY.updateParams(this.posYSpringParams);
3167
+ for (const group of this.currentLyricGroups) {
3168
+ group.posY.updateParams(this.posYSpringParams);
3169
+ group.bgSlideY.updateParams(this.posYSpringParams);
3170
+ }
3164
3171
  }
3165
3172
  /**
3166
3173
  * 设置所有歌词行在​缩放大小上的弹簧属性,包括重量、弹力和阻力。
@@ -3176,8 +3183,10 @@ var LyricPlayerBase = class extends EventTarget {
3176
3183
  ...this.scaleForBGSpringParams,
3177
3184
  ...params
3178
3185
  };
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);
3186
+ for (const group of this.currentLyricGroups) {
3187
+ group.mainLine.lineTransforms.scale.updateParams(this.scaleSpringParams);
3188
+ group.bgLine?.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
3189
+ }
3181
3190
  }
3182
3191
  /**
3183
3192
  * 暂停部分效果演出,目前会暂停播放间奏点的动画,且将背景歌词显示出来
@@ -3249,6 +3258,23 @@ var LyricPlayerBase = class extends EventTarget {
3249
3258
  getCurrentTime() {
3250
3259
  return this.timelineState.currentTime;
3251
3260
  }
3261
+ /**
3262
+ * 设置是否让背景人声行始终后置显示
3263
+ *
3264
+ * 默认情况下,如果背景歌词开始时间早于主歌词,会在主歌词上方展示;
3265
+ * 如果设置为 `true`,则无论时间顺序如何,背景歌词都会始终在主歌词下方展示
3266
+ * @param enable 是否启用始终后置
3267
+ */
3268
+ setAlwaysPostpositionBackground(enable) {
3269
+ if (this.alwaysPostpositionBackground === enable) return;
3270
+ this.alwaysPostpositionBackground = enable;
3271
+ this.rebuildLyricLines();
3272
+ this.calcLayout();
3273
+ }
3274
+ /** 获取当前是否设置了让背景人声行始终后置显示 */
3275
+ getAlwaysPostpositionBackground() {
3276
+ return this.alwaysPostpositionBackground;
3277
+ }
3252
3278
  getElement() {
3253
3279
  return this.element;
3254
3280
  }
@@ -3259,6 +3285,194 @@ var LyricPlayerBase = class extends EventTarget {
3259
3285
  }
3260
3286
  };
3261
3287
  //#endregion
3288
+ //#region src/lyric-player/base/group.ts
3289
+ var LyricLineGroupBase = class {
3290
+ mainLine;
3291
+ bgLine;
3292
+ posY = new Spring(0);
3293
+ bgSlideY = new Spring(-80);
3294
+ top = 0;
3295
+ delay = 0;
3296
+ isActive = false;
3297
+ opacity = 1;
3298
+ blur = 0;
3299
+ isBgFirst = false;
3300
+ constructor(mainLine, bgLine) {
3301
+ this.mainLine = mainLine;
3302
+ this.bgLine = bgLine;
3303
+ }
3304
+ get startTime() {
3305
+ return this.mainLine.getLine().startTime;
3306
+ }
3307
+ get endTime() {
3308
+ return this.mainLine.getLine().endTime;
3309
+ }
3310
+ onLineSizeChange(size) {
3311
+ this.mainLine.onLineSizeChange(size);
3312
+ this.bgLine?.onLineSizeChange(size);
3313
+ }
3314
+ setTransform(top, force, delay, isActive, opacity, blur) {
3315
+ this.top = top;
3316
+ this.delay = delay;
3317
+ this.isActive = isActive;
3318
+ this.opacity = opacity;
3319
+ this.blur = blur;
3320
+ this.setLineTransformations(force, delay);
3321
+ const enableSpring = this.lyricPlayer.getEnableSpring();
3322
+ const hiddenSlideY = (this.lyricPlayer.getAlwaysPostpositionBackground() ? false : this.isBgFirst) ? 80 : -80;
3323
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3324
+ const targetBgSlideY = isActive || !isPlaying ? 0 : hiddenSlideY;
3325
+ if (force || !enableSpring) {
3326
+ this.posY.setPosition(top);
3327
+ this.bgSlideY.setPosition(targetBgSlideY);
3328
+ this.renderStyles();
3329
+ } else {
3330
+ this.posY.setTargetPosition(top, delay);
3331
+ this.bgSlideY.setTargetPosition(targetBgSlideY, delay);
3332
+ }
3333
+ }
3334
+ setLineTransformations(force, delay) {
3335
+ const enableScale = this.lyricPlayer.getEnableScale();
3336
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3337
+ const renderMode = this.isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID;
3338
+ const SCALE_ASPECT = enableScale ? 97 : 100;
3339
+ let mainScale = 100;
3340
+ if (!this.isActive && isPlaying) mainScale = SCALE_ASPECT;
3341
+ this.mainLine.setTransform(mainScale, 1, 0, force, delay, renderMode);
3342
+ let bgScale = 100;
3343
+ if (!this.isActive && isPlaying) bgScale = 75;
3344
+ this.bgLine?.setTransform(bgScale, 1, 0, force, delay, renderMode);
3345
+ }
3346
+ update(delta) {
3347
+ if (this.lyricPlayer.getEnableSpring()) {
3348
+ this.posY.update(delta);
3349
+ this.bgSlideY.update(delta);
3350
+ this.renderStyles();
3351
+ }
3352
+ this.mainLine.update(delta);
3353
+ this.bgLine?.update(delta);
3354
+ }
3355
+ rebuildAllLines() {
3356
+ this.mainLine.rebuildElement();
3357
+ this.bgLine?.rebuildElement();
3358
+ }
3359
+ enable(time, shouldPlay) {
3360
+ this.mainLine.enable(time, shouldPlay);
3361
+ this.bgLine?.enable(time, shouldPlay);
3362
+ }
3363
+ disable() {
3364
+ this.mainLine.disable();
3365
+ this.bgLine?.disable();
3366
+ }
3367
+ dispose() {
3368
+ this.mainLine.dispose();
3369
+ this.bgLine?.dispose();
3370
+ }
3371
+ };
3372
+ //#endregion
3373
+ //#region src/lyric-player/dom/lyric-group.ts
3374
+ var LyricLineGroup = class extends LyricLineGroupBase {
3375
+ lyricPlayer;
3376
+ element;
3377
+ bgWrapper;
3378
+ lastIsActive;
3379
+ constructor(lyricPlayer, mainLine) {
3380
+ super(mainLine);
3381
+ this.lyricPlayer = lyricPlayer;
3382
+ this.element = document.createElement("div");
3383
+ this.element.className = lyric_player_module_default.lyricLineWrapper;
3384
+ this.element.appendChild(mainLine.getElement());
3385
+ this.posY.setPosition(window.innerHeight * 2);
3386
+ lyricPlayer.resizeObserver.observe(this.element);
3387
+ }
3388
+ get isInSight() {
3389
+ const t = this.posY.getCurrentPosition();
3390
+ let h = this.lyricPlayer.lyricGroupSize?.get(this)?.[1];
3391
+ if (h === void 0 || h === 0) h = this.element.clientHeight || 0;
3392
+ const pb = this.lyricPlayer.size[1];
3393
+ const ov = this.lyricPlayer.getOverscanPx();
3394
+ return !(t > pb + h + ov || t < -h - ov);
3395
+ }
3396
+ show() {
3397
+ if (!this.element.parentElement) {
3398
+ const playerEl = this.lyricPlayer.getElement();
3399
+ const groups = this.lyricPlayer.currentLyricGroups;
3400
+ const myIndex = groups.indexOf(this);
3401
+ let referenceNode = null;
3402
+ if (myIndex !== -1) {
3403
+ for (let i = myIndex + 1; i < groups.length; i++) if (groups[i].element.parentElement === playerEl) {
3404
+ referenceNode = groups[i].element;
3405
+ break;
3406
+ }
3407
+ }
3408
+ playerEl.insertBefore(this.element, referenceNode);
3409
+ this.lyricPlayer.resizeObserver.observe(this.element);
3410
+ }
3411
+ this.mainLine.show();
3412
+ this.bgLine?.show();
3413
+ }
3414
+ hide() {
3415
+ if (this.element.parentElement) {
3416
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3417
+ this.element.remove();
3418
+ this.mainLine.teardownContent();
3419
+ this.bgLine?.teardownContent();
3420
+ }
3421
+ }
3422
+ update(delta) {
3423
+ if (this.isInSight) this.show();
3424
+ else this.hide();
3425
+ super.update(delta);
3426
+ }
3427
+ addBgLine(bgLine) {
3428
+ if (this.bgLine) this.bgLine.dispose();
3429
+ if (this.bgWrapper) this.bgWrapper.remove();
3430
+ this.bgLine = bgLine;
3431
+ const bgStartTime = bgLine.getLine().words[0]?.startTime ?? bgLine.getLine().startTime;
3432
+ const mainStartTime = this.mainLine.getLine().words[0]?.startTime ?? this.mainLine.getLine().startTime;
3433
+ this.isBgFirst = bgStartTime < mainStartTime;
3434
+ if (this.mainLine.getLine().isDuet) bgLine.getElement().classList.add(lyric_player_module_default.lyricDuetLine);
3435
+ this.bgWrapper = document.createElement("div");
3436
+ this.bgWrapper.className = lyric_player_module_default.bgWrapper;
3437
+ this.bgWrapper.appendChild(bgLine.getElement());
3438
+ if (!this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst) {
3439
+ this.bgWrapper.classList.add(lyric_player_module_default.bgWrapperTop);
3440
+ this.element.insertBefore(this.bgWrapper, this.mainLine.getElement());
3441
+ this.bgSlideY.setPosition(80);
3442
+ } else this.element.appendChild(this.bgWrapper);
3443
+ }
3444
+ renderStyles() {
3445
+ const y = this.posY.getCurrentPosition().toFixed(1);
3446
+ this.element.style.transform = `translateY(${y}px)`;
3447
+ this.element.style.opacity = this.opacity.toString();
3448
+ this.element.style.filter = `blur(${Math.min(5, this.blur)}px)`;
3449
+ if (!this.lyricPlayer.getEnableSpring()) this.element.style.transitionDelay = `${this.delay}ms`;
3450
+ if (this.bgWrapper) {
3451
+ if (this.lastIsActive !== this.isActive) {
3452
+ this.lastIsActive = this.isActive;
3453
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperActive, this.isActive);
3454
+ }
3455
+ const slideY = this.bgSlideY.getCurrentPosition();
3456
+ const slideYStr = slideY.toFixed(1);
3457
+ const activeProgress = clamp01(1 - Math.abs(slideY) / 80);
3458
+ const scaleStr = (.8 + activeProgress * .2).toFixed(3);
3459
+ this.bgWrapper.style.transform = `translateY(${slideYStr}%) scale(${scaleStr})`;
3460
+ const shouldBgFirst = !this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst;
3461
+ if (shouldBgFirst) {
3462
+ const currentMarginTop = -(this.bgWrapper.clientHeight || 0) * (1 - activeProgress);
3463
+ this.bgWrapper.style.marginTop = `${currentMarginTop.toFixed(1)}px`;
3464
+ } else this.bgWrapper.style.marginTop = "";
3465
+ const isHidden = slideYStr === (shouldBgFirst ? "80.0" : "-80.0") && !this.isActive;
3466
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperHidden, isHidden);
3467
+ }
3468
+ }
3469
+ dispose() {
3470
+ super.dispose();
3471
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3472
+ this.element.remove();
3473
+ }
3474
+ };
3475
+ //#endregion
3262
3476
  //#region src/utils/is-cjk.ts
3263
3477
  const isCJK = (char) => {
3264
3478
  return /^[\p{Unified_Ideograph}\u0800-\u9FFC]+$/u.test(char);
@@ -3275,10 +3489,7 @@ var LyricLineBase = class extends EventTarget {
3275
3489
  blur = 0;
3276
3490
  opacity = 1;
3277
3491
  delay = 0;
3278
- lineTransforms = {
3279
- posY: new Spring(0),
3280
- scale: new Spring(100)
3281
- };
3492
+ lineTransforms = { scale: new Spring(100) };
3282
3493
  /**
3283
3494
  * 用于 CJK 词语边界检测的分词器
3284
3495
  */
@@ -3288,9 +3499,7 @@ var LyricLineBase = class extends EventTarget {
3288
3499
  * 用于正确处理 emoji、复合字符等
3289
3500
  */
3290
3501
  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;
3502
+ setTransform(scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3294
3503
  this.scale = scale;
3295
3504
  this.opacity = opacity;
3296
3505
  this.blur = blur;
@@ -3413,6 +3622,7 @@ function getMeasurementContext() {
3413
3622
  * 用于平衡歌词行在换行后的各行长度
3414
3623
  */
3415
3624
  var LineBalancer = class {
3625
+ mainElement;
3416
3626
  isBalancing = false;
3417
3627
  lastBalancedContainerWidth = -1;
3418
3628
  constructor(mainElement) {
@@ -3736,13 +3946,9 @@ function generateFadeGradient(width, padding = 0, bright = "rgba(0,0,0,var(--bri
3736
3946
  const leftPos = (1 - widthInTotal) / 2;
3737
3947
  return [`linear-gradient(to right,${bright} ${leftPos * 100}%,${dark} ${(leftPos + widthInTotal) * 100}%)`, totalAspect];
3738
3948
  }
3739
- var RawLyricLineMouseEvent = class extends MouseEvent {
3740
- constructor(line, event) {
3741
- super(event.type, event);
3742
- this.line = line;
3743
- }
3744
- };
3745
3949
  var LyricLineEl = class extends LyricLineBase {
3950
+ lyricPlayer;
3951
+ lyricLine;
3746
3952
  element = document.createElement("div");
3747
3953
  splittedWords = [];
3748
3954
  built = false;
@@ -3768,12 +3974,9 @@ var LyricLineEl = class extends LyricLineBase {
3768
3974
  super();
3769
3975
  this.lyricPlayer = lyricPlayer;
3770
3976
  this.lyricLine = lyricLine;
3771
- this._prevParentEl = lyricPlayer.getElement();
3772
- lyricPlayer.resizeObserver.observe(this.element);
3773
3977
  this.element.setAttribute("class", lyric_player_module_default.lyricLine);
3774
3978
  if (this.lyricLine.isBG) this.element.classList.add(lyric_player_module_default.lyricBgLine);
3775
3979
  if (this.lyricLine.isDuet) this.element.classList.add(lyric_player_module_default.lyricDuetLine);
3776
- this.lineTransforms.posY.setPosition(window.innerHeight * 2);
3777
3980
  this.element.appendChild(document.createElement("div"));
3778
3981
  this.element.appendChild(document.createElement("div"));
3779
3982
  this.element.appendChild(document.createElement("div"));
@@ -3786,33 +3989,6 @@ var LyricLineEl = class extends LyricLineBase {
3786
3989
  if (LyricLineBase.wordSegmenter) this.balancer = new LineBalancer(main);
3787
3990
  this.rebuildStyle();
3788
3991
  }
3789
- listenersMap = /* @__PURE__ */ new Map();
3790
- onMouseEvent = (e) => {
3791
- const wrapped = new RawLyricLineMouseEvent(this, e);
3792
- for (const listener of this.listenersMap.get(e.type) ?? []) listener.call(this, wrapped);
3793
- if (!this.dispatchEvent(wrapped) || wrapped.defaultPrevented) {
3794
- e.preventDefault();
3795
- e.stopPropagation();
3796
- e.stopImmediatePropagation();
3797
- }
3798
- };
3799
- addMouseEventListener(type, callback, options) {
3800
- if (callback) {
3801
- const listeners = this.listenersMap.get(type) ?? /* @__PURE__ */ new Set();
3802
- if (listeners.size === 0) this.element.addEventListener(type, this.onMouseEvent, options);
3803
- listeners.add(callback);
3804
- this.listenersMap.set(type, listeners);
3805
- }
3806
- }
3807
- removeMouseEventListener(type, callback, options) {
3808
- if (callback) {
3809
- const listeners = this.listenersMap.get(type);
3810
- if (listeners) {
3811
- listeners.delete(callback);
3812
- if (listeners.size === 0) this.element.removeEventListener(type, this.onMouseEvent, options);
3813
- }
3814
- }
3815
- }
3816
3992
  areWordsOnSameLine(word1, word2) {
3817
3993
  if (word1?.mainElement && word2?.mainElement) {
3818
3994
  const word1el = word1.mainElement;
@@ -3905,34 +4081,18 @@ var LyricLineEl = class extends LyricLineBase {
3905
4081
  getLine() {
3906
4082
  return this.lyricLine;
3907
4083
  }
3908
- _prevParentEl;
3909
4084
  lastStyle = "";
3910
4085
  show() {
3911
- if (!this.element.parentElement) {
3912
- this._prevParentEl.appendChild(this.element);
3913
- this.lyricPlayer.resizeObserver.observe(this.element);
3914
- }
3915
4086
  if (!this.built) {
3916
4087
  this.rebuildElement();
3917
4088
  this.built = true;
3918
4089
  this.updateMaskImageSync();
3919
4090
  }
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
4091
  }
3932
4092
  rebuildStyle() {
3933
4093
  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;`;
4094
+ style += `transform: scale(${(this.lineTransforms.scale.getCurrentPosition() / 100).toFixed(4)});`;
4095
+ if (!this.lyricPlayer.getEnableSpring()) style += `transition-delay:${this.delay}ms;`;
3936
4096
  style += `filter:blur(${Math.min(5, this.blur)}px);`;
3937
4097
  if (style !== this.lastStyle) {
3938
4098
  this.lastStyle = style;
@@ -3945,7 +4105,7 @@ var LyricLineEl = class extends LyricLineBase {
3945
4105
  const trans = this.element.children[1];
3946
4106
  const roman = this.element.children[2];
3947
4107
  if (this.lyricPlayer._getIsNonDynamic()) {
3948
- main.innerText = this.lyricLine.words.map((w) => this.lyricPlayer.processObsceneWord(w)).join("");
4108
+ main.textContent = this.lyricLine.words.map((w) => this.lyricPlayer.processObsceneWord(w)).join("");
3949
4109
  this.setSubLinesText(trans, roman);
3950
4110
  return;
3951
4111
  }
@@ -3958,8 +4118,8 @@ var LyricLineEl = class extends LyricLineBase {
3958
4118
  }
3959
4119
  /** 设置翻译与音译行文本 */
3960
4120
  setSubLinesText(trans, roman) {
3961
- trans.innerText = this.lyricLine.translatedLyric;
3962
- roman.innerText = this.lyricLine.romanLyric;
4121
+ trans.textContent = this.lyricLine.translatedLyric;
4122
+ roman.textContent = this.lyricLine.romanLyric;
3963
4123
  }
3964
4124
  getRubyCharCount(word) {
3965
4125
  return (word.ruby ?? []).reduce((total, ruby) => total + ruby.word.length, 0);
@@ -3977,7 +4137,7 @@ var LyricLineEl = class extends LyricLineBase {
3977
4137
  const rubySegments = this.getRubySegments(word);
3978
4138
  for (const ruby of rubySegments) {
3979
4139
  const rubyPartEl = document.createElement("span");
3980
- rubyPartEl.innerText = ruby.word;
4140
+ rubyPartEl.textContent = ruby.word;
3981
4141
  rubyPartEl.dataset.startTime = String(ruby.startTime);
3982
4142
  rubyPartEl.dataset.endTime = String(ruby.endTime);
3983
4143
  rubyWordEl.appendChild(rubyPartEl);
@@ -3994,24 +4154,24 @@ var LyricLineEl = class extends LyricLineBase {
3994
4154
  const trimmedWord = displayWord.trim();
3995
4155
  if (LyricLineBase.graphemeSegmenter) for (const { segment } of LyricLineBase.graphemeSegmenter.segment(trimmedWord)) {
3996
4156
  const charEl = document.createElement("span");
3997
- charEl.innerText = segment;
4157
+ charEl.textContent = segment;
3998
4158
  subElements.push(charEl);
3999
4159
  wordContainer.appendChild(charEl);
4000
4160
  }
4001
4161
  else for (const segment of Array.from(trimmedWord)) {
4002
4162
  const charEl = document.createElement("span");
4003
- charEl.innerText = segment;
4163
+ charEl.textContent = segment;
4004
4164
  subElements.push(charEl);
4005
4165
  wordContainer.appendChild(charEl);
4006
4166
  }
4007
4167
  } else if (hasRomanLine) {
4008
4168
  const wordEl = document.createElement("div");
4009
- wordEl.innerText = displayWord.trim();
4169
+ wordEl.textContent = displayWord.trim();
4010
4170
  wordContainer.appendChild(wordEl);
4011
- } else if (romanWord.length === 0) wordContainer.innerText = displayWord.trim();
4171
+ } else if (romanWord.length === 0) wordContainer.textContent = displayWord.trim();
4012
4172
  if (hasRomanLine) {
4013
4173
  const romanWordEl = document.createElement("div");
4014
- romanWordEl.innerText = romanWord.length > 0 ? romanWord : "\xA0";
4174
+ romanWordEl.textContent = romanWord.length > 0 ? romanWord : "\xA0";
4015
4175
  romanWordEl.classList.add(lyric_player_module_default.romanWord);
4016
4176
  wordContainer.appendChild(romanWordEl);
4017
4177
  }
@@ -4125,7 +4285,7 @@ var LyricLineEl = class extends LyricLineBase {
4125
4285
  const glow = el.animate(frames, {
4126
4286
  duration: animateDu,
4127
4287
  delay: Number.isFinite(wordDe) ? wordDe : 0,
4128
- id: `emphasize-word-${el.innerText}-${i}`,
4288
+ id: `emphasize-word-${el.textContent}-${i}`,
4129
4289
  iterations: 1,
4130
4290
  composite: "replace",
4131
4291
  fill: "both"
@@ -4375,25 +4535,19 @@ var LyricLineEl = class extends LyricLineBase {
4375
4535
  this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
4376
4536
  this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
4377
4537
  }
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);
4538
+ setTransform(scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4539
+ super.setTransform(scale, opacity, blur, force, delay);
4380
4540
  this.renderMode = mode;
4381
- const beforeInSight = this.isInSight;
4382
4541
  const enableSpring = this.lyricPlayer.getEnableSpring();
4383
- this.top = top;
4542
+ this.top = 0;
4384
4543
  this.scale = scale;
4385
4544
  this.delay = delay * 1e3 | 0;
4386
4545
  const main = this.element.children[0];
4387
4546
  main.style.opacity = `${opacity}`;
4388
4547
  if (force || !enableSpring) {
4389
4548
  this.blur = Math.min(32, blur);
4390
- this.lineTransforms.posY.setPosition(top);
4391
4549
  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();
4550
+ this.rebuildStyle();
4397
4551
  const currentScale = this.lineTransforms.scale.getCurrentPosition();
4398
4552
  this.updateMaskAlphaTargets(currentScale / 100);
4399
4553
  this.currentBrightAlpha = this.targetBrightAlpha;
@@ -4401,35 +4555,31 @@ var LyricLineEl = class extends LyricLineBase {
4401
4555
  this.element.style.setProperty("--bright-mask-alpha", String(this.currentBrightAlpha));
4402
4556
  this.element.style.setProperty("--dark-mask-alpha", String(this.currentDarkAlpha));
4403
4557
  } else {
4404
- this.lineTransforms.posY.setTargetPosition(top, delay);
4405
4558
  this.lineTransforms.scale.setTargetPosition(scale);
4406
4559
  if (this.blur !== Math.min(5, blur)) {
4407
4560
  this.blur = Math.min(5, blur);
4408
- const roundedBlur = blur.toFixed(3);
4409
- this.element.style.filter = `blur(${roundedBlur}px)`;
4561
+ this.element.style.filter = `blur(${blur.toFixed(3)}px)`;
4410
4562
  }
4411
4563
  }
4412
4564
  }
4413
4565
  update(delta = 0) {
4414
4566
  if (!this.lyricPlayer.getEnableSpring()) return;
4415
- this.lineTransforms.posY.update(delta);
4416
4567
  this.lineTransforms.scale.update(delta);
4417
- if (this.isInSight) this.show();
4418
- else this.hide();
4568
+ this.rebuildStyle();
4569
+ if (!this.built) return;
4419
4570
  const currentScale = this.lineTransforms.scale.getCurrentPosition() / 100;
4420
4571
  this.updateMaskAlphaTargets(currentScale);
4421
4572
  this.applyAlphaToDom(delta);
4422
4573
  }
4574
+ /** @internal */
4423
4575
  _getDebugTargetPos() {
4424
4576
  return `[位移: ${this.top}; 缩放: ${this.scale}; 延时: ${this.delay}]`;
4425
4577
  }
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);
4578
+ teardownContent() {
4579
+ if (this.built) {
4580
+ this.disposeElements();
4581
+ this.built = false;
4582
+ }
4433
4583
  }
4434
4584
  disposeElements() {
4435
4585
  this.balancer?.reset();
@@ -4462,13 +4612,29 @@ var LyricLineEl = class extends LyricLineBase {
4462
4612
  //#endregion
4463
4613
  //#region src/lyric-player/dom/index.ts
4464
4614
  /**
4465
- * 歌词行鼠标相关事件,可以获取到歌词行的索引和歌词行元素
4615
+ * 歌词行鼠标相关事件,可以获取到歌词行的索引、主歌词行以及背景歌词行(如果有)元素
4466
4616
  */
4467
4617
  var LyricLineMouseEvent = class extends MouseEvent {
4468
- constructor(lineIndex, line, event) {
4618
+ lineIndex;
4619
+ line;
4620
+ bgLine;
4621
+ /**
4622
+ * 自定义标志位,用于记录外部是否调用了 `stopPropagation`
4623
+ */
4624
+ isPropagationStopped = false;
4625
+ constructor(lineIndex, line, bgLine, event) {
4469
4626
  super(`line-${event.type}`, event);
4470
4627
  this.lineIndex = lineIndex;
4471
4628
  this.line = line;
4629
+ this.bgLine = bgLine;
4630
+ }
4631
+ stopPropagation() {
4632
+ this.isPropagationStopped = true;
4633
+ super.stopPropagation();
4634
+ }
4635
+ stopImmediatePropagation() {
4636
+ this.isPropagationStopped = true;
4637
+ super.stopImmediatePropagation();
4472
4638
  }
4473
4639
  };
4474
4640
  /**
@@ -4477,7 +4643,8 @@ var LyricLineMouseEvent = class extends MouseEvent {
4477
4643
  * 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
4478
4644
  */
4479
4645
  var DomLyricPlayer = class extends LyricPlayerBase {
4480
- currentLyricLineObjects = [];
4646
+ abortController = new AbortController();
4647
+ currentLyricGroups = [];
4481
4648
  onResize() {
4482
4649
  const computedStyles = getComputedStyle(this.element);
4483
4650
  this._baseFontSize = Number.parseFloat(computedStyles.fontSize);
@@ -4486,10 +4653,18 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4486
4653
  supportPlusLighter = CSS.supports("mix-blend-mode", "plus-lighter");
4487
4654
  supportMaskImage = CSS.supports("mask-image", "none");
4488
4655
  innerSize = [0, 0];
4489
- onLineClickedHandler = (e) => {
4490
- const evt = new LyricLineMouseEvent(this.lyricLinesIndexes.get(e.line) ?? -1, e.line, e);
4491
- if (!this.dispatchEvent(evt)) {
4492
- e.preventDefault();
4656
+ onMouseEventHandler = (e) => {
4657
+ const target = e.target;
4658
+ if (!(target instanceof Element)) return;
4659
+ const groupEl = target.closest(`.${lyric_player_module_default.lyricLineWrapper}`);
4660
+ if (!groupEl) return;
4661
+ const group = this.lyricGroupElementMap.get(groupEl);
4662
+ if (!group) return;
4663
+ const mainLine = group.mainLine;
4664
+ const bgLine = group.bgLine;
4665
+ const evt = new LyricLineMouseEvent(this.lyricLinesIndexes.get(mainLine) ?? -1, mainLine, bgLine, e);
4666
+ if (!this.dispatchEvent(evt) || evt.defaultPrevented) e.preventDefault();
4667
+ if (evt.isPropagationStopped) {
4493
4668
  e.stopPropagation();
4494
4669
  e.stopImmediatePropagation();
4495
4670
  }
@@ -4510,11 +4685,16 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4510
4685
  this.onResize();
4511
4686
  this.element.classList.add("amll-lyric-player", "dom");
4512
4687
  if (this.disableSpring) this.element.classList.add(lyric_player_module_default.disableSpring);
4688
+ this.element.addEventListener("click", this.onMouseEventHandler, { signal: this.abortController.signal });
4689
+ this.element.addEventListener("contextmenu", this.onMouseEventHandler, { signal: this.abortController.signal });
4513
4690
  }
4514
4691
  rebuildStyle() {}
4515
4692
  setWordFadeWidth(value = .5) {
4516
4693
  super.setWordFadeWidth(value);
4517
- for (const el of this.currentLyricLineObjects) el.updateMaskImageSync();
4694
+ for (const group of this.currentLyricGroups) {
4695
+ group.mainLine.updateMaskImageSync();
4696
+ group.bgLine?.updateMaskImageSync();
4697
+ }
4518
4698
  }
4519
4699
  /**
4520
4700
  * 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
@@ -4526,36 +4706,43 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4526
4706
  if (this.hasDuetLine) this.element.classList.add(lyric_player_module_default.hasDuetLine);
4527
4707
  else this.element.classList.remove(lyric_player_module_default.hasDuetLine);
4528
4708
  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();
4533
- }
4534
- this.currentLyricLineObjects = this.processedLines.map((line, i) => {
4709
+ for (const group of this.currentLyricGroups) group.dispose();
4710
+ this.currentLyricGroups = [];
4711
+ let currentGroup = null;
4712
+ for (let i = 0; i < this.processedLines.length; i++) {
4713
+ const line = this.processedLines[i];
4535
4714
  const lineEl = new LyricLineEl(this, line);
4536
- lineEl.addMouseEventListener("click", this.onLineClickedHandler);
4537
- lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
4538
4715
  this.lyricLinesIndexes.set(lineEl, i);
4539
- this.lyricLineElementMap.set(lineEl.getElement(), lineEl);
4540
- return lineEl;
4541
- });
4716
+ if (!line.isBG || !currentGroup) {
4717
+ currentGroup = new LyricLineGroup(this, lineEl);
4718
+ this.currentLyricGroups.push(currentGroup);
4719
+ this.lyricGroupElementMap.set(currentGroup.element, currentGroup);
4720
+ } else currentGroup.addBgLine(lineEl);
4721
+ }
4542
4722
  this.setLinePosXSpringParams({});
4543
4723
  this.setLinePosYSpringParams({});
4544
4724
  this.setLineScaleSpringParams({});
4725
+ this.setCurrentTime(initialTime, true);
4545
4726
  this.calcLayout(true);
4546
4727
  this.update(0);
4547
4728
  }
4548
4729
  pause() {
4549
4730
  super.pause();
4550
- this.element.classList.remove("playing");
4731
+ this.element.classList.remove(lyric_player_module_default.playing);
4551
4732
  this.interludeDots.pause();
4552
- for (const line of this.currentLyricLineObjects) line.pause();
4733
+ for (const group of this.currentLyricGroups) {
4734
+ group.mainLine.pause();
4735
+ group.bgLine?.pause();
4736
+ }
4553
4737
  }
4554
4738
  resume() {
4555
4739
  super.resume();
4556
- this.element.classList.add("playing");
4740
+ this.element.classList.add(lyric_player_module_default.playing);
4557
4741
  this.interludeDots.resume();
4558
- for (const line of this.currentLyricLineObjects) line.resume();
4742
+ for (const group of this.currentLyricGroups) {
4743
+ group.mainLine.resume();
4744
+ group.bgLine?.resume();
4745
+ }
4559
4746
  }
4560
4747
  update(delta = 0) {
4561
4748
  if (!this.timelineState.initialLayoutFinished) return;
@@ -4563,12 +4750,13 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4563
4750
  if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${this.timelineState.currentTime}`);
4564
4751
  if (!this.isPageVisible) return;
4565
4752
  const deltaS = delta / 1e3;
4566
- for (const line of this.currentLyricLineObjects) line.update(deltaS);
4753
+ for (const group of this.currentLyricGroups) group.update(deltaS);
4567
4754
  }
4568
4755
  dispose() {
4569
4756
  super.dispose();
4757
+ this.abortController.abort();
4570
4758
  this.element.remove();
4571
- for (const el of this.currentLyricLineObjects) el.dispose();
4759
+ for (const group of this.currentLyricGroups) group.dispose();
4572
4760
  this.bottomLine.dispose();
4573
4761
  this.interludeDots.dispose();
4574
4762
  }