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