@applemusic-like-lyrics/core 0.4.2 → 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.
@@ -35,6 +35,9 @@ let bezier_easing = require("bezier-easing");
35
35
  bezier_easing = __toESM(bezier_easing, 1);
36
36
  //#region src/bg-render/base.ts
37
37
  var AbstractBaseRenderer = class {};
38
+ function clamp1(x) {
39
+ return Math.max(1, x);
40
+ }
38
41
  var BaseRenderer = class extends AbstractBaseRenderer {
39
42
  observer;
40
43
  flowSpeed = 1;
@@ -43,8 +46,8 @@ var BaseRenderer = class extends AbstractBaseRenderer {
43
46
  super();
44
47
  this.canvas = canvas;
45
48
  this.observer = new ResizeObserver(() => {
46
- const width = Math.max(1, canvas.clientWidth * window.devicePixelRatio * this.currerntRenderScale);
47
- const height = Math.max(1, canvas.clientHeight * window.devicePixelRatio * this.currerntRenderScale);
49
+ const width = clamp1(canvas.clientWidth * window.devicePixelRatio * this.currerntRenderScale);
50
+ const height = clamp1(canvas.clientHeight * window.devicePixelRatio * this.currerntRenderScale);
48
51
  this.onResize(width, height);
49
52
  });
50
53
  this.observer.observe(canvas);
@@ -232,6 +235,17 @@ function blurImage(imageData, radius, quality) {
232
235
  }
233
236
  }
234
237
  //#endregion
238
+ //#region src/utils/clamp.ts
239
+ function clamp(x, min, max) {
240
+ return Math.min(Math.max(x, min), max);
241
+ }
242
+ function clamp01(x) {
243
+ return clamp(x, 0, 1);
244
+ }
245
+ function clampPositive(x) {
246
+ return Math.max(0, x);
247
+ }
248
+ //#endregion
235
249
  //#region src/bg-render/mesh-renderer/cp-presets.ts
236
250
  /** @internal */
237
251
  const p = (cx, cy, x, y, ur = 0, vr = 0, up = 1, vp = 1) => Object.freeze({
@@ -404,11 +418,8 @@ const CONTROL_POINT_PRESETS = [
404
418
  * 目的是取代原先大量的预设控制点代码
405
419
  */
406
420
  const randomRange = (min, max) => Math.random() * (max - min) + min;
407
- function clamp$1(x, min, max) {
408
- return Math.min(Math.max(x, min), max);
409
- }
410
421
  function smoothstep(edge0, edge1, x) {
411
- const t = clamp$1((x - edge0) / (edge1 - edge0), 0, 1);
422
+ const t = clamp01((x - edge0) / (edge1 - edge0));
412
423
  return t * t * (3 - 2 * t);
413
424
  }
414
425
  function smoothifyControlPoints(conf, w, h, iterations = 2, factor = .5, factorIterationModifier = .1) {
@@ -478,7 +489,7 @@ function smoothifyControlPoints(conf, w, h, iterations = 2, factor = .5, factorI
478
489
  }
479
490
  }
480
491
  grid = newGrid;
481
- f = Math.min(1, Math.max(f + factorIterationModifier, 0));
492
+ f = clamp01(f + factorIterationModifier);
482
493
  }
483
494
  for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) conf[j * w + i] = grid[j][i];
484
495
  }
@@ -1267,7 +1278,7 @@ var MeshGradientRenderer = class extends BaseRenderer {
1267
1278
  gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1268
1279
  this.quadProgram.use();
1269
1280
  this.quadProgram.setUniform1i("u_texture", 0);
1270
- this.quadProgram.setUniform1f("u_alpha", easeInOutSine(Math.min(1, Math.max(0, state.alpha))));
1281
+ this.quadProgram.setUniform1f("u_alpha", easeInOutSine(clamp01(state.alpha)));
1271
1282
  gl.activeTexture(gl.TEXTURE0);
1272
1283
  gl.bindTexture(gl.TEXTURE_2D, this.fboTexture);
1273
1284
  gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
@@ -1502,7 +1513,7 @@ var PixiRenderer = class extends BaseRenderer {
1502
1513
  lastContainer = /* @__PURE__ */ new Set();
1503
1514
  onTick = (delta) => {
1504
1515
  for (const lastContainer of this.lastContainer) {
1505
- lastContainer.alpha = Math.max(0, lastContainer.alpha - delta / 60);
1516
+ lastContainer.alpha = clampPositive(lastContainer.alpha - delta / 60);
1506
1517
  if (lastContainer.alpha <= 0) {
1507
1518
  this.app.stage.removeChild(lastContainer);
1508
1519
  this.lastContainer.delete(lastContainer);
@@ -1720,6 +1731,10 @@ var BackgroundRender = class BackgroundRender {
1720
1731
  //#region src/styles/lyric-player.module.css
1721
1732
  var lyric_player_module_default = {
1722
1733
  "active": "FmKaba_active",
1734
+ "bgWrapper": "FmKaba_bgWrapper",
1735
+ "bgWrapperActive": "FmKaba_bgWrapperActive",
1736
+ "bgWrapperHidden": "FmKaba_bgWrapperHidden",
1737
+ "bgWrapperTop": "FmKaba_bgWrapperTop",
1723
1738
  "bottomLine": "FmKaba_bottomLine",
1724
1739
  "dirty": "FmKaba_dirty",
1725
1740
  "disableSpring": "FmKaba_disableSpring",
@@ -1732,8 +1747,10 @@ var lyric_player_module_default = {
1732
1747
  "lyricBgLine": "FmKaba_lyricBgLine",
1733
1748
  "lyricDuetLine": "FmKaba_lyricDuetLine",
1734
1749
  "lyricLine": "FmKaba_lyricLine",
1750
+ "lyricLineWrapper": "FmKaba_lyricLineWrapper",
1735
1751
  "lyricMainLine": "FmKaba_lyricMainLine",
1736
1752
  "lyricSubLine": "FmKaba_lyricSubLine",
1753
+ "playing": "FmKaba_playing",
1737
1754
  "romanWord": "FmKaba_romanWord",
1738
1755
  "rubyWord": "FmKaba_rubyWord",
1739
1756
  "tmpDisableTransition": "FmKaba_tmpDisableTransition",
@@ -1741,14 +1758,6 @@ var lyric_player_module_default = {
1741
1758
  "wordWithRuby": "FmKaba_wordWithRuby"
1742
1759
  };
1743
1760
  //#endregion
1744
- //#region src/utils/eq-set.ts
1745
- const eqSet = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x));
1746
- //#endregion
1747
- //#region src/utils/is-cjk.ts
1748
- const isCJK = (char) => {
1749
- return /^[\p{Unified_Ideograph}\u0800-\u9FFC]+$/u.test(char);
1750
- };
1751
- //#endregion
1752
1761
  //#region src/utils/optimize-lyric.ts
1753
1762
  const DEFAULT_OPTIMIZE_OPTIONS = {
1754
1763
  normalizeSpaces: true,
@@ -1842,27 +1851,31 @@ function cleanUnintentionalOverlaps(lines) {
1842
1851
  * 尝试让歌词提前最多 600ms 开始,如果有重叠则尝试最多提前 400ms 或上一行时长的 30%
1843
1852
  */
1844
1853
  function tryAdvanceStartTime(lines) {
1845
- for (let i = lines.length - 1; i >= 0; i--) {
1854
+ const defaultAdvanceAmount = 600;
1855
+ const fallbackAdvanceAmount = 400;
1856
+ const fallbackAdvanceRatio = .3;
1857
+ let prevLineStartTime = 0;
1858
+ let prevLineEndTime = 0;
1859
+ let prevMainGroupStartTime = 0;
1860
+ let prevMainGroupEndTime = 0;
1861
+ let hasPrevLine = false;
1862
+ for (let i = 0; i < lines.length; i++) {
1846
1863
  const line = lines[i];
1847
1864
  if (line.isBG) continue;
1848
- let prevLine = null;
1849
- if (i > 0) {
1850
- let prevIdx = i - 1;
1851
- if (lines[prevIdx].isBG) prevIdx--;
1852
- if (prevIdx >= 0) prevLine = lines[prevIdx];
1853
- }
1865
+ const originalStartTime = line.startTime;
1866
+ const originalEndTime = line.endTime;
1854
1867
  let targetAdvanceAmount = 0;
1855
1868
  let safeBoundary = 0;
1856
- if (prevLine) if (line.startTime >= prevLine.endTime) {
1857
- targetAdvanceAmount = 600;
1858
- safeBoundary = prevLine.endTime;
1869
+ if (hasPrevLine) if (originalStartTime >= prevLineEndTime) {
1870
+ targetAdvanceAmount = defaultAdvanceAmount;
1871
+ safeBoundary = prevMainGroupEndTime;
1859
1872
  } else {
1860
- targetAdvanceAmount = 400;
1861
- const prevDuration = prevLine.endTime - prevLine.startTime;
1862
- safeBoundary = prevLine.startTime + prevDuration * .3;
1873
+ targetAdvanceAmount = fallbackAdvanceAmount;
1874
+ const prevDuration = prevLineEndTime - prevLineStartTime;
1875
+ safeBoundary = prevLineStartTime + prevDuration * fallbackAdvanceRatio;
1863
1876
  }
1864
1877
  else {
1865
- targetAdvanceAmount = 600;
1878
+ targetAdvanceAmount = defaultAdvanceAmount;
1866
1879
  safeBoundary = 0;
1867
1880
  }
1868
1881
  const targetTime = line.startTime - targetAdvanceAmount;
@@ -1870,6 +1883,20 @@ function tryAdvanceStartTime(lines) {
1870
1883
  if (newStartTime < line.startTime) line.startTime = newStartTime;
1871
1884
  const nextLine = lines[i + 1];
1872
1885
  if (nextLine?.isBG) nextLine.startTime = line.startTime;
1886
+ if (hasPrevLine) if (originalStartTime < prevMainGroupEndTime && originalEndTime > prevMainGroupStartTime) {
1887
+ prevMainGroupStartTime = Math.min(prevMainGroupStartTime, originalStartTime);
1888
+ prevMainGroupEndTime = Math.max(prevMainGroupEndTime, originalEndTime);
1889
+ } else {
1890
+ prevMainGroupStartTime = originalStartTime;
1891
+ prevMainGroupEndTime = originalEndTime;
1892
+ }
1893
+ else {
1894
+ prevMainGroupStartTime = originalStartTime;
1895
+ prevMainGroupEndTime = originalEndTime;
1896
+ }
1897
+ prevLineStartTime = originalStartTime;
1898
+ prevLineEndTime = originalEndTime;
1899
+ hasPrevLine = true;
1873
1900
  }
1874
1901
  }
1875
1902
  /**
@@ -1892,6 +1919,145 @@ function optimizeLyricLines(lines, options) {
1892
1919
  if (config.tryAdvanceStartTime) tryAdvanceStartTime(lines);
1893
1920
  }
1894
1921
  //#endregion
1922
+ //#region src/lyric-player/dom/interlude-dots.ts
1923
+ function easeInOutBack(x) {
1924
+ const c2 = 1.70158 * 1.525;
1925
+ return x < .5 ? (2 * x) ** 2 * ((c2 + 1) * 2 * x - c2) / 2 : ((2 * x - 2) ** 2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
1926
+ }
1927
+ function easeOutExpo(x) {
1928
+ return x === 1 ? 1 : 1 - 2 ** (-10 * x);
1929
+ }
1930
+ var InterludeDots = class {
1931
+ element = document.createElement("div");
1932
+ dot0 = document.createElement("span");
1933
+ dot1 = document.createElement("span");
1934
+ dot2 = document.createElement("span");
1935
+ left = 0;
1936
+ top = 0;
1937
+ playing = true;
1938
+ lastStyle = "";
1939
+ currentInterlude;
1940
+ currentTime = 0;
1941
+ targetBreatheDuration = 1500;
1942
+ constructor() {
1943
+ this.element.className = lyric_player_module_default.interludeDots;
1944
+ this.element.appendChild(this.dot0);
1945
+ this.element.appendChild(this.dot1);
1946
+ this.element.appendChild(this.dot2);
1947
+ }
1948
+ getElement() {
1949
+ return this.element;
1950
+ }
1951
+ setTransform(left = this.left, top = this.top) {
1952
+ this.left = left;
1953
+ this.top = top;
1954
+ this.update();
1955
+ }
1956
+ setInterlude(interlude) {
1957
+ this.currentInterlude = interlude;
1958
+ this.currentTime = interlude?.[0] ?? 0;
1959
+ if (interlude) this.element.classList.add(lyric_player_module_default.enabled);
1960
+ else this.element.classList.remove(lyric_player_module_default.enabled);
1961
+ }
1962
+ pause() {
1963
+ this.playing = false;
1964
+ this.element.classList.remove(lyric_player_module_default.playing);
1965
+ }
1966
+ resume() {
1967
+ this.playing = true;
1968
+ this.element.classList.add(lyric_player_module_default.playing);
1969
+ }
1970
+ update(delta = 0) {
1971
+ if (!this.playing) return;
1972
+ this.currentTime += delta;
1973
+ let curStyle = "";
1974
+ curStyle += `transform:translate(${this.left.toFixed(2)}px, ${this.top.toFixed(2)}px)`;
1975
+ if (this.currentInterlude) {
1976
+ const interludeDuration = this.currentInterlude[1] - this.currentInterlude[0];
1977
+ const currentDuration = this.currentTime - this.currentInterlude[0];
1978
+ if (currentDuration <= interludeDuration) {
1979
+ const breatheDuration = interludeDuration / Math.ceil(interludeDuration / this.targetBreatheDuration);
1980
+ let scale = 1;
1981
+ let globalOpacity = 1;
1982
+ scale *= Math.sin(1.5 * Math.PI - currentDuration / breatheDuration * 2) / 20 + 1;
1983
+ if (currentDuration < 2e3) scale *= easeOutExpo(currentDuration / 2e3);
1984
+ if (currentDuration < 500) globalOpacity = 0;
1985
+ else if (currentDuration < 1e3) globalOpacity *= (currentDuration - 500) / 500;
1986
+ if (interludeDuration - currentDuration < 750) scale *= 1 - easeInOutBack((750 - (interludeDuration - currentDuration)) / 750 / 2);
1987
+ if (interludeDuration - currentDuration < 375) globalOpacity *= clamp01((interludeDuration - currentDuration) / 375);
1988
+ const dotsDuration = clampPositive(interludeDuration - 750);
1989
+ scale = clampPositive(scale) * .7;
1990
+ curStyle += ` scale(${scale})`;
1991
+ const dot0Opacity = clamp(.25, currentDuration * 3 / dotsDuration * .75, 1);
1992
+ const dot1Opacity = clamp(.25, (currentDuration - dotsDuration / 3) * 3 / dotsDuration * .75, 1);
1993
+ const dot2Opacity = clamp(.25, (currentDuration - dotsDuration / 3 * 2) * 3 / dotsDuration * .75, 1);
1994
+ this.dot0.style.opacity = `${clamp01(globalOpacity * dot0Opacity)}`;
1995
+ this.dot1.style.opacity = `${clamp01(globalOpacity * dot1Opacity)}`;
1996
+ this.dot2.style.opacity = `${clamp01(globalOpacity * dot2Opacity)}`;
1997
+ } else {
1998
+ curStyle += " scale(0)";
1999
+ this.dot0.style.opacity = "0";
2000
+ this.dot1.style.opacity = "0";
2001
+ this.dot2.style.opacity = "0";
2002
+ }
2003
+ curStyle += ";";
2004
+ if (this.lastStyle !== curStyle) {
2005
+ this.element.setAttribute("style", curStyle);
2006
+ this.lastStyle = curStyle;
2007
+ }
2008
+ }
2009
+ }
2010
+ dispose() {
2011
+ this.element.remove();
2012
+ }
2013
+ };
2014
+ //#endregion
2015
+ //#region src/utils/schedule.ts
2016
+ const measureTasks = [];
2017
+ const mutateTasks = [];
2018
+ let scheduled = false;
2019
+ function onFlush() {
2020
+ let tmp = mutateTasks.shift();
2021
+ while (tmp) {
2022
+ try {
2023
+ tmp.resolve(tmp.task());
2024
+ } catch (error) {
2025
+ tmp.reject(error);
2026
+ }
2027
+ tmp = mutateTasks.shift();
2028
+ }
2029
+ tmp = measureTasks.shift();
2030
+ while (tmp) {
2031
+ try {
2032
+ tmp.resolve(tmp.task());
2033
+ } catch (error) {
2034
+ tmp.reject(error);
2035
+ }
2036
+ tmp = measureTasks.shift();
2037
+ }
2038
+ scheduled = false;
2039
+ }
2040
+ function scheduleFlush() {
2041
+ if (!scheduled) {
2042
+ scheduled = true;
2043
+ requestAnimationFrame(onFlush);
2044
+ }
2045
+ }
2046
+ function measure(callback) {
2047
+ const task = {
2048
+ task: callback,
2049
+ resolve: () => {},
2050
+ reject: () => {}
2051
+ };
2052
+ const promise = new Promise((resolve, reject) => {
2053
+ task.resolve = resolve;
2054
+ task.reject = reject;
2055
+ });
2056
+ measureTasks.push(task);
2057
+ scheduleFlush();
2058
+ return promise;
2059
+ }
2060
+ //#endregion
1895
2061
  //#region src/utils/derivative.ts
1896
2062
  function derivative(f) {
1897
2063
  const h = .001;
@@ -2006,67 +2172,7 @@ function solveSpring(from, velocity, to, delay = 0, params) {
2006
2172
  };
2007
2173
  }
2008
2174
  //#endregion
2009
- //#region src/utils/schedule.ts
2010
- const measureTasks = [];
2011
- const mutateTasks = [];
2012
- let scheduled = false;
2013
- function onFlush() {
2014
- let tmp = mutateTasks.shift();
2015
- while (tmp) {
2016
- try {
2017
- tmp.resolve(tmp.task());
2018
- } catch (error) {
2019
- tmp.reject(error);
2020
- }
2021
- tmp = mutateTasks.shift();
2022
- }
2023
- tmp = measureTasks.shift();
2024
- while (tmp) {
2025
- try {
2026
- tmp.resolve(tmp.task());
2027
- } catch (error) {
2028
- tmp.reject(error);
2029
- }
2030
- tmp = measureTasks.shift();
2031
- }
2032
- scheduled = false;
2033
- }
2034
- function scheduleFlush() {
2035
- if (!scheduled) {
2036
- scheduled = true;
2037
- requestAnimationFrame(onFlush);
2038
- }
2039
- }
2040
- function measure(callback) {
2041
- const task = {
2042
- task: callback,
2043
- resolve: () => {},
2044
- reject: () => {}
2045
- };
2046
- const promise = new Promise((resolve, reject) => {
2047
- task.resolve = resolve;
2048
- task.reject = reject;
2049
- });
2050
- measureTasks.push(task);
2051
- scheduleFlush();
2052
- return promise;
2053
- }
2054
- function mutate(callback) {
2055
- const task = {
2056
- task: callback,
2057
- resolve: () => {},
2058
- reject: () => {}
2059
- };
2060
- const promise = new Promise((resolve, reject) => {
2061
- task.resolve = resolve;
2062
- task.reject = reject;
2063
- });
2064
- mutateTasks.push(task);
2065
- scheduleFlush();
2066
- return promise;
2067
- }
2068
- //#endregion
2069
- //#region src/lyric-player/bottom-line.ts
2175
+ //#region src/lyric-player/base/bottom-line.ts
2070
2176
  var BottomLineEl = class {
2071
2177
  element = document.createElement("div");
2072
2178
  left = 0;
@@ -2155,150 +2261,437 @@ var BottomLineEl = class {
2155
2261
  }
2156
2262
  };
2157
2263
  //#endregion
2158
- //#region src/lyric-player/dom/interlude-dots.ts
2159
- function easeInOutBack(x) {
2160
- const c2 = 1.70158 * 1.525;
2161
- return x < .5 ? (2 * x) ** 2 * ((c2 + 1) * 2 * x - c2) / 2 : ((2 * x - 2) ** 2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
2264
+ //#region src/lyric-player/base/consts.ts
2265
+ /** 歌词中不雅用语的掩码模式 */
2266
+ const MaskObsceneWordsMode = {
2267
+ /** 禁用任何不雅用语掩码 */
2268
+ Disabled: "",
2269
+ /** 完全掩码所有不雅用语 */
2270
+ FullMask: "full-mask",
2271
+ /** 保留首尾字符,屏蔽中间字符 */
2272
+ PartialMask: "partial-mask"
2273
+ };
2274
+ /**
2275
+ * 歌词行的渲染模式
2276
+ * @internal
2277
+ */
2278
+ const LyricLineRenderMode = {
2279
+ SOLID: 0,
2280
+ GRADIENT: 1
2281
+ };
2282
+ /** 布局对齐锚点 */
2283
+ const LayoutAlignAnchor = {
2284
+ Top: "top",
2285
+ Center: "center",
2286
+ Bottom: "bottom"
2287
+ };
2288
+ //#endregion
2289
+ //#region src/lyric-player/base/layout.ts
2290
+ /**
2291
+ * 根据当前时间与当前目标行,计算当前是否处于某个可展示的间奏区间。
2292
+ *
2293
+ * 仅识别时间轴上的间奏空档,不涉及具体 DOM 元素的创建与摆放。
2294
+ * 若当前不应展示间奏动画,则返回 `undefined`。
2295
+ */
2296
+ function computeCurrentInterlude(input) {
2297
+ const currentTime = input.currentTime + 20;
2298
+ const currentIndex = input.scrollToIndex;
2299
+ const groups = input.currentGroups;
2300
+ const checkGap = (k) => {
2301
+ if (k < -1 || k >= groups.length - 1) return void 0;
2302
+ const prevGroup = k === -1 ? null : groups[k];
2303
+ const nextGroup = groups[k + 1];
2304
+ const gapStart = prevGroup ? prevGroup.endTime : 0;
2305
+ const gapEnd = Math.max(gapStart, nextGroup.startTime - 250);
2306
+ if (gapEnd - gapStart < 4e3) return void 0;
2307
+ if (gapEnd > currentTime && gapStart < currentTime) return {
2308
+ startTime: Math.max(gapStart, currentTime),
2309
+ endTime: gapEnd,
2310
+ anchorLineIndex: k,
2311
+ isNextDuet: nextGroup.mainLine.getLine().isDuet
2312
+ };
2313
+ };
2314
+ return checkGap(currentIndex - 1) || checkGap(currentIndex) || checkGap(currentIndex + 1);
2162
2315
  }
2163
- function easeOutExpo(x) {
2164
- return x === 1 ? 1 : 1 - 2 ** (-10 * x);
2316
+ /**
2317
+ * 根据当前播放上下文计算歌词纵向滚动动画的弹簧参数。
2318
+ *
2319
+ * 其策略为:
2320
+ * - seeking 或间奏时使用更稳定的固定参数
2321
+ * - 普通播放时根据相邻歌词的时间间隔动态调整 stiffness / damping
2322
+ */
2323
+ function computeLinePosYSpringParams(input) {
2324
+ const { enabled, currentGroups, scrollToIndex, isSeeking, isInterludeActive } = input;
2325
+ if (!enabled || currentGroups.length === 0) return { shouldUpdate: false };
2326
+ if (isSeeking || isInterludeActive) return {
2327
+ shouldUpdate: true,
2328
+ params: {
2329
+ stiffness: 90,
2330
+ damping: 15
2331
+ }
2332
+ };
2333
+ const currentGroup = currentGroups[scrollToIndex];
2334
+ const prevGroup = currentGroups[scrollToIndex - 1];
2335
+ if (!currentGroup || !prevGroup) return { shouldUpdate: false };
2336
+ const interval = currentGroup.startTime - prevGroup.startTime;
2337
+ const MIN_INTERVAL = 100;
2338
+ const MAX_INTERVAL = 800;
2339
+ const clampedInterval = clamp(interval, MIN_INTERVAL, MAX_INTERVAL);
2340
+ const MAX_STIFFNESS = 220;
2341
+ const MIN_STIFFNESS = 170;
2342
+ let ratio = 1 - (clampedInterval - MIN_INTERVAL) / (MAX_INTERVAL - MIN_INTERVAL);
2343
+ ratio = ratio ** .2;
2344
+ const targetStiffness = MIN_STIFFNESS + ratio * (MAX_STIFFNESS - MIN_STIFFNESS);
2345
+ return {
2346
+ shouldUpdate: true,
2347
+ params: {
2348
+ stiffness: targetStiffness,
2349
+ damping: Math.sqrt(targetStiffness) * 2.2
2350
+ }
2351
+ };
2165
2352
  }
2166
- const clamp = (min, cur, max) => Math.max(min, Math.min(cur, max));
2167
- var InterludeDots = class {
2168
- element = document.createElement("div");
2169
- dot0 = document.createElement("span");
2170
- dot1 = document.createElement("span");
2171
- dot2 = document.createElement("span");
2172
- left = 0;
2173
- top = 0;
2174
- playing = true;
2175
- lastStyle = "";
2176
- currentInterlude;
2177
- currentTime = 0;
2178
- targetBreatheDuration = 1500;
2179
- constructor() {
2180
- this.element.className = lyric_player_module_default.interludeDots;
2181
- this.element.appendChild(this.dot0);
2182
- this.element.appendChild(this.dot1);
2183
- this.element.appendChild(this.dot2);
2184
- }
2185
- getElement() {
2186
- return this.element;
2187
- }
2188
- setTransform(left = this.left, top = this.top) {
2189
- this.left = left;
2190
- this.top = top;
2191
- this.update();
2192
- }
2193
- setInterlude(interlude) {
2194
- this.currentInterlude = interlude;
2195
- this.currentTime = interlude?.[0] ?? 0;
2196
- if (interlude) this.element.classList.add(lyric_player_module_default.enabled);
2197
- else this.element.classList.remove(lyric_player_module_default.enabled);
2198
- }
2199
- pause() {
2200
- this.playing = false;
2201
- this.element.classList.remove(lyric_player_module_default.playing);
2202
- }
2203
- resume() {
2204
- this.playing = true;
2205
- this.element.classList.add(lyric_player_module_default.playing);
2206
- }
2207
- update(delta = 0) {
2208
- if (!this.playing) return;
2209
- this.currentTime += delta;
2210
- let curStyle = "";
2211
- curStyle += `transform:translate(${this.left.toFixed(2)}px, ${this.top.toFixed(2)}px)`;
2212
- if (this.currentInterlude) {
2213
- const interludeDuration = this.currentInterlude[1] - this.currentInterlude[0];
2214
- const currentDuration = this.currentTime - this.currentInterlude[0];
2215
- if (currentDuration <= interludeDuration) {
2216
- const breatheDuration = interludeDuration / Math.ceil(interludeDuration / this.targetBreatheDuration);
2217
- let scale = 1;
2218
- let globalOpacity = 1;
2219
- scale *= Math.sin(1.5 * Math.PI - currentDuration / breatheDuration * 2) / 20 + 1;
2220
- if (currentDuration < 2e3) scale *= easeOutExpo(currentDuration / 2e3);
2221
- if (currentDuration < 500) globalOpacity = 0;
2222
- else if (currentDuration < 1e3) globalOpacity *= (currentDuration - 500) / 500;
2223
- if (interludeDuration - currentDuration < 750) scale *= 1 - easeInOutBack((750 - (interludeDuration - currentDuration)) / 750 / 2);
2224
- if (interludeDuration - currentDuration < 375) globalOpacity *= clamp(0, (interludeDuration - currentDuration) / 375, 1);
2225
- const dotsDuration = Math.max(0, interludeDuration - 750);
2226
- scale = Math.max(0, scale) * .7;
2227
- curStyle += ` scale(${scale})`;
2228
- const dot0Opacity = clamp(.25, currentDuration * 3 / dotsDuration * .75, 1);
2229
- const dot1Opacity = clamp(.25, (currentDuration - dotsDuration / 3) * 3 / dotsDuration * .75, 1);
2230
- const dot2Opacity = clamp(.25, (currentDuration - dotsDuration / 3 * 2) * 3 / dotsDuration * .75, 1);
2231
- this.dot0.style.opacity = `${clamp(0, Math.max(0, globalOpacity * dot0Opacity), 1)}`;
2232
- this.dot1.style.opacity = `${clamp(0, Math.max(0, globalOpacity * dot1Opacity), 1)}`;
2233
- this.dot2.style.opacity = `${clamp(0, Math.max(0, globalOpacity * dot2Opacity), 1)}`;
2353
+ /**
2354
+ * 计算一组歌词在当前布局中的视觉呈现参数。
2355
+ *
2356
+ * 根据播放状态、缓冲状态、布局模式与间奏信息,
2357
+ * 生成一组歌词最终应使用的活跃状态、不透明度与模糊值。
2358
+ */
2359
+ function computeGroupPresentation(input) {
2360
+ const { groupIndex, scrollToIndex, latestIndex, hasBuffered, hidePassedLines, isPlaying, isNonDynamic, enableBlur, isUserScrolling, isCompact, interlude } = input;
2361
+ const isActive = hasBuffered || groupIndex >= scrollToIndex && groupIndex < latestIndex;
2362
+ const blurLevel = computeLineBlur({
2363
+ enableBlur,
2364
+ isUserScrolling,
2365
+ isActive,
2366
+ itemIndex: groupIndex,
2367
+ scrollToIndex,
2368
+ latestIndex,
2369
+ isCompact
2370
+ });
2371
+ let targetOpacity;
2372
+ if (hidePassedLines) if (groupIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2373
+ else if (hasBuffered) targetOpacity = .85;
2374
+ else targetOpacity = isNonDynamic ? .2 : 1;
2375
+ else if (hasBuffered) targetOpacity = .85;
2376
+ else targetOpacity = isNonDynamic ? .2 : 1;
2377
+ return {
2378
+ isActive,
2379
+ targetOpacity,
2380
+ blurLevel
2381
+ };
2382
+ }
2383
+ /**
2384
+ * 计算一行歌词在当前布局中的模糊等级。
2385
+ *
2386
+ * 越远离当前对齐区域的歌词会得到更高的模糊值;
2387
+ * 活跃行、滚动交互中或关闭模糊效果时返回 `0`。
2388
+ */
2389
+ function computeLineBlur(input) {
2390
+ const { enableBlur, isUserScrolling, isActive, itemIndex, scrollToIndex, latestIndex, isCompact } = input;
2391
+ if (!enableBlur || isUserScrolling || isActive) return 0;
2392
+ let blurLevel = 1;
2393
+ if (itemIndex < scrollToIndex) blurLevel += Math.abs(scrollToIndex - itemIndex) + 1;
2394
+ else blurLevel += Math.abs(itemIndex - Math.max(scrollToIndex, latestIndex));
2395
+ return isCompact ? blurLevel * .8 : blurLevel;
2396
+ }
2397
+ //#endregion
2398
+ //#region src/lyric-player/base/scroll.ts
2399
+ /**
2400
+ * 将滚动偏移量限制在当前允许的滚动边界内。
2401
+ *
2402
+ * 当手势滚动、滚轮滚动或惯性滚动更新了 {@link PlayerScrollState.scrollOffset}
2403
+ * 后,应调用本函数以避免视图越界。
2404
+ */
2405
+ function clampPlayerScrollOffset(scrollState) {
2406
+ scrollState.scrollOffset = clamp(scrollState.scrollOffset, scrollState.scrollBoundary.minOffset, scrollState.scrollBoundary.maxOffset);
2407
+ }
2408
+ /**
2409
+ * 重置滚动状态到未发生用户滚动时的初始状态。
2410
+ *
2411
+ * 本函数会清除当前偏移,并结束“已滚动”与“正在滚动”的标记;
2412
+ * **不会清理**外部持有的计时器或事件监听器。
2413
+ */
2414
+ function resetPlayerScrollState(scrollState) {
2415
+ scrollState.isScrolled = false;
2416
+ scrollState.scrollOffset = 0;
2417
+ scrollState.isUserScrolling = false;
2418
+ }
2419
+ /**
2420
+ * 向指定元素挂载歌词滚动相关的交互处理器。
2421
+ *
2422
+ * 该函数会处理:
2423
+ * - 触摸拖拽滚动
2424
+ * - 触摸结束后的惯性滚动
2425
+ * - 滚轮滚动
2426
+ * - 轻触时的点击透传
2427
+ *
2428
+ * 只更新 {@link PlayerScrollState} 并通过回调通知宿主执行布局或其它副作用,
2429
+ * 不直接依赖具体的播放器类实现。
2430
+ */
2431
+ function attachPlayerScrollHandlers(element, scrollState, callbacks) {
2432
+ let startScrollY = 0;
2433
+ let startTouchPosY = 0;
2434
+ let startTouchStartX = 0;
2435
+ let startTouchStartY = 0;
2436
+ let lastMoveY = 0;
2437
+ let startScrollTime = 0;
2438
+ let scrollSpeed = 0;
2439
+ let curScrollId = 0;
2440
+ element.addEventListener("touchstart", (evt) => {
2441
+ if (callbacks.onBeginScroll()) {
2442
+ scrollState.isUserScrolling = true;
2443
+ evt.preventDefault();
2444
+ startScrollY = scrollState.scrollOffset;
2445
+ startTouchPosY = evt.touches[0].screenY;
2446
+ lastMoveY = startTouchPosY;
2447
+ startTouchStartX = evt.touches[0].screenX;
2448
+ startTouchStartY = evt.touches[0].screenY;
2449
+ startScrollTime = Date.now();
2450
+ scrollSpeed = 0;
2451
+ callbacks.onLayout(true, true);
2452
+ }
2453
+ });
2454
+ element.addEventListener("touchmove", (evt) => {
2455
+ if (callbacks.onBeginScroll()) {
2456
+ evt.preventDefault();
2457
+ const currentY = evt.touches[0].screenY;
2458
+ const deltaY = currentY - startTouchPosY;
2459
+ scrollState.scrollOffset = startScrollY - deltaY;
2460
+ clampPlayerScrollOffset(scrollState);
2461
+ const now = Date.now();
2462
+ const dt = now - startScrollTime;
2463
+ if (dt > 0) scrollSpeed = (currentY - lastMoveY) / dt;
2464
+ lastMoveY = currentY;
2465
+ startScrollTime = now;
2466
+ callbacks.onLayout(true, true);
2467
+ }
2468
+ });
2469
+ element.addEventListener("touchend", (evt) => {
2470
+ if (callbacks.onBeginScroll()) {
2471
+ evt.preventDefault();
2472
+ const touch = evt.changedTouches[0];
2473
+ const moveX = Math.abs(touch.screenX - startTouchStartX);
2474
+ const moveY = Math.abs(touch.screenY - startTouchStartY);
2475
+ if (moveX < 10 && moveY < 10) {
2476
+ const target = document.elementFromPoint(touch.clientX, touch.clientY);
2477
+ if (target instanceof HTMLElement && callbacks.containsTarget(target)) callbacks.clickTarget(target);
2478
+ scrollState.isUserScrolling = false;
2479
+ callbacks.onEndScroll();
2480
+ return;
2481
+ }
2482
+ startTouchPosY = 0;
2483
+ const scrollId = ++curScrollId;
2484
+ if (Math.abs(scrollSpeed) < .1) scrollSpeed = 0;
2485
+ let lastFrameTime = performance.now();
2486
+ const onScrollFrame = (time) => {
2487
+ if (scrollId !== curScrollId) return;
2488
+ const dt = time - lastFrameTime;
2489
+ lastFrameTime = time;
2490
+ if (dt <= 0 || dt > 100) {
2491
+ requestAnimationFrame(onScrollFrame);
2492
+ return;
2493
+ }
2494
+ if (Math.abs(scrollSpeed) > .05) {
2495
+ scrollState.scrollOffset -= scrollSpeed * dt;
2496
+ clampPlayerScrollOffset(scrollState);
2497
+ const frictionFactor = .95 ** (dt / 16);
2498
+ scrollSpeed *= frictionFactor;
2499
+ callbacks.onLayout(true, true);
2500
+ requestAnimationFrame(onScrollFrame);
2501
+ } else {
2502
+ scrollState.isUserScrolling = false;
2503
+ callbacks.onEndScroll();
2504
+ }
2505
+ };
2506
+ requestAnimationFrame(onScrollFrame);
2507
+ } else scrollState.isUserScrolling = false;
2508
+ });
2509
+ element.addEventListener("wheel", (evt) => {
2510
+ if (callbacks.onBeginScroll()) {
2511
+ evt.preventDefault();
2512
+ if (evt.deltaMode === evt.DOM_DELTA_PIXEL) {
2513
+ scrollState.scrollOffset += evt.deltaY;
2514
+ clampPlayerScrollOffset(scrollState);
2515
+ callbacks.onLayout(true, false);
2234
2516
  } else {
2235
- curStyle += " scale(0)";
2236
- this.dot0.style.opacity = "0";
2237
- this.dot1.style.opacity = "0";
2238
- this.dot2.style.opacity = "0";
2517
+ scrollState.scrollOffset += evt.deltaY * 50;
2518
+ clampPlayerScrollOffset(scrollState);
2519
+ callbacks.onLayout(false, false);
2239
2520
  }
2240
- curStyle += ";";
2241
- if (this.lastStyle !== curStyle) {
2242
- this.element.setAttribute("style", curStyle);
2243
- this.lastStyle = curStyle;
2521
+ }
2522
+ }, { passive: false });
2523
+ }
2524
+ //#endregion
2525
+ //#region src/utils/eq-set.ts
2526
+ const eqSet = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x));
2527
+ //#endregion
2528
+ //#region src/lyric-player/base/timeline.ts
2529
+ /**
2530
+ * 计算指定时间点的热行/缓冲行状态转移的纯函数。其行为包括:
2531
+ *
2532
+ * - 根据当前时间和已有的热行状态,计算出新的热行状态,并返回应新增的热行 ID 和应移除的热行 ID
2533
+ * - 根据新的热行状态和已有的缓冲行状态,计算出应移除的缓冲行 ID
2534
+ */
2535
+ function computePlayerTimeState(input) {
2536
+ const { time, currentGroups, timelineState: { hotGroups, bufferedGroups } } = input;
2537
+ const nextHotGroups = new Set(hotGroups);
2538
+ const addedIds = /* @__PURE__ */ new Set();
2539
+ const removedHotIds = /* @__PURE__ */ new Set();
2540
+ const removedBufferedIds = /* @__PURE__ */ new Set();
2541
+ for (const lastHotId of hotGroups) {
2542
+ const group = currentGroups[lastHotId];
2543
+ if (!group || time < group.startTime || group.endTime <= time) {
2544
+ nextHotGroups.delete(lastHotId);
2545
+ removedHotIds.add(lastHotId);
2546
+ }
2547
+ }
2548
+ for (let id = 0; id < currentGroups.length; id++) {
2549
+ const group = currentGroups[id];
2550
+ if (!group) continue;
2551
+ if (group.startTime <= time && group.endTime > time && !nextHotGroups.has(id)) {
2552
+ nextHotGroups.add(id);
2553
+ addedIds.add(id);
2554
+ }
2555
+ }
2556
+ for (const id of bufferedGroups) if (!nextHotGroups.has(id)) removedBufferedIds.add(id);
2557
+ return {
2558
+ nextHotGroups,
2559
+ addedIds,
2560
+ removedHotIds,
2561
+ removedBufferedIds
2562
+ };
2563
+ }
2564
+ /**
2565
+ * 在 seeking 场景下,根据当前时间选出应对齐滚动到的目标行索引。
2566
+ *
2567
+ * 若当前仍存在缓冲行,则优先对齐到最靠前的缓冲行;
2568
+ * 否则对齐到第一条开始时间不小于当前时间的歌词行。
2569
+ */
2570
+ function pickScrollToIndexForSeek(time, currentGroups, bufferedGroups) {
2571
+ if (bufferedGroups.size > 0) return Math.min(...bufferedGroups);
2572
+ const foundIndex = currentGroups.findIndex((group) => group.startTime >= time);
2573
+ return foundIndex === -1 ? currentGroups.length : foundIndex;
2574
+ }
2575
+ /**
2576
+ * 提交时间线状态转移的纯函数。
2577
+ *
2578
+ * 把一次时间线状态转移写回 {@link PlayerTimelineState},
2579
+ * 并返回一份供宿主执行的副作用应用计划,例如启用/禁用哪些歌词行、
2580
+ * 是否需要重置用户滚动状态、是否需要触发布局。
2581
+ */
2582
+ function commitPlayerTimeState(input) {
2583
+ const { timelineState, time, currentGroups, hasBottomContent, stateResult } = input;
2584
+ const { addedIds, removedHotIds, removedBufferedIds } = stateResult;
2585
+ const { isSeeking } = timelineState;
2586
+ timelineState.currentTime = time;
2587
+ timelineState.hotGroups = stateResult.nextHotGroups;
2588
+ let shouldLayout = false;
2589
+ let shouldResetScroll = false;
2590
+ const groupsToEnable = [];
2591
+ const groupsToDisable = /* @__PURE__ */ new Set();
2592
+ if (isSeeking) {
2593
+ timelineState.bufferedGroups = new Set([...timelineState.hotGroups]);
2594
+ timelineState.scrollToIndex = pickScrollToIndexForSeek(time, currentGroups, timelineState.bufferedGroups);
2595
+ for (const id of removedHotIds) groupsToDisable.add(id);
2596
+ for (const id of timelineState.hotGroups) groupsToEnable.push(id);
2597
+ for (const id of removedBufferedIds) groupsToDisable.add(id);
2598
+ shouldResetScroll = true;
2599
+ shouldLayout = true;
2600
+ } else if (addedIds.size > 0) {
2601
+ for (const id of addedIds) {
2602
+ timelineState.bufferedGroups.add(id);
2603
+ groupsToEnable.push(id);
2604
+ }
2605
+ for (const id of removedBufferedIds) {
2606
+ timelineState.bufferedGroups.delete(id);
2607
+ groupsToDisable.add(id);
2608
+ }
2609
+ if (timelineState.bufferedGroups.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedGroups);
2610
+ shouldLayout = true;
2611
+ } else if (removedBufferedIds.size > 0 && eqSet(removedBufferedIds, timelineState.bufferedGroups)) {
2612
+ for (const id of timelineState.bufferedGroups) {
2613
+ if (timelineState.hotGroups.has(id)) continue;
2614
+ timelineState.bufferedGroups.delete(id);
2615
+ groupsToDisable.add(id);
2616
+ }
2617
+ shouldLayout = true;
2618
+ }
2619
+ if (timelineState.bufferedGroups.size === 0 && currentGroups.length > 0) {
2620
+ if (time >= currentGroups[currentGroups.length - 1].endTime) {
2621
+ const targetIndex = hasBottomContent ? currentGroups.length : currentGroups.length - 1;
2622
+ if (timelineState.scrollToIndex !== targetIndex) {
2623
+ timelineState.scrollToIndex = targetIndex;
2624
+ shouldLayout = true;
2244
2625
  }
2245
2626
  }
2246
2627
  }
2247
- dispose() {
2248
- this.element.remove();
2249
- }
2250
- };
2628
+ timelineState.lastCurrentTime = time;
2629
+ return {
2630
+ shouldLayout,
2631
+ shouldResetScroll,
2632
+ groupsToEnable,
2633
+ groupsToDisable: [...groupsToDisable]
2634
+ };
2635
+ }
2251
2636
  //#endregion
2252
- //#region src/lyric-player/base.ts
2637
+ //#region src/lyric-player/base/index.ts
2253
2638
  /**
2254
- * 歌词播放器的基类,已经包含了有关歌词操作和排版的功能,子类需要为其实现对应的显示展示操作
2639
+ * 歌词播放器的基类,已经包含了有关歌词操作和排版的功能,
2640
+ * 子类需要为其实现对应的显示展示操作
2255
2641
  */
2256
2642
  var LyricPlayerBase = class extends EventTarget {
2257
2643
  element = document.createElement("div");
2258
- currentTime = 0;
2259
- /** @internal */
2260
- lyricLinesSize = /* @__PURE__ */ new WeakMap();
2644
+ /** 播放时间线状态 */
2645
+ timelineState = {
2646
+ currentTime: 0,
2647
+ lastCurrentTime: 0,
2648
+ hotGroups: /* @__PURE__ */ new Set(),
2649
+ bufferedGroups: /* @__PURE__ */ new Set(),
2650
+ scrollToIndex: 0,
2651
+ isSeeking: false,
2652
+ isPlaying: true,
2653
+ initialLayoutFinished: false
2654
+ };
2261
2655
  /** @internal */
2262
- lyricLineElementMap = /* @__PURE__ */ new WeakMap();
2656
+ lyricGroupElementMap = /* @__PURE__ */ new WeakMap();
2263
2657
  currentLyricLines = [];
2264
2658
  processedLines = [];
2265
2659
  lyricLinesIndexes = /* @__PURE__ */ new WeakMap();
2266
- hotLines = /* @__PURE__ */ new Set();
2267
- bufferedLines = /* @__PURE__ */ new Set();
2268
2660
  isNonDynamic = false;
2269
2661
  hasDuetLine = false;
2270
- scrollToIndex = 0;
2271
2662
  disableSpring = false;
2272
- interludeDotsSize = [0, 0];
2663
+ layoutState = {
2664
+ interludeDotsSize: [0, 0],
2665
+ targetAlignIndex: 0,
2666
+ lastInterludeState: false,
2667
+ alignAnchor: LayoutAlignAnchor.Center,
2668
+ alignPosition: .35,
2669
+ overscanPx: 300
2670
+ };
2273
2671
  interludeDots = new InterludeDots();
2274
2672
  bottomLine = new BottomLineEl(this);
2275
2673
  enableBlur = true;
2276
2674
  enableScale = true;
2277
- maskObsceneWords = "";
2675
+ maskObsceneWords = MaskObsceneWordsMode.Disabled;
2278
2676
  maskObsceneWordChar = "*";
2279
2677
  hidePassedLines = false;
2280
- scrollBoundary = [0, 0];
2281
- currentLyricLineObjects = [];
2282
- isSeeking = false;
2283
- lastCurrentTime = 0;
2284
- alignAnchor = "center";
2285
- alignPosition = .35;
2286
- scrollOffset = 0;
2678
+ scrollState = {
2679
+ scrollBoundary: {
2680
+ minOffset: 0,
2681
+ maxOffset: 0
2682
+ },
2683
+ scrollOffset: 0,
2684
+ allowScroll: true,
2685
+ isScrolled: false,
2686
+ isUserScrolling: false
2687
+ };
2688
+ currentLyricGroups = [];
2689
+ lyricGroupSize = /* @__PURE__ */ new WeakMap();
2287
2690
  size = [0, 0];
2288
- allowScroll = true;
2289
2691
  isPageVisible = true;
2290
2692
  optimizeOptions = {};
2291
- initialLayoutFinished = false;
2292
- /**
2293
- * 标记用户是否正在进行滚动交互
2294
- */
2295
- isUserScrolling = false;
2296
- wheelTimeout;
2297
- /**
2298
- * 视图额外预渲染(overscan)距离,单位:像素。
2299
- * 用于决定在视口之外多少距离内也认为是“可见”,以便提前创建/保留行元素。
2300
- */
2301
- overscanPx = 300;
2693
+ /** 是否强制让背景人声行始终后置(即始终在主歌词下方显示,不前置背景人声) */
2694
+ alwaysPostpositionBackground = false;
2302
2695
  posXSpringParams = {
2303
2696
  mass: 1,
2304
2697
  damping: 10,
@@ -2321,13 +2714,12 @@ var LyricPlayerBase = class extends EventTarget {
2321
2714
  };
2322
2715
  onPageShow = () => {
2323
2716
  this.isPageVisible = true;
2324
- this.setCurrentTime(this.currentTime, true);
2717
+ this.setCurrentTime(this.timelineState.currentTime, true);
2325
2718
  };
2326
2719
  onPageHide = () => {
2327
2720
  this.isPageVisible = false;
2328
2721
  };
2329
2722
  scrolledHandler;
2330
- isScrolled = false;
2331
2723
  /** @internal */
2332
2724
  resizeObserver = new ResizeObserver(((entries) => {
2333
2725
  let shouldRelayout = false;
@@ -2338,8 +2730,8 @@ var LyricPlayerBase = class extends EventTarget {
2338
2730
  this.size[1] = rect.height;
2339
2731
  shouldRebuildPlayerStyle = true;
2340
2732
  } else if (entry.target === this.interludeDots.getElement()) {
2341
- this.interludeDotsSize[0] = entry.target.clientWidth;
2342
- this.interludeDotsSize[1] = entry.target.clientHeight;
2733
+ this.layoutState.interludeDotsSize[0] = entry.target.clientWidth;
2734
+ this.layoutState.interludeDotsSize[1] = entry.target.clientHeight;
2343
2735
  shouldRelayout = true;
2344
2736
  } else if (entry.target === this.bottomLine.getElement()) {
2345
2737
  const newSize = [entry.target.clientWidth, entry.target.clientHeight];
@@ -2349,13 +2741,13 @@ var LyricPlayerBase = class extends EventTarget {
2349
2741
  shouldRelayout = true;
2350
2742
  }
2351
2743
  } else {
2352
- const lineObj = this.lyricLineElementMap.get(entry.target);
2353
- if (lineObj) {
2744
+ const groupObj = this.lyricGroupElementMap.get(entry.target);
2745
+ if (groupObj) {
2354
2746
  const newSize = [entry.target.clientWidth, entry.target.clientHeight];
2355
- const oldSize = this.lyricLinesSize.get(lineObj) ?? [0, 0];
2747
+ const oldSize = this.lyricGroupSize.get(groupObj) ?? [0, 0];
2356
2748
  if (newSize[0] !== oldSize[0] || newSize[1] !== oldSize[1]) {
2357
- this.lyricLinesSize.set(lineObj, newSize);
2358
- lineObj.onLineSizeChange(newSize);
2749
+ this.lyricGroupSize.set(groupObj, newSize);
2750
+ groupObj.onLineSizeChange(newSize);
2359
2751
  shouldRelayout = true;
2360
2752
  }
2361
2753
  }
@@ -2364,8 +2756,6 @@ var LyricPlayerBase = class extends EventTarget {
2364
2756
  if (shouldRebuildPlayerStyle) this.onResize();
2365
2757
  }));
2366
2758
  wordFadeWidth = .5;
2367
- targetAlignIndex = 0;
2368
- lastInterludeState = false;
2369
2759
  constructor(element) {
2370
2760
  super();
2371
2761
  if (element) this.element = element;
@@ -2377,114 +2767,27 @@ var LyricPlayerBase = class extends EventTarget {
2377
2767
  this.interludeDots.setTransform(0, 200);
2378
2768
  window.addEventListener("pageshow", this.onPageShow);
2379
2769
  window.addEventListener("pagehide", this.onPageHide);
2380
- let startScrollY = 0;
2381
- let startTouchPosY = 0;
2382
- let startTouchStartX = 0;
2383
- let startTouchStartY = 0;
2384
- let lastMoveY = 0;
2385
- let startScrollTime = 0;
2386
- let scrollSpeed = 0;
2387
- let curScrollId = 0;
2388
- this.element.addEventListener("touchstart", (evt) => {
2389
- if (this.beginScrollHandler()) {
2390
- this.isUserScrolling = true;
2391
- evt.preventDefault();
2392
- startScrollY = this.scrollOffset;
2393
- startTouchPosY = evt.touches[0].screenY;
2394
- lastMoveY = startTouchPosY;
2395
- startTouchStartX = evt.touches[0].screenX;
2396
- startTouchStartY = evt.touches[0].screenY;
2397
- startScrollTime = Date.now();
2398
- scrollSpeed = 0;
2399
- this.calcLayout(true, true);
2400
- }
2401
- });
2402
- this.element.addEventListener("touchmove", (evt) => {
2403
- if (this.beginScrollHandler()) {
2404
- evt.preventDefault();
2405
- const currentY = evt.touches[0].screenY;
2406
- const deltaY = currentY - startTouchPosY;
2407
- this.scrollOffset = startScrollY - deltaY;
2408
- this.limitScrollOffset();
2409
- const now = Date.now();
2410
- const dt = now - startScrollTime;
2411
- if (dt > 0) scrollSpeed = (currentY - lastMoveY) / dt;
2412
- lastMoveY = currentY;
2413
- startScrollTime = now;
2414
- this.calcLayout(true, true);
2415
- }
2416
- });
2417
- this.element.addEventListener("touchend", (evt) => {
2418
- if (this.beginScrollHandler()) {
2419
- evt.preventDefault();
2420
- const touch = evt.changedTouches[0];
2421
- const moveX = Math.abs(touch.screenX - startTouchStartX);
2422
- const moveY = Math.abs(touch.screenY - startTouchStartY);
2423
- if (moveX < 10 && moveY < 10) {
2424
- const target = document.elementFromPoint(touch.clientX, touch.clientY);
2425
- if (target && this.element.contains(target)) target.click();
2426
- this.isUserScrolling = false;
2427
- this.endScrollHandler();
2428
- return;
2429
- }
2430
- startTouchPosY = 0;
2431
- const scrollId = ++curScrollId;
2432
- if (Math.abs(scrollSpeed) < .1) scrollSpeed = 0;
2433
- let lastFrameTime = performance.now();
2434
- const onScrollFrame = (time) => {
2435
- if (scrollId !== curScrollId) return;
2436
- const dt = time - lastFrameTime;
2437
- lastFrameTime = time;
2438
- if (dt <= 0 || dt > 100) {
2439
- requestAnimationFrame(onScrollFrame);
2440
- return;
2441
- }
2442
- if (Math.abs(scrollSpeed) > .05) {
2443
- this.scrollOffset -= scrollSpeed * dt;
2444
- this.limitScrollOffset();
2445
- const frictionFactor = .95 ** (dt / 16);
2446
- scrollSpeed *= frictionFactor;
2447
- this.calcLayout(true, true);
2448
- requestAnimationFrame(onScrollFrame);
2449
- } else {
2450
- this.isUserScrolling = false;
2451
- this.endScrollHandler();
2452
- }
2453
- };
2454
- requestAnimationFrame(onScrollFrame);
2455
- } else this.isUserScrolling = false;
2770
+ attachPlayerScrollHandlers(this.element, this.scrollState, {
2771
+ onBeginScroll: () => this.beginScrollHandler(),
2772
+ onEndScroll: () => this.endScrollHandler(),
2773
+ onLayout: (sync, force) => this.calcLayout(sync, force),
2774
+ containsTarget: (target) => this.element.contains(target),
2775
+ clickTarget: (target) => target.click()
2456
2776
  });
2457
- this.element.addEventListener("wheel", (evt) => {
2458
- if (this.beginScrollHandler()) {
2459
- evt.preventDefault();
2460
- if (evt.deltaMode === evt.DOM_DELTA_PIXEL) {
2461
- this.scrollOffset += evt.deltaY;
2462
- this.limitScrollOffset();
2463
- this.calcLayout(true, false);
2464
- } else {
2465
- this.scrollOffset += evt.deltaY * 50;
2466
- this.limitScrollOffset();
2467
- this.calcLayout(false, false);
2468
- }
2469
- }
2470
- }, { passive: false });
2471
2777
  }
2472
2778
  beginScrollHandler() {
2473
- const allowed = this.allowScroll;
2779
+ const allowed = this.scrollState.allowScroll;
2474
2780
  if (allowed) {
2475
- this.isScrolled = true;
2781
+ this.scrollState.isScrolled = true;
2476
2782
  clearTimeout(this.scrolledHandler);
2477
2783
  this.scrolledHandler = setTimeout(() => {
2478
- this.isScrolled = false;
2479
- this.scrollOffset = 0;
2784
+ this.scrollState.isScrolled = false;
2785
+ this.scrollState.scrollOffset = 0;
2480
2786
  }, 5e3);
2481
2787
  }
2482
2788
  return allowed;
2483
2789
  }
2484
2790
  endScrollHandler() {}
2485
- limitScrollOffset() {
2486
- this.scrollOffset = Math.max(Math.min(this.scrollBoundary[1], this.scrollOffset), this.scrollBoundary[0]);
2487
- }
2488
2791
  /**
2489
2792
  * 设置文字动画的渐变宽度,单位以歌词行的主文字字体大小的倍数为单位,默认为 0.5,即一个全角字符的一半宽度
2490
2793
  *
@@ -2526,7 +2829,7 @@ var LyricPlayerBase = class extends EventTarget {
2526
2829
  return this.wordFadeWidth;
2527
2830
  }
2528
2831
  setIsSeeking(isSeeking) {
2529
- this.isSeeking = isSeeking;
2832
+ this.timelineState.isSeeking = isSeeking;
2530
2833
  }
2531
2834
  /**
2532
2835
  * 设置是否隐藏已经播放过的歌词行,默认不隐藏
@@ -2564,13 +2867,13 @@ var LyricPlayerBase = class extends EventTarget {
2564
2867
  const c = char.charAt(0) || "*";
2565
2868
  if (this.maskObsceneWordChar === c) return;
2566
2869
  this.maskObsceneWordChar = c;
2567
- if (this.maskObsceneWords !== "") {
2870
+ if (this.maskObsceneWords !== MaskObsceneWordsMode.Disabled) {
2568
2871
  this.rebuildLyricLines();
2569
2872
  this.calcLayout();
2570
2873
  }
2571
2874
  }
2572
2875
  rebuildLyricLines() {
2573
- for (const lineObj of this.currentLyricLineObjects) lineObj.rebuildElement();
2876
+ for (const group of this.currentLyricGroups) group.rebuildAllLines();
2574
2877
  }
2575
2878
  /**
2576
2879
  * 根据当前配置处理不雅用语单词
@@ -2579,10 +2882,10 @@ var LyricPlayerBase = class extends EventTarget {
2579
2882
  */
2580
2883
  processObsceneWord(word) {
2581
2884
  const text = word.word;
2582
- if (!word.obscene || this.maskObsceneWords === "") return text;
2885
+ if (!word.obscene || this.maskObsceneWords === MaskObsceneWordsMode.Disabled) return text;
2583
2886
  const maskChar = this.maskObsceneWordChar;
2584
- if (this.maskObsceneWords === "full-mask") return text.replace(/\S/g, maskChar);
2585
- if (this.maskObsceneWords === "partial-mask") {
2887
+ if (this.maskObsceneWords === MaskObsceneWordsMode.FullMask) return text.replace(/\S/g, maskChar);
2888
+ if (this.maskObsceneWords === MaskObsceneWordsMode.PartialMask) {
2586
2889
  const trimmed = text.trim();
2587
2890
  if (trimmed.length <= 2) return text.replace(/\S/g, maskChar);
2588
2891
  const startPos = text.indexOf(trimmed);
@@ -2600,25 +2903,25 @@ var LyricPlayerBase = class extends EventTarget {
2600
2903
  * @param alignAnchor 歌词行对齐方式,详情见函数说明
2601
2904
  */
2602
2905
  setAlignAnchor(alignAnchor) {
2603
- this.alignAnchor = alignAnchor;
2906
+ this.layoutState.alignAnchor = alignAnchor;
2604
2907
  }
2605
2908
  /**
2606
2909
  * 设置默认的歌词行对齐位置,相对于整个歌词播放组件的大小位置,默认为 `0.5`
2607
2910
  * @param alignPosition 一个 `[0.0-1.0]` 之间的任意数字,代表组件高度由上到下的比例位置
2608
2911
  */
2609
2912
  setAlignPosition(alignPosition) {
2610
- this.alignPosition = alignPosition;
2913
+ this.layoutState.alignPosition = alignPosition;
2611
2914
  }
2612
2915
  /**
2613
2916
  * 设置 overscan(视图上下额外缓冲渲染区)距离,单位:像素。
2614
2917
  * @param px 像素值,默认 300
2615
2918
  */
2616
2919
  setOverscanPx(px) {
2617
- this.overscanPx = Math.max(0, px | 0);
2920
+ this.layoutState.overscanPx = clampPositive(px | 0);
2618
2921
  }
2619
2922
  /** 获取当前 overscan 像素距离 */
2620
2923
  getOverscanPx() {
2621
- return this.overscanPx;
2924
+ return this.layoutState.overscanPx;
2622
2925
  }
2623
2926
  /**
2624
2927
  * 设置是否使用物理弹簧算法实现歌词动画效果,默认启用
@@ -2641,34 +2944,6 @@ var LyricPlayerBase = class extends EventTarget {
2641
2944
  return !this.disableSpring;
2642
2945
  }
2643
2946
  /**
2644
- * 获取当前播放时间里是否处于间奏区间
2645
- * 如果是则会返回单位为毫秒的始末时间
2646
- * 否则返回 undefined
2647
- *
2648
- * 这个只允许内部调用
2649
- * @returns [开始时间,结束时间,大概处于的歌词行ID,下一句是否为对唱歌词] 或 undefined 如果不处于间奏区间
2650
- */
2651
- getCurrentInterlude() {
2652
- const currentTime = this.currentTime + 20;
2653
- const currentIndex = this.scrollToIndex;
2654
- const lines = this.processedLines;
2655
- const checkGap = (k) => {
2656
- if (k < -1 || k >= lines.length - 1) return void 0;
2657
- const prevLine = k === -1 ? null : lines[k];
2658
- const nextLine = lines[k + 1];
2659
- const gapStart = prevLine ? prevLine.endTime : 0;
2660
- const gapEnd = Math.max(gapStart, nextLine.startTime - 250);
2661
- if (gapEnd - gapStart < 4e3) return;
2662
- if (gapEnd > currentTime && gapStart < currentTime) return [
2663
- Math.max(gapStart, currentTime),
2664
- gapEnd,
2665
- k,
2666
- nextLine.isDuet
2667
- ];
2668
- };
2669
- return checkGap(currentIndex - 1) || checkGap(currentIndex) || checkGap(currentIndex + 1);
2670
- }
2671
- /**
2672
2947
  * 设置歌词的优化配置项,这些配置项默认全部开启
2673
2948
  *
2674
2949
  * 注意,如果在 `setLyricLines` 之后修改此配置,需要重新调用 `setLyricLines()` 才能对当前歌词生效
@@ -2688,9 +2963,9 @@ var LyricPlayerBase = class extends EventTarget {
2688
2963
  */
2689
2964
  setLyricLines(lines, initialTime = 0) {
2690
2965
  if (process.env.NODE_ENV !== "production") console.log("设置歌词行", lines, initialTime);
2691
- this.initialLayoutFinished = true;
2692
- this.lastCurrentTime = initialTime;
2693
- this.currentTime = initialTime;
2966
+ this.timelineState.initialLayoutFinished = true;
2967
+ this.timelineState.lastCurrentTime = initialTime;
2968
+ this.timelineState.currentTime = initialTime;
2694
2969
  this.currentLyricLines = (0, _ungap_structured_clone.default)(lines);
2695
2970
  this.processedLines = (0, _ungap_structured_clone.default)(this.currentLyricLines);
2696
2971
  optimizeLyricLines(this.processedLines, this.optimizeOptions);
@@ -2700,11 +2975,11 @@ var LyricPlayerBase = class extends EventTarget {
2700
2975
  break;
2701
2976
  }
2702
2977
  this.hasDuetLine = this.processedLines.some((line) => line.isDuet);
2703
- for (const line of this.currentLyricLineObjects) line.dispose();
2978
+ for (const group of this.currentLyricGroups) group.dispose();
2979
+ this.currentLyricGroups = [];
2704
2980
  this.interludeDots.setInterlude(void 0);
2705
- this.hotLines.clear();
2706
- this.bufferedLines.clear();
2707
- this.setCurrentTime(0, true);
2981
+ this.timelineState.hotGroups.clear();
2982
+ this.timelineState.bufferedGroups.clear();
2708
2983
  if (process.env.NODE_ENV !== "production") console.log("歌词处理完成", this);
2709
2984
  }
2710
2985
  /**
@@ -2712,143 +2987,39 @@ var LyricPlayerBase = class extends EventTarget {
2712
2987
  * @returns 当前是否在播放
2713
2988
  */
2714
2989
  getIsPlaying() {
2715
- return this.isPlaying;
2990
+ return this.timelineState.isPlaying;
2716
2991
  }
2717
2992
  /**
2718
- * 设置当前播放进度,单位为毫秒且**必须是整数**,此时将会更新内部的歌词进度信息
2719
- * 内部会根据调用间隔和播放进度自动决定如何滚动和显示歌词,所以这个的调用频率越快越准确越好
2993
+ * 设置当前播放进度,此时将会更新内部的歌词进度信息。
2994
+ *
2995
+ * 内部会根据调用间隔和播放进度自动决定如何滚动和显示歌词,所以这个的调用频率越快越准确越好。
2996
+ * 调用完成后,应每帧调用 {@link update} 方法来执行歌词动画效果。**此函数本身不会触发动画效果**。
2720
2997
  *
2721
- * 调用完成后,可以每帧调用 `update` 函数来执行歌词动画效果
2722
2998
  * @param time 当前播放进度,单位为毫秒
2723
2999
  */
2724
3000
  setCurrentTime(time, isSeek = false) {
2725
- this.currentTime = time;
2726
- if (!this.initialLayoutFinished && !isSeek) return;
2727
- const removedHotIds = /* @__PURE__ */ new Set();
2728
- const removedIds = /* @__PURE__ */ new Set();
2729
- const addedIds = /* @__PURE__ */ new Set();
2730
- for (const lastHotId of this.hotLines) {
2731
- const line = this.processedLines[lastHotId];
2732
- if (line) {
2733
- if (line.isBG) continue;
2734
- const nextLine = this.processedLines[lastHotId + 1];
2735
- if (nextLine?.isBG) {
2736
- const nextMainLine = this.processedLines[lastHotId + 2];
2737
- const startTime = Math.min(line.startTime, nextLine?.startTime);
2738
- const endTime = Math.min(Math.max(line.endTime, nextMainLine?.startTime ?? Number.MAX_VALUE), Math.max(line.endTime, nextLine?.endTime));
2739
- if (startTime > time || endTime <= time) {
2740
- this.hotLines.delete(lastHotId);
2741
- removedHotIds.add(lastHotId);
2742
- this.hotLines.delete(lastHotId + 1);
2743
- removedHotIds.add(lastHotId + 1);
2744
- if (isSeek) {
2745
- this.currentLyricLineObjects[lastHotId]?.disable();
2746
- this.currentLyricLineObjects[lastHotId + 1]?.disable();
2747
- }
2748
- }
2749
- } else if (line.startTime > time || line.endTime <= time) {
2750
- this.hotLines.delete(lastHotId);
2751
- removedHotIds.add(lastHotId);
2752
- if (isSeek) this.currentLyricLineObjects[lastHotId]?.disable();
2753
- }
2754
- } else {
2755
- this.hotLines.delete(lastHotId);
2756
- removedHotIds.add(lastHotId);
2757
- if (isSeek) this.currentLyricLineObjects[lastHotId]?.disable();
2758
- }
2759
- }
2760
- this.currentLyricLineObjects.forEach((lineObj, id, arr) => {
2761
- const line = lineObj.getLine();
2762
- if (!line.isBG && line.startTime <= time && line.endTime > time) {
2763
- if (isSeek) lineObj.enable(time, this.isPlaying);
2764
- if (!this.hotLines.has(id)) {
2765
- this.hotLines.add(id);
2766
- addedIds.add(id);
2767
- if (!isSeek) lineObj.enable();
2768
- if (arr[id + 1]?.getLine()?.isBG) {
2769
- this.hotLines.add(id + 1);
2770
- addedIds.add(id + 1);
2771
- if (isSeek) arr[id + 1].enable(time, this.isPlaying);
2772
- else arr[id + 1].enable();
2773
- }
2774
- }
2775
- }
3001
+ time = Math.round(time);
3002
+ const { timelineState } = this;
3003
+ timelineState.isSeeking = Boolean(isSeek);
3004
+ timelineState.currentTime = time;
3005
+ if (!timelineState.initialLayoutFinished && !timelineState.isSeeking) return;
3006
+ const stateResult = computePlayerTimeState({
3007
+ time,
3008
+ currentGroups: this.currentLyricGroups,
3009
+ timelineState
2776
3010
  });
2777
- for (const v of this.bufferedLines) if (!this.hotLines.has(v)) {
2778
- removedIds.add(v);
2779
- if (isSeek) this.currentLyricLineObjects[v]?.disable();
2780
- }
2781
- if (isSeek) {
2782
- this.bufferedLines.clear();
2783
- for (const v of this.hotLines) this.bufferedLines.add(v);
2784
- if (this.bufferedLines.size > 0) this.scrollToIndex = Math.min(...this.bufferedLines);
2785
- else {
2786
- const foundIndex = this.processedLines.findIndex((line) => line.startTime >= time);
2787
- this.scrollToIndex = foundIndex === -1 ? this.processedLines.length : foundIndex;
2788
- }
2789
- this.resetScroll();
2790
- this.calcLayout();
2791
- } else if (removedIds.size > 0 || addedIds.size > 0) if (removedIds.size === 0 && addedIds.size > 0) {
2792
- for (const v of addedIds) {
2793
- this.bufferedLines.add(v);
2794
- this.currentLyricLineObjects[v]?.enable();
2795
- }
2796
- this.scrollToIndex = Math.min(...this.bufferedLines);
2797
- this.calcLayout();
2798
- } else if (addedIds.size === 0 && removedIds.size > 0) {
2799
- if (eqSet(removedIds, this.bufferedLines)) {
2800
- for (const v of this.bufferedLines) if (!this.hotLines.has(v)) {
2801
- this.bufferedLines.delete(v);
2802
- this.currentLyricLineObjects[v]?.disable();
2803
- }
2804
- this.calcLayout();
2805
- }
2806
- } else {
2807
- for (const v of addedIds) {
2808
- this.bufferedLines.add(v);
2809
- this.currentLyricLineObjects[v]?.enable();
2810
- }
2811
- for (const v of removedIds) {
2812
- this.bufferedLines.delete(v);
2813
- this.currentLyricLineObjects[v]?.disable();
2814
- }
2815
- if (this.bufferedLines.size > 0) this.scrollToIndex = Math.min(...this.bufferedLines);
2816
- this.calcLayout();
2817
- }
2818
- if (this.bufferedLines.size === 0 && this.processedLines.length > 0) {
2819
- const lastLine = this.processedLines[this.processedLines.length - 1];
2820
- const hasBottomContent = this.bottomLine.getElement().innerHTML.trim().length > 0;
2821
- if (time >= lastLine.endTime) {
2822
- const targetIndex = hasBottomContent ? this.processedLines.length : this.processedLines.length - 1;
2823
- if (this.scrollToIndex !== targetIndex) {
2824
- this.scrollToIndex = targetIndex;
2825
- this.calcLayout();
2826
- }
2827
- }
2828
- }
2829
- this.lastCurrentTime = time;
2830
- }
2831
- updateDynamicSpringParams() {
2832
- if (!this.getEnableSpring() || this.processedLines.length === 0) return;
2833
- const currentIndex = this.scrollToIndex;
2834
- const currentLine = this.processedLines[currentIndex];
2835
- const prevLine = this.processedLines[currentIndex - 1];
2836
- if (currentLine && prevLine) {
2837
- const interval = currentLine.startTime - (prevLine?.words[0]?.startTime ?? prevLine.startTime);
2838
- const MIN_INTERVAL = 100;
2839
- const MAX_INTERVAL = 800;
2840
- const clampedInterval = Math.max(MIN_INTERVAL, Math.min(MAX_INTERVAL, interval));
2841
- const MAX_STIFFNESS = 220;
2842
- const MIN_STIFFNESS = 170;
2843
- let ratio = 1 - (clampedInterval - MIN_INTERVAL) / (MAX_INTERVAL - MIN_INTERVAL);
2844
- ratio = ratio ** .2;
2845
- const targetStiffness = MIN_STIFFNESS + ratio * (MAX_STIFFNESS - MIN_STIFFNESS);
2846
- const targetDamping = Math.sqrt(targetStiffness) * 2.2;
2847
- this.setLinePosYSpringParams({
2848
- stiffness: targetStiffness,
2849
- damping: targetDamping
2850
- });
2851
- }
3011
+ const hasBottomContent = this.bottomLine.getElement().innerHTML.trim().length > 0;
3012
+ const commitResult = commitPlayerTimeState({
3013
+ timelineState,
3014
+ time,
3015
+ currentGroups: this.currentLyricGroups,
3016
+ hasBottomContent,
3017
+ stateResult
3018
+ });
3019
+ for (const id of commitResult.groupsToDisable) this.currentLyricGroups[id]?.disable();
3020
+ for (const id of commitResult.groupsToEnable) this.currentLyricGroups[id]?.enable();
3021
+ if (commitResult.shouldResetScroll) this.resetScroll();
3022
+ if (commitResult.shouldLayout) this.calcLayout();
2852
3023
  }
2853
3024
  /**
2854
3025
  * 重新布局定位歌词行的位置,调用完成后再逐帧调用 `update`
@@ -2868,102 +3039,102 @@ var LyricPlayerBase = class extends EventTarget {
2868
3039
  * @param force 是否绕过弹簧效果强制更新位置
2869
3040
  */
2870
3041
  async calcLayout(sync = false, force = false) {
2871
- const interlude = this.getCurrentInterlude();
3042
+ const interlude = computeCurrentInterlude({
3043
+ currentTime: this.timelineState.currentTime,
3044
+ scrollToIndex: this.timelineState.scrollToIndex,
3045
+ currentGroups: this.currentLyricGroups
3046
+ });
2872
3047
  const isInterludeActive = !!interlude;
2873
- if (this.targetAlignIndex !== this.scrollToIndex || this.lastInterludeState !== isInterludeActive) {
2874
- this.lastInterludeState = isInterludeActive;
2875
- if (this.isSeeking) this.setLinePosYSpringParams({
2876
- stiffness: 90,
2877
- damping: 15
2878
- });
2879
- else if (isInterludeActive) this.setLinePosYSpringParams({
2880
- stiffness: 90,
2881
- damping: 15
3048
+ if (this.layoutState.targetAlignIndex !== this.timelineState.scrollToIndex || this.layoutState.lastInterludeState !== isInterludeActive) {
3049
+ this.layoutState.lastInterludeState = isInterludeActive;
3050
+ const springParams = computeLinePosYSpringParams({
3051
+ enabled: this.getEnableSpring(),
3052
+ currentGroups: this.currentLyricGroups,
3053
+ scrollToIndex: this.timelineState.scrollToIndex,
3054
+ isSeeking: this.timelineState.isSeeking,
3055
+ isInterludeActive
2882
3056
  });
2883
- else this.updateDynamicSpringParams();
3057
+ if (springParams.shouldUpdate && springParams.params) this.setLinePosYSpringParams(springParams.params);
2884
3058
  }
2885
- let curPos = -this.scrollOffset;
2886
- const targetAlignIndex = this.scrollToIndex;
3059
+ let curPos = -this.scrollState.scrollOffset;
3060
+ const targetAlignIndex = this.timelineState.scrollToIndex;
2887
3061
  let isNextDuet = false;
2888
- if (interlude) isNextDuet = interlude[3];
3062
+ if (interlude) isNextDuet = interlude.isNextDuet;
2889
3063
  else this.interludeDots.setInterlude(void 0);
2890
3064
  const dotMargin = (this.baseFontSize || 24) * .4;
2891
- const totalInterludeHeight = this.interludeDotsSize[1] + dotMargin * 2;
3065
+ const totalInterludeHeight = this.layoutState.interludeDotsSize[1] + dotMargin * 2;
2892
3066
  if (interlude) {
2893
- if (interlude[2] !== -1) curPos -= totalInterludeHeight;
3067
+ if (interlude.anchorLineIndex !== -1) curPos -= totalInterludeHeight;
2894
3068
  }
2895
3069
  const LINE_HEIGHT_FALLBACK = this.size[1] / 5;
2896
- const scrollOffset = this.currentLyricLineObjects.slice(0, targetAlignIndex).reduce((acc, el) => acc + (el.getLine().isBG && this.isPlaying ? 0 : this.lyricLinesSize.get(el)?.[1] ?? LINE_HEIGHT_FALLBACK), 0);
2897
- this.scrollBoundary[0] = -scrollOffset;
3070
+ const scrollOffset = this.currentLyricGroups.slice(0, targetAlignIndex).reduce((acc, group) => acc + (this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK), 0);
3071
+ this.scrollState.scrollBoundary.minOffset = -scrollOffset;
2898
3072
  curPos -= scrollOffset;
2899
- curPos += this.size[1] * this.alignPosition;
2900
- const curLine = this.currentLyricLineObjects[targetAlignIndex];
2901
- this.targetAlignIndex = targetAlignIndex;
2902
- const isBottomFocused = targetAlignIndex === this.currentLyricLineObjects.length;
3073
+ curPos += this.size[1] * this.layoutState.alignPosition;
3074
+ const curGroup = this.currentLyricGroups[targetAlignIndex];
3075
+ this.layoutState.targetAlignIndex = targetAlignIndex;
3076
+ const isBottomFocused = targetAlignIndex === this.currentLyricGroups.length;
2903
3077
  this.bottomLine.setFocused(isBottomFocused);
2904
- let targetLineHeight = 0;
2905
- if (curLine) targetLineHeight = this.lyricLinesSize.get(curLine)?.[1] ?? LINE_HEIGHT_FALLBACK;
2906
- else if (isBottomFocused) targetLineHeight = this.bottomLine.lineSize[1];
2907
- if (targetLineHeight > 0) switch (this.alignAnchor) {
2908
- case "bottom":
3078
+ const targetLineHeight = curGroup ? this.lyricGroupSize.get(curGroup)?.[1] ?? LINE_HEIGHT_FALLBACK : isBottomFocused ? this.bottomLine.lineSize[1] : 0;
3079
+ if (targetLineHeight > 0) switch (this.layoutState.alignAnchor) {
3080
+ case LayoutAlignAnchor.Bottom:
2909
3081
  curPos -= targetLineHeight;
2910
3082
  break;
2911
- case "center":
3083
+ case LayoutAlignAnchor.Center:
2912
3084
  curPos -= targetLineHeight / 2;
2913
3085
  break;
2914
- case "top": break;
3086
+ case LayoutAlignAnchor.Top: break;
2915
3087
  }
2916
- const latestIndex = Math.max(...this.bufferedLines);
3088
+ const latestIndex = Math.max(...this.timelineState.bufferedGroups);
2917
3089
  let delay = 0;
2918
3090
  let baseDelay = sync ? 0 : .05;
2919
3091
  let setDots = false;
2920
- this.currentLyricLineObjects.forEach((lineObj, i) => {
2921
- const hasBuffered = this.bufferedLines.has(i);
2922
- const isActive = hasBuffered || i >= this.scrollToIndex && i < latestIndex;
2923
- const line = lineObj.getLine();
2924
- const shouldShowDots = interlude && i === interlude[2] + 1;
3092
+ this.currentLyricGroups.forEach((group, i) => {
3093
+ const hasBuffered = this.timelineState.bufferedGroups.has(i);
3094
+ const shouldShowDots = interlude && i === interlude.anchorLineIndex + 1;
2925
3095
  if (!setDots && shouldShowDots) {
2926
3096
  setDots = true;
2927
3097
  curPos += dotMargin;
2928
3098
  let targetX = 0;
2929
- if (interlude && isNextDuet) targetX = this.size[0] - this.interludeDotsSize[0];
3099
+ if (interlude && isNextDuet) targetX = this.size[0] - this.layoutState.interludeDotsSize[0];
2930
3100
  this.interludeDots.setTransform(targetX, curPos);
2931
- if (interlude) this.interludeDots.setInterlude([interlude[0], interlude[1]]);
2932
- curPos += this.interludeDotsSize[1];
3101
+ if (interlude) this.interludeDots.setInterlude([interlude.startTime, interlude.endTime]);
3102
+ curPos += this.layoutState.interludeDotsSize[1];
2933
3103
  curPos += dotMargin;
2934
3104
  }
2935
- let targetOpacity;
2936
- if (this.hidePassedLines) if (i < (interlude ? interlude[2] + 1 : this.scrollToIndex) && this.isPlaying) targetOpacity = 1e-5;
2937
- else if (hasBuffered) targetOpacity = .85;
2938
- else targetOpacity = this.isNonDynamic ? .2 : 1;
2939
- else if (hasBuffered) targetOpacity = .85;
2940
- else targetOpacity = this.isNonDynamic ? .2 : 1;
2941
- const blurLevel = this.calculateBlur(i, isActive, latestIndex);
2942
- const SCALE_ASPECT = this.enableScale ? 97 : 100;
2943
- let targetScale = 100;
2944
- if (!isActive && this.isPlaying) if (line.isBG) targetScale = 75;
2945
- else targetScale = SCALE_ASPECT;
2946
- const renderMode = isActive ? 1 : 0;
2947
- lineObj.setTransform(curPos, targetScale, targetOpacity, blurLevel, force, delay, renderMode);
2948
- if (line.isBG && (isActive || !this.isPlaying)) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
2949
- else if (!line.isBG) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
2950
- if (curPos >= 0 && !this.isSeeking) {
2951
- if (!line.isBG) delay += baseDelay;
2952
- if (i >= this.scrollToIndex) baseDelay /= 1.05;
3105
+ const presentation = computeGroupPresentation({
3106
+ groupIndex: i,
3107
+ scrollToIndex: this.timelineState.scrollToIndex,
3108
+ latestIndex,
3109
+ hasBuffered,
3110
+ hidePassedLines: this.hidePassedLines,
3111
+ isPlaying: this.timelineState.isPlaying,
3112
+ isNonDynamic: this.isNonDynamic,
3113
+ enableBlur: this.enableBlur,
3114
+ isUserScrolling: this.scrollState.isUserScrolling,
3115
+ isCompact: window.innerWidth <= 1024,
3116
+ interlude
3117
+ });
3118
+ group.setTransform(curPos, force, delay, presentation.isActive, presentation.targetOpacity, presentation.blurLevel);
3119
+ curPos += this.lyricGroupSize.get(group)?.[1] ?? LINE_HEIGHT_FALLBACK;
3120
+ if (curPos >= 0 && !this.timelineState.isSeeking) {
3121
+ delay += baseDelay;
3122
+ if (i >= this.timelineState.scrollToIndex) baseDelay /= 1.05;
2953
3123
  }
2954
3124
  });
2955
- this.scrollBoundary[1] = curPos + this.scrollOffset - this.size[1] / 2;
2956
- const bottomIndex = this.currentLyricLineObjects.length;
2957
- const finalBottomBlur = this.calculateBlur(bottomIndex, isBottomFocused, latestIndex);
3125
+ this.scrollState.scrollBoundary.maxOffset = curPos + this.scrollState.scrollOffset - this.size[1] / 2;
3126
+ const bottomIndex = this.currentLyricGroups.length;
3127
+ const finalBottomBlur = computeLineBlur({
3128
+ enableBlur: this.enableBlur,
3129
+ isUserScrolling: this.scrollState.isUserScrolling,
3130
+ isActive: isBottomFocused,
3131
+ itemIndex: bottomIndex,
3132
+ scrollToIndex: this.timelineState.scrollToIndex,
3133
+ latestIndex,
3134
+ isCompact: window.innerWidth <= 1024
3135
+ });
2958
3136
  this.bottomLine.setTransform(0, curPos, finalBottomBlur, force, delay);
2959
3137
  }
2960
- calculateBlur(itemIndex, isActive, latestIndex) {
2961
- if (!this.enableBlur || this.isUserScrolling || isActive) return 0;
2962
- let blurLevel = 1;
2963
- if (itemIndex < this.scrollToIndex) blurLevel += Math.abs(this.scrollToIndex - itemIndex) + 1;
2964
- else blurLevel += Math.abs(itemIndex - Math.max(this.scrollToIndex, latestIndex));
2965
- return window.innerWidth <= 1024 ? blurLevel * .8 : blurLevel;
2966
- }
2967
3138
  /**
2968
3139
  * 设置所有歌词行在横坐标上的弹簧属性,包括重量、弹力和阻力。
2969
3140
  *
@@ -2982,7 +3153,10 @@ var LyricPlayerBase = class extends EventTarget {
2982
3153
  ...params
2983
3154
  };
2984
3155
  this.bottomLine.lineTransforms.posY.updateParams(this.posYSpringParams);
2985
- for (const line of this.currentLyricLineObjects) line.lineTransforms.posY.updateParams(this.posYSpringParams);
3156
+ for (const group of this.currentLyricGroups) {
3157
+ group.posY.updateParams(this.posYSpringParams);
3158
+ group.bgSlideY.updateParams(this.posYSpringParams);
3159
+ }
2986
3160
  }
2987
3161
  /**
2988
3162
  * 设置所有歌词行在​缩放大小上的弹簧属性,包括重量、弹力和阻力。
@@ -2998,17 +3172,18 @@ var LyricPlayerBase = class extends EventTarget {
2998
3172
  ...this.scaleForBGSpringParams,
2999
3173
  ...params
3000
3174
  };
3001
- for (const lineObj of this.currentLyricLineObjects) if (lineObj.getLine().isBG) lineObj.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
3002
- else lineObj.lineTransforms.scale.updateParams(this.scaleSpringParams);
3175
+ for (const group of this.currentLyricGroups) {
3176
+ group.mainLine.lineTransforms.scale.updateParams(this.scaleSpringParams);
3177
+ group.bgLine?.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
3178
+ }
3003
3179
  }
3004
- isPlaying = true;
3005
3180
  /**
3006
3181
  * 暂停部分效果演出,目前会暂停播放间奏点的动画,且将背景歌词显示出来
3007
3182
  */
3008
3183
  pause() {
3009
3184
  this.interludeDots.pause();
3010
- if (this.isPlaying) {
3011
- this.isPlaying = false;
3185
+ if (this.timelineState.isPlaying) {
3186
+ this.timelineState.isPlaying = false;
3012
3187
  this.calcLayout();
3013
3188
  }
3014
3189
  }
@@ -3017,8 +3192,8 @@ var LyricPlayerBase = class extends EventTarget {
3017
3192
  */
3018
3193
  resume() {
3019
3194
  this.interludeDots.resume();
3020
- if (!this.isPlaying) {
3021
- this.isPlaying = true;
3195
+ if (!this.timelineState.isPlaying) {
3196
+ this.timelineState.isPlaying = true;
3022
3197
  this.calcLayout();
3023
3198
  }
3024
3199
  }
@@ -3051,8 +3226,7 @@ var LyricPlayerBase = class extends EventTarget {
3051
3226
  * 请在用户完成滚动点击跳转歌词时调用本事件再调用 `calcLayout` 以正确滚动到目标位置
3052
3227
  */
3053
3228
  resetScroll() {
3054
- this.isScrolled = false;
3055
- this.scrollOffset = 0;
3229
+ resetPlayerScrollState(this.scrollState);
3056
3230
  clearTimeout(this.scrolledHandler);
3057
3231
  }
3058
3232
  /**
@@ -3071,7 +3245,24 @@ var LyricPlayerBase = class extends EventTarget {
3071
3245
  * @returns 当前播放位置
3072
3246
  */
3073
3247
  getCurrentTime() {
3074
- return this.currentTime;
3248
+ return this.timelineState.currentTime;
3249
+ }
3250
+ /**
3251
+ * 设置是否让背景人声行始终后置显示
3252
+ *
3253
+ * 默认情况下,如果背景歌词开始时间早于主歌词,会在主歌词上方展示;
3254
+ * 如果设置为 `true`,则无论时间顺序如何,背景歌词都会始终在主歌词下方展示
3255
+ * @param enable 是否启用始终后置
3256
+ */
3257
+ setAlwaysPostpositionBackground(enable) {
3258
+ if (this.alwaysPostpositionBackground === enable) return;
3259
+ this.alwaysPostpositionBackground = enable;
3260
+ this.rebuildLyricLines();
3261
+ this.calcLayout();
3262
+ }
3263
+ /** 获取当前是否设置了让背景人声行始终后置显示 */
3264
+ getAlwaysPostpositionBackground() {
3265
+ return this.alwaysPostpositionBackground;
3075
3266
  }
3076
3267
  getElement() {
3077
3268
  return this.element;
@@ -3082,6 +3273,198 @@ var LyricPlayerBase = class extends EventTarget {
3082
3273
  window.removeEventListener("pagehide", this.onPageHide);
3083
3274
  }
3084
3275
  };
3276
+ //#endregion
3277
+ //#region src/lyric-player/base/group.ts
3278
+ var LyricLineGroupBase = class {
3279
+ posY = new Spring(0);
3280
+ bgSlideY = new Spring(-80);
3281
+ top = 0;
3282
+ delay = 0;
3283
+ isActive = false;
3284
+ opacity = 1;
3285
+ blur = 0;
3286
+ isBgFirst = false;
3287
+ constructor(mainLine, bgLine) {
3288
+ this.mainLine = mainLine;
3289
+ this.bgLine = bgLine;
3290
+ }
3291
+ get startTime() {
3292
+ return this.mainLine.getLine().startTime;
3293
+ }
3294
+ get endTime() {
3295
+ return this.mainLine.getLine().endTime;
3296
+ }
3297
+ onLineSizeChange(size) {
3298
+ this.mainLine.onLineSizeChange(size);
3299
+ this.bgLine?.onLineSizeChange(size);
3300
+ }
3301
+ setTransform(top, force, delay, isActive, opacity, blur) {
3302
+ this.top = top;
3303
+ this.delay = delay;
3304
+ this.isActive = isActive;
3305
+ this.opacity = opacity;
3306
+ this.blur = blur;
3307
+ this.setLineTransformations(force, delay);
3308
+ const enableSpring = this.lyricPlayer.getEnableSpring();
3309
+ const hiddenSlideY = (this.lyricPlayer.getAlwaysPostpositionBackground() ? false : this.isBgFirst) ? 80 : -80;
3310
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3311
+ const targetBgSlideY = isActive || !isPlaying ? 0 : hiddenSlideY;
3312
+ if (force || !enableSpring) {
3313
+ this.posY.setPosition(top);
3314
+ this.bgSlideY.setPosition(targetBgSlideY);
3315
+ this.renderStyles();
3316
+ } else {
3317
+ this.posY.setTargetPosition(top, delay);
3318
+ this.bgSlideY.setTargetPosition(targetBgSlideY, delay);
3319
+ }
3320
+ }
3321
+ setLineTransformations(force, delay) {
3322
+ const enableScale = this.lyricPlayer.getEnableScale();
3323
+ const isPlaying = this.lyricPlayer.getIsPlaying();
3324
+ const renderMode = this.isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID;
3325
+ const SCALE_ASPECT = enableScale ? 97 : 100;
3326
+ let mainScale = 100;
3327
+ if (!this.isActive && isPlaying) mainScale = SCALE_ASPECT;
3328
+ this.mainLine.setTransform(mainScale, 1, 0, force, delay, renderMode);
3329
+ let bgScale = 100;
3330
+ if (!this.isActive && isPlaying) bgScale = 75;
3331
+ this.bgLine?.setTransform(bgScale, 1, 0, force, delay, renderMode);
3332
+ }
3333
+ update(delta) {
3334
+ if (this.lyricPlayer.getEnableSpring()) {
3335
+ this.posY.update(delta);
3336
+ this.bgSlideY.update(delta);
3337
+ this.renderStyles();
3338
+ }
3339
+ this.mainLine.update(delta);
3340
+ this.bgLine?.update(delta);
3341
+ }
3342
+ rebuildAllLines() {
3343
+ this.mainLine.rebuildElement();
3344
+ this.bgLine?.rebuildElement();
3345
+ }
3346
+ enable(time, shouldPlay) {
3347
+ this.mainLine.enable(time, shouldPlay);
3348
+ this.bgLine?.enable(time, shouldPlay);
3349
+ }
3350
+ disable() {
3351
+ this.mainLine.disable();
3352
+ this.bgLine?.disable();
3353
+ }
3354
+ dispose() {
3355
+ this.mainLine.dispose();
3356
+ this.bgLine?.dispose();
3357
+ }
3358
+ };
3359
+ //#endregion
3360
+ //#region src/lyric-player/dom/lyric-group.ts
3361
+ var LyricLineGroup = class extends LyricLineGroupBase {
3362
+ element;
3363
+ bgWrapper;
3364
+ lastIsActive;
3365
+ constructor(lyricPlayer, mainLine) {
3366
+ super(mainLine);
3367
+ this.lyricPlayer = lyricPlayer;
3368
+ this.element = document.createElement("div");
3369
+ this.element.className = lyric_player_module_default.lyricLineWrapper;
3370
+ this.element.appendChild(mainLine.getElement());
3371
+ this.posY.setPosition(window.innerHeight * 2);
3372
+ lyricPlayer.resizeObserver.observe(this.element);
3373
+ }
3374
+ get isInSight() {
3375
+ const t = this.posY.getCurrentPosition();
3376
+ let h = this.lyricPlayer.lyricGroupSize?.get(this)?.[1];
3377
+ if (h === void 0 || h === 0) h = this.element.clientHeight || 0;
3378
+ const pb = this.lyricPlayer.size[1];
3379
+ const ov = this.lyricPlayer.getOverscanPx();
3380
+ return !(t > pb + h + ov || t < -h - ov);
3381
+ }
3382
+ show() {
3383
+ if (!this.element.parentElement) {
3384
+ const playerEl = this.lyricPlayer.getElement();
3385
+ const groups = this.lyricPlayer.currentLyricGroups;
3386
+ const myIndex = groups.indexOf(this);
3387
+ let referenceNode = null;
3388
+ if (myIndex !== -1) {
3389
+ for (let i = myIndex + 1; i < groups.length; i++) if (groups[i].element.parentElement === playerEl) {
3390
+ referenceNode = groups[i].element;
3391
+ break;
3392
+ }
3393
+ }
3394
+ playerEl.insertBefore(this.element, referenceNode);
3395
+ this.lyricPlayer.resizeObserver.observe(this.element);
3396
+ }
3397
+ this.mainLine.show();
3398
+ this.bgLine?.show();
3399
+ }
3400
+ hide() {
3401
+ if (this.element.parentElement) {
3402
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3403
+ this.element.remove();
3404
+ this.mainLine.teardownContent();
3405
+ this.bgLine?.teardownContent();
3406
+ }
3407
+ }
3408
+ update(delta) {
3409
+ if (this.isInSight) this.show();
3410
+ else this.hide();
3411
+ super.update(delta);
3412
+ }
3413
+ addBgLine(bgLine) {
3414
+ if (this.bgLine) this.bgLine.dispose();
3415
+ if (this.bgWrapper) this.bgWrapper.remove();
3416
+ this.bgLine = bgLine;
3417
+ const bgStartTime = bgLine.getLine().words[0]?.startTime ?? bgLine.getLine().startTime;
3418
+ const mainStartTime = this.mainLine.getLine().words[0]?.startTime ?? this.mainLine.getLine().startTime;
3419
+ this.isBgFirst = bgStartTime < mainStartTime;
3420
+ if (this.mainLine.getLine().isDuet) bgLine.getElement().classList.add(lyric_player_module_default.lyricDuetLine);
3421
+ this.bgWrapper = document.createElement("div");
3422
+ this.bgWrapper.className = lyric_player_module_default.bgWrapper;
3423
+ this.bgWrapper.appendChild(bgLine.getElement());
3424
+ if (!this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst) {
3425
+ this.bgWrapper.classList.add(lyric_player_module_default.bgWrapperTop);
3426
+ this.element.insertBefore(this.bgWrapper, this.mainLine.getElement());
3427
+ this.bgSlideY.setPosition(80);
3428
+ } else this.element.appendChild(this.bgWrapper);
3429
+ }
3430
+ renderStyles() {
3431
+ const y = this.posY.getCurrentPosition().toFixed(1);
3432
+ this.element.style.transform = `translateY(${y}px)`;
3433
+ this.element.style.opacity = this.opacity.toString();
3434
+ this.element.style.filter = `blur(${Math.min(5, this.blur)}px)`;
3435
+ if (!this.lyricPlayer.getEnableSpring()) this.element.style.transitionDelay = `${this.delay}ms`;
3436
+ if (this.bgWrapper) {
3437
+ if (this.lastIsActive !== this.isActive) {
3438
+ this.lastIsActive = this.isActive;
3439
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperActive, this.isActive);
3440
+ }
3441
+ const slideY = this.bgSlideY.getCurrentPosition();
3442
+ const slideYStr = slideY.toFixed(1);
3443
+ const activeProgress = clamp01(1 - Math.abs(slideY) / 80);
3444
+ const scaleStr = (.8 + activeProgress * .2).toFixed(3);
3445
+ this.bgWrapper.style.transform = `translateY(${slideYStr}%) scale(${scaleStr})`;
3446
+ const shouldBgFirst = !this.lyricPlayer.getAlwaysPostpositionBackground() && this.isBgFirst;
3447
+ if (shouldBgFirst) {
3448
+ const currentMarginTop = -(this.bgWrapper.clientHeight || 0) * (1 - activeProgress);
3449
+ this.bgWrapper.style.marginTop = `${currentMarginTop.toFixed(1)}px`;
3450
+ } else this.bgWrapper.style.marginTop = "";
3451
+ const isHidden = slideYStr === (shouldBgFirst ? "80.0" : "-80.0") && !this.isActive;
3452
+ this.bgWrapper.classList.toggle(lyric_player_module_default.bgWrapperHidden, isHidden);
3453
+ }
3454
+ }
3455
+ dispose() {
3456
+ super.dispose();
3457
+ this.lyricPlayer.resizeObserver.unobserve(this.element);
3458
+ this.element.remove();
3459
+ }
3460
+ };
3461
+ //#endregion
3462
+ //#region src/utils/is-cjk.ts
3463
+ const isCJK = (char) => {
3464
+ return /^[\p{Unified_Ideograph}\u0800-\u9FFC]+$/u.test(char);
3465
+ };
3466
+ //#endregion
3467
+ //#region src/lyric-player/base/line.ts
3085
3468
  /**
3086
3469
  * 所有标准歌词行的基类
3087
3470
  * @internal
@@ -3092,10 +3475,7 @@ var LyricLineBase = class extends EventTarget {
3092
3475
  blur = 0;
3093
3476
  opacity = 1;
3094
3477
  delay = 0;
3095
- lineTransforms = {
3096
- posY: new Spring(0),
3097
- scale: new Spring(100)
3098
- };
3478
+ lineTransforms = { scale: new Spring(100) };
3099
3479
  /**
3100
3480
  * 用于 CJK 词语边界检测的分词器
3101
3481
  */
@@ -3105,9 +3485,7 @@ var LyricLineBase = class extends EventTarget {
3105
3485
  * 用于正确处理 emoji、复合字符等
3106
3486
  */
3107
3487
  static graphemeSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
3108
- onLineSizeChange(_size) {}
3109
- setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = 0) {
3110
- this.top = top;
3488
+ setTransform(scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3111
3489
  this.scale = scale;
3112
3490
  this.opacity = opacity;
3113
3491
  this.blur = blur;
@@ -3333,7 +3711,7 @@ var LineBalancer = class {
3333
3711
  const marginLeft = Number.parseFloat(elStyle.marginLeft) || 0;
3334
3712
  const marginRight = Number.parseFloat(elStyle.marginRight) || 0;
3335
3713
  childInfos.push({
3336
- width: Math.max(0, rect.width + marginLeft + marginRight),
3714
+ width: clampPositive(rect.width + marginLeft + marginRight),
3337
3715
  text: el.textContent ?? "",
3338
3716
  isSpace: false
3339
3717
  });
@@ -3537,34 +3915,34 @@ function matrix4ToCSS(m, fractionDigits = 4) {
3537
3915
  }
3538
3916
  //#endregion
3539
3917
  //#region src/lyric-player/dom/lyric-line.ts
3540
- const ANIMATION_FRAME_QUANTITY$1 = 32;
3541
- const norNum$1 = (min, max) => (x) => Math.min(1, Math.max(0, (x - min) / (max - min)));
3542
- const EMP_EASING_MID$1 = .5;
3543
- const beginNum$1 = norNum$1(0, EMP_EASING_MID$1);
3544
- const endNum$1 = norNum$1(EMP_EASING_MID$1, 1);
3545
- const bezIn$1 = (0, bezier_easing.default)(.2, .4, .58, 1);
3546
- const bezOut$1 = (0, bezier_easing.default)(.3, 0, .58, 1);
3547
- const makeEmpEasing$1 = (mid) => {
3548
- return (x) => x < mid ? bezIn$1(beginNum$1(x)) : 1 - bezOut$1(endNum$1(x));
3549
- };
3550
- function generateFadeGradient$1(width, padding = 0, bright = "rgba(0,0,0,var(--bright-mask-alpha, 1.0))", dark = "rgba(0,0,0,var(--dark-mask-alpha, 1.0))") {
3551
- const totalAspect = 2 + width + padding;
3552
- const widthInTotal = width / totalAspect;
3918
+ const ANIMATION_FRAME_QUANTITY = 32;
3919
+ const norNum = (min, max) => (x) => clamp01((x - min) / (max - min));
3920
+ const EMP_EASING_MID = .5;
3921
+ const beginNum = norNum(0, EMP_EASING_MID);
3922
+ const endNum = norNum(EMP_EASING_MID, 1);
3923
+ const bezIn = (0, bezier_easing.default)(.2, .4, .58, 1);
3924
+ const bezOut = (0, bezier_easing.default)(.3, 0, .58, 1);
3925
+ const makeEmpEasing = (mid) => {
3926
+ return (x) => x < mid ? bezIn(beginNum(x)) : 1 - bezOut(endNum(x));
3927
+ };
3928
+ function generateFadeGradient(width, padding = 0, bright = "rgba(0,0,0,var(--bright-mask-alpha, 1.0))", dark = "rgba(0,0,0,var(--dark-mask-alpha, 1.0))") {
3929
+ const totalAspect = 2 + width + padding;
3930
+ const widthInTotal = width / totalAspect;
3553
3931
  const leftPos = (1 - widthInTotal) / 2;
3554
3932
  return [`linear-gradient(to right,${bright} ${leftPos * 100}%,${dark} ${(leftPos + widthInTotal) * 100}%)`, totalAspect];
3555
3933
  }
3556
- var RawLyricLineMouseEvent$1 = class extends MouseEvent {
3934
+ var RawLyricLineMouseEvent = class extends MouseEvent {
3557
3935
  constructor(line, event) {
3558
3936
  super(event.type, event);
3559
3937
  this.line = line;
3560
3938
  }
3561
3939
  };
3562
- var LyricLineEl$1 = class extends LyricLineBase {
3940
+ var LyricLineEl = class extends LyricLineBase {
3563
3941
  element = document.createElement("div");
3564
3942
  splittedWords = [];
3565
3943
  built = false;
3566
3944
  lineSize = [0, 0];
3567
- renderMode = 0;
3945
+ renderMode = LyricLineRenderMode.SOLID;
3568
3946
  currentBrightAlpha = 1;
3569
3947
  currentDarkAlpha = .2;
3570
3948
  targetBrightAlpha = 1;
@@ -3585,12 +3963,9 @@ var LyricLineEl$1 = class extends LyricLineBase {
3585
3963
  super();
3586
3964
  this.lyricPlayer = lyricPlayer;
3587
3965
  this.lyricLine = lyricLine;
3588
- this._prevParentEl = lyricPlayer.getElement();
3589
- lyricPlayer.resizeObserver.observe(this.element);
3590
3966
  this.element.setAttribute("class", lyric_player_module_default.lyricLine);
3591
3967
  if (this.lyricLine.isBG) this.element.classList.add(lyric_player_module_default.lyricBgLine);
3592
3968
  if (this.lyricLine.isDuet) this.element.classList.add(lyric_player_module_default.lyricDuetLine);
3593
- this.lineTransforms.posY.setPosition(window.innerHeight * 2);
3594
3969
  this.element.appendChild(document.createElement("div"));
3595
3970
  this.element.appendChild(document.createElement("div"));
3596
3971
  this.element.appendChild(document.createElement("div"));
@@ -3605,7 +3980,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3605
3980
  }
3606
3981
  listenersMap = /* @__PURE__ */ new Map();
3607
3982
  onMouseEvent = (e) => {
3608
- const wrapped = new RawLyricLineMouseEvent$1(this, e);
3983
+ const wrapped = new RawLyricLineMouseEvent(this, e);
3609
3984
  for (const listener of this.listenersMap.get(e.type) ?? []) listener.call(this, wrapped);
3610
3985
  if (!this.dispatchEvent(wrapped) || wrapped.defaultPrevented) {
3611
3986
  e.preventDefault();
@@ -3641,30 +4016,28 @@ var LyricLineEl$1 = class extends LyricLineBase {
3641
4016
  return true;
3642
4017
  }
3643
4018
  isEnabled = false;
3644
- async enable(maskAnimationTime = this.lyricLine.startTime, shouldPlay = true) {
4019
+ async enable(maskAnimationTime = this.lyricPlayer.getCurrentTime(), shouldPlay = this.lyricPlayer.getIsPlaying()) {
3645
4020
  this.isEnabled = true;
3646
4021
  this.element.classList.add(lyric_player_module_default.active);
3647
4022
  const main = this.element.children[0];
3648
- const relativeTime = Math.max(0, maskAnimationTime - this.lyricLine.startTime);
3649
- const actualMaskTime = maskAnimationTime === this.lyricLine.startTime ? this.lyricPlayer.getCurrentTime() : maskAnimationTime;
3650
- const maskRelativeTime = Math.max(0, actualMaskTime - this.lyricLine.startTime);
4023
+ const relativeTime = clampPositive(maskAnimationTime - this.lyricLine.startTime);
3651
4024
  for (const word of this.splittedWords) {
3652
4025
  for (const a of word.elementAnimations) {
3653
4026
  a.currentTime = relativeTime;
3654
4027
  a.playbackRate = 1;
3655
4028
  const timing = a.effect?.getComputedTiming();
3656
- const duration = timing?.duration || 0;
3657
- const endTime = (timing?.delay || 0) + duration;
4029
+ const duration = Number(timing?.duration ?? 0);
4030
+ const endTime = Number(timing?.delay ?? 0) + duration;
3658
4031
  if (shouldPlay && relativeTime < endTime) a.play();
3659
4032
  else a.pause();
3660
4033
  }
3661
4034
  for (const a of word.maskAnimations) {
3662
- const t = Math.min(this.totalDuration, maskRelativeTime);
4035
+ const t = Math.min(this.totalDuration, relativeTime);
3663
4036
  a.currentTime = t;
3664
4037
  a.playbackRate = 1;
3665
4038
  const timing = a.effect?.getComputedTiming();
3666
- const duration = timing?.duration || 0;
3667
- const endTime = (timing?.delay || 0) + duration;
4039
+ const duration = Number(timing?.duration ?? 0);
4040
+ const endTime = Number(timing?.delay ?? 0) + duration;
3668
4041
  if (shouldPlay && t < endTime) a.play();
3669
4042
  else a.pause();
3670
4043
  }
@@ -3674,7 +4047,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3674
4047
  disable() {
3675
4048
  this.isEnabled = false;
3676
4049
  this.element.classList.remove(lyric_player_module_default.active);
3677
- this.renderMode = 0;
4050
+ this.renderMode = LyricLineRenderMode.SOLID;
3678
4051
  const main = this.element.children[0];
3679
4052
  for (const word of this.splittedWords) {
3680
4053
  for (const a of word.elementAnimations) if (a.id === "float-word" || a.id.includes("emphasize-word-float-only")) {
@@ -3715,7 +4088,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3715
4088
  setMaskAnimationState(maskAnimationTime = 0) {
3716
4089
  const t = maskAnimationTime - this.lyricLine.startTime;
3717
4090
  for (const word of this.splittedWords) for (const a of word.maskAnimations) {
3718
- a.currentTime = Math.min(this.totalDuration, Math.max(0, t));
4091
+ a.currentTime = clamp(t, 0, this.totalDuration);
3719
4092
  a.playbackRate = 1;
3720
4093
  if (t >= 0 && t < this.totalDuration) a.play();
3721
4094
  else a.pause();
@@ -3724,34 +4097,18 @@ var LyricLineEl$1 = class extends LyricLineBase {
3724
4097
  getLine() {
3725
4098
  return this.lyricLine;
3726
4099
  }
3727
- _prevParentEl;
3728
4100
  lastStyle = "";
3729
4101
  show() {
3730
- if (!this.element.parentElement) {
3731
- this._prevParentEl.appendChild(this.element);
3732
- this.lyricPlayer.resizeObserver.observe(this.element);
3733
- }
3734
4102
  if (!this.built) {
3735
4103
  this.rebuildElement();
3736
4104
  this.built = true;
3737
4105
  this.updateMaskImageSync();
3738
4106
  }
3739
- this.rebuildStyle();
3740
- }
3741
- hide() {
3742
- if (this.element.parentElement) {
3743
- this._prevParentEl.removeChild(this.element);
3744
- this.lyricPlayer.resizeObserver.unobserve(this.element);
3745
- }
3746
- if (this.built) {
3747
- this.disposeElements();
3748
- this.built = false;
3749
- }
3750
4107
  }
3751
4108
  rebuildStyle() {
3752
4109
  let style = "";
3753
- style += `transform:translateY(${this.lineTransforms.posY.getCurrentPosition().toFixed(1)}px) scale(${(this.lineTransforms.scale.getCurrentPosition() / 100).toFixed(4)});`;
3754
- if (!this.lyricPlayer.getEnableSpring() && this.isInSight) style += `transition-delay:${this.delay}ms;`;
4110
+ style += `transform: scale(${(this.lineTransforms.scale.getCurrentPosition() / 100).toFixed(4)});`;
4111
+ if (!this.lyricPlayer.getEnableSpring()) style += `transition-delay:${this.delay}ms;`;
3755
4112
  style += `filter:blur(${Math.min(5, this.blur)}px);`;
3756
4113
  if (style !== this.lastStyle) {
3757
4114
  this.lastStyle = style;
@@ -3906,7 +4263,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3906
4263
  return a;
3907
4264
  }
3908
4265
  initEmphasizeAnimation(word, characterElements, duration, delay, rubyCharCount) {
3909
- const de = Math.max(0, delay);
4266
+ const de = clampPositive(delay);
3910
4267
  let du = Math.max(1e3, duration);
3911
4268
  const anchorCharCount = rubyCharCount > 0 ? rubyCharCount : Math.max(1, characterElements.length);
3912
4269
  let result = [];
@@ -3924,12 +4281,12 @@ var LyricLineEl$1 = class extends LyricLineBase {
3924
4281
  amount = Math.min(1.2, amount);
3925
4282
  blur = Math.min(.8, blur);
3926
4283
  const animateDu = Number.isFinite(du) ? du : 0;
3927
- const empEasing = makeEmpEasing$1(EMP_EASING_MID$1);
4284
+ const empEasing = makeEmpEasing(EMP_EASING_MID);
3928
4285
  result = characterElements.flatMap((el, i, arr) => {
3929
4286
  const wordDe = de + du / 2.5 / anchorCharCount * i;
3930
4287
  const result = [];
3931
- const frames = new Array(ANIMATION_FRAME_QUANTITY$1).fill(0).map((_, j) => {
3932
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY$1;
4288
+ const frames = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4289
+ const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
3933
4290
  const transX = empEasing(x);
3934
4291
  const glowLevel = empEasing(x) * blur;
3935
4292
  const mat = scaleMatrix4(createMatrix4(), 1 + transX * .1 * amount);
@@ -3954,8 +4311,8 @@ var LyricLineEl$1 = class extends LyricLineBase {
3954
4311
  };
3955
4312
  glow.pause();
3956
4313
  result.push(glow);
3957
- const floatFrame = new Array(ANIMATION_FRAME_QUANTITY$1).fill(0).map((_, j) => {
3958
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY$1;
4314
+ const floatFrame = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4315
+ const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
3959
4316
  let y = Math.sin(x * Math.PI);
3960
4317
  if (this.lyricLine.isBG) y *= 2;
3961
4318
  return {
@@ -4014,7 +4371,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4014
4371
  word.width = wordEl.clientWidth;
4015
4372
  word.height = wordEl.clientHeight;
4016
4373
  const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
4017
- const [maskImage, totalAspect] = generateFadeGradient$1(fadeWidth / word.width);
4374
+ const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / word.width);
4018
4375
  const totalAspectStr = `${totalAspect * 100}% 100%`;
4019
4376
  if (this.lyricPlayer.supportMaskImage) {
4020
4377
  wordEl.style.maskImage = maskImage;
@@ -4035,12 +4392,12 @@ var LyricLineEl$1 = class extends LyricLineBase {
4035
4392
  }
4036
4393
  }
4037
4394
  generateWebAnimationBasedMaskImage() {
4038
- const totalFadeDuration = Math.max(this.splittedWords.reduce((pv, w) => Math.max(w.endTime, pv), 0), this.lyricLine.endTime) - this.lyricLine.startTime;
4395
+ const totalFadeDuration = Math.max(0, ...this.splittedWords.map((w) => w.endTime), this.lyricLine.endTime) - this.lyricLine.startTime;
4039
4396
  this.splittedWords.forEach((word, i) => {
4040
4397
  const wordEl = word.mainElement;
4041
4398
  if (wordEl) {
4042
4399
  const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
4043
- const [maskImage, totalAspect] = generateFadeGradient$1(fadeWidth / (word.width + word.padding * 2));
4400
+ const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / (word.width + word.padding * 2));
4044
4401
  const totalAspectStr = `${totalAspect * 100}% 100%`;
4045
4402
  if (this.lyricPlayer.supportMaskImage) {
4046
4403
  wordEl.style.maskImage = maskImage;
@@ -4055,7 +4412,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4055
4412
  }
4056
4413
  const widthBeforeSelf = this.splittedWords.slice(0, i).reduce((a, b) => a + b.width, 0) + (this.splittedWords[0] ? fadeWidth : 0);
4057
4414
  const minOffset = -(word.width + word.padding * 2 + fadeWidth);
4058
- const clampOffset = (x) => Math.max(minOffset, Math.min(0, x));
4415
+ const clampOffset = (x) => clamp(x, minOffset, 0);
4059
4416
  let curPos = -widthBeforeSelf - word.width - word.padding - fadeWidth;
4060
4417
  let timeOffset = 0;
4061
4418
  const frames = [];
@@ -4063,7 +4420,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4063
4420
  let lastTime = 0;
4064
4421
  const pushFrame = () => {
4065
4422
  const moveOffset = curPos - lastPos;
4066
- const time = Math.max(0, Math.min(1, timeOffset));
4423
+ const time = clamp01(timeOffset);
4067
4424
  const duration = time - lastTime;
4068
4425
  const d = Math.abs(duration / moveOffset);
4069
4426
  if (curPos > minOffset && lastPos < minOffset) {
@@ -4103,7 +4460,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4103
4460
  lastTimeStamp = curTimeStamp;
4104
4461
  }
4105
4462
  {
4106
- const fadeDuration = Math.max(0, otherWord.endTime - otherWord.startTime);
4463
+ const fadeDuration = clampPositive(otherWord.endTime - otherWord.startTime);
4107
4464
  const rubySegments = this.getRubySegments(otherWord);
4108
4465
  const rubyCharCount = rubySegments.reduce((total, ruby) => total + ruby.word.length, 0);
4109
4466
  if (rubyCharCount > 0) {
@@ -4119,7 +4476,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4119
4476
  timeOffset += rubyStaticDuration / totalFadeDuration;
4120
4477
  if (rubyStaticDuration > 0) pushFrame();
4121
4478
  lastTimeStamp = rubyStartStamp;
4122
- const perCharDuration = Math.max(0, rubyEnd - rubyStart) / ruby.word.length;
4479
+ const perCharDuration = clampPositive(rubyEnd - rubyStart) / ruby.word.length;
4123
4480
  for (let rubyCharIndex = 0; rubyCharIndex < ruby.word.length; rubyCharIndex++) {
4124
4481
  timeOffset += perCharDuration / totalFadeDuration;
4125
4482
  curPos += widthPerChar;
@@ -4169,10 +4526,10 @@ var LyricLineEl$1 = class extends LyricLineBase {
4169
4526
  return this.element;
4170
4527
  }
4171
4528
  updateMaskAlphaTargets(scale) {
4172
- const factor = Math.max(0, Math.min(1, (scale - .97) / .03));
4529
+ const factor = clamp01((scale - .97) / .03);
4173
4530
  const dynamicDarkAlpha = factor * .2 + .2;
4174
4531
  const dynamicBrightAlpha = factor * .8 + .2;
4175
- if (this.renderMode === 0) {
4532
+ if (this.renderMode === LyricLineRenderMode.SOLID) {
4176
4533
  this.targetBrightAlpha = dynamicDarkAlpha;
4177
4534
  this.targetDarkAlpha = dynamicDarkAlpha;
4178
4535
  } else {
@@ -4194,25 +4551,19 @@ var LyricLineEl$1 = class extends LyricLineBase {
4194
4551
  this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
4195
4552
  this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
4196
4553
  }
4197
- setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = 0) {
4198
- super.setTransform(top, scale, opacity, blur, force, delay);
4554
+ setTransform(scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4555
+ super.setTransform(scale, opacity, blur, force, delay);
4199
4556
  this.renderMode = mode;
4200
- const beforeInSight = this.isInSight;
4201
4557
  const enableSpring = this.lyricPlayer.getEnableSpring();
4202
- this.top = top;
4558
+ this.top = 0;
4203
4559
  this.scale = scale;
4204
4560
  this.delay = delay * 1e3 | 0;
4205
4561
  const main = this.element.children[0];
4206
4562
  main.style.opacity = `${opacity}`;
4207
4563
  if (force || !enableSpring) {
4208
4564
  this.blur = Math.min(32, blur);
4209
- this.lineTransforms.posY.setPosition(top);
4210
4565
  this.lineTransforms.scale.setPosition(scale);
4211
- if (!enableSpring) {
4212
- const afterInSight = this.isInSight;
4213
- if (beforeInSight || afterInSight) this.show();
4214
- else this.hide();
4215
- } else this.rebuildStyle();
4566
+ this.rebuildStyle();
4216
4567
  const currentScale = this.lineTransforms.scale.getCurrentPosition();
4217
4568
  this.updateMaskAlphaTargets(currentScale / 100);
4218
4569
  this.currentBrightAlpha = this.targetBrightAlpha;
@@ -4220,21 +4571,18 @@ var LyricLineEl$1 = class extends LyricLineBase {
4220
4571
  this.element.style.setProperty("--bright-mask-alpha", String(this.currentBrightAlpha));
4221
4572
  this.element.style.setProperty("--dark-mask-alpha", String(this.currentDarkAlpha));
4222
4573
  } else {
4223
- this.lineTransforms.posY.setTargetPosition(top, delay);
4224
4574
  this.lineTransforms.scale.setTargetPosition(scale);
4225
4575
  if (this.blur !== Math.min(5, blur)) {
4226
4576
  this.blur = Math.min(5, blur);
4227
- const roundedBlur = blur.toFixed(3);
4228
- this.element.style.filter = `blur(${roundedBlur}px)`;
4577
+ this.element.style.filter = `blur(${blur.toFixed(3)}px)`;
4229
4578
  }
4230
4579
  }
4231
4580
  }
4232
4581
  update(delta = 0) {
4233
4582
  if (!this.lyricPlayer.getEnableSpring()) return;
4234
- this.lineTransforms.posY.update(delta);
4235
4583
  this.lineTransforms.scale.update(delta);
4236
- if (this.isInSight) this.show();
4237
- else this.hide();
4584
+ this.rebuildStyle();
4585
+ if (!this.built) return;
4238
4586
  const currentScale = this.lineTransforms.scale.getCurrentPosition() / 100;
4239
4587
  this.updateMaskAlphaTargets(currentScale);
4240
4588
  this.applyAlphaToDom(delta);
@@ -4242,13 +4590,11 @@ var LyricLineEl$1 = class extends LyricLineBase {
4242
4590
  _getDebugTargetPos() {
4243
4591
  return `[位移: ${this.top}; 缩放: ${this.scale}; 延时: ${this.delay}]`;
4244
4592
  }
4245
- get isInSight() {
4246
- const t = this.lineTransforms.posY.getCurrentPosition();
4247
- const h = this.lyricPlayer.lyricLinesSize.get(this)?.[1] ?? 0;
4248
- const b = t + h;
4249
- const pb = this.lyricPlayer.size[1];
4250
- const ov = this.lyricPlayer.getOverscanPx();
4251
- return !(t > pb + h + ov || b < -h - ov);
4593
+ teardownContent() {
4594
+ if (this.built) {
4595
+ this.disposeElements();
4596
+ this.built = false;
4597
+ }
4252
4598
  }
4253
4599
  disposeElements() {
4254
4600
  this.balancer?.reset();
@@ -4296,7 +4642,7 @@ var LyricLineMouseEvent = class extends MouseEvent {
4296
4642
  * 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
4297
4643
  */
4298
4644
  var DomLyricPlayer = class extends LyricPlayerBase {
4299
- currentLyricLineObjects = [];
4645
+ currentLyricGroups = [];
4300
4646
  onResize() {
4301
4647
  const computedStyles = getComputedStyle(this.element);
4302
4648
  this._baseFontSize = Number.parseFloat(computedStyles.fontSize);
@@ -4333,7 +4679,10 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4333
4679
  rebuildStyle() {}
4334
4680
  setWordFadeWidth(value = .5) {
4335
4681
  super.setWordFadeWidth(value);
4336
- for (const el of this.currentLyricLineObjects) el.updateMaskImageSync();
4682
+ for (const group of this.currentLyricGroups) {
4683
+ group.mainLine.updateMaskImageSync();
4684
+ group.bgLine?.updateMaskImageSync();
4685
+ }
4337
4686
  }
4338
4687
  /**
4339
4688
  * 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
@@ -4345,943 +4694,75 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4345
4694
  if (this.hasDuetLine) this.element.classList.add(lyric_player_module_default.hasDuetLine);
4346
4695
  else this.element.classList.remove(lyric_player_module_default.hasDuetLine);
4347
4696
  if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${initialTime}`);
4348
- for (const line of this.currentLyricLineObjects) {
4349
- line.removeMouseEventListener("click", this.onLineClickedHandler);
4350
- line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
4351
- line.dispose();
4697
+ for (const group of this.currentLyricGroups) {
4698
+ const linesToDispose = [group.mainLine, group.bgLine].filter((line) => !!line);
4699
+ for (const line of linesToDispose) {
4700
+ line.removeMouseEventListener("click", this.onLineClickedHandler);
4701
+ line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
4702
+ }
4703
+ group.dispose();
4352
4704
  }
4353
- this.currentLyricLineObjects = this.processedLines.map((line, i) => {
4354
- const lineEl = new LyricLineEl$1(this, line);
4705
+ this.currentLyricGroups = [];
4706
+ let currentGroup = null;
4707
+ for (let i = 0; i < this.processedLines.length; i++) {
4708
+ const line = this.processedLines[i];
4709
+ const lineEl = new LyricLineEl(this, line);
4355
4710
  lineEl.addMouseEventListener("click", this.onLineClickedHandler);
4356
4711
  lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
4357
4712
  this.lyricLinesIndexes.set(lineEl, i);
4358
- this.lyricLineElementMap.set(lineEl.getElement(), lineEl);
4359
- return lineEl;
4360
- });
4713
+ if (!line.isBG || !currentGroup) {
4714
+ currentGroup = new LyricLineGroup(this, lineEl);
4715
+ this.currentLyricGroups.push(currentGroup);
4716
+ this.lyricGroupElementMap.set(currentGroup.element, currentGroup);
4717
+ } else currentGroup.addBgLine(lineEl);
4718
+ }
4361
4719
  this.setLinePosXSpringParams({});
4362
4720
  this.setLinePosYSpringParams({});
4363
4721
  this.setLineScaleSpringParams({});
4722
+ this.setCurrentTime(initialTime, true);
4364
4723
  this.calcLayout(true);
4365
4724
  this.update(0);
4366
4725
  }
4367
4726
  pause() {
4368
4727
  super.pause();
4369
- this.element.classList.remove("playing");
4728
+ this.element.classList.remove(lyric_player_module_default.playing);
4370
4729
  this.interludeDots.pause();
4371
- for (const line of this.currentLyricLineObjects) line.pause();
4730
+ for (const group of this.currentLyricGroups) {
4731
+ group.mainLine.pause();
4732
+ group.bgLine?.pause();
4733
+ }
4372
4734
  }
4373
4735
  resume() {
4374
4736
  super.resume();
4375
- this.element.classList.add("playing");
4737
+ this.element.classList.add(lyric_player_module_default.playing);
4376
4738
  this.interludeDots.resume();
4377
- for (const line of this.currentLyricLineObjects) line.resume();
4739
+ for (const group of this.currentLyricGroups) {
4740
+ group.mainLine.resume();
4741
+ group.bgLine?.resume();
4742
+ }
4378
4743
  }
4379
4744
  update(delta = 0) {
4380
- if (!this.initialLayoutFinished) return;
4745
+ if (!this.timelineState.initialLayoutFinished) return;
4381
4746
  super.update(delta);
4382
- if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${this.currentTime}`);
4747
+ if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${this.timelineState.currentTime}`);
4383
4748
  if (!this.isPageVisible) return;
4384
4749
  const deltaS = delta / 1e3;
4385
- for (const line of this.currentLyricLineObjects) line.update(deltaS);
4750
+ for (const group of this.currentLyricGroups) group.update(deltaS);
4386
4751
  }
4387
4752
  dispose() {
4388
4753
  super.dispose();
4389
4754
  this.element.remove();
4390
- for (const el of this.currentLyricLineObjects) el.dispose();
4755
+ for (const group of this.currentLyricGroups) group.dispose();
4391
4756
  this.bottomLine.dispose();
4392
4757
  this.interludeDots.dispose();
4393
4758
  }
4394
4759
  };
4395
4760
  //#endregion
4396
- //#region src/utils/debounce.ts
4397
- function debounce(cb, wait = 20) {
4398
- let h;
4399
- const callable = (...args) => {
4400
- clearTimeout(h);
4401
- h = setTimeout(() => cb(...args), wait);
4402
- };
4403
- return callable;
4404
- }
4405
- //#endregion
4406
- //#region src/lyric-player/dom-slim/index.module.css
4407
- var index_module_default = {
4408
- "active": "KxF9Iq_active",
4409
- "duet": "KxF9Iq_duet",
4410
- "enabled": "KxF9Iq_enabled",
4411
- "hasDuetLine": "KxF9Iq_hasDuetLine",
4412
- "interludeDots": "KxF9Iq_interludeDots",
4413
- "lyricBgLine": "KxF9Iq_lyricBgLine",
4414
- "lyricDuetLine": "KxF9Iq_lyricDuetLine",
4415
- "lyricLine": "KxF9Iq_lyricLine",
4416
- "lyricMainLine": "KxF9Iq_lyricMainLine",
4417
- "lyricSubLine": "KxF9Iq_lyricSubLine",
4418
- "romanWord": "KxF9Iq_romanWord",
4419
- "rubyWord": "KxF9Iq_rubyWord",
4420
- "tmpDisableTransition": "KxF9Iq_tmpDisableTransition",
4421
- "wordBody": "KxF9Iq_wordBody",
4422
- "wordWithRuby": "KxF9Iq_wordWithRuby"
4423
- };
4424
- //#endregion
4425
- //#region src/utils/mutex.ts
4426
- function mutexifyFunction(func) {
4427
- const awaitingTasks = [];
4428
- function processNextTask() {
4429
- const task = awaitingTasks[0];
4430
- if (!task) return;
4431
- func(...task.args).then((value) => {
4432
- task.resolve(value);
4433
- }).catch((reason) => {
4434
- task.reject(reason);
4435
- }).finally(() => {
4436
- awaitingTasks.shift();
4437
- if (awaitingTasks.length > 0) processNextTask();
4438
- });
4439
- }
4440
- return ((...args) => {
4441
- return new Promise((resolve, reject) => {
4442
- awaitingTasks.push({
4443
- resolve,
4444
- reject,
4445
- args
4446
- });
4447
- if (awaitingTasks.length === 1) processNextTask();
4448
- });
4449
- });
4450
- }
4451
- //#endregion
4452
- //#region src/lyric-player/dom-slim/lyric-line.ts
4453
- const ANIMATION_FRAME_QUANTITY = 32;
4454
- const norNum = (min, max) => (x) => Math.min(1, Math.max(0, (x - min) / (max - min)));
4455
- const EMP_EASING_MID = .5;
4456
- const beginNum = norNum(0, EMP_EASING_MID);
4457
- const endNum = norNum(EMP_EASING_MID, 1);
4458
- const bezIn = (0, bezier_easing.default)(.2, .4, .58, 1);
4459
- const bezOut = (0, bezier_easing.default)(.3, 0, .58, 1);
4460
- const makeEmpEasing = (mid) => {
4461
- return (x) => x < mid ? bezIn(beginNum(x)) : 1 - bezOut(endNum(x));
4462
- };
4463
- function generateFadeGradient(width, padding = 0, bright = "rgba(0,0,0,var(--bright-mask-alpha, 1.0))", dark = "rgba(0,0,0,var(--dark-mask-alpha, 1.0))") {
4464
- const totalAspect = 2 + width + padding;
4465
- const widthInTotal = width / totalAspect;
4466
- const leftPos = (1 - widthInTotal) / 2;
4467
- return [`linear-gradient(to right,${bright} ${leftPos * 100}%,${dark} ${(leftPos + widthInTotal) * 100}%)`, totalAspect];
4468
- }
4469
- var RawLyricLineMouseEvent = class extends MouseEvent {
4470
- constructor(line, event) {
4471
- super(event.type, event);
4472
- this.line = line;
4473
- }
4474
- };
4475
- function getScaleFromTransform(transform) {
4476
- const match = transform.match(/matrix\(([^)]+)\)/);
4477
- if (match) {
4478
- const values = match[1].split(", ");
4479
- return (Number.parseFloat(values[0]) + Number.parseFloat(values[3])) / 2;
4480
- }
4481
- return 1;
4482
- }
4483
- var LyricLineEl = class extends LyricLineBase {
4484
- element = document.createElement("div");
4485
- splittedWords = [];
4486
- lineSize = [0, 0];
4487
- constructor(lyricPlayer, lyricLine = {
4488
- words: [],
4489
- translatedLyric: "",
4490
- romanLyric: "",
4491
- startTime: 0,
4492
- endTime: 0,
4493
- isBG: false,
4494
- isDuet: false
4495
- }) {
4496
- super();
4497
- this.lyricPlayer = lyricPlayer;
4498
- this.lyricLine = lyricLine;
4499
- this.element.setAttribute("class", index_module_default.lyricLine);
4500
- if (this.lyricLine.isBG) this.element.classList.add(index_module_default.lyricBgLine);
4501
- if (this.lyricLine.isDuet) this.element.classList.add(index_module_default.lyricDuetLine);
4502
- this.element.appendChild(document.createElement("div"));
4503
- this.element.appendChild(document.createElement("div"));
4504
- this.element.appendChild(document.createElement("div"));
4505
- const main = this.element.children[0];
4506
- const trans = this.element.children[1];
4507
- const roman = this.element.children[2];
4508
- main.setAttribute("class", index_module_default.lyricMainLine);
4509
- trans.setAttribute("class", index_module_default.lyricSubLine);
4510
- roman.setAttribute("class", index_module_default.lyricSubLine);
4511
- this.rebuildElement();
4512
- this.rebuildStyle();
4513
- this.markMaskImageDirty("Initial construction");
4514
- }
4515
- listenersMap = /* @__PURE__ */ new Map();
4516
- onMouseEvent = (e) => {
4517
- const wrapped = new RawLyricLineMouseEvent(this, e);
4518
- for (const listener of this.listenersMap.get(e.type) ?? []) listener.call(this, wrapped);
4519
- if (!this.dispatchEvent(wrapped) || wrapped.defaultPrevented) {
4520
- e.preventDefault();
4521
- e.stopPropagation();
4522
- e.stopImmediatePropagation();
4523
- }
4524
- };
4525
- addMouseEventListener(type, callback, options) {
4526
- if (callback) {
4527
- const listeners = this.listenersMap.get(type) ?? /* @__PURE__ */ new Set();
4528
- if (listeners.size === 0) this.element.addEventListener(type, this.onMouseEvent, options);
4529
- listeners.add(callback);
4530
- this.listenersMap.set(type, listeners);
4531
- }
4532
- }
4533
- removeMouseEventListener(type, callback, options) {
4534
- if (callback) {
4535
- const listeners = this.listenersMap.get(type);
4536
- if (listeners) {
4537
- listeners.delete(callback);
4538
- if (listeners.size === 0) this.element.removeEventListener(type, this.onMouseEvent, options);
4539
- }
4540
- }
4541
- }
4542
- areWordsOnSameLine(word1, word2) {
4543
- if (word1?.mainElement && word2?.mainElement) {
4544
- const word1el = word1.mainElement;
4545
- const word2el = word2.mainElement;
4546
- const rect1 = word1el.getBoundingClientRect();
4547
- const rect2 = word2el.getBoundingClientRect();
4548
- return Math.abs(rect1.top - rect2.top) < 10;
4549
- }
4550
- return true;
4551
- }
4552
- isEnabled = false;
4553
- async enable(maskAnimationTime = this.lyricLine.startTime) {
4554
- this.isEnabled = true;
4555
- this.element.classList.add(index_module_default.active);
4556
- await this.waitMaskImageUpdated();
4557
- const main = this.element.children[0];
4558
- for (const word of this.splittedWords) {
4559
- for (const a of word.elementAnimations) {
4560
- a.currentTime = 0;
4561
- a.playbackRate = 1;
4562
- a.play();
4563
- }
4564
- for (const a of word.maskAnimations) {
4565
- a.currentTime = Math.min(this.totalDuration, Math.max(0, maskAnimationTime - this.lyricLine.startTime));
4566
- a.playbackRate = 1;
4567
- a.play();
4568
- }
4569
- }
4570
- main.classList.add(index_module_default.active);
4571
- }
4572
- disable() {
4573
- this.isEnabled = false;
4574
- this.element.classList.remove(index_module_default.active);
4575
- const main = this.element.children[0];
4576
- for (const word of this.splittedWords) for (const a of word.elementAnimations) if (a.id === "float-word" || a.id.includes("emphasize-word-float-only")) {
4577
- a.playbackRate = -1;
4578
- a.play();
4579
- }
4580
- main.classList.remove(index_module_default.active);
4581
- }
4582
- lastWord;
4583
- async resume() {
4584
- await this.waitMaskImageUpdated();
4585
- if (!this.isEnabled) return;
4586
- for (const word of this.splittedWords) {
4587
- for (const a of word.elementAnimations) if (!this.lastWord || this.splittedWords.indexOf(this.lastWord) < this.splittedWords.indexOf(word)) a.play();
4588
- for (const a of word.maskAnimations) if (!this.lastWord || this.splittedWords.indexOf(this.lastWord) < this.splittedWords.indexOf(word)) a.play();
4589
- }
4590
- }
4591
- async pause() {
4592
- await this.waitMaskImageUpdated();
4593
- if (!this.isEnabled) return;
4594
- for (const word of this.splittedWords) {
4595
- for (const a of word.elementAnimations) a.pause();
4596
- for (const a of word.maskAnimations) a.pause();
4597
- }
4598
- }
4599
- setMaskAnimationState(maskAnimationTime = 0) {
4600
- const t = maskAnimationTime - this.lyricLine.startTime;
4601
- for (const word of this.splittedWords) for (const a of word.maskAnimations) {
4602
- a.currentTime = Math.min(this.totalDuration, Math.max(0, t));
4603
- a.playbackRate = 1;
4604
- if (t >= 0 && t < this.totalDuration) a.play();
4605
- else a.pause();
4606
- }
4607
- }
4608
- measureLockMark = false;
4609
- measureLock = mutexifyFunction(async (callback) => {
4610
- if (this.measureLockMark) return;
4611
- this.measureLockMark = true;
4612
- await callback();
4613
- this.measureLockMark = false;
4614
- });
4615
- getLine() {
4616
- return this.lyricLine;
4617
- }
4618
- show() {
4619
- this.rebuildStyle();
4620
- }
4621
- hide() {}
4622
- rebuildStyle() {}
4623
- getRubySegments(word) {
4624
- return (word.ruby ?? []).filter((ruby) => (ruby?.word?.trim().length ?? 0) > 0);
4625
- }
4626
- buildWordElement(word, shouldEmphasize, hasRubyLine, hasRomanLine, displayWord) {
4627
- const mainWordEl = document.createElement("span");
4628
- const subElements = [];
4629
- const romanWord = word.romanWord?.trim() ?? "";
4630
- let wordContainer = mainWordEl;
4631
- if (hasRubyLine || hasRomanLine) {
4632
- wordContainer = document.createElement("div");
4633
- mainWordEl.appendChild(wordContainer);
4634
- }
4635
- if (hasRubyLine) {
4636
- const rubyWordEl = document.createElement("div");
4637
- const rubySegments = this.getRubySegments(word);
4638
- for (const ruby of rubySegments) {
4639
- const rubyPartEl = document.createElement("span");
4640
- rubyPartEl.innerText = ruby.word;
4641
- rubyPartEl.dataset.startTime = String(ruby.startTime);
4642
- rubyPartEl.dataset.endTime = String(ruby.endTime);
4643
- rubyWordEl.appendChild(rubyPartEl);
4644
- }
4645
- rubyWordEl.classList.add(index_module_default.rubyWord);
4646
- mainWordEl.classList.add(index_module_default.wordWithRuby);
4647
- wordContainer.classList.add(index_module_default.wordBody);
4648
- mainWordEl.insertBefore(rubyWordEl, wordContainer);
4649
- }
4650
- if (shouldEmphasize) {
4651
- mainWordEl.classList.add(index_module_default.emphasize);
4652
- for (const char of displayWord.trim()) {
4653
- const charEl = document.createElement("span");
4654
- charEl.innerText = char;
4655
- subElements.push(charEl);
4656
- wordContainer.appendChild(charEl);
4657
- }
4658
- } else if (hasRomanLine) {
4659
- const wordEl = document.createElement("div");
4660
- wordEl.innerText = displayWord;
4661
- wordContainer.appendChild(wordEl);
4662
- } else mainWordEl.innerText = displayWord;
4663
- if (hasRomanLine) {
4664
- const romanWordEl = document.createElement("div");
4665
- romanWordEl.innerText = romanWord.length > 0 ? romanWord : "\xA0";
4666
- romanWordEl.classList.add(index_module_default.romanWord);
4667
- wordContainer.appendChild(romanWordEl);
4668
- }
4669
- return {
4670
- mainWordEl,
4671
- subElements
4672
- };
4673
- }
4674
- rebuildElement() {
4675
- this.disposeElements();
4676
- const main = this.element.children[0];
4677
- const trans = this.element.children[1];
4678
- const roman = this.element.children[2];
4679
- if (this.lyricPlayer._getIsNonDynamic()) {
4680
- main.innerText = this.lyricLine.words.map((w) => this.lyricPlayer.processObsceneWord(w)).join("");
4681
- trans.innerText = this.lyricLine.translatedLyric;
4682
- roman.innerText = this.lyricLine.romanLyric;
4683
- return;
4684
- }
4685
- const chunkedWords = chunkAndSplitLyricWords(this.lyricLine.words);
4686
- const hasRubyLine = this.lyricLine.words.some((word) => (word.ruby?.length ?? 0) > 0);
4687
- const hasRomanLine = this.lyricLine.words.some((word) => (word.romanWord?.trim().length ?? 0) > 0);
4688
- main.innerHTML = "";
4689
- for (const chunk of chunkedWords) if (Array.isArray(chunk)) {
4690
- if (chunk.length === 0) continue;
4691
- const merged = chunk.reduce((a, b) => {
4692
- a.endTime = Math.max(a.endTime, b.endTime);
4693
- a.startTime = Math.min(a.startTime, b.startTime);
4694
- a.word += b.word;
4695
- return a;
4696
- }, {
4697
- word: "",
4698
- romanWord: "",
4699
- startTime: Number.POSITIVE_INFINITY,
4700
- endTime: Number.NEGATIVE_INFINITY,
4701
- wordType: "normal",
4702
- obscene: false
4703
- });
4704
- const emp = chunk.map((word) => LyricLineBase.shouldEmphasize(word)).reduce((a, b) => a || b, LyricLineBase.shouldEmphasize(merged));
4705
- const wrapperWordEl = document.createElement("span");
4706
- wrapperWordEl.classList.add(index_module_default.emphasizeWrapper);
4707
- const characterElements = [];
4708
- for (const word of chunk) {
4709
- const { mainWordEl, subElements } = this.buildWordElement(word, emp, hasRubyLine, hasRomanLine, this.lyricPlayer.processObsceneWord(word));
4710
- if (emp) characterElements.push(...subElements);
4711
- this.splittedWords.push({
4712
- ...word,
4713
- mainElement: mainWordEl,
4714
- subElements,
4715
- elementAnimations: [],
4716
- maskAnimations: [],
4717
- width: 0,
4718
- height: 0,
4719
- padding: 0,
4720
- shouldEmphasize: emp
4721
- });
4722
- wrapperWordEl.appendChild(mainWordEl);
4723
- }
4724
- if (emp) this.splittedWords[this.splittedWords.length - 1].elementAnimations.push(...this.initEmphasizeAnimation(merged, characterElements, merged.endTime - merged.startTime, merged.startTime - this.lyricLine.startTime));
4725
- if (merged.word.trimStart() !== merged.word) main.appendChild(document.createTextNode(" "));
4726
- main.appendChild(wrapperWordEl);
4727
- if (merged.word.trimEnd() !== merged.word && LyricLineBase.shouldEmphasize(merged)) main.appendChild(document.createTextNode(" "));
4728
- } else if (chunk.word.trim().length === 0) main.appendChild(document.createTextNode(" "));
4729
- else {
4730
- const emp = LyricLineBase.shouldEmphasize(chunk);
4731
- const { mainWordEl, subElements } = this.buildWordElement(chunk, emp, hasRubyLine, hasRomanLine, this.lyricPlayer.processObsceneWord(chunk).trim());
4732
- const realWord = {
4733
- ...chunk,
4734
- mainElement: mainWordEl,
4735
- subElements,
4736
- elementAnimations: [],
4737
- maskAnimations: [],
4738
- width: 0,
4739
- height: 0,
4740
- padding: 0,
4741
- shouldEmphasize: emp
4742
- };
4743
- if (emp) {
4744
- const duration = Math.abs(realWord.endTime - realWord.startTime);
4745
- realWord.elementAnimations.push(...this.initEmphasizeAnimation(chunk, subElements, duration, realWord.startTime - this.lyricLine.startTime));
4746
- }
4747
- if (chunk.word.trimStart() !== chunk.word) main.appendChild(document.createTextNode(" "));
4748
- main.appendChild(mainWordEl);
4749
- if (chunk.word.trimEnd() !== chunk.word) main.appendChild(document.createTextNode(" "));
4750
- this.splittedWords.push(realWord);
4751
- }
4752
- trans.innerText = this.lyricLine.translatedLyric;
4753
- roman.innerText = this.lyricLine.romanLyric;
4754
- }
4755
- initEmphasizeAnimation(word, characterElements, duration, delay) {
4756
- const de = Math.max(0, delay);
4757
- let du = Math.max(1e3, duration);
4758
- let result = [];
4759
- let amount = du / 2e3;
4760
- amount = amount > 1 ? Math.sqrt(amount) : amount ** 3;
4761
- let blur = du / 3e3;
4762
- blur = blur > 1 ? Math.sqrt(blur) : blur ** 3;
4763
- amount *= .6;
4764
- blur *= .5;
4765
- if (this.lyricLine.words.length > 0 && word.word.includes(this.lyricLine.words[this.lyricLine.words.length - 1].word)) {
4766
- amount *= 1.6;
4767
- blur *= 1.5;
4768
- du *= 1.2;
4769
- }
4770
- amount = Math.min(1.2, amount);
4771
- blur = Math.min(.8, blur);
4772
- const animateDu = Number.isFinite(du) ? du : 0;
4773
- const empEasing = makeEmpEasing(EMP_EASING_MID);
4774
- result = characterElements.flatMap((el, i, arr) => {
4775
- const wordDe = de + du / 2.5 / arr.length * i;
4776
- const result = [];
4777
- const frames = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4778
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
4779
- const transX = empEasing(x);
4780
- const glowLevel = empEasing(x) * blur;
4781
- const mat = scaleMatrix4(createMatrix4(), 1 + transX * .1 * amount);
4782
- const offsetX = -transX * .03 * amount * (arr.length / 2 - i);
4783
- const offsetY = -transX * .025 * amount;
4784
- return {
4785
- offset: x,
4786
- transform: `${matrix4ToCSS(mat, 4)} translate(${offsetX}em, ${offsetY}em)`,
4787
- textShadow: `0 0 ${Math.min(.3, blur * .3)}em rgba(255, 255, 255, ${glowLevel})`
4788
- };
4789
- });
4790
- const glow = el.animate(frames, {
4791
- duration: animateDu,
4792
- delay: Number.isFinite(wordDe) ? wordDe : 0,
4793
- id: `emphasize-word-${el.innerText}-${i}`,
4794
- iterations: 1,
4795
- composite: "replace",
4796
- fill: "both"
4797
- });
4798
- glow.onfinish = () => {
4799
- glow.pause();
4800
- };
4801
- glow.pause();
4802
- result.push(glow);
4803
- const floatFrame = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4804
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
4805
- let y = Math.sin(x * Math.PI);
4806
- if (this.lyricLine.isBG) y *= 2;
4807
- return {
4808
- offset: x,
4809
- transform: `translateY(${-y * .05}em)`
4810
- };
4811
- });
4812
- const float = el.animate(floatFrame, {
4813
- duration: animateDu * 1.4,
4814
- delay: Number.isFinite(wordDe) ? wordDe - 400 : 0,
4815
- id: "emphasize-word-float",
4816
- iterations: 1,
4817
- composite: "add",
4818
- fill: "both"
4819
- });
4820
- float.onfinish = () => {
4821
- float.pause();
4822
- };
4823
- float.pause();
4824
- result.push(float);
4825
- return result;
4826
- });
4827
- return result;
4828
- }
4829
- get totalDuration() {
4830
- return this.lyricLine.endTime - this.lyricLine.startTime;
4831
- }
4832
- maskImageDirty = false;
4833
- markImageDirtyPromiseResolve = /* @__PURE__ */ new Set();
4834
- markImageDirtyPromise = new Promise((resolve) => {
4835
- this.markImageDirtyPromiseResolve.add(resolve);
4836
- });
4837
- markMaskImageDirty(_debugReason = "") {
4838
- this.maskImageDirty = true;
4839
- if (!this.element.classList.contains(index_module_default.dirty)) this.element.classList.add(index_module_default.dirty);
4840
- const newPromise = Promise.all([this.markImageDirtyPromise, new Promise((resolve) => {
4841
- this.markImageDirtyPromiseResolve.add(resolve);
4842
- })]).then(() => {});
4843
- this.markImageDirtyPromise = newPromise;
4844
- return newPromise;
4845
- }
4846
- waitMaskImageUpdated() {
4847
- return this.markImageDirtyPromise;
4848
- }
4849
- async updateMaskImage() {
4850
- if (!this.element.checkVisibility({ contentVisibilityAuto: true })) return;
4851
- this.maskImageDirty = false;
4852
- await this.measureLock(async () => {
4853
- await Promise.all(this.splittedWords.map(async (word) => {
4854
- const el = word.mainElement;
4855
- if (el) await measure(() => {
4856
- word.padding = Number.parseFloat(getComputedStyle(el).paddingLeft);
4857
- word.width = el.clientWidth - word.padding * 2;
4858
- word.height = el.clientHeight - word.padding * 2;
4859
- });
4860
- else {
4861
- word.width = 0;
4862
- word.height = 0;
4863
- word.padding = 0;
4864
- }
4865
- if (word.width * word.height === 0) console.warn("Word size is zero");
4866
- }));
4867
- await mutate(() => {
4868
- if (this.lyricPlayer.supportMaskImage) this.generateWebAnimationBasedMaskImage();
4869
- else this.generateCalcBasedMaskImage();
4870
- });
4871
- });
4872
- for (const resolve of this.markImageDirtyPromiseResolve) {
4873
- resolve();
4874
- this.markImageDirtyPromiseResolve.delete(resolve);
4875
- }
4876
- await mutate(() => {
4877
- this.element.classList.remove(index_module_default.dirty);
4878
- });
4879
- }
4880
- generateCalcBasedMaskImage() {
4881
- for (const word of this.splittedWords) {
4882
- const wordEl = word.mainElement;
4883
- if (wordEl) {
4884
- word.width = wordEl.clientWidth;
4885
- word.height = wordEl.clientHeight;
4886
- const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
4887
- const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / word.width);
4888
- const totalAspectStr = `${totalAspect * 100}% 100%`;
4889
- if (this.lyricPlayer.supportMaskImage) {
4890
- wordEl.style.maskImage = maskImage;
4891
- wordEl.style.maskRepeat = "no-repeat";
4892
- wordEl.style.maskOrigin = "left";
4893
- wordEl.style.maskSize = totalAspectStr;
4894
- } else {
4895
- wordEl.style.webkitMaskImage = maskImage;
4896
- wordEl.style.webkitMaskRepeat = "no-repeat";
4897
- wordEl.style.webkitMaskOrigin = "left";
4898
- wordEl.style.webkitMaskSize = totalAspectStr;
4899
- }
4900
- const w = word.width + fadeWidth;
4901
- const maskPos = `clamp(${-w}px,calc(${-w}px + (var(--amll-player-time) - ${word.startTime})*${w / Math.abs(word.endTime - word.startTime)}px),0px) 0px, left top`;
4902
- wordEl.style.maskPosition = maskPos;
4903
- wordEl.style.webkitMaskPosition = maskPos;
4904
- }
4905
- }
4906
- }
4907
- generateWebAnimationBasedMaskImage() {
4908
- const totalFadeDuration = Math.max(this.splittedWords.reduce((pv, w) => Math.max(w.endTime, pv), 0), this.lyricLine.endTime) - this.lyricLine.startTime;
4909
- this.splittedWords.forEach((word, i) => {
4910
- const wordEl = word.mainElement;
4911
- if (wordEl) {
4912
- const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
4913
- const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / (word.width + word.padding * 2));
4914
- const totalAspectStr = `${totalAspect * 100}% 100%`;
4915
- if (this.lyricPlayer.supportMaskImage) {
4916
- wordEl.style.maskImage = maskImage;
4917
- wordEl.style.maskRepeat = "no-repeat";
4918
- wordEl.style.maskOrigin = "left";
4919
- wordEl.style.maskSize = totalAspectStr;
4920
- } else {
4921
- wordEl.style.webkitMaskImage = maskImage;
4922
- wordEl.style.webkitMaskRepeat = "no-repeat";
4923
- wordEl.style.webkitMaskOrigin = "left";
4924
- wordEl.style.webkitMaskSize = totalAspectStr;
4925
- }
4926
- const widthBeforeSelf = this.splittedWords.slice(0, i).reduce((a, b) => a + b.width, 0) + (this.splittedWords[0] ? fadeWidth : 0);
4927
- const minOffset = -(word.width + word.padding * 2 + fadeWidth);
4928
- const clampOffset = (x) => Math.max(minOffset, Math.min(0, x));
4929
- let curPos = -widthBeforeSelf - word.width - word.padding - fadeWidth;
4930
- let timeOffset = 0;
4931
- const frames = [];
4932
- let lastPos = curPos;
4933
- let lastTime = 0;
4934
- const pushFrame = () => {
4935
- const moveOffset = curPos - lastPos;
4936
- const time = Math.max(0, Math.min(1, timeOffset));
4937
- const duration = time - lastTime;
4938
- const d = Math.abs(duration / moveOffset);
4939
- if (curPos > minOffset && lastPos < minOffset) {
4940
- const staticTime = Math.abs(lastPos - minOffset) * d;
4941
- const value = `${clampOffset(lastPos)}px 0`;
4942
- const frame = {
4943
- offset: lastTime + staticTime,
4944
- maskPosition: value
4945
- };
4946
- frames.push(frame);
4947
- }
4948
- if (curPos > 0 && lastPos < 0) {
4949
- const staticTime = Math.abs(lastPos) * d;
4950
- const value = `${clampOffset(curPos)}px 0`;
4951
- const frame = {
4952
- offset: lastTime + staticTime,
4953
- maskPosition: value
4954
- };
4955
- frames.push(frame);
4956
- }
4957
- const frame = {
4958
- offset: time,
4959
- maskPosition: `${clampOffset(curPos)}px 0`
4960
- };
4961
- frames.push(frame);
4962
- lastPos = curPos;
4963
- lastTime = time;
4964
- };
4965
- pushFrame();
4966
- let lastTimeStamp = 0;
4967
- this.splittedWords.forEach((otherWord, j) => {
4968
- {
4969
- const curTimeStamp = otherWord.startTime - this.lyricLine.startTime;
4970
- const staticDuration = curTimeStamp - lastTimeStamp;
4971
- timeOffset += staticDuration / totalFadeDuration;
4972
- if (staticDuration > 0) pushFrame();
4973
- lastTimeStamp = curTimeStamp;
4974
- }
4975
- {
4976
- const fadeDuration = otherWord.endTime - otherWord.startTime;
4977
- const rubySegments = this.getRubySegments(otherWord);
4978
- const rubyCharCount = rubySegments.reduce((total, ruby) => total + ruby.word.length, 0);
4979
- if (rubyCharCount > 0) {
4980
- const widthPerChar = otherWord.width / rubyCharCount;
4981
- let charIndex = 0;
4982
- for (const ruby of rubySegments) {
4983
- const rubyStartTime = Number.isFinite(ruby.startTime) ? ruby.startTime : otherWord.startTime;
4984
- const rubyEndTime = Number.isFinite(ruby.endTime) ? ruby.endTime : otherWord.endTime;
4985
- const rubyStart = Math.max(rubyStartTime, otherWord.startTime);
4986
- const rubyEnd = Math.min(Math.max(rubyEndTime, rubyStart), otherWord.endTime);
4987
- const rubyStartStamp = rubyStart - this.lyricLine.startTime;
4988
- const rubyStaticDuration = rubyStartStamp - lastTimeStamp;
4989
- timeOffset += rubyStaticDuration / totalFadeDuration;
4990
- if (rubyStaticDuration > 0) pushFrame();
4991
- lastTimeStamp = rubyStartStamp;
4992
- const perCharDuration = Math.max(0, rubyEnd - rubyStart) / ruby.word.length;
4993
- for (let rubyCharIndex = 0; rubyCharIndex < ruby.word.length; rubyCharIndex++) {
4994
- timeOffset += perCharDuration / totalFadeDuration;
4995
- curPos += widthPerChar;
4996
- if (j === 0 && charIndex === 0) curPos += fadeWidth * 1.5;
4997
- if (j === this.splittedWords.length - 1 && charIndex === rubyCharCount - 1) curPos += fadeWidth * .5;
4998
- if (perCharDuration > 0) pushFrame();
4999
- lastTimeStamp += perCharDuration;
5000
- charIndex++;
5001
- }
5002
- }
5003
- const wordEndStamp = Math.max(otherWord.endTime - this.lyricLine.startTime, lastTimeStamp);
5004
- const wordTailDuration = wordEndStamp - lastTimeStamp;
5005
- timeOffset += wordTailDuration / totalFadeDuration;
5006
- if (wordTailDuration > 0) pushFrame();
5007
- lastTimeStamp = wordEndStamp;
5008
- } else {
5009
- timeOffset += fadeDuration / totalFadeDuration;
5010
- curPos += otherWord.width;
5011
- if (j === 0) curPos += fadeWidth * 1.5;
5012
- if (j === this.splittedWords.length - 1) curPos += fadeWidth * .5;
5013
- if (fadeDuration > 0) pushFrame();
5014
- lastTimeStamp += fadeDuration;
5015
- }
5016
- }
5017
- });
5018
- for (const a of word.maskAnimations) a.cancel();
5019
- try {
5020
- const ani = wordEl.animate(frames, {
5021
- duration: totalFadeDuration || 1,
5022
- id: `fade-word-${word.word}-${i}`,
5023
- fill: "both"
5024
- });
5025
- ani.pause();
5026
- word.maskAnimations = [ani];
5027
- } catch (err) {
5028
- console.warn("应用渐变动画发生错误", frames, totalFadeDuration, err);
5029
- }
5030
- }
5031
- });
5032
- }
5033
- getElement() {
5034
- return this.element;
5035
- }
5036
- setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0) {
5037
- super.setTransform(top, scale, opacity, blur, force, delay);
5038
- const beforeInSight = this.isInSight;
5039
- const enableSpring = this.lyricPlayer.getEnableSpring();
5040
- this.top = top;
5041
- this.scale = scale;
5042
- this.delay = delay * 1e3 | 0;
5043
- const main = this.element.children[0];
5044
- main.style.opacity = `${opacity}`;
5045
- if (force || !enableSpring) {
5046
- if (force) this.element.classList.add(index_module_default.tmpDisableTransition);
5047
- this.lineTransforms.posY.setPosition(top);
5048
- this.lineTransforms.scale.setPosition(scale);
5049
- if (!enableSpring) {
5050
- const afterInSight = this.isInSight;
5051
- if (beforeInSight || afterInSight) this.show();
5052
- else this.hide();
5053
- } else this.rebuildStyle();
5054
- if (force) requestAnimationFrame(() => {
5055
- this.element.classList.remove(index_module_default.tmpDisableTransition);
5056
- });
5057
- } else {
5058
- this.lineTransforms.posY.setTargetPosition(top, delay);
5059
- this.lineTransforms.scale.setTargetPosition(scale);
5060
- }
5061
- }
5062
- update(delta = 0) {
5063
- if (!this.lyricPlayer.getEnableSpring()) return;
5064
- this.lineTransforms.posY.update(delta);
5065
- this.lineTransforms.scale.update(delta);
5066
- if (this.isInSight) {
5067
- this.show();
5068
- if (this.maskImageDirty) this.updateMaskImage();
5069
- } else this.hide();
5070
- if (this.lyricPlayer.getEnableSpring()) {
5071
- this.element.style.setProperty("--bright-mask-alpha", `${Math.max(0, Math.min(1, this.lineTransforms.scale.getCurrentPosition() / 100 - .97) / .03) * .8 + .2}`);
5072
- this.element.style.setProperty("--dark-mask-alpha", `${Math.max(0, Math.min(1, this.lineTransforms.scale.getCurrentPosition() / 100 - .97) / .03) * .2 + .2}`);
5073
- } else {
5074
- const transform = window.getComputedStyle(this.element).transform;
5075
- const scale = getScaleFromTransform(transform);
5076
- this.element.style.setProperty("--bright-mask-alpha", `${Math.max(0, Math.min(1, (scale - .97) / .03)) * .8 + .2}`);
5077
- this.element.style.setProperty("--dark-mask-alpha", `${Math.max(0, Math.min(1, (scale - .97) / .03)) * .2 + .2}`);
5078
- }
5079
- }
5080
- _getDebugTargetPos() {
5081
- return `[位移: ${this.top}; 缩放: ${this.scale}; 延时: ${this.delay}]`;
5082
- }
5083
- get isInSight() {
5084
- const t = this.lineTransforms.posY.getCurrentPosition();
5085
- const h = this.lineSize[1];
5086
- const b = t + h;
5087
- return !(t > this.lyricPlayer.size[1] + h || b < -h);
5088
- }
5089
- disposeElements() {
5090
- for (const realWord of this.splittedWords) {
5091
- for (const a of realWord.elementAnimations) a.cancel();
5092
- for (const a of realWord.maskAnimations) a.cancel();
5093
- for (const sub of realWord.subElements) {
5094
- sub.remove();
5095
- sub.parentNode?.removeChild(sub);
5096
- }
5097
- realWord.elementAnimations = [];
5098
- realWord.maskAnimations = [];
5099
- realWord.subElements = [];
5100
- realWord.mainElement.remove();
5101
- realWord.mainElement.parentNode?.removeChild(realWord.mainElement);
5102
- }
5103
- this.splittedWords = [];
5104
- }
5105
- dispose() {
5106
- this.disposeElements();
5107
- this.element.remove();
5108
- }
5109
- };
5110
- //#endregion
5111
- //#region src/lyric-player/dom-slim/index.ts
5112
- /**
5113
- * 歌词播放组件,本框架的核心组件
5114
- *
5115
- * 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
5116
- */
5117
- var DomSlimLyricPlayer = class extends LyricPlayerBase {
5118
- currentLyricLineObjects = [];
5119
- debounceCalcLayout = debounce(() => this.calcLayout(true).then(() => this.currentLyricLineObjects.map(async (el, i) => {
5120
- el.markMaskImageDirty("DomLyricPlayer onResize");
5121
- await el.waitMaskImageUpdated();
5122
- if (this.hotLines.has(i)) {
5123
- el.enable(this.currentTime);
5124
- el.resume();
5125
- }
5126
- })), 1e3);
5127
- onResize() {
5128
- const computedStyles = getComputedStyle(this.element);
5129
- this._baseFontSize = Number.parseFloat(computedStyles.fontSize);
5130
- const innerWidth = this.element.clientWidth - Number.parseFloat(computedStyles.paddingLeft) - Number.parseFloat(computedStyles.paddingRight);
5131
- const innerHeight = this.element.clientHeight - Number.parseFloat(computedStyles.paddingTop) - Number.parseFloat(computedStyles.paddingBottom);
5132
- this.innerSize[0] = innerWidth;
5133
- this.innerSize[1] = innerHeight;
5134
- this.rebuildStyle();
5135
- this.debounceCalcLayout();
5136
- }
5137
- supportPlusLighter = CSS.supports("mix-blend-mode", "plus-lighter");
5138
- supportMaskImage = CSS.supports("mask-image", "none");
5139
- innerSize = [0, 0];
5140
- onLineClickedHandler = (e) => {
5141
- const evt = new LyricLineMouseEvent(this.lyricLinesIndexes.get(e.line) ?? -1, e.line, e);
5142
- if (!this.dispatchEvent(evt)) {
5143
- e.preventDefault();
5144
- e.stopPropagation();
5145
- e.stopImmediatePropagation();
5146
- }
5147
- };
5148
- /**
5149
- * 是否为非逐词歌词
5150
- * @internal
5151
- */
5152
- _getIsNonDynamic() {
5153
- return this.isNonDynamic;
5154
- }
5155
- _baseFontSize = Number.parseFloat(getComputedStyle(this.element).fontSize);
5156
- get baseFontSize() {
5157
- return this._baseFontSize;
5158
- }
5159
- constructor() {
5160
- super();
5161
- this.onResize();
5162
- this.element.classList.add("amll-lyric-player", "dom-slim");
5163
- if (this.disableSpring) this.element.classList.add(index_module_default.disableSpring);
5164
- }
5165
- rebuildStyle() {
5166
- const width = this.innerSize[0];
5167
- const height = this.innerSize[1];
5168
- this.element.style.setProperty("--amll-lp-width", `${width.toFixed(4)}px`);
5169
- this.element.style.setProperty("--amll-lp-height", `${height.toFixed(4)}px`);
5170
- }
5171
- setWordFadeWidth(value = .5) {
5172
- super.setWordFadeWidth(value);
5173
- for (const el of this.currentLyricLineObjects) el.markMaskImageDirty("DomLyricPlayer setWordFadeWidth");
5174
- }
5175
- /**
5176
- * 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
5177
- * @param lines 歌词数组
5178
- * @param initialTime 初始时间,默认为 0
5179
- */
5180
- setLyricLines(lines, initialTime = 0) {
5181
- super.setLyricLines(lines, initialTime);
5182
- if (this.hasDuetLine) this.element.classList.add(index_module_default.hasDuetLine);
5183
- else this.element.classList.remove(index_module_default.hasDuetLine);
5184
- for (const line of this.currentLyricLineObjects) {
5185
- line.removeMouseEventListener("click", this.onLineClickedHandler);
5186
- line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
5187
- line.dispose();
5188
- }
5189
- this.currentLyricLineObjects = this.processedLines.map((line, i) => {
5190
- const lineEl = new LyricLineEl(this, line);
5191
- lineEl.addMouseEventListener("click", this.onLineClickedHandler);
5192
- lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
5193
- this.element.appendChild(lineEl.getElement());
5194
- this.lyricLinesIndexes.set(lineEl, i);
5195
- lineEl.markMaskImageDirty("DomLyricPlayer setLyricLines");
5196
- return lineEl;
5197
- });
5198
- this.setLinePosXSpringParams({});
5199
- this.setLinePosYSpringParams({});
5200
- this.setLineScaleSpringParams({});
5201
- this.calcLayout(true).then(() => {
5202
- this.initialLayoutFinished = true;
5203
- });
5204
- }
5205
- pause() {
5206
- super.pause();
5207
- this.interludeDots.pause();
5208
- for (const line of this.currentLyricLineObjects) line.pause();
5209
- }
5210
- resume() {
5211
- super.resume();
5212
- this.interludeDots.resume();
5213
- for (const line of this.currentLyricLineObjects) line.resume();
5214
- }
5215
- update(delta = 0) {
5216
- if (!this.initialLayoutFinished) return;
5217
- super.update(delta);
5218
- if (!this.isPageVisible) return;
5219
- const deltaS = delta / 1e3;
5220
- for (const line of this.currentLyricLineObjects) line.update(deltaS);
5221
- }
5222
- async calcLayout(sync) {
5223
- await super.calcLayout(sync);
5224
- const curLine = this.currentLyricLineObjects[this.targetAlignIndex];
5225
- const curLineEl = curLine.getElement();
5226
- const curLineVisibility = curLineEl.checkVisibility({ contentVisibilityAuto: true });
5227
- const playerTop = this.element.getBoundingClientRect().top;
5228
- if (!curLineVisibility) curLineEl.scrollIntoView({
5229
- block: "center",
5230
- behavior: "instant"
5231
- });
5232
- const curLineHeight = curLineEl.clientHeight;
5233
- let scrollToPos = curLineEl.getBoundingClientRect().top - playerTop - this.size[1] * this.alignPosition;
5234
- if (curLine) switch (this.alignAnchor) {
5235
- case "bottom":
5236
- scrollToPos += curLineHeight;
5237
- break;
5238
- case "center":
5239
- scrollToPos += curLineHeight / 2;
5240
- break;
5241
- case "top": break;
5242
- }
5243
- this.element.scrollBy({
5244
- top: scrollToPos,
5245
- behavior: "smooth"
5246
- });
5247
- }
5248
- dispose() {
5249
- super.dispose();
5250
- this.element.remove();
5251
- for (const el of this.currentLyricLineObjects) el.dispose();
5252
- this.bottomLine.dispose();
5253
- this.interludeDots.dispose();
5254
- }
5255
- };
5256
- //#endregion
5257
- //#region src/lyric-player/index.ts
5258
- /**
5259
- * 歌词中不雅用语的掩码模式
5260
- */
5261
- var MaskObsceneWordsMode = /* @__PURE__ */ function(MaskObsceneWordsMode) {
5262
- /** 禁用任何不雅用语掩码 */
5263
- MaskObsceneWordsMode["Disabled"] = "";
5264
- /** 完全掩码所有不雅用语 */
5265
- MaskObsceneWordsMode["FullMask"] = "full-mask";
5266
- /** 保留首尾字符,屏蔽中间字符 */
5267
- MaskObsceneWordsMode["PartialMask"] = "partial-mask";
5268
- return MaskObsceneWordsMode;
5269
- }(MaskObsceneWordsMode || {});
5270
- /**
5271
- * 歌词行的渲染模式
5272
- * @internal
5273
- */
5274
- var LyricLineRenderMode = /* @__PURE__ */ function(LyricLineRenderMode) {
5275
- LyricLineRenderMode[LyricLineRenderMode["SOLID"] = 0] = "SOLID";
5276
- LyricLineRenderMode[LyricLineRenderMode["GRADIENT"] = 1] = "GRADIENT";
5277
- return LyricLineRenderMode;
5278
- }(LyricLineRenderMode || {});
5279
- //#endregion
5280
4761
  exports.AbstractBaseRenderer = AbstractBaseRenderer;
5281
4762
  exports.BackgroundRender = BackgroundRender;
5282
4763
  exports.BaseRenderer = BaseRenderer;
5283
4764
  exports.DomLyricPlayer = DomLyricPlayer;
5284
- exports.DomSlimLyricPlayer = DomSlimLyricPlayer;
4765
+ exports.LayoutAlignAnchor = LayoutAlignAnchor;
5285
4766
  exports.LyricLineMouseEvent = LyricLineMouseEvent;
5286
4767
  exports.LyricLineRenderMode = LyricLineRenderMode;
5287
4768
  exports.LyricPlayer = DomLyricPlayer;