@applemusic-like-lyrics/core 0.4.1 → 0.5.0

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);
@@ -1716,14 +1727,6 @@ var lyric_player_module_default = {
1716
1727
  "wordWithRuby": "FmKaba_wordWithRuby"
1717
1728
  };
1718
1729
  //#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
1730
  //#region src/utils/optimize-lyric.ts
1728
1731
  const DEFAULT_OPTIMIZE_OPTIONS = {
1729
1732
  normalizeSpaces: true,
@@ -1867,6 +1870,145 @@ function optimizeLyricLines(lines, options) {
1867
1870
  if (config.tryAdvanceStartTime) tryAdvanceStartTime(lines);
1868
1871
  }
1869
1872
  //#endregion
1873
+ //#region src/lyric-player/dom/interlude-dots.ts
1874
+ function easeInOutBack(x) {
1875
+ const c2 = 1.70158 * 1.525;
1876
+ return x < .5 ? (2 * x) ** 2 * ((c2 + 1) * 2 * x - c2) / 2 : ((2 * x - 2) ** 2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
1877
+ }
1878
+ function easeOutExpo(x) {
1879
+ return x === 1 ? 1 : 1 - 2 ** (-10 * x);
1880
+ }
1881
+ var InterludeDots = class {
1882
+ element = document.createElement("div");
1883
+ dot0 = document.createElement("span");
1884
+ dot1 = document.createElement("span");
1885
+ dot2 = document.createElement("span");
1886
+ left = 0;
1887
+ top = 0;
1888
+ playing = true;
1889
+ lastStyle = "";
1890
+ currentInterlude;
1891
+ currentTime = 0;
1892
+ targetBreatheDuration = 1500;
1893
+ constructor() {
1894
+ this.element.className = lyric_player_module_default.interludeDots;
1895
+ this.element.appendChild(this.dot0);
1896
+ this.element.appendChild(this.dot1);
1897
+ this.element.appendChild(this.dot2);
1898
+ }
1899
+ getElement() {
1900
+ return this.element;
1901
+ }
1902
+ setTransform(left = this.left, top = this.top) {
1903
+ this.left = left;
1904
+ this.top = top;
1905
+ this.update();
1906
+ }
1907
+ setInterlude(interlude) {
1908
+ this.currentInterlude = interlude;
1909
+ this.currentTime = interlude?.[0] ?? 0;
1910
+ if (interlude) this.element.classList.add(lyric_player_module_default.enabled);
1911
+ else this.element.classList.remove(lyric_player_module_default.enabled);
1912
+ }
1913
+ pause() {
1914
+ this.playing = false;
1915
+ this.element.classList.remove(lyric_player_module_default.playing);
1916
+ }
1917
+ resume() {
1918
+ this.playing = true;
1919
+ this.element.classList.add(lyric_player_module_default.playing);
1920
+ }
1921
+ update(delta = 0) {
1922
+ if (!this.playing) return;
1923
+ this.currentTime += delta;
1924
+ let curStyle = "";
1925
+ curStyle += `transform:translate(${this.left.toFixed(2)}px, ${this.top.toFixed(2)}px)`;
1926
+ if (this.currentInterlude) {
1927
+ const interludeDuration = this.currentInterlude[1] - this.currentInterlude[0];
1928
+ const currentDuration = this.currentTime - this.currentInterlude[0];
1929
+ if (currentDuration <= interludeDuration) {
1930
+ const breatheDuration = interludeDuration / Math.ceil(interludeDuration / this.targetBreatheDuration);
1931
+ let scale = 1;
1932
+ let globalOpacity = 1;
1933
+ scale *= Math.sin(1.5 * Math.PI - currentDuration / breatheDuration * 2) / 20 + 1;
1934
+ if (currentDuration < 2e3) scale *= easeOutExpo(currentDuration / 2e3);
1935
+ if (currentDuration < 500) globalOpacity = 0;
1936
+ else if (currentDuration < 1e3) globalOpacity *= (currentDuration - 500) / 500;
1937
+ if (interludeDuration - currentDuration < 750) scale *= 1 - easeInOutBack((750 - (interludeDuration - currentDuration)) / 750 / 2);
1938
+ if (interludeDuration - currentDuration < 375) globalOpacity *= clamp01((interludeDuration - currentDuration) / 375);
1939
+ const dotsDuration = clampPositive(interludeDuration - 750);
1940
+ scale = clampPositive(scale) * .7;
1941
+ curStyle += ` scale(${scale})`;
1942
+ const dot0Opacity = clamp(.25, currentDuration * 3 / dotsDuration * .75, 1);
1943
+ const dot1Opacity = clamp(.25, (currentDuration - dotsDuration / 3) * 3 / dotsDuration * .75, 1);
1944
+ const dot2Opacity = clamp(.25, (currentDuration - dotsDuration / 3 * 2) * 3 / dotsDuration * .75, 1);
1945
+ this.dot0.style.opacity = `${clamp01(globalOpacity * dot0Opacity)}`;
1946
+ this.dot1.style.opacity = `${clamp01(globalOpacity * dot1Opacity)}`;
1947
+ this.dot2.style.opacity = `${clamp01(globalOpacity * dot2Opacity)}`;
1948
+ } else {
1949
+ curStyle += " scale(0)";
1950
+ this.dot0.style.opacity = "0";
1951
+ this.dot1.style.opacity = "0";
1952
+ this.dot2.style.opacity = "0";
1953
+ }
1954
+ curStyle += ";";
1955
+ if (this.lastStyle !== curStyle) {
1956
+ this.element.setAttribute("style", curStyle);
1957
+ this.lastStyle = curStyle;
1958
+ }
1959
+ }
1960
+ }
1961
+ dispose() {
1962
+ this.element.remove();
1963
+ }
1964
+ };
1965
+ //#endregion
1966
+ //#region src/utils/schedule.ts
1967
+ const measureTasks = [];
1968
+ const mutateTasks = [];
1969
+ let scheduled = false;
1970
+ function onFlush() {
1971
+ let tmp = mutateTasks.shift();
1972
+ while (tmp) {
1973
+ try {
1974
+ tmp.resolve(tmp.task());
1975
+ } catch (error) {
1976
+ tmp.reject(error);
1977
+ }
1978
+ tmp = mutateTasks.shift();
1979
+ }
1980
+ tmp = measureTasks.shift();
1981
+ while (tmp) {
1982
+ try {
1983
+ tmp.resolve(tmp.task());
1984
+ } catch (error) {
1985
+ tmp.reject(error);
1986
+ }
1987
+ tmp = measureTasks.shift();
1988
+ }
1989
+ scheduled = false;
1990
+ }
1991
+ function scheduleFlush() {
1992
+ if (!scheduled) {
1993
+ scheduled = true;
1994
+ requestAnimationFrame(onFlush);
1995
+ }
1996
+ }
1997
+ function measure(callback) {
1998
+ const task = {
1999
+ task: callback,
2000
+ resolve: () => {},
2001
+ reject: () => {}
2002
+ };
2003
+ const promise = new Promise((resolve, reject) => {
2004
+ task.resolve = resolve;
2005
+ task.reject = reject;
2006
+ });
2007
+ measureTasks.push(task);
2008
+ scheduleFlush();
2009
+ return promise;
2010
+ }
2011
+ //#endregion
1870
2012
  //#region src/utils/derivative.ts
1871
2013
  function derivative(f) {
1872
2014
  const h = .001;
@@ -1981,67 +2123,7 @@ function solveSpring(from, velocity, to, delay = 0, params) {
1981
2123
  };
1982
2124
  }
1983
2125
  //#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
2126
+ //#region src/lyric-player/base/bottom-line.ts
2045
2127
  var BottomLineEl = class {
2046
2128
  element = document.createElement("div");
2047
2129
  left = 0;
@@ -2130,107 +2212,423 @@ var BottomLineEl = class {
2130
2212
  }
2131
2213
  };
2132
2214
  //#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;
2215
+ //#region src/lyric-player/base/consts.ts
2216
+ /** 歌词中不雅用语的掩码模式 */
2217
+ const MaskObsceneWordsMode = {
2218
+ /** 禁用任何不雅用语掩码 */
2219
+ Disabled: "",
2220
+ /** 完全掩码所有不雅用语 */
2221
+ FullMask: "full-mask",
2222
+ /** 保留首尾字符,屏蔽中间字符 */
2223
+ PartialMask: "partial-mask"
2224
+ };
2225
+ /**
2226
+ * 歌词行的渲染模式
2227
+ * @internal
2228
+ */
2229
+ const LyricLineRenderMode = {
2230
+ SOLID: 0,
2231
+ GRADIENT: 1
2232
+ };
2233
+ /** 布局对齐锚点 */
2234
+ const LayoutAlignAnchor = {
2235
+ Top: "top",
2236
+ Center: "center",
2237
+ Bottom: "bottom"
2238
+ };
2239
+ //#endregion
2240
+ //#region src/lyric-player/base/layout.ts
2241
+ /**
2242
+ * 根据当前时间与当前目标行,计算当前是否处于某个可展示的间奏区间。
2243
+ *
2244
+ * 仅识别时间轴上的间奏空档,不涉及具体 DOM 元素的创建与摆放。
2245
+ * 若当前不应展示间奏动画,则返回 `undefined`。
2246
+ */
2247
+ function computeCurrentInterlude(input) {
2248
+ const currentTime = input.currentTime + 20;
2249
+ const currentIndex = input.scrollToIndex;
2250
+ const lines = input.processedLines;
2251
+ const checkGap = (k) => {
2252
+ if (k < -1 || k >= lines.length - 1) return void 0;
2253
+ const prevLine = k === -1 ? null : lines[k];
2254
+ const nextLine = lines[k + 1];
2255
+ const gapStart = prevLine ? prevLine.endTime : 0;
2256
+ const gapEnd = Math.max(gapStart, nextLine.startTime - 250);
2257
+ if (gapEnd - gapStart < 4e3) return;
2258
+ if (gapEnd > currentTime && gapStart < currentTime) return {
2259
+ startTime: Math.max(gapStart, currentTime),
2260
+ endTime: gapEnd,
2261
+ anchorLineIndex: k,
2262
+ isNextDuet: nextLine.isDuet
2263
+ };
2264
+ };
2265
+ return checkGap(currentIndex - 1) || checkGap(currentIndex) || checkGap(currentIndex + 1);
2137
2266
  }
2138
- function easeOutExpo(x) {
2139
- return x === 1 ? 1 : 1 - 2 ** (-10 * x);
2267
+ /**
2268
+ * 根据当前播放上下文计算歌词纵向滚动动画的弹簧参数。
2269
+ *
2270
+ * 其策略为:
2271
+ * - seeking 或间奏时使用更稳定的固定参数
2272
+ * - 普通播放时根据相邻歌词的时间间隔动态调整 stiffness / damping
2273
+ */
2274
+ function computeLinePosYSpringParams(input) {
2275
+ const { enabled, processedLines, scrollToIndex, isSeeking, isInterludeActive } = input;
2276
+ if (!enabled || processedLines.length === 0) return { shouldUpdate: false };
2277
+ if (isSeeking || isInterludeActive) return {
2278
+ shouldUpdate: true,
2279
+ params: {
2280
+ stiffness: 90,
2281
+ damping: 15
2282
+ }
2283
+ };
2284
+ const currentLine = processedLines[scrollToIndex];
2285
+ const prevLine = processedLines[scrollToIndex - 1];
2286
+ if (!currentLine || !prevLine) return { shouldUpdate: false };
2287
+ const interval = currentLine.startTime - (prevLine.words[0]?.startTime ?? prevLine.startTime);
2288
+ const MIN_INTERVAL = 100;
2289
+ const MAX_INTERVAL = 800;
2290
+ const clampedInterval = clamp(interval, MIN_INTERVAL, MAX_INTERVAL);
2291
+ const MAX_STIFFNESS = 220;
2292
+ const MIN_STIFFNESS = 170;
2293
+ let ratio = 1 - (clampedInterval - MIN_INTERVAL) / (MAX_INTERVAL - MIN_INTERVAL);
2294
+ ratio = ratio ** .2;
2295
+ const targetStiffness = MIN_STIFFNESS + ratio * (MAX_STIFFNESS - MIN_STIFFNESS);
2296
+ return {
2297
+ shouldUpdate: true,
2298
+ params: {
2299
+ stiffness: targetStiffness,
2300
+ damping: Math.sqrt(targetStiffness) * 2.2
2301
+ }
2302
+ };
2140
2303
  }
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)}`;
2304
+ /**
2305
+ * 计算单行歌词在当前布局中的视觉呈现参数。
2306
+ *
2307
+ * 根据播放状态、缓冲状态、布局模式与间奏信息,
2308
+ * 生成一行歌词最终应使用的 opacity、scale、blur 和 render mode。
2309
+ */
2310
+ function computeLinePresentation(input) {
2311
+ const { line, lineIndex, scrollToIndex, latestIndex, hasBuffered, hidePassedLines, isPlaying, isNonDynamic, enableScale, enableBlur, isUserScrolling, isCompact, interlude } = input;
2312
+ const isActive = hasBuffered || lineIndex >= scrollToIndex && lineIndex < latestIndex;
2313
+ const blurLevel = computeLineBlur({
2314
+ enableBlur,
2315
+ isUserScrolling,
2316
+ isActive,
2317
+ itemIndex: lineIndex,
2318
+ scrollToIndex,
2319
+ latestIndex,
2320
+ isCompact
2321
+ });
2322
+ let targetOpacity;
2323
+ if (hidePassedLines) if (lineIndex < (interlude ? interlude.anchorLineIndex + 1 : scrollToIndex) && isPlaying) targetOpacity = 1e-4;
2324
+ else if (hasBuffered) targetOpacity = .85;
2325
+ else targetOpacity = isNonDynamic ? .2 : 1;
2326
+ else if (hasBuffered) targetOpacity = .85;
2327
+ else targetOpacity = isNonDynamic ? .2 : 1;
2328
+ const SCALE_ASPECT = enableScale ? 97 : 100;
2329
+ let targetScale = 100;
2330
+ if (!isActive && isPlaying) targetScale = line.isBG ? 75 : SCALE_ASPECT;
2331
+ return {
2332
+ isActive,
2333
+ targetOpacity,
2334
+ targetScale,
2335
+ blurLevel,
2336
+ renderMode: isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID
2337
+ };
2338
+ }
2339
+ /**
2340
+ * 计算一行歌词在当前布局中的模糊等级。
2341
+ *
2342
+ * 越远离当前对齐区域的歌词会得到更高的模糊值;
2343
+ * 活跃行、滚动交互中或关闭模糊效果时返回 `0`。
2344
+ */
2345
+ function computeLineBlur(input) {
2346
+ const { enableBlur, isUserScrolling, isActive, itemIndex, scrollToIndex, latestIndex, isCompact } = input;
2347
+ if (!enableBlur || isUserScrolling || isActive) return 0;
2348
+ let blurLevel = 1;
2349
+ if (itemIndex < scrollToIndex) blurLevel += Math.abs(scrollToIndex - itemIndex) + 1;
2350
+ else blurLevel += Math.abs(itemIndex - Math.max(scrollToIndex, latestIndex));
2351
+ return isCompact ? blurLevel * .8 : blurLevel;
2352
+ }
2353
+ //#endregion
2354
+ //#region src/lyric-player/base/scroll.ts
2355
+ /**
2356
+ * 将滚动偏移量限制在当前允许的滚动边界内。
2357
+ *
2358
+ * 当手势滚动、滚轮滚动或惯性滚动更新了 {@link PlayerScrollState.scrollOffset}
2359
+ * 后,应调用本函数以避免视图越界。
2360
+ */
2361
+ function clampPlayerScrollOffset(scrollState) {
2362
+ scrollState.scrollOffset = clamp(scrollState.scrollOffset, scrollState.scrollBoundary.minOffset, scrollState.scrollBoundary.maxOffset);
2363
+ }
2364
+ /**
2365
+ * 重置滚动状态到未发生用户滚动时的初始状态。
2366
+ *
2367
+ * 本函数会清除当前偏移,并结束“已滚动”与“正在滚动”的标记;
2368
+ * **不会清理**外部持有的计时器或事件监听器。
2369
+ */
2370
+ function resetPlayerScrollState(scrollState) {
2371
+ scrollState.isScrolled = false;
2372
+ scrollState.scrollOffset = 0;
2373
+ scrollState.isUserScrolling = false;
2374
+ }
2375
+ /**
2376
+ * 向指定元素挂载歌词滚动相关的交互处理器。
2377
+ *
2378
+ * 该函数会处理:
2379
+ * - 触摸拖拽滚动
2380
+ * - 触摸结束后的惯性滚动
2381
+ * - 滚轮滚动
2382
+ * - 轻触时的点击透传
2383
+ *
2384
+ * 只更新 {@link PlayerScrollState} 并通过回调通知宿主执行布局或其它副作用,
2385
+ * 不直接依赖具体的播放器类实现。
2386
+ */
2387
+ function attachPlayerScrollHandlers(element, scrollState, callbacks) {
2388
+ let startScrollY = 0;
2389
+ let startTouchPosY = 0;
2390
+ let startTouchStartX = 0;
2391
+ let startTouchStartY = 0;
2392
+ let lastMoveY = 0;
2393
+ let startScrollTime = 0;
2394
+ let scrollSpeed = 0;
2395
+ let curScrollId = 0;
2396
+ element.addEventListener("touchstart", (evt) => {
2397
+ if (callbacks.onBeginScroll()) {
2398
+ scrollState.isUserScrolling = true;
2399
+ evt.preventDefault();
2400
+ startScrollY = scrollState.scrollOffset;
2401
+ startTouchPosY = evt.touches[0].screenY;
2402
+ lastMoveY = startTouchPosY;
2403
+ startTouchStartX = evt.touches[0].screenX;
2404
+ startTouchStartY = evt.touches[0].screenY;
2405
+ startScrollTime = Date.now();
2406
+ scrollSpeed = 0;
2407
+ callbacks.onLayout(true, true);
2408
+ }
2409
+ });
2410
+ element.addEventListener("touchmove", (evt) => {
2411
+ if (callbacks.onBeginScroll()) {
2412
+ evt.preventDefault();
2413
+ const currentY = evt.touches[0].screenY;
2414
+ const deltaY = currentY - startTouchPosY;
2415
+ scrollState.scrollOffset = startScrollY - deltaY;
2416
+ clampPlayerScrollOffset(scrollState);
2417
+ const now = Date.now();
2418
+ const dt = now - startScrollTime;
2419
+ if (dt > 0) scrollSpeed = (currentY - lastMoveY) / dt;
2420
+ lastMoveY = currentY;
2421
+ startScrollTime = now;
2422
+ callbacks.onLayout(true, true);
2423
+ }
2424
+ });
2425
+ element.addEventListener("touchend", (evt) => {
2426
+ if (callbacks.onBeginScroll()) {
2427
+ evt.preventDefault();
2428
+ const touch = evt.changedTouches[0];
2429
+ const moveX = Math.abs(touch.screenX - startTouchStartX);
2430
+ const moveY = Math.abs(touch.screenY - startTouchStartY);
2431
+ if (moveX < 10 && moveY < 10) {
2432
+ const target = document.elementFromPoint(touch.clientX, touch.clientY);
2433
+ if (target instanceof HTMLElement && callbacks.containsTarget(target)) callbacks.clickTarget(target);
2434
+ scrollState.isUserScrolling = false;
2435
+ callbacks.onEndScroll();
2436
+ return;
2437
+ }
2438
+ startTouchPosY = 0;
2439
+ const scrollId = ++curScrollId;
2440
+ if (Math.abs(scrollSpeed) < .1) scrollSpeed = 0;
2441
+ let lastFrameTime = performance.now();
2442
+ const onScrollFrame = (time) => {
2443
+ if (scrollId !== curScrollId) return;
2444
+ const dt = time - lastFrameTime;
2445
+ lastFrameTime = time;
2446
+ if (dt <= 0 || dt > 100) {
2447
+ requestAnimationFrame(onScrollFrame);
2448
+ return;
2449
+ }
2450
+ if (Math.abs(scrollSpeed) > .05) {
2451
+ scrollState.scrollOffset -= scrollSpeed * dt;
2452
+ clampPlayerScrollOffset(scrollState);
2453
+ const frictionFactor = .95 ** (dt / 16);
2454
+ scrollSpeed *= frictionFactor;
2455
+ callbacks.onLayout(true, true);
2456
+ requestAnimationFrame(onScrollFrame);
2457
+ } else {
2458
+ scrollState.isUserScrolling = false;
2459
+ callbacks.onEndScroll();
2460
+ }
2461
+ };
2462
+ requestAnimationFrame(onScrollFrame);
2463
+ } else scrollState.isUserScrolling = false;
2464
+ });
2465
+ element.addEventListener("wheel", (evt) => {
2466
+ if (callbacks.onBeginScroll()) {
2467
+ evt.preventDefault();
2468
+ if (evt.deltaMode === evt.DOM_DELTA_PIXEL) {
2469
+ scrollState.scrollOffset += evt.deltaY;
2470
+ clampPlayerScrollOffset(scrollState);
2471
+ callbacks.onLayout(true, false);
2209
2472
  } else {
2210
- curStyle += " scale(0)";
2211
- this.dot0.style.opacity = "0";
2212
- this.dot1.style.opacity = "0";
2213
- this.dot2.style.opacity = "0";
2473
+ scrollState.scrollOffset += evt.deltaY * 50;
2474
+ clampPlayerScrollOffset(scrollState);
2475
+ callbacks.onLayout(false, false);
2214
2476
  }
2215
- curStyle += ";";
2216
- if (this.lastStyle !== curStyle) {
2217
- this.element.setAttribute("style", curStyle);
2218
- this.lastStyle = curStyle;
2477
+ }
2478
+ }, { passive: false });
2479
+ }
2480
+ //#endregion
2481
+ //#region src/utils/eq-set.ts
2482
+ const eqSet = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x));
2483
+ //#endregion
2484
+ //#region src/lyric-player/base/timeline.ts
2485
+ /**
2486
+ * 计算指定时间点的热行/缓冲行状态转移的纯函数。其行为包括:
2487
+ *
2488
+ * - 根据当前时间和已有的热行状态,计算出新的热行状态,并返回应新增的热行 ID 和应移除的热行 ID
2489
+ * - 根据新的热行状态和已有的缓冲行状态,计算出应移除的缓冲行 ID
2490
+ */
2491
+ function computePlayerTimeState(input) {
2492
+ const { time, processedLines, timelineState: { hotLines, bufferedLines } } = input;
2493
+ const nextHotLines = new Set(hotLines);
2494
+ const addedIds = /* @__PURE__ */ new Set();
2495
+ const removedHotIds = /* @__PURE__ */ new Set();
2496
+ const removedBufferedIds = /* @__PURE__ */ new Set();
2497
+ for (const lastHotId of hotLines) {
2498
+ const line = processedLines[lastHotId];
2499
+ if (!line) {
2500
+ nextHotLines.delete(lastHotId);
2501
+ removedHotIds.add(lastHotId);
2502
+ continue;
2503
+ }
2504
+ if (line.isBG) continue;
2505
+ const nextLine = processedLines[lastHotId + 1];
2506
+ if (nextLine?.isBG) {
2507
+ const nextMainLine = processedLines[lastHotId + 2];
2508
+ const startTime = Math.min(line.startTime, nextLine.startTime);
2509
+ const endTime = Math.min(Math.max(line.endTime, nextMainLine?.startTime ?? Number.MAX_VALUE), Math.max(line.endTime, nextLine.endTime));
2510
+ if (time < startTime || endTime <= time) {
2511
+ nextHotLines.delete(lastHotId);
2512
+ removedHotIds.add(lastHotId);
2513
+ nextHotLines.delete(lastHotId + 1);
2514
+ removedHotIds.add(lastHotId + 1);
2515
+ }
2516
+ } else if (time < line.startTime || line.endTime <= time) {
2517
+ nextHotLines.delete(lastHotId);
2518
+ removedHotIds.add(lastHotId);
2519
+ }
2520
+ }
2521
+ for (let id = 0; id < processedLines.length; id++) {
2522
+ const line = processedLines[id];
2523
+ if (!line || line.isBG) continue;
2524
+ if (line.startTime <= time && line.endTime > time && !nextHotLines.has(id)) {
2525
+ nextHotLines.add(id);
2526
+ addedIds.add(id);
2527
+ if (processedLines[id + 1]?.isBG) {
2528
+ nextHotLines.add(id + 1);
2529
+ addedIds.add(id + 1);
2219
2530
  }
2220
2531
  }
2221
2532
  }
2222
- dispose() {
2223
- this.element.remove();
2533
+ for (const id of bufferedLines) if (!nextHotLines.has(id)) removedBufferedIds.add(id);
2534
+ return {
2535
+ nextHotLines,
2536
+ addedIds,
2537
+ removedHotIds,
2538
+ removedBufferedIds
2539
+ };
2540
+ }
2541
+ /**
2542
+ * 在 seeking 场景下,根据当前时间选出应对齐滚动到的目标行索引。
2543
+ *
2544
+ * 若当前仍存在缓冲行,则优先对齐到最靠前的缓冲行;
2545
+ * 否则对齐到第一条开始时间不小于当前时间的歌词行。
2546
+ */
2547
+ function pickScrollToIndexForSeek(time, processedLines, bufferedLines) {
2548
+ if (bufferedLines.size > 0) return Math.min(...bufferedLines);
2549
+ const foundIndex = processedLines.findIndex((line) => line.startTime >= time);
2550
+ return foundIndex === -1 ? processedLines.length : foundIndex;
2551
+ }
2552
+ /**
2553
+ * 提交时间线状态转移的纯函数。
2554
+ *
2555
+ * 把一次时间线状态转移写回 {@link PlayerTimelineState},
2556
+ * 并返回一份供宿主执行的副作用应用计划,例如启用/禁用哪些歌词行、
2557
+ * 是否需要重置用户滚动状态、是否需要触发布局。
2558
+ */
2559
+ function commitPlayerTimeState(input) {
2560
+ const { timelineState, time, processedLines, hasBottomContent, stateResult } = input;
2561
+ const { addedIds, removedHotIds, removedBufferedIds } = stateResult;
2562
+ const { isSeeking } = timelineState;
2563
+ timelineState.currentTime = time;
2564
+ timelineState.hotLines = stateResult.nextHotLines;
2565
+ let shouldLayout = false;
2566
+ let shouldResetScroll = false;
2567
+ const linesToEnable = [];
2568
+ const linesToDisable = /* @__PURE__ */ new Set();
2569
+ if (isSeeking) {
2570
+ timelineState.bufferedLines = new Set([...timelineState.hotLines]);
2571
+ timelineState.scrollToIndex = pickScrollToIndexForSeek(time, processedLines, timelineState.bufferedLines);
2572
+ for (const id of removedHotIds) linesToDisable.add(id);
2573
+ for (const id of timelineState.hotLines) linesToEnable.push(id);
2574
+ for (const id of removedBufferedIds) linesToDisable.add(id);
2575
+ shouldResetScroll = true;
2576
+ shouldLayout = true;
2577
+ } else if (addedIds.size > 0) {
2578
+ for (const id of addedIds) {
2579
+ timelineState.bufferedLines.add(id);
2580
+ linesToEnable.push(id);
2581
+ }
2582
+ for (const id of removedBufferedIds) {
2583
+ timelineState.bufferedLines.delete(id);
2584
+ linesToDisable.add(id);
2585
+ }
2586
+ if (timelineState.bufferedLines.size > 0) timelineState.scrollToIndex = Math.min(...timelineState.bufferedLines);
2587
+ shouldLayout = true;
2588
+ } else if (removedBufferedIds.size > 0 && eqSet(removedBufferedIds, timelineState.bufferedLines)) {
2589
+ for (const id of timelineState.bufferedLines) {
2590
+ if (timelineState.hotLines.has(id)) continue;
2591
+ timelineState.bufferedLines.delete(id);
2592
+ linesToDisable.add(id);
2593
+ }
2594
+ shouldLayout = true;
2595
+ }
2596
+ if (timelineState.bufferedLines.size === 0 && processedLines.length > 0) {
2597
+ if (time >= processedLines[processedLines.length - 1].endTime) {
2598
+ const targetIndex = hasBottomContent ? processedLines.length : processedLines.length - 1;
2599
+ if (timelineState.scrollToIndex !== targetIndex) {
2600
+ timelineState.scrollToIndex = targetIndex;
2601
+ shouldLayout = true;
2602
+ }
2603
+ }
2224
2604
  }
2225
- };
2605
+ timelineState.lastCurrentTime = time;
2606
+ return {
2607
+ shouldLayout,
2608
+ shouldResetScroll,
2609
+ linesToEnable,
2610
+ linesToDisable: [...linesToDisable]
2611
+ };
2612
+ }
2226
2613
  //#endregion
2227
- //#region src/lyric-player/base.ts
2614
+ //#region src/lyric-player/base/index.ts
2228
2615
  /**
2229
- * 歌词播放器的基类,已经包含了有关歌词操作和排版的功能,子类需要为其实现对应的显示展示操作
2616
+ * 歌词播放器的基类,已经包含了有关歌词操作和排版的功能,
2617
+ * 子类需要为其实现对应的显示展示操作
2230
2618
  */
2231
2619
  var LyricPlayerBase = class extends EventTarget {
2232
2620
  element = document.createElement("div");
2233
- currentTime = 0;
2621
+ /** 播放时间线状态 */
2622
+ timelineState = {
2623
+ currentTime: 0,
2624
+ lastCurrentTime: 0,
2625
+ hotLines: /* @__PURE__ */ new Set(),
2626
+ bufferedLines: /* @__PURE__ */ new Set(),
2627
+ scrollToIndex: 0,
2628
+ isSeeking: false,
2629
+ isPlaying: true,
2630
+ initialLayoutFinished: false
2631
+ };
2234
2632
  /** @internal */
2235
2633
  lyricLinesSize = /* @__PURE__ */ new WeakMap();
2236
2634
  /** @internal */
@@ -2238,42 +2636,38 @@ var LyricPlayerBase = class extends EventTarget {
2238
2636
  currentLyricLines = [];
2239
2637
  processedLines = [];
2240
2638
  lyricLinesIndexes = /* @__PURE__ */ new WeakMap();
2241
- hotLines = /* @__PURE__ */ new Set();
2242
- bufferedLines = /* @__PURE__ */ new Set();
2243
2639
  isNonDynamic = false;
2244
2640
  hasDuetLine = false;
2245
- scrollToIndex = 0;
2246
2641
  disableSpring = false;
2247
- interludeDotsSize = [0, 0];
2642
+ layoutState = {
2643
+ interludeDotsSize: [0, 0],
2644
+ targetAlignIndex: 0,
2645
+ lastInterludeState: false,
2646
+ alignAnchor: LayoutAlignAnchor.Center,
2647
+ alignPosition: .35,
2648
+ overscanPx: 300
2649
+ };
2248
2650
  interludeDots = new InterludeDots();
2249
2651
  bottomLine = new BottomLineEl(this);
2250
2652
  enableBlur = true;
2251
2653
  enableScale = true;
2252
- maskObsceneWords = "";
2654
+ maskObsceneWords = MaskObsceneWordsMode.Disabled;
2253
2655
  maskObsceneWordChar = "*";
2254
2656
  hidePassedLines = false;
2255
- scrollBoundary = [0, 0];
2657
+ scrollState = {
2658
+ scrollBoundary: {
2659
+ minOffset: 0,
2660
+ maxOffset: 0
2661
+ },
2662
+ scrollOffset: 0,
2663
+ allowScroll: true,
2664
+ isScrolled: false,
2665
+ isUserScrolling: false
2666
+ };
2256
2667
  currentLyricLineObjects = [];
2257
- isSeeking = false;
2258
- lastCurrentTime = 0;
2259
- alignAnchor = "center";
2260
- alignPosition = .35;
2261
- scrollOffset = 0;
2262
2668
  size = [0, 0];
2263
- allowScroll = true;
2264
2669
  isPageVisible = true;
2265
2670
  optimizeOptions = {};
2266
- initialLayoutFinished = false;
2267
- /**
2268
- * 标记用户是否正在进行滚动交互
2269
- */
2270
- isUserScrolling = false;
2271
- wheelTimeout;
2272
- /**
2273
- * 视图额外预渲染(overscan)距离,单位:像素。
2274
- * 用于决定在视口之外多少距离内也认为是“可见”,以便提前创建/保留行元素。
2275
- */
2276
- overscanPx = 300;
2277
2671
  posXSpringParams = {
2278
2672
  mass: 1,
2279
2673
  damping: 10,
@@ -2296,13 +2690,12 @@ var LyricPlayerBase = class extends EventTarget {
2296
2690
  };
2297
2691
  onPageShow = () => {
2298
2692
  this.isPageVisible = true;
2299
- this.setCurrentTime(this.currentTime, true);
2693
+ this.setCurrentTime(this.timelineState.currentTime, true);
2300
2694
  };
2301
2695
  onPageHide = () => {
2302
2696
  this.isPageVisible = false;
2303
2697
  };
2304
2698
  scrolledHandler;
2305
- isScrolled = false;
2306
2699
  /** @internal */
2307
2700
  resizeObserver = new ResizeObserver(((entries) => {
2308
2701
  let shouldRelayout = false;
@@ -2313,8 +2706,8 @@ var LyricPlayerBase = class extends EventTarget {
2313
2706
  this.size[1] = rect.height;
2314
2707
  shouldRebuildPlayerStyle = true;
2315
2708
  } else if (entry.target === this.interludeDots.getElement()) {
2316
- this.interludeDotsSize[0] = entry.target.clientWidth;
2317
- this.interludeDotsSize[1] = entry.target.clientHeight;
2709
+ this.layoutState.interludeDotsSize[0] = entry.target.clientWidth;
2710
+ this.layoutState.interludeDotsSize[1] = entry.target.clientHeight;
2318
2711
  shouldRelayout = true;
2319
2712
  } else if (entry.target === this.bottomLine.getElement()) {
2320
2713
  const newSize = [entry.target.clientWidth, entry.target.clientHeight];
@@ -2339,8 +2732,6 @@ var LyricPlayerBase = class extends EventTarget {
2339
2732
  if (shouldRebuildPlayerStyle) this.onResize();
2340
2733
  }));
2341
2734
  wordFadeWidth = .5;
2342
- targetAlignIndex = 0;
2343
- lastInterludeState = false;
2344
2735
  constructor(element) {
2345
2736
  super();
2346
2737
  if (element) this.element = element;
@@ -2352,114 +2743,27 @@ var LyricPlayerBase = class extends EventTarget {
2352
2743
  this.interludeDots.setTransform(0, 200);
2353
2744
  window.addEventListener("pageshow", this.onPageShow);
2354
2745
  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;
2746
+ attachPlayerScrollHandlers(this.element, this.scrollState, {
2747
+ onBeginScroll: () => this.beginScrollHandler(),
2748
+ onEndScroll: () => this.endScrollHandler(),
2749
+ onLayout: (sync, force) => this.calcLayout(sync, force),
2750
+ containsTarget: (target) => this.element.contains(target),
2751
+ clickTarget: (target) => target.click()
2431
2752
  });
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
2753
  }
2447
2754
  beginScrollHandler() {
2448
- const allowed = this.allowScroll;
2755
+ const allowed = this.scrollState.allowScroll;
2449
2756
  if (allowed) {
2450
- this.isScrolled = true;
2757
+ this.scrollState.isScrolled = true;
2451
2758
  clearTimeout(this.scrolledHandler);
2452
2759
  this.scrolledHandler = setTimeout(() => {
2453
- this.isScrolled = false;
2454
- this.scrollOffset = 0;
2760
+ this.scrollState.isScrolled = false;
2761
+ this.scrollState.scrollOffset = 0;
2455
2762
  }, 5e3);
2456
2763
  }
2457
2764
  return allowed;
2458
2765
  }
2459
2766
  endScrollHandler() {}
2460
- limitScrollOffset() {
2461
- this.scrollOffset = Math.max(Math.min(this.scrollBoundary[1], this.scrollOffset), this.scrollBoundary[0]);
2462
- }
2463
2767
  /**
2464
2768
  * 设置文字动画的渐变宽度,单位以歌词行的主文字字体大小的倍数为单位,默认为 0.5,即一个全角字符的一半宽度
2465
2769
  *
@@ -2501,7 +2805,7 @@ var LyricPlayerBase = class extends EventTarget {
2501
2805
  return this.wordFadeWidth;
2502
2806
  }
2503
2807
  setIsSeeking(isSeeking) {
2504
- this.isSeeking = isSeeking;
2808
+ this.timelineState.isSeeking = isSeeking;
2505
2809
  }
2506
2810
  /**
2507
2811
  * 设置是否隐藏已经播放过的歌词行,默认不隐藏
@@ -2539,7 +2843,7 @@ var LyricPlayerBase = class extends EventTarget {
2539
2843
  const c = char.charAt(0) || "*";
2540
2844
  if (this.maskObsceneWordChar === c) return;
2541
2845
  this.maskObsceneWordChar = c;
2542
- if (this.maskObsceneWords !== "") {
2846
+ if (this.maskObsceneWords !== MaskObsceneWordsMode.Disabled) {
2543
2847
  this.rebuildLyricLines();
2544
2848
  this.calcLayout();
2545
2849
  }
@@ -2554,10 +2858,10 @@ var LyricPlayerBase = class extends EventTarget {
2554
2858
  */
2555
2859
  processObsceneWord(word) {
2556
2860
  const text = word.word;
2557
- if (!word.obscene || this.maskObsceneWords === "") return text;
2861
+ if (!word.obscene || this.maskObsceneWords === MaskObsceneWordsMode.Disabled) return text;
2558
2862
  const maskChar = this.maskObsceneWordChar;
2559
- if (this.maskObsceneWords === "full-mask") return text.replace(/\S/g, maskChar);
2560
- if (this.maskObsceneWords === "partial-mask") {
2863
+ if (this.maskObsceneWords === MaskObsceneWordsMode.FullMask) return text.replace(/\S/g, maskChar);
2864
+ if (this.maskObsceneWords === MaskObsceneWordsMode.PartialMask) {
2561
2865
  const trimmed = text.trim();
2562
2866
  if (trimmed.length <= 2) return text.replace(/\S/g, maskChar);
2563
2867
  const startPos = text.indexOf(trimmed);
@@ -2575,25 +2879,25 @@ var LyricPlayerBase = class extends EventTarget {
2575
2879
  * @param alignAnchor 歌词行对齐方式,详情见函数说明
2576
2880
  */
2577
2881
  setAlignAnchor(alignAnchor) {
2578
- this.alignAnchor = alignAnchor;
2882
+ this.layoutState.alignAnchor = alignAnchor;
2579
2883
  }
2580
2884
  /**
2581
2885
  * 设置默认的歌词行对齐位置,相对于整个歌词播放组件的大小位置,默认为 `0.5`
2582
2886
  * @param alignPosition 一个 `[0.0-1.0]` 之间的任意数字,代表组件高度由上到下的比例位置
2583
2887
  */
2584
2888
  setAlignPosition(alignPosition) {
2585
- this.alignPosition = alignPosition;
2889
+ this.layoutState.alignPosition = alignPosition;
2586
2890
  }
2587
2891
  /**
2588
2892
  * 设置 overscan(视图上下额外缓冲渲染区)距离,单位:像素。
2589
2893
  * @param px 像素值,默认 300
2590
2894
  */
2591
2895
  setOverscanPx(px) {
2592
- this.overscanPx = Math.max(0, px | 0);
2896
+ this.layoutState.overscanPx = clampPositive(px | 0);
2593
2897
  }
2594
2898
  /** 获取当前 overscan 像素距离 */
2595
2899
  getOverscanPx() {
2596
- return this.overscanPx;
2900
+ return this.layoutState.overscanPx;
2597
2901
  }
2598
2902
  /**
2599
2903
  * 设置是否使用物理弹簧算法实现歌词动画效果,默认启用
@@ -2616,34 +2920,6 @@ var LyricPlayerBase = class extends EventTarget {
2616
2920
  return !this.disableSpring;
2617
2921
  }
2618
2922
  /**
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
2923
  * 设置歌词的优化配置项,这些配置项默认全部开启
2648
2924
  *
2649
2925
  * 注意,如果在 `setLyricLines` 之后修改此配置,需要重新调用 `setLyricLines()` 才能对当前歌词生效
@@ -2663,9 +2939,9 @@ var LyricPlayerBase = class extends EventTarget {
2663
2939
  */
2664
2940
  setLyricLines(lines, initialTime = 0) {
2665
2941
  if (process.env.NODE_ENV !== "production") console.log("设置歌词行", lines, initialTime);
2666
- this.initialLayoutFinished = true;
2667
- this.lastCurrentTime = initialTime;
2668
- this.currentTime = initialTime;
2942
+ this.timelineState.initialLayoutFinished = true;
2943
+ this.timelineState.lastCurrentTime = initialTime;
2944
+ this.timelineState.currentTime = initialTime;
2669
2945
  this.currentLyricLines = structuredClone(lines);
2670
2946
  this.processedLines = structuredClone(this.currentLyricLines);
2671
2947
  optimizeLyricLines(this.processedLines, this.optimizeOptions);
@@ -2677,8 +2953,8 @@ var LyricPlayerBase = class extends EventTarget {
2677
2953
  this.hasDuetLine = this.processedLines.some((line) => line.isDuet);
2678
2954
  for (const line of this.currentLyricLineObjects) line.dispose();
2679
2955
  this.interludeDots.setInterlude(void 0);
2680
- this.hotLines.clear();
2681
- this.bufferedLines.clear();
2956
+ this.timelineState.hotLines.clear();
2957
+ this.timelineState.bufferedLines.clear();
2682
2958
  this.setCurrentTime(0, true);
2683
2959
  if (process.env.NODE_ENV !== "production") console.log("歌词处理完成", this);
2684
2960
  }
@@ -2687,143 +2963,39 @@ var LyricPlayerBase = class extends EventTarget {
2687
2963
  * @returns 当前是否在播放
2688
2964
  */
2689
2965
  getIsPlaying() {
2690
- return this.isPlaying;
2966
+ return this.timelineState.isPlaying;
2691
2967
  }
2692
2968
  /**
2693
- * 设置当前播放进度,单位为毫秒且**必须是整数**,此时将会更新内部的歌词进度信息
2694
- * 内部会根据调用间隔和播放进度自动决定如何滚动和显示歌词,所以这个的调用频率越快越准确越好
2969
+ * 设置当前播放进度,此时将会更新内部的歌词进度信息。
2970
+ *
2971
+ * 内部会根据调用间隔和播放进度自动决定如何滚动和显示歌词,所以这个的调用频率越快越准确越好。
2972
+ * 调用完成后,应每帧调用 {@link update} 方法来执行歌词动画效果。**此函数本身不会触发动画效果**。
2695
2973
  *
2696
- * 调用完成后,可以每帧调用 `update` 函数来执行歌词动画效果
2697
2974
  * @param time 当前播放进度,单位为毫秒
2698
2975
  */
2699
2976
  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
- }
2977
+ time = Math.round(time);
2978
+ const { timelineState } = this;
2979
+ timelineState.isSeeking = Boolean(isSeek);
2980
+ timelineState.currentTime = time;
2981
+ if (!timelineState.initialLayoutFinished && !timelineState.isSeeking) return;
2982
+ const stateResult = computePlayerTimeState({
2983
+ time,
2984
+ processedLines: this.processedLines,
2985
+ timelineState
2751
2986
  });
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
- }
2987
+ const hasBottomContent = this.bottomLine.getElement().innerHTML.trim().length > 0;
2988
+ const commitResult = commitPlayerTimeState({
2989
+ timelineState,
2990
+ time,
2991
+ processedLines: this.processedLines,
2992
+ hasBottomContent,
2993
+ stateResult
2994
+ });
2995
+ for (const id of commitResult.linesToDisable) this.currentLyricLineObjects[id]?.disable();
2996
+ for (const id of commitResult.linesToEnable) this.currentLyricLineObjects[id]?.enable();
2997
+ if (commitResult.shouldResetScroll) this.resetScroll();
2998
+ if (commitResult.shouldLayout) this.calcLayout();
2827
2999
  }
2828
3000
  /**
2829
3001
  * 重新布局定位歌词行的位置,调用完成后再逐帧调用 `update`
@@ -2843,102 +3015,108 @@ var LyricPlayerBase = class extends EventTarget {
2843
3015
  * @param force 是否绕过弹簧效果强制更新位置
2844
3016
  */
2845
3017
  async calcLayout(sync = false, force = false) {
2846
- const interlude = this.getCurrentInterlude();
3018
+ const interlude = computeCurrentInterlude({
3019
+ currentTime: this.timelineState.currentTime,
3020
+ scrollToIndex: this.timelineState.scrollToIndex,
3021
+ processedLines: this.processedLines
3022
+ });
2847
3023
  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
3024
+ if (this.layoutState.targetAlignIndex !== this.timelineState.scrollToIndex || this.layoutState.lastInterludeState !== isInterludeActive) {
3025
+ this.layoutState.lastInterludeState = isInterludeActive;
3026
+ const springParams = computeLinePosYSpringParams({
3027
+ enabled: this.getEnableSpring(),
3028
+ processedLines: this.processedLines,
3029
+ scrollToIndex: this.timelineState.scrollToIndex,
3030
+ isSeeking: this.timelineState.isSeeking,
3031
+ isInterludeActive
2857
3032
  });
2858
- else this.updateDynamicSpringParams();
3033
+ if (springParams.shouldUpdate && springParams.params) this.setLinePosYSpringParams(springParams.params);
2859
3034
  }
2860
- let curPos = -this.scrollOffset;
2861
- const targetAlignIndex = this.scrollToIndex;
3035
+ let curPos = -this.scrollState.scrollOffset;
3036
+ const targetAlignIndex = this.timelineState.scrollToIndex;
2862
3037
  let isNextDuet = false;
2863
- if (interlude) isNextDuet = interlude[3];
3038
+ if (interlude) isNextDuet = interlude.isNextDuet;
2864
3039
  else this.interludeDots.setInterlude(void 0);
2865
3040
  const dotMargin = (this.baseFontSize || 24) * .4;
2866
- const totalInterludeHeight = this.interludeDotsSize[1] + dotMargin * 2;
3041
+ const totalInterludeHeight = this.layoutState.interludeDotsSize[1] + dotMargin * 2;
2867
3042
  if (interlude) {
2868
- if (interlude[2] !== -1) curPos -= totalInterludeHeight;
3043
+ if (interlude.anchorLineIndex !== -1) curPos -= totalInterludeHeight;
2869
3044
  }
2870
3045
  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;
3046
+ const scrollOffset = this.currentLyricLineObjects.slice(0, targetAlignIndex).reduce((acc, el) => acc + (el.getLine().isBG && this.timelineState.isPlaying ? 0 : this.lyricLinesSize.get(el)?.[1] ?? LINE_HEIGHT_FALLBACK), 0);
3047
+ this.scrollState.scrollBoundary.minOffset = -scrollOffset;
2873
3048
  curPos -= scrollOffset;
2874
- curPos += this.size[1] * this.alignPosition;
3049
+ curPos += this.size[1] * this.layoutState.alignPosition;
2875
3050
  const curLine = this.currentLyricLineObjects[targetAlignIndex];
2876
- this.targetAlignIndex = targetAlignIndex;
3051
+ this.layoutState.targetAlignIndex = targetAlignIndex;
2877
3052
  const isBottomFocused = targetAlignIndex === this.currentLyricLineObjects.length;
2878
3053
  this.bottomLine.setFocused(isBottomFocused);
2879
3054
  let targetLineHeight = 0;
2880
3055
  if (curLine) targetLineHeight = this.lyricLinesSize.get(curLine)?.[1] ?? LINE_HEIGHT_FALLBACK;
2881
3056
  else if (isBottomFocused) targetLineHeight = this.bottomLine.lineSize[1];
2882
- if (targetLineHeight > 0) switch (this.alignAnchor) {
2883
- case "bottom":
3057
+ if (targetLineHeight > 0) switch (this.layoutState.alignAnchor) {
3058
+ case LayoutAlignAnchor.Bottom:
2884
3059
  curPos -= targetLineHeight;
2885
3060
  break;
2886
- case "center":
3061
+ case LayoutAlignAnchor.Center:
2887
3062
  curPos -= targetLineHeight / 2;
2888
3063
  break;
2889
- case "top": break;
3064
+ case LayoutAlignAnchor.Top: break;
2890
3065
  }
2891
- const latestIndex = Math.max(...this.bufferedLines);
3066
+ const latestIndex = Math.max(...this.timelineState.bufferedLines);
2892
3067
  let delay = 0;
2893
3068
  let baseDelay = sync ? 0 : .05;
2894
3069
  let setDots = false;
2895
3070
  this.currentLyricLineObjects.forEach((lineObj, i) => {
2896
- const hasBuffered = this.bufferedLines.has(i);
2897
- const isActive = hasBuffered || i >= this.scrollToIndex && i < latestIndex;
3071
+ const hasBuffered = this.timelineState.bufferedLines.has(i);
2898
3072
  const line = lineObj.getLine();
2899
- const shouldShowDots = interlude && i === interlude[2] + 1;
3073
+ const shouldShowDots = interlude && i === interlude.anchorLineIndex + 1;
2900
3074
  if (!setDots && shouldShowDots) {
2901
3075
  setDots = true;
2902
3076
  curPos += dotMargin;
2903
3077
  let targetX = 0;
2904
- if (interlude && isNextDuet) targetX = this.size[0] - this.interludeDotsSize[0];
3078
+ if (interlude && isNextDuet) targetX = this.size[0] - this.layoutState.interludeDotsSize[0];
2905
3079
  this.interludeDots.setTransform(targetX, curPos);
2906
- if (interlude) this.interludeDots.setInterlude([interlude[0], interlude[1]]);
2907
- curPos += this.interludeDotsSize[1];
3080
+ if (interlude) this.interludeDots.setInterlude([interlude.startTime, interlude.endTime]);
3081
+ curPos += this.layoutState.interludeDotsSize[1];
2908
3082
  curPos += dotMargin;
2909
3083
  }
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;
3084
+ const presentation = computeLinePresentation({
3085
+ line,
3086
+ lineIndex: i,
3087
+ scrollToIndex: this.timelineState.scrollToIndex,
3088
+ latestIndex,
3089
+ hasBuffered,
3090
+ hidePassedLines: this.hidePassedLines,
3091
+ isPlaying: this.timelineState.isPlaying,
3092
+ isNonDynamic: this.isNonDynamic,
3093
+ enableScale: this.enableScale,
3094
+ enableBlur: this.enableBlur,
3095
+ isUserScrolling: this.scrollState.isUserScrolling,
3096
+ isCompact: window.innerWidth <= 1024,
3097
+ interlude
3098
+ });
3099
+ lineObj.setTransform(curPos, presentation.targetScale, presentation.targetOpacity, presentation.blurLevel, force, delay, presentation.renderMode);
3100
+ if (line.isBG && (presentation.isActive || !this.timelineState.isPlaying)) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
2924
3101
  else if (!line.isBG) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
2925
- if (curPos >= 0 && !this.isSeeking) {
3102
+ if (curPos >= 0 && !this.timelineState.isSeeking) {
2926
3103
  if (!line.isBG) delay += baseDelay;
2927
- if (i >= this.scrollToIndex) baseDelay /= 1.05;
3104
+ if (i >= this.timelineState.scrollToIndex) baseDelay /= 1.05;
2928
3105
  }
2929
3106
  });
2930
- this.scrollBoundary[1] = curPos + this.scrollOffset - this.size[1] / 2;
3107
+ this.scrollState.scrollBoundary.maxOffset = curPos + this.scrollState.scrollOffset - this.size[1] / 2;
2931
3108
  const bottomIndex = this.currentLyricLineObjects.length;
2932
- const finalBottomBlur = this.calculateBlur(bottomIndex, isBottomFocused, latestIndex);
3109
+ const finalBottomBlur = computeLineBlur({
3110
+ enableBlur: this.enableBlur,
3111
+ isUserScrolling: this.scrollState.isUserScrolling,
3112
+ isActive: isBottomFocused,
3113
+ itemIndex: bottomIndex,
3114
+ scrollToIndex: this.timelineState.scrollToIndex,
3115
+ latestIndex,
3116
+ isCompact: window.innerWidth <= 1024
3117
+ });
2933
3118
  this.bottomLine.setTransform(0, curPos, finalBottomBlur, force, delay);
2934
3119
  }
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
3120
  /**
2943
3121
  * 设置所有歌词行在横坐标上的弹簧属性,包括重量、弹力和阻力。
2944
3122
  *
@@ -2976,14 +3154,13 @@ var LyricPlayerBase = class extends EventTarget {
2976
3154
  for (const lineObj of this.currentLyricLineObjects) if (lineObj.getLine().isBG) lineObj.lineTransforms.scale.updateParams(this.scaleForBGSpringParams);
2977
3155
  else lineObj.lineTransforms.scale.updateParams(this.scaleSpringParams);
2978
3156
  }
2979
- isPlaying = true;
2980
3157
  /**
2981
3158
  * 暂停部分效果演出,目前会暂停播放间奏点的动画,且将背景歌词显示出来
2982
3159
  */
2983
3160
  pause() {
2984
3161
  this.interludeDots.pause();
2985
- if (this.isPlaying) {
2986
- this.isPlaying = false;
3162
+ if (this.timelineState.isPlaying) {
3163
+ this.timelineState.isPlaying = false;
2987
3164
  this.calcLayout();
2988
3165
  }
2989
3166
  }
@@ -2992,8 +3169,8 @@ var LyricPlayerBase = class extends EventTarget {
2992
3169
  */
2993
3170
  resume() {
2994
3171
  this.interludeDots.resume();
2995
- if (!this.isPlaying) {
2996
- this.isPlaying = true;
3172
+ if (!this.timelineState.isPlaying) {
3173
+ this.timelineState.isPlaying = true;
2997
3174
  this.calcLayout();
2998
3175
  }
2999
3176
  }
@@ -3026,8 +3203,7 @@ var LyricPlayerBase = class extends EventTarget {
3026
3203
  * 请在用户完成滚动点击跳转歌词时调用本事件再调用 `calcLayout` 以正确滚动到目标位置
3027
3204
  */
3028
3205
  resetScroll() {
3029
- this.isScrolled = false;
3030
- this.scrollOffset = 0;
3206
+ resetPlayerScrollState(this.scrollState);
3031
3207
  clearTimeout(this.scrolledHandler);
3032
3208
  }
3033
3209
  /**
@@ -3046,7 +3222,7 @@ var LyricPlayerBase = class extends EventTarget {
3046
3222
  * @returns 当前播放位置
3047
3223
  */
3048
3224
  getCurrentTime() {
3049
- return this.currentTime;
3225
+ return this.timelineState.currentTime;
3050
3226
  }
3051
3227
  getElement() {
3052
3228
  return this.element;
@@ -3057,6 +3233,13 @@ var LyricPlayerBase = class extends EventTarget {
3057
3233
  window.removeEventListener("pagehide", this.onPageHide);
3058
3234
  }
3059
3235
  };
3236
+ //#endregion
3237
+ //#region src/utils/is-cjk.ts
3238
+ const isCJK = (char) => {
3239
+ return /^[\p{Unified_Ideograph}\u0800-\u9FFC]+$/u.test(char);
3240
+ };
3241
+ //#endregion
3242
+ //#region src/lyric-player/base/line.ts
3060
3243
  /**
3061
3244
  * 所有标准歌词行的基类
3062
3245
  * @internal
@@ -3081,7 +3264,7 @@ var LyricLineBase = class extends EventTarget {
3081
3264
  */
3082
3265
  static graphemeSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
3083
3266
  onLineSizeChange(_size) {}
3084
- setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = 0) {
3267
+ setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3085
3268
  this.top = top;
3086
3269
  this.scale = scale;
3087
3270
  this.opacity = opacity;
@@ -3126,6 +3309,13 @@ const NORMAL_BREAK_PENALTY_RATIO = .5;
3126
3309
  */
3127
3310
  const SPACE_BREAK_REWARD_RATIO = .4;
3128
3311
  /**
3312
+ * 在标点符号处断开的奖励比例
3313
+ *
3314
+ * 比空格更高以便优先一点在标点处换行
3315
+ */
3316
+ const PUNCTUATION_BREAK_REWARD_RATIO = .6;
3317
+ const PUNCTUATION_REGEX = /[,.;:!?,。;:!?、)】》」』’”)[\]}>~…]$/;
3318
+ /**
3129
3319
  * 计算平均行长度的断点位置
3130
3320
  * @param children 子节点信息
3131
3321
  * @param containerWidth 容器可用内容宽度
@@ -3166,9 +3356,13 @@ function calcBalancedBreaks(children, containerWidth, fullText, segmenter) {
3166
3356
  else continue;
3167
3357
  else lineCost = (containerWidth - w) ** 2;
3168
3358
  let breakPenalty = 0;
3169
- if (j < n) if (children[j - 1].isSpace) breakPenalty = -((containerWidth * SPACE_BREAK_REWARD_RATIO) ** 2);
3170
- else if (cjkBoundaries.has(charOffsets[j])) breakPenalty = PENALTY_CJK;
3171
- else breakPenalty = PENALTY_NORMAL;
3359
+ if (j < n) {
3360
+ const prevChild = children[j - 1];
3361
+ if (PUNCTUATION_REGEX.test(prevChild.text)) breakPenalty = -((containerWidth * PUNCTUATION_BREAK_REWARD_RATIO) ** 2);
3362
+ else if (prevChild.isSpace) breakPenalty = -((containerWidth * SPACE_BREAK_REWARD_RATIO) ** 2);
3363
+ else if (cjkBoundaries.has(charOffsets[j])) breakPenalty = PENALTY_CJK;
3364
+ else breakPenalty = PENALTY_NORMAL;
3365
+ }
3172
3366
  const totalCost = lineCost + breakPenalty + dp[j];
3173
3367
  if (totalCost < dp[i]) {
3174
3368
  dp[i] = totalCost;
@@ -3193,13 +3387,9 @@ function getMeasurementContext() {
3193
3387
  /**
3194
3388
  * 用于平衡歌词行在换行后的各行长度
3195
3389
  */
3196
- var LineBalancer = class LineBalancer {
3390
+ var LineBalancer = class {
3197
3391
  isBalancing = false;
3198
3392
  lastBalancedContainerWidth = -1;
3199
- /**
3200
- * 防止误差导致的意外换行
3201
- */
3202
- static SAFE_WIDTH_PADDING = 25;
3203
3393
  constructor(mainElement) {
3204
3394
  this.mainElement = mainElement;
3205
3395
  }
@@ -3226,32 +3416,49 @@ var LineBalancer = class LineBalancer {
3226
3416
  adapter.resetDOM();
3227
3417
  const prevWhiteSpace = this.mainElement.style.whiteSpace;
3228
3418
  this.mainElement.style.whiteSpace = "nowrap";
3419
+ const parentElement = this.mainElement.parentElement;
3420
+ let prevTransform = "";
3421
+ let transformChanged = false;
3422
+ if (parentElement) {
3423
+ prevTransform = parentElement.style.transform;
3424
+ if (prevTransform && prevTransform !== "none") {
3425
+ parentElement.style.transform = "none";
3426
+ transformChanged = true;
3427
+ }
3428
+ }
3429
+ let lockAcquired = false;
3229
3430
  try {
3230
- const range = document.createRange();
3231
- range.selectNodeContents(this.mainElement);
3232
- const lineWidth = range.getBoundingClientRect().width;
3233
- const safeContainerWidth = Math.max(1, containerWidth - LineBalancer.SAFE_WIDTH_PADDING);
3234
- if (lineWidth <= safeContainerWidth) {
3431
+ const { childInfos, fullText } = adapter.buildChildInfos();
3432
+ let layoutWidth = childInfos.reduce((sum, c) => sum + c.width, 0);
3433
+ if (adapter.needsCalibration) {
3434
+ const range = document.createRange();
3435
+ range.selectNodeContents(this.mainElement);
3436
+ const visualWidth = range.getBoundingClientRect().width;
3437
+ if (layoutWidth > 0 && visualWidth > 0) {
3438
+ const scale = visualWidth / layoutWidth;
3439
+ for (const info of childInfos) info.width *= scale;
3440
+ }
3441
+ layoutWidth = visualWidth;
3442
+ }
3443
+ const safeContainerWidth = Math.max(1, containerWidth);
3444
+ if (layoutWidth <= safeContainerWidth) {
3235
3445
  this.lastBalancedContainerWidth = containerWidth;
3236
3446
  return;
3237
3447
  }
3238
- const { childInfos, fullText } = adapter.buildChildInfos();
3239
- const measuredTotal = childInfos.reduce((sum, c) => sum + c.width, 0);
3240
- if (measuredTotal > 0 && lineWidth > 0) {
3241
- const scale = lineWidth / measuredTotal;
3242
- for (const info of childInfos) info.width *= scale;
3243
- }
3244
3448
  const breaks = calcBalancedBreaks(childInfos, safeContainerWidth, fullText, wordSegmenter);
3245
3449
  if (breaks.length === 0) {
3246
3450
  this.lastBalancedContainerWidth = containerWidth;
3247
3451
  return;
3248
3452
  }
3249
3453
  this.isBalancing = true;
3454
+ lockAcquired = true;
3250
3455
  adapter.applyBreaks(breaks, childInfos);
3251
3456
  this.lastBalancedContainerWidth = containerWidth;
3252
3457
  this.isBalancing = false;
3253
3458
  } finally {
3254
3459
  this.mainElement.style.whiteSpace = prevWhiteSpace;
3460
+ if (transformChanged && parentElement) parentElement.style.transform = prevTransform;
3461
+ if (lockAcquired) this.isBalancing = false;
3255
3462
  }
3256
3463
  }
3257
3464
  balanceDynamicLineBreaks(containerWidth, wordSegmenter) {
@@ -3284,7 +3491,7 @@ var LineBalancer = class LineBalancer {
3284
3491
  const marginLeft = Number.parseFloat(elStyle.marginLeft) || 0;
3285
3492
  const marginRight = Number.parseFloat(elStyle.marginRight) || 0;
3286
3493
  childInfos.push({
3287
- width: Math.max(0, rect.width + marginLeft + marginRight),
3494
+ width: clampPositive(rect.width + marginLeft + marginRight),
3288
3495
  text: el.textContent ?? "",
3289
3496
  isSpace: false
3290
3497
  });
@@ -3300,7 +3507,8 @@ var LineBalancer = class LineBalancer {
3300
3507
  const breakIndex = breaks[i];
3301
3508
  if (breakIndex >= 0 && breakIndex < infoToNode.length) this.mainElement.insertBefore(document.createElement("br"), infoToNode[breakIndex]);
3302
3509
  }
3303
- }
3510
+ },
3511
+ needsCalibration: false
3304
3512
  }, wordSegmenter);
3305
3513
  }
3306
3514
  balanceNonDynamicLineBreaks(containerWidth, computedStyle, wordSegmenter) {
@@ -3343,7 +3551,8 @@ var LineBalancer = class LineBalancer {
3343
3551
  fragment.appendChild(document.createTextNode(childInfos[i].text));
3344
3552
  }
3345
3553
  this.mainElement.appendChild(fragment);
3346
- }
3554
+ },
3555
+ needsCalibration: true
3347
3556
  }, wordSegmenter);
3348
3557
  }
3349
3558
  };
@@ -3486,34 +3695,34 @@ function matrix4ToCSS(m, fractionDigits = 4) {
3486
3695
  }
3487
3696
  //#endregion
3488
3697
  //#region src/lyric-player/dom/lyric-line.ts
3489
- const ANIMATION_FRAME_QUANTITY$1 = 32;
3490
- const norNum$1 = (min, max) => (x) => Math.min(1, Math.max(0, (x - min) / (max - min)));
3491
- const EMP_EASING_MID$1 = .5;
3492
- const beginNum$1 = norNum$1(0, EMP_EASING_MID$1);
3493
- const endNum$1 = norNum$1(EMP_EASING_MID$1, 1);
3494
- const bezIn$1 = bezier(.2, .4, .58, 1);
3495
- const bezOut$1 = bezier(.3, 0, .58, 1);
3496
- const makeEmpEasing$1 = (mid) => {
3497
- return (x) => x < mid ? bezIn$1(beginNum$1(x)) : 1 - bezOut$1(endNum$1(x));
3698
+ const ANIMATION_FRAME_QUANTITY = 32;
3699
+ const norNum = (min, max) => (x) => clamp01((x - min) / (max - min));
3700
+ const EMP_EASING_MID = .5;
3701
+ const beginNum = norNum(0, EMP_EASING_MID);
3702
+ const endNum = norNum(EMP_EASING_MID, 1);
3703
+ const bezIn = bezier(.2, .4, .58, 1);
3704
+ const bezOut = bezier(.3, 0, .58, 1);
3705
+ const makeEmpEasing = (mid) => {
3706
+ return (x) => x < mid ? bezIn(beginNum(x)) : 1 - bezOut(endNum(x));
3498
3707
  };
3499
- 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))") {
3708
+ 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))") {
3500
3709
  const totalAspect = 2 + width + padding;
3501
3710
  const widthInTotal = width / totalAspect;
3502
3711
  const leftPos = (1 - widthInTotal) / 2;
3503
3712
  return [`linear-gradient(to right,${bright} ${leftPos * 100}%,${dark} ${(leftPos + widthInTotal) * 100}%)`, totalAspect];
3504
3713
  }
3505
- var RawLyricLineMouseEvent$1 = class extends MouseEvent {
3714
+ var RawLyricLineMouseEvent = class extends MouseEvent {
3506
3715
  constructor(line, event) {
3507
3716
  super(event.type, event);
3508
3717
  this.line = line;
3509
3718
  }
3510
3719
  };
3511
- var LyricLineEl$1 = class extends LyricLineBase {
3720
+ var LyricLineEl = class extends LyricLineBase {
3512
3721
  element = document.createElement("div");
3513
3722
  splittedWords = [];
3514
3723
  built = false;
3515
3724
  lineSize = [0, 0];
3516
- renderMode = 0;
3725
+ renderMode = LyricLineRenderMode.SOLID;
3517
3726
  currentBrightAlpha = 1;
3518
3727
  currentDarkAlpha = .2;
3519
3728
  targetBrightAlpha = 1;
@@ -3554,7 +3763,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3554
3763
  }
3555
3764
  listenersMap = /* @__PURE__ */ new Map();
3556
3765
  onMouseEvent = (e) => {
3557
- const wrapped = new RawLyricLineMouseEvent$1(this, e);
3766
+ const wrapped = new RawLyricLineMouseEvent(this, e);
3558
3767
  for (const listener of this.listenersMap.get(e.type) ?? []) listener.call(this, wrapped);
3559
3768
  if (!this.dispatchEvent(wrapped) || wrapped.defaultPrevented) {
3560
3769
  e.preventDefault();
@@ -3590,30 +3799,28 @@ var LyricLineEl$1 = class extends LyricLineBase {
3590
3799
  return true;
3591
3800
  }
3592
3801
  isEnabled = false;
3593
- async enable(maskAnimationTime = this.lyricLine.startTime, shouldPlay = true) {
3802
+ async enable(maskAnimationTime = this.lyricPlayer.getCurrentTime(), shouldPlay = this.lyricPlayer.getIsPlaying()) {
3594
3803
  this.isEnabled = true;
3595
3804
  this.element.classList.add(lyric_player_module_default.active);
3596
3805
  const main = this.element.children[0];
3597
- const relativeTime = Math.max(0, maskAnimationTime - this.lyricLine.startTime);
3598
- const actualMaskTime = maskAnimationTime === this.lyricLine.startTime ? this.lyricPlayer.getCurrentTime() : maskAnimationTime;
3599
- const maskRelativeTime = Math.max(0, actualMaskTime - this.lyricLine.startTime);
3806
+ const relativeTime = clampPositive(maskAnimationTime - this.lyricLine.startTime);
3600
3807
  for (const word of this.splittedWords) {
3601
3808
  for (const a of word.elementAnimations) {
3602
3809
  a.currentTime = relativeTime;
3603
3810
  a.playbackRate = 1;
3604
3811
  const timing = a.effect?.getComputedTiming();
3605
- const duration = timing?.duration || 0;
3606
- const endTime = (timing?.delay || 0) + duration;
3812
+ const duration = Number(timing?.duration ?? 0);
3813
+ const endTime = Number(timing?.delay ?? 0) + duration;
3607
3814
  if (shouldPlay && relativeTime < endTime) a.play();
3608
3815
  else a.pause();
3609
3816
  }
3610
3817
  for (const a of word.maskAnimations) {
3611
- const t = Math.min(this.totalDuration, maskRelativeTime);
3818
+ const t = Math.min(this.totalDuration, relativeTime);
3612
3819
  a.currentTime = t;
3613
3820
  a.playbackRate = 1;
3614
3821
  const timing = a.effect?.getComputedTiming();
3615
- const duration = timing?.duration || 0;
3616
- const endTime = (timing?.delay || 0) + duration;
3822
+ const duration = Number(timing?.duration ?? 0);
3823
+ const endTime = Number(timing?.delay ?? 0) + duration;
3617
3824
  if (shouldPlay && t < endTime) a.play();
3618
3825
  else a.pause();
3619
3826
  }
@@ -3623,7 +3830,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3623
3830
  disable() {
3624
3831
  this.isEnabled = false;
3625
3832
  this.element.classList.remove(lyric_player_module_default.active);
3626
- this.renderMode = 0;
3833
+ this.renderMode = LyricLineRenderMode.SOLID;
3627
3834
  const main = this.element.children[0];
3628
3835
  for (const word of this.splittedWords) {
3629
3836
  for (const a of word.elementAnimations) if (a.id === "float-word" || a.id.includes("emphasize-word-float-only")) {
@@ -3664,7 +3871,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3664
3871
  setMaskAnimationState(maskAnimationTime = 0) {
3665
3872
  const t = maskAnimationTime - this.lyricLine.startTime;
3666
3873
  for (const word of this.splittedWords) for (const a of word.maskAnimations) {
3667
- a.currentTime = Math.min(this.totalDuration, Math.max(0, t));
3874
+ a.currentTime = clamp(t, 0, this.totalDuration);
3668
3875
  a.playbackRate = 1;
3669
3876
  if (t >= 0 && t < this.totalDuration) a.play();
3670
3877
  else a.pause();
@@ -3855,7 +4062,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3855
4062
  return a;
3856
4063
  }
3857
4064
  initEmphasizeAnimation(word, characterElements, duration, delay, rubyCharCount) {
3858
- const de = Math.max(0, delay);
4065
+ const de = clampPositive(delay);
3859
4066
  let du = Math.max(1e3, duration);
3860
4067
  const anchorCharCount = rubyCharCount > 0 ? rubyCharCount : Math.max(1, characterElements.length);
3861
4068
  let result = [];
@@ -3873,12 +4080,12 @@ var LyricLineEl$1 = class extends LyricLineBase {
3873
4080
  amount = Math.min(1.2, amount);
3874
4081
  blur = Math.min(.8, blur);
3875
4082
  const animateDu = Number.isFinite(du) ? du : 0;
3876
- const empEasing = makeEmpEasing$1(EMP_EASING_MID$1);
4083
+ const empEasing = makeEmpEasing(EMP_EASING_MID);
3877
4084
  result = characterElements.flatMap((el, i, arr) => {
3878
4085
  const wordDe = de + du / 2.5 / anchorCharCount * i;
3879
4086
  const result = [];
3880
- const frames = new Array(ANIMATION_FRAME_QUANTITY$1).fill(0).map((_, j) => {
3881
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY$1;
4087
+ const frames = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4088
+ const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
3882
4089
  const transX = empEasing(x);
3883
4090
  const glowLevel = empEasing(x) * blur;
3884
4091
  const mat = scaleMatrix4(createMatrix4(), 1 + transX * .1 * amount);
@@ -3903,8 +4110,8 @@ var LyricLineEl$1 = class extends LyricLineBase {
3903
4110
  };
3904
4111
  glow.pause();
3905
4112
  result.push(glow);
3906
- const floatFrame = new Array(ANIMATION_FRAME_QUANTITY$1).fill(0).map((_, j) => {
3907
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY$1;
4113
+ const floatFrame = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4114
+ const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
3908
4115
  let y = Math.sin(x * Math.PI);
3909
4116
  if (this.lyricLine.isBG) y *= 2;
3910
4117
  return {
@@ -3963,7 +4170,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3963
4170
  word.width = wordEl.clientWidth;
3964
4171
  word.height = wordEl.clientHeight;
3965
4172
  const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
3966
- const [maskImage, totalAspect] = generateFadeGradient$1(fadeWidth / word.width);
4173
+ const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / word.width);
3967
4174
  const totalAspectStr = `${totalAspect * 100}% 100%`;
3968
4175
  if (this.lyricPlayer.supportMaskImage) {
3969
4176
  wordEl.style.maskImage = maskImage;
@@ -3984,12 +4191,12 @@ var LyricLineEl$1 = class extends LyricLineBase {
3984
4191
  }
3985
4192
  }
3986
4193
  generateWebAnimationBasedMaskImage() {
3987
- const totalFadeDuration = Math.max(this.splittedWords.reduce((pv, w) => Math.max(w.endTime, pv), 0), this.lyricLine.endTime) - this.lyricLine.startTime;
4194
+ const totalFadeDuration = Math.max(0, ...this.splittedWords.map((w) => w.endTime), this.lyricLine.endTime) - this.lyricLine.startTime;
3988
4195
  this.splittedWords.forEach((word, i) => {
3989
4196
  const wordEl = word.mainElement;
3990
4197
  if (wordEl) {
3991
4198
  const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
3992
- const [maskImage, totalAspect] = generateFadeGradient$1(fadeWidth / (word.width + word.padding * 2));
4199
+ const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / (word.width + word.padding * 2));
3993
4200
  const totalAspectStr = `${totalAspect * 100}% 100%`;
3994
4201
  if (this.lyricPlayer.supportMaskImage) {
3995
4202
  wordEl.style.maskImage = maskImage;
@@ -4004,7 +4211,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4004
4211
  }
4005
4212
  const widthBeforeSelf = this.splittedWords.slice(0, i).reduce((a, b) => a + b.width, 0) + (this.splittedWords[0] ? fadeWidth : 0);
4006
4213
  const minOffset = -(word.width + word.padding * 2 + fadeWidth);
4007
- const clampOffset = (x) => Math.max(minOffset, Math.min(0, x));
4214
+ const clampOffset = (x) => clamp(x, minOffset, 0);
4008
4215
  let curPos = -widthBeforeSelf - word.width - word.padding - fadeWidth;
4009
4216
  let timeOffset = 0;
4010
4217
  const frames = [];
@@ -4012,7 +4219,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4012
4219
  let lastTime = 0;
4013
4220
  const pushFrame = () => {
4014
4221
  const moveOffset = curPos - lastPos;
4015
- const time = Math.max(0, Math.min(1, timeOffset));
4222
+ const time = clamp01(timeOffset);
4016
4223
  const duration = time - lastTime;
4017
4224
  const d = Math.abs(duration / moveOffset);
4018
4225
  if (curPos > minOffset && lastPos < minOffset) {
@@ -4052,7 +4259,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4052
4259
  lastTimeStamp = curTimeStamp;
4053
4260
  }
4054
4261
  {
4055
- const fadeDuration = Math.max(0, otherWord.endTime - otherWord.startTime);
4262
+ const fadeDuration = clampPositive(otherWord.endTime - otherWord.startTime);
4056
4263
  const rubySegments = this.getRubySegments(otherWord);
4057
4264
  const rubyCharCount = rubySegments.reduce((total, ruby) => total + ruby.word.length, 0);
4058
4265
  if (rubyCharCount > 0) {
@@ -4068,7 +4275,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4068
4275
  timeOffset += rubyStaticDuration / totalFadeDuration;
4069
4276
  if (rubyStaticDuration > 0) pushFrame();
4070
4277
  lastTimeStamp = rubyStartStamp;
4071
- const perCharDuration = Math.max(0, rubyEnd - rubyStart) / ruby.word.length;
4278
+ const perCharDuration = clampPositive(rubyEnd - rubyStart) / ruby.word.length;
4072
4279
  for (let rubyCharIndex = 0; rubyCharIndex < ruby.word.length; rubyCharIndex++) {
4073
4280
  timeOffset += perCharDuration / totalFadeDuration;
4074
4281
  curPos += widthPerChar;
@@ -4118,10 +4325,10 @@ var LyricLineEl$1 = class extends LyricLineBase {
4118
4325
  return this.element;
4119
4326
  }
4120
4327
  updateMaskAlphaTargets(scale) {
4121
- const factor = Math.max(0, Math.min(1, (scale - .97) / .03));
4328
+ const factor = clamp01((scale - .97) / .03);
4122
4329
  const dynamicDarkAlpha = factor * .2 + .2;
4123
4330
  const dynamicBrightAlpha = factor * .8 + .2;
4124
- if (this.renderMode === 0) {
4331
+ if (this.renderMode === LyricLineRenderMode.SOLID) {
4125
4332
  this.targetBrightAlpha = dynamicDarkAlpha;
4126
4333
  this.targetDarkAlpha = dynamicDarkAlpha;
4127
4334
  } else {
@@ -4143,7 +4350,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
4143
4350
  this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
4144
4351
  this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
4145
4352
  }
4146
- setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = 0) {
4353
+ setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4147
4354
  super.setTransform(top, scale, opacity, blur, force, delay);
4148
4355
  this.renderMode = mode;
4149
4356
  const beforeInSight = this.isInSight;
@@ -4300,7 +4507,7 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4300
4507
  line.dispose();
4301
4508
  }
4302
4509
  this.currentLyricLineObjects = this.processedLines.map((line, i) => {
4303
- const lineEl = new LyricLineEl$1(this, line);
4510
+ const lineEl = new LyricLineEl(this, line);
4304
4511
  lineEl.addMouseEventListener("click", this.onLineClickedHandler);
4305
4512
  lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
4306
4513
  this.lyricLinesIndexes.set(lineEl, i);
@@ -4326,9 +4533,9 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4326
4533
  for (const line of this.currentLyricLineObjects) line.resume();
4327
4534
  }
4328
4535
  update(delta = 0) {
4329
- if (!this.initialLayoutFinished) return;
4536
+ if (!this.timelineState.initialLayoutFinished) return;
4330
4537
  super.update(delta);
4331
- if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${this.currentTime}`);
4538
+ if (!this.supportMaskImage) this.element.style.setProperty("--amll-player-time", `${this.timelineState.currentTime}`);
4332
4539
  if (!this.isPageVisible) return;
4333
4540
  const deltaS = delta / 1e3;
4334
4541
  for (const line of this.currentLyricLineObjects) line.update(deltaS);
@@ -4342,890 +4549,6 @@ var DomLyricPlayer = class extends LyricPlayerBase {
4342
4549
  }
4343
4550
  };
4344
4551
  //#endregion
4345
- //#region src/utils/debounce.ts
4346
- function debounce(cb, wait = 20) {
4347
- let h;
4348
- const callable = (...args) => {
4349
- clearTimeout(h);
4350
- h = setTimeout(() => cb(...args), wait);
4351
- };
4352
- return callable;
4353
- }
4354
- //#endregion
4355
- //#region src/lyric-player/dom-slim/index.module.css
4356
- var index_module_default = {
4357
- "active": "KxF9Iq_active",
4358
- "duet": "KxF9Iq_duet",
4359
- "enabled": "KxF9Iq_enabled",
4360
- "hasDuetLine": "KxF9Iq_hasDuetLine",
4361
- "interludeDots": "KxF9Iq_interludeDots",
4362
- "lyricBgLine": "KxF9Iq_lyricBgLine",
4363
- "lyricDuetLine": "KxF9Iq_lyricDuetLine",
4364
- "lyricLine": "KxF9Iq_lyricLine",
4365
- "lyricMainLine": "KxF9Iq_lyricMainLine",
4366
- "lyricSubLine": "KxF9Iq_lyricSubLine",
4367
- "romanWord": "KxF9Iq_romanWord",
4368
- "rubyWord": "KxF9Iq_rubyWord",
4369
- "tmpDisableTransition": "KxF9Iq_tmpDisableTransition",
4370
- "wordBody": "KxF9Iq_wordBody",
4371
- "wordWithRuby": "KxF9Iq_wordWithRuby"
4372
- };
4373
- //#endregion
4374
- //#region src/utils/mutex.ts
4375
- function mutexifyFunction(func) {
4376
- const awaitingTasks = [];
4377
- function processNextTask() {
4378
- const task = awaitingTasks[0];
4379
- if (!task) return;
4380
- func(...task.args).then((value) => {
4381
- task.resolve(value);
4382
- }).catch((reason) => {
4383
- task.reject(reason);
4384
- }).finally(() => {
4385
- awaitingTasks.shift();
4386
- if (awaitingTasks.length > 0) processNextTask();
4387
- });
4388
- }
4389
- return ((...args) => {
4390
- return new Promise((resolve, reject) => {
4391
- awaitingTasks.push({
4392
- resolve,
4393
- reject,
4394
- args
4395
- });
4396
- if (awaitingTasks.length === 1) processNextTask();
4397
- });
4398
- });
4399
- }
4400
- //#endregion
4401
- //#region src/lyric-player/dom-slim/lyric-line.ts
4402
- const ANIMATION_FRAME_QUANTITY = 32;
4403
- const norNum = (min, max) => (x) => Math.min(1, Math.max(0, (x - min) / (max - min)));
4404
- const EMP_EASING_MID = .5;
4405
- const beginNum = norNum(0, EMP_EASING_MID);
4406
- const endNum = norNum(EMP_EASING_MID, 1);
4407
- const bezIn = bezier(.2, .4, .58, 1);
4408
- const bezOut = bezier(.3, 0, .58, 1);
4409
- const makeEmpEasing = (mid) => {
4410
- return (x) => x < mid ? bezIn(beginNum(x)) : 1 - bezOut(endNum(x));
4411
- };
4412
- 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))") {
4413
- const totalAspect = 2 + width + padding;
4414
- const widthInTotal = width / totalAspect;
4415
- const leftPos = (1 - widthInTotal) / 2;
4416
- return [`linear-gradient(to right,${bright} ${leftPos * 100}%,${dark} ${(leftPos + widthInTotal) * 100}%)`, totalAspect];
4417
- }
4418
- var RawLyricLineMouseEvent = class extends MouseEvent {
4419
- constructor(line, event) {
4420
- super(event.type, event);
4421
- this.line = line;
4422
- }
4423
- };
4424
- function getScaleFromTransform(transform) {
4425
- const match = transform.match(/matrix\(([^)]+)\)/);
4426
- if (match) {
4427
- const values = match[1].split(", ");
4428
- return (Number.parseFloat(values[0]) + Number.parseFloat(values[3])) / 2;
4429
- }
4430
- return 1;
4431
- }
4432
- var LyricLineEl = class extends LyricLineBase {
4433
- element = document.createElement("div");
4434
- splittedWords = [];
4435
- lineSize = [0, 0];
4436
- constructor(lyricPlayer, lyricLine = {
4437
- words: [],
4438
- translatedLyric: "",
4439
- romanLyric: "",
4440
- startTime: 0,
4441
- endTime: 0,
4442
- isBG: false,
4443
- isDuet: false
4444
- }) {
4445
- super();
4446
- this.lyricPlayer = lyricPlayer;
4447
- this.lyricLine = lyricLine;
4448
- this.element.setAttribute("class", index_module_default.lyricLine);
4449
- if (this.lyricLine.isBG) this.element.classList.add(index_module_default.lyricBgLine);
4450
- if (this.lyricLine.isDuet) this.element.classList.add(index_module_default.lyricDuetLine);
4451
- this.element.appendChild(document.createElement("div"));
4452
- this.element.appendChild(document.createElement("div"));
4453
- this.element.appendChild(document.createElement("div"));
4454
- const main = this.element.children[0];
4455
- const trans = this.element.children[1];
4456
- const roman = this.element.children[2];
4457
- main.setAttribute("class", index_module_default.lyricMainLine);
4458
- trans.setAttribute("class", index_module_default.lyricSubLine);
4459
- roman.setAttribute("class", index_module_default.lyricSubLine);
4460
- this.rebuildElement();
4461
- this.rebuildStyle();
4462
- this.markMaskImageDirty("Initial construction");
4463
- }
4464
- listenersMap = /* @__PURE__ */ new Map();
4465
- onMouseEvent = (e) => {
4466
- const wrapped = new RawLyricLineMouseEvent(this, e);
4467
- for (const listener of this.listenersMap.get(e.type) ?? []) listener.call(this, wrapped);
4468
- if (!this.dispatchEvent(wrapped) || wrapped.defaultPrevented) {
4469
- e.preventDefault();
4470
- e.stopPropagation();
4471
- e.stopImmediatePropagation();
4472
- }
4473
- };
4474
- addMouseEventListener(type, callback, options) {
4475
- if (callback) {
4476
- const listeners = this.listenersMap.get(type) ?? /* @__PURE__ */ new Set();
4477
- if (listeners.size === 0) this.element.addEventListener(type, this.onMouseEvent, options);
4478
- listeners.add(callback);
4479
- this.listenersMap.set(type, listeners);
4480
- }
4481
- }
4482
- removeMouseEventListener(type, callback, options) {
4483
- if (callback) {
4484
- const listeners = this.listenersMap.get(type);
4485
- if (listeners) {
4486
- listeners.delete(callback);
4487
- if (listeners.size === 0) this.element.removeEventListener(type, this.onMouseEvent, options);
4488
- }
4489
- }
4490
- }
4491
- areWordsOnSameLine(word1, word2) {
4492
- if (word1?.mainElement && word2?.mainElement) {
4493
- const word1el = word1.mainElement;
4494
- const word2el = word2.mainElement;
4495
- const rect1 = word1el.getBoundingClientRect();
4496
- const rect2 = word2el.getBoundingClientRect();
4497
- return Math.abs(rect1.top - rect2.top) < 10;
4498
- }
4499
- return true;
4500
- }
4501
- isEnabled = false;
4502
- async enable(maskAnimationTime = this.lyricLine.startTime) {
4503
- this.isEnabled = true;
4504
- this.element.classList.add(index_module_default.active);
4505
- await this.waitMaskImageUpdated();
4506
- const main = this.element.children[0];
4507
- for (const word of this.splittedWords) {
4508
- for (const a of word.elementAnimations) {
4509
- a.currentTime = 0;
4510
- a.playbackRate = 1;
4511
- a.play();
4512
- }
4513
- for (const a of word.maskAnimations) {
4514
- a.currentTime = Math.min(this.totalDuration, Math.max(0, maskAnimationTime - this.lyricLine.startTime));
4515
- a.playbackRate = 1;
4516
- a.play();
4517
- }
4518
- }
4519
- main.classList.add(index_module_default.active);
4520
- }
4521
- disable() {
4522
- this.isEnabled = false;
4523
- this.element.classList.remove(index_module_default.active);
4524
- const main = this.element.children[0];
4525
- for (const word of this.splittedWords) for (const a of word.elementAnimations) if (a.id === "float-word" || a.id.includes("emphasize-word-float-only")) {
4526
- a.playbackRate = -1;
4527
- a.play();
4528
- }
4529
- main.classList.remove(index_module_default.active);
4530
- }
4531
- lastWord;
4532
- async resume() {
4533
- await this.waitMaskImageUpdated();
4534
- if (!this.isEnabled) return;
4535
- for (const word of this.splittedWords) {
4536
- for (const a of word.elementAnimations) if (!this.lastWord || this.splittedWords.indexOf(this.lastWord) < this.splittedWords.indexOf(word)) a.play();
4537
- for (const a of word.maskAnimations) if (!this.lastWord || this.splittedWords.indexOf(this.lastWord) < this.splittedWords.indexOf(word)) a.play();
4538
- }
4539
- }
4540
- async pause() {
4541
- await this.waitMaskImageUpdated();
4542
- if (!this.isEnabled) return;
4543
- for (const word of this.splittedWords) {
4544
- for (const a of word.elementAnimations) a.pause();
4545
- for (const a of word.maskAnimations) a.pause();
4546
- }
4547
- }
4548
- setMaskAnimationState(maskAnimationTime = 0) {
4549
- const t = maskAnimationTime - this.lyricLine.startTime;
4550
- for (const word of this.splittedWords) for (const a of word.maskAnimations) {
4551
- a.currentTime = Math.min(this.totalDuration, Math.max(0, t));
4552
- a.playbackRate = 1;
4553
- if (t >= 0 && t < this.totalDuration) a.play();
4554
- else a.pause();
4555
- }
4556
- }
4557
- measureLockMark = false;
4558
- measureLock = mutexifyFunction(async (callback) => {
4559
- if (this.measureLockMark) return;
4560
- this.measureLockMark = true;
4561
- await callback();
4562
- this.measureLockMark = false;
4563
- });
4564
- getLine() {
4565
- return this.lyricLine;
4566
- }
4567
- show() {
4568
- this.rebuildStyle();
4569
- }
4570
- hide() {}
4571
- rebuildStyle() {}
4572
- getRubySegments(word) {
4573
- return (word.ruby ?? []).filter((ruby) => (ruby?.word?.trim().length ?? 0) > 0);
4574
- }
4575
- buildWordElement(word, shouldEmphasize, hasRubyLine, hasRomanLine, displayWord) {
4576
- const mainWordEl = document.createElement("span");
4577
- const subElements = [];
4578
- const romanWord = word.romanWord?.trim() ?? "";
4579
- let wordContainer = mainWordEl;
4580
- if (hasRubyLine || hasRomanLine) {
4581
- wordContainer = document.createElement("div");
4582
- mainWordEl.appendChild(wordContainer);
4583
- }
4584
- if (hasRubyLine) {
4585
- const rubyWordEl = document.createElement("div");
4586
- const rubySegments = this.getRubySegments(word);
4587
- for (const ruby of rubySegments) {
4588
- const rubyPartEl = document.createElement("span");
4589
- rubyPartEl.innerText = ruby.word;
4590
- rubyPartEl.dataset.startTime = String(ruby.startTime);
4591
- rubyPartEl.dataset.endTime = String(ruby.endTime);
4592
- rubyWordEl.appendChild(rubyPartEl);
4593
- }
4594
- rubyWordEl.classList.add(index_module_default.rubyWord);
4595
- mainWordEl.classList.add(index_module_default.wordWithRuby);
4596
- wordContainer.classList.add(index_module_default.wordBody);
4597
- mainWordEl.insertBefore(rubyWordEl, wordContainer);
4598
- }
4599
- if (shouldEmphasize) {
4600
- mainWordEl.classList.add(index_module_default.emphasize);
4601
- for (const char of displayWord.trim()) {
4602
- const charEl = document.createElement("span");
4603
- charEl.innerText = char;
4604
- subElements.push(charEl);
4605
- wordContainer.appendChild(charEl);
4606
- }
4607
- } else if (hasRomanLine) {
4608
- const wordEl = document.createElement("div");
4609
- wordEl.innerText = displayWord;
4610
- wordContainer.appendChild(wordEl);
4611
- } else mainWordEl.innerText = displayWord;
4612
- if (hasRomanLine) {
4613
- const romanWordEl = document.createElement("div");
4614
- romanWordEl.innerText = romanWord.length > 0 ? romanWord : "\xA0";
4615
- romanWordEl.classList.add(index_module_default.romanWord);
4616
- wordContainer.appendChild(romanWordEl);
4617
- }
4618
- return {
4619
- mainWordEl,
4620
- subElements
4621
- };
4622
- }
4623
- rebuildElement() {
4624
- this.disposeElements();
4625
- const main = this.element.children[0];
4626
- const trans = this.element.children[1];
4627
- const roman = this.element.children[2];
4628
- if (this.lyricPlayer._getIsNonDynamic()) {
4629
- main.innerText = this.lyricLine.words.map((w) => this.lyricPlayer.processObsceneWord(w)).join("");
4630
- trans.innerText = this.lyricLine.translatedLyric;
4631
- roman.innerText = this.lyricLine.romanLyric;
4632
- return;
4633
- }
4634
- const chunkedWords = chunkAndSplitLyricWords(this.lyricLine.words);
4635
- const hasRubyLine = this.lyricLine.words.some((word) => (word.ruby?.length ?? 0) > 0);
4636
- const hasRomanLine = this.lyricLine.words.some((word) => (word.romanWord?.trim().length ?? 0) > 0);
4637
- main.innerHTML = "";
4638
- for (const chunk of chunkedWords) if (Array.isArray(chunk)) {
4639
- if (chunk.length === 0) continue;
4640
- const merged = chunk.reduce((a, b) => {
4641
- a.endTime = Math.max(a.endTime, b.endTime);
4642
- a.startTime = Math.min(a.startTime, b.startTime);
4643
- a.word += b.word;
4644
- return a;
4645
- }, {
4646
- word: "",
4647
- romanWord: "",
4648
- startTime: Number.POSITIVE_INFINITY,
4649
- endTime: Number.NEGATIVE_INFINITY,
4650
- wordType: "normal",
4651
- obscene: false
4652
- });
4653
- const emp = chunk.map((word) => LyricLineBase.shouldEmphasize(word)).reduce((a, b) => a || b, LyricLineBase.shouldEmphasize(merged));
4654
- const wrapperWordEl = document.createElement("span");
4655
- wrapperWordEl.classList.add(index_module_default.emphasizeWrapper);
4656
- const characterElements = [];
4657
- for (const word of chunk) {
4658
- const { mainWordEl, subElements } = this.buildWordElement(word, emp, hasRubyLine, hasRomanLine, this.lyricPlayer.processObsceneWord(word));
4659
- if (emp) characterElements.push(...subElements);
4660
- this.splittedWords.push({
4661
- ...word,
4662
- mainElement: mainWordEl,
4663
- subElements,
4664
- elementAnimations: [],
4665
- maskAnimations: [],
4666
- width: 0,
4667
- height: 0,
4668
- padding: 0,
4669
- shouldEmphasize: emp
4670
- });
4671
- wrapperWordEl.appendChild(mainWordEl);
4672
- }
4673
- if (emp) this.splittedWords[this.splittedWords.length - 1].elementAnimations.push(...this.initEmphasizeAnimation(merged, characterElements, merged.endTime - merged.startTime, merged.startTime - this.lyricLine.startTime));
4674
- if (merged.word.trimStart() !== merged.word) main.appendChild(document.createTextNode(" "));
4675
- main.appendChild(wrapperWordEl);
4676
- if (merged.word.trimEnd() !== merged.word && LyricLineBase.shouldEmphasize(merged)) main.appendChild(document.createTextNode(" "));
4677
- } else if (chunk.word.trim().length === 0) main.appendChild(document.createTextNode(" "));
4678
- else {
4679
- const emp = LyricLineBase.shouldEmphasize(chunk);
4680
- const { mainWordEl, subElements } = this.buildWordElement(chunk, emp, hasRubyLine, hasRomanLine, this.lyricPlayer.processObsceneWord(chunk).trim());
4681
- const realWord = {
4682
- ...chunk,
4683
- mainElement: mainWordEl,
4684
- subElements,
4685
- elementAnimations: [],
4686
- maskAnimations: [],
4687
- width: 0,
4688
- height: 0,
4689
- padding: 0,
4690
- shouldEmphasize: emp
4691
- };
4692
- if (emp) {
4693
- const duration = Math.abs(realWord.endTime - realWord.startTime);
4694
- realWord.elementAnimations.push(...this.initEmphasizeAnimation(chunk, subElements, duration, realWord.startTime - this.lyricLine.startTime));
4695
- }
4696
- if (chunk.word.trimStart() !== chunk.word) main.appendChild(document.createTextNode(" "));
4697
- main.appendChild(mainWordEl);
4698
- if (chunk.word.trimEnd() !== chunk.word) main.appendChild(document.createTextNode(" "));
4699
- this.splittedWords.push(realWord);
4700
- }
4701
- trans.innerText = this.lyricLine.translatedLyric;
4702
- roman.innerText = this.lyricLine.romanLyric;
4703
- }
4704
- initEmphasizeAnimation(word, characterElements, duration, delay) {
4705
- const de = Math.max(0, delay);
4706
- let du = Math.max(1e3, duration);
4707
- let result = [];
4708
- let amount = du / 2e3;
4709
- amount = amount > 1 ? Math.sqrt(amount) : amount ** 3;
4710
- let blur = du / 3e3;
4711
- blur = blur > 1 ? Math.sqrt(blur) : blur ** 3;
4712
- amount *= .6;
4713
- blur *= .5;
4714
- if (this.lyricLine.words.length > 0 && word.word.includes(this.lyricLine.words[this.lyricLine.words.length - 1].word)) {
4715
- amount *= 1.6;
4716
- blur *= 1.5;
4717
- du *= 1.2;
4718
- }
4719
- amount = Math.min(1.2, amount);
4720
- blur = Math.min(.8, blur);
4721
- const animateDu = Number.isFinite(du) ? du : 0;
4722
- const empEasing = makeEmpEasing(EMP_EASING_MID);
4723
- result = characterElements.flatMap((el, i, arr) => {
4724
- const wordDe = de + du / 2.5 / arr.length * i;
4725
- const result = [];
4726
- const frames = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4727
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
4728
- const transX = empEasing(x);
4729
- const glowLevel = empEasing(x) * blur;
4730
- const mat = scaleMatrix4(createMatrix4(), 1 + transX * .1 * amount);
4731
- const offsetX = -transX * .03 * amount * (arr.length / 2 - i);
4732
- const offsetY = -transX * .025 * amount;
4733
- return {
4734
- offset: x,
4735
- transform: `${matrix4ToCSS(mat, 4)} translate(${offsetX}em, ${offsetY}em)`,
4736
- textShadow: `0 0 ${Math.min(.3, blur * .3)}em rgba(255, 255, 255, ${glowLevel})`
4737
- };
4738
- });
4739
- const glow = el.animate(frames, {
4740
- duration: animateDu,
4741
- delay: Number.isFinite(wordDe) ? wordDe : 0,
4742
- id: `emphasize-word-${el.innerText}-${i}`,
4743
- iterations: 1,
4744
- composite: "replace",
4745
- fill: "both"
4746
- });
4747
- glow.onfinish = () => {
4748
- glow.pause();
4749
- };
4750
- glow.pause();
4751
- result.push(glow);
4752
- const floatFrame = new Array(ANIMATION_FRAME_QUANTITY).fill(0).map((_, j) => {
4753
- const x = (j + 1) / ANIMATION_FRAME_QUANTITY;
4754
- let y = Math.sin(x * Math.PI);
4755
- if (this.lyricLine.isBG) y *= 2;
4756
- return {
4757
- offset: x,
4758
- transform: `translateY(${-y * .05}em)`
4759
- };
4760
- });
4761
- const float = el.animate(floatFrame, {
4762
- duration: animateDu * 1.4,
4763
- delay: Number.isFinite(wordDe) ? wordDe - 400 : 0,
4764
- id: "emphasize-word-float",
4765
- iterations: 1,
4766
- composite: "add",
4767
- fill: "both"
4768
- });
4769
- float.onfinish = () => {
4770
- float.pause();
4771
- };
4772
- float.pause();
4773
- result.push(float);
4774
- return result;
4775
- });
4776
- return result;
4777
- }
4778
- get totalDuration() {
4779
- return this.lyricLine.endTime - this.lyricLine.startTime;
4780
- }
4781
- maskImageDirty = false;
4782
- markImageDirtyPromiseResolve = /* @__PURE__ */ new Set();
4783
- markImageDirtyPromise = new Promise((resolve) => {
4784
- this.markImageDirtyPromiseResolve.add(resolve);
4785
- });
4786
- markMaskImageDirty(_debugReason = "") {
4787
- this.maskImageDirty = true;
4788
- if (!this.element.classList.contains(index_module_default.dirty)) this.element.classList.add(index_module_default.dirty);
4789
- const newPromise = Promise.all([this.markImageDirtyPromise, new Promise((resolve) => {
4790
- this.markImageDirtyPromiseResolve.add(resolve);
4791
- })]).then(() => {});
4792
- this.markImageDirtyPromise = newPromise;
4793
- return newPromise;
4794
- }
4795
- waitMaskImageUpdated() {
4796
- return this.markImageDirtyPromise;
4797
- }
4798
- async updateMaskImage() {
4799
- if (!this.element.checkVisibility({ contentVisibilityAuto: true })) return;
4800
- this.maskImageDirty = false;
4801
- await this.measureLock(async () => {
4802
- await Promise.all(this.splittedWords.map(async (word) => {
4803
- const el = word.mainElement;
4804
- if (el) await measure(() => {
4805
- word.padding = Number.parseFloat(getComputedStyle(el).paddingLeft);
4806
- word.width = el.clientWidth - word.padding * 2;
4807
- word.height = el.clientHeight - word.padding * 2;
4808
- });
4809
- else {
4810
- word.width = 0;
4811
- word.height = 0;
4812
- word.padding = 0;
4813
- }
4814
- if (word.width * word.height === 0) console.warn("Word size is zero");
4815
- }));
4816
- await mutate(() => {
4817
- if (this.lyricPlayer.supportMaskImage) this.generateWebAnimationBasedMaskImage();
4818
- else this.generateCalcBasedMaskImage();
4819
- });
4820
- });
4821
- for (const resolve of this.markImageDirtyPromiseResolve) {
4822
- resolve();
4823
- this.markImageDirtyPromiseResolve.delete(resolve);
4824
- }
4825
- await mutate(() => {
4826
- this.element.classList.remove(index_module_default.dirty);
4827
- });
4828
- }
4829
- generateCalcBasedMaskImage() {
4830
- for (const word of this.splittedWords) {
4831
- const wordEl = word.mainElement;
4832
- if (wordEl) {
4833
- word.width = wordEl.clientWidth;
4834
- word.height = wordEl.clientHeight;
4835
- const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
4836
- const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / word.width);
4837
- const totalAspectStr = `${totalAspect * 100}% 100%`;
4838
- if (this.lyricPlayer.supportMaskImage) {
4839
- wordEl.style.maskImage = maskImage;
4840
- wordEl.style.maskRepeat = "no-repeat";
4841
- wordEl.style.maskOrigin = "left";
4842
- wordEl.style.maskSize = totalAspectStr;
4843
- } else {
4844
- wordEl.style.webkitMaskImage = maskImage;
4845
- wordEl.style.webkitMaskRepeat = "no-repeat";
4846
- wordEl.style.webkitMaskOrigin = "left";
4847
- wordEl.style.webkitMaskSize = totalAspectStr;
4848
- }
4849
- const w = word.width + fadeWidth;
4850
- 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`;
4851
- wordEl.style.maskPosition = maskPos;
4852
- wordEl.style.webkitMaskPosition = maskPos;
4853
- }
4854
- }
4855
- }
4856
- generateWebAnimationBasedMaskImage() {
4857
- const totalFadeDuration = Math.max(this.splittedWords.reduce((pv, w) => Math.max(w.endTime, pv), 0), this.lyricLine.endTime) - this.lyricLine.startTime;
4858
- this.splittedWords.forEach((word, i) => {
4859
- const wordEl = word.mainElement;
4860
- if (wordEl) {
4861
- const fadeWidth = word.height * this.lyricPlayer.getWordFadeWidth();
4862
- const [maskImage, totalAspect] = generateFadeGradient(fadeWidth / (word.width + word.padding * 2));
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 widthBeforeSelf = this.splittedWords.slice(0, i).reduce((a, b) => a + b.width, 0) + (this.splittedWords[0] ? fadeWidth : 0);
4876
- const minOffset = -(word.width + word.padding * 2 + fadeWidth);
4877
- const clampOffset = (x) => Math.max(minOffset, Math.min(0, x));
4878
- let curPos = -widthBeforeSelf - word.width - word.padding - fadeWidth;
4879
- let timeOffset = 0;
4880
- const frames = [];
4881
- let lastPos = curPos;
4882
- let lastTime = 0;
4883
- const pushFrame = () => {
4884
- const moveOffset = curPos - lastPos;
4885
- const time = Math.max(0, Math.min(1, timeOffset));
4886
- const duration = time - lastTime;
4887
- const d = Math.abs(duration / moveOffset);
4888
- if (curPos > minOffset && lastPos < minOffset) {
4889
- const staticTime = Math.abs(lastPos - minOffset) * d;
4890
- const value = `${clampOffset(lastPos)}px 0`;
4891
- const frame = {
4892
- offset: lastTime + staticTime,
4893
- maskPosition: value
4894
- };
4895
- frames.push(frame);
4896
- }
4897
- if (curPos > 0 && lastPos < 0) {
4898
- const staticTime = Math.abs(lastPos) * d;
4899
- const value = `${clampOffset(curPos)}px 0`;
4900
- const frame = {
4901
- offset: lastTime + staticTime,
4902
- maskPosition: value
4903
- };
4904
- frames.push(frame);
4905
- }
4906
- const frame = {
4907
- offset: time,
4908
- maskPosition: `${clampOffset(curPos)}px 0`
4909
- };
4910
- frames.push(frame);
4911
- lastPos = curPos;
4912
- lastTime = time;
4913
- };
4914
- pushFrame();
4915
- let lastTimeStamp = 0;
4916
- this.splittedWords.forEach((otherWord, j) => {
4917
- {
4918
- const curTimeStamp = otherWord.startTime - this.lyricLine.startTime;
4919
- const staticDuration = curTimeStamp - lastTimeStamp;
4920
- timeOffset += staticDuration / totalFadeDuration;
4921
- if (staticDuration > 0) pushFrame();
4922
- lastTimeStamp = curTimeStamp;
4923
- }
4924
- {
4925
- const fadeDuration = otherWord.endTime - otherWord.startTime;
4926
- const rubySegments = this.getRubySegments(otherWord);
4927
- const rubyCharCount = rubySegments.reduce((total, ruby) => total + ruby.word.length, 0);
4928
- if (rubyCharCount > 0) {
4929
- const widthPerChar = otherWord.width / rubyCharCount;
4930
- let charIndex = 0;
4931
- for (const ruby of rubySegments) {
4932
- const rubyStartTime = Number.isFinite(ruby.startTime) ? ruby.startTime : otherWord.startTime;
4933
- const rubyEndTime = Number.isFinite(ruby.endTime) ? ruby.endTime : otherWord.endTime;
4934
- const rubyStart = Math.max(rubyStartTime, otherWord.startTime);
4935
- const rubyEnd = Math.min(Math.max(rubyEndTime, rubyStart), otherWord.endTime);
4936
- const rubyStartStamp = rubyStart - this.lyricLine.startTime;
4937
- const rubyStaticDuration = rubyStartStamp - lastTimeStamp;
4938
- timeOffset += rubyStaticDuration / totalFadeDuration;
4939
- if (rubyStaticDuration > 0) pushFrame();
4940
- lastTimeStamp = rubyStartStamp;
4941
- const perCharDuration = Math.max(0, rubyEnd - rubyStart) / ruby.word.length;
4942
- for (let rubyCharIndex = 0; rubyCharIndex < ruby.word.length; rubyCharIndex++) {
4943
- timeOffset += perCharDuration / totalFadeDuration;
4944
- curPos += widthPerChar;
4945
- if (j === 0 && charIndex === 0) curPos += fadeWidth * 1.5;
4946
- if (j === this.splittedWords.length - 1 && charIndex === rubyCharCount - 1) curPos += fadeWidth * .5;
4947
- if (perCharDuration > 0) pushFrame();
4948
- lastTimeStamp += perCharDuration;
4949
- charIndex++;
4950
- }
4951
- }
4952
- const wordEndStamp = Math.max(otherWord.endTime - this.lyricLine.startTime, lastTimeStamp);
4953
- const wordTailDuration = wordEndStamp - lastTimeStamp;
4954
- timeOffset += wordTailDuration / totalFadeDuration;
4955
- if (wordTailDuration > 0) pushFrame();
4956
- lastTimeStamp = wordEndStamp;
4957
- } else {
4958
- timeOffset += fadeDuration / totalFadeDuration;
4959
- curPos += otherWord.width;
4960
- if (j === 0) curPos += fadeWidth * 1.5;
4961
- if (j === this.splittedWords.length - 1) curPos += fadeWidth * .5;
4962
- if (fadeDuration > 0) pushFrame();
4963
- lastTimeStamp += fadeDuration;
4964
- }
4965
- }
4966
- });
4967
- for (const a of word.maskAnimations) a.cancel();
4968
- try {
4969
- const ani = wordEl.animate(frames, {
4970
- duration: totalFadeDuration || 1,
4971
- id: `fade-word-${word.word}-${i}`,
4972
- fill: "both"
4973
- });
4974
- ani.pause();
4975
- word.maskAnimations = [ani];
4976
- } catch (err) {
4977
- console.warn("应用渐变动画发生错误", frames, totalFadeDuration, err);
4978
- }
4979
- }
4980
- });
4981
- }
4982
- getElement() {
4983
- return this.element;
4984
- }
4985
- setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0) {
4986
- super.setTransform(top, scale, opacity, blur, force, delay);
4987
- const beforeInSight = this.isInSight;
4988
- const enableSpring = this.lyricPlayer.getEnableSpring();
4989
- this.top = top;
4990
- this.scale = scale;
4991
- this.delay = delay * 1e3 | 0;
4992
- const main = this.element.children[0];
4993
- main.style.opacity = `${opacity}`;
4994
- if (force || !enableSpring) {
4995
- if (force) this.element.classList.add(index_module_default.tmpDisableTransition);
4996
- this.lineTransforms.posY.setPosition(top);
4997
- this.lineTransforms.scale.setPosition(scale);
4998
- if (!enableSpring) {
4999
- const afterInSight = this.isInSight;
5000
- if (beforeInSight || afterInSight) this.show();
5001
- else this.hide();
5002
- } else this.rebuildStyle();
5003
- if (force) requestAnimationFrame(() => {
5004
- this.element.classList.remove(index_module_default.tmpDisableTransition);
5005
- });
5006
- } else {
5007
- this.lineTransforms.posY.setTargetPosition(top, delay);
5008
- this.lineTransforms.scale.setTargetPosition(scale);
5009
- }
5010
- }
5011
- update(delta = 0) {
5012
- if (!this.lyricPlayer.getEnableSpring()) return;
5013
- this.lineTransforms.posY.update(delta);
5014
- this.lineTransforms.scale.update(delta);
5015
- if (this.isInSight) {
5016
- this.show();
5017
- if (this.maskImageDirty) this.updateMaskImage();
5018
- } else this.hide();
5019
- if (this.lyricPlayer.getEnableSpring()) {
5020
- this.element.style.setProperty("--bright-mask-alpha", `${Math.max(0, Math.min(1, this.lineTransforms.scale.getCurrentPosition() / 100 - .97) / .03) * .8 + .2}`);
5021
- this.element.style.setProperty("--dark-mask-alpha", `${Math.max(0, Math.min(1, this.lineTransforms.scale.getCurrentPosition() / 100 - .97) / .03) * .2 + .2}`);
5022
- } else {
5023
- const transform = window.getComputedStyle(this.element).transform;
5024
- const scale = getScaleFromTransform(transform);
5025
- this.element.style.setProperty("--bright-mask-alpha", `${Math.max(0, Math.min(1, (scale - .97) / .03)) * .8 + .2}`);
5026
- this.element.style.setProperty("--dark-mask-alpha", `${Math.max(0, Math.min(1, (scale - .97) / .03)) * .2 + .2}`);
5027
- }
5028
- }
5029
- _getDebugTargetPos() {
5030
- return `[位移: ${this.top}; 缩放: ${this.scale}; 延时: ${this.delay}]`;
5031
- }
5032
- get isInSight() {
5033
- const t = this.lineTransforms.posY.getCurrentPosition();
5034
- const h = this.lineSize[1];
5035
- const b = t + h;
5036
- return !(t > this.lyricPlayer.size[1] + h || b < -h);
5037
- }
5038
- disposeElements() {
5039
- for (const realWord of this.splittedWords) {
5040
- for (const a of realWord.elementAnimations) a.cancel();
5041
- for (const a of realWord.maskAnimations) a.cancel();
5042
- for (const sub of realWord.subElements) {
5043
- sub.remove();
5044
- sub.parentNode?.removeChild(sub);
5045
- }
5046
- realWord.elementAnimations = [];
5047
- realWord.maskAnimations = [];
5048
- realWord.subElements = [];
5049
- realWord.mainElement.remove();
5050
- realWord.mainElement.parentNode?.removeChild(realWord.mainElement);
5051
- }
5052
- this.splittedWords = [];
5053
- }
5054
- dispose() {
5055
- this.disposeElements();
5056
- this.element.remove();
5057
- }
5058
- };
5059
- //#endregion
5060
- //#region src/lyric-player/dom-slim/index.ts
5061
- /**
5062
- * 歌词播放组件,本框架的核心组件
5063
- *
5064
- * 尽可能贴切 Apple Music for iPad 的歌词效果设计,且做了力所能及的优化措施
5065
- */
5066
- var DomSlimLyricPlayer = class extends LyricPlayerBase {
5067
- currentLyricLineObjects = [];
5068
- debounceCalcLayout = debounce(() => this.calcLayout(true).then(() => this.currentLyricLineObjects.map(async (el, i) => {
5069
- el.markMaskImageDirty("DomLyricPlayer onResize");
5070
- await el.waitMaskImageUpdated();
5071
- if (this.hotLines.has(i)) {
5072
- el.enable(this.currentTime);
5073
- el.resume();
5074
- }
5075
- })), 1e3);
5076
- onResize() {
5077
- const computedStyles = getComputedStyle(this.element);
5078
- this._baseFontSize = Number.parseFloat(computedStyles.fontSize);
5079
- const innerWidth = this.element.clientWidth - Number.parseFloat(computedStyles.paddingLeft) - Number.parseFloat(computedStyles.paddingRight);
5080
- const innerHeight = this.element.clientHeight - Number.parseFloat(computedStyles.paddingTop) - Number.parseFloat(computedStyles.paddingBottom);
5081
- this.innerSize[0] = innerWidth;
5082
- this.innerSize[1] = innerHeight;
5083
- this.rebuildStyle();
5084
- this.debounceCalcLayout();
5085
- }
5086
- supportPlusLighter = CSS.supports("mix-blend-mode", "plus-lighter");
5087
- supportMaskImage = CSS.supports("mask-image", "none");
5088
- innerSize = [0, 0];
5089
- onLineClickedHandler = (e) => {
5090
- const evt = new LyricLineMouseEvent(this.lyricLinesIndexes.get(e.line) ?? -1, e.line, e);
5091
- if (!this.dispatchEvent(evt)) {
5092
- e.preventDefault();
5093
- e.stopPropagation();
5094
- e.stopImmediatePropagation();
5095
- }
5096
- };
5097
- /**
5098
- * 是否为非逐词歌词
5099
- * @internal
5100
- */
5101
- _getIsNonDynamic() {
5102
- return this.isNonDynamic;
5103
- }
5104
- _baseFontSize = Number.parseFloat(getComputedStyle(this.element).fontSize);
5105
- get baseFontSize() {
5106
- return this._baseFontSize;
5107
- }
5108
- constructor() {
5109
- super();
5110
- this.onResize();
5111
- this.element.classList.add("amll-lyric-player", "dom-slim");
5112
- if (this.disableSpring) this.element.classList.add(index_module_default.disableSpring);
5113
- }
5114
- rebuildStyle() {
5115
- const width = this.innerSize[0];
5116
- const height = this.innerSize[1];
5117
- this.element.style.setProperty("--amll-lp-width", `${width.toFixed(4)}px`);
5118
- this.element.style.setProperty("--amll-lp-height", `${height.toFixed(4)}px`);
5119
- }
5120
- setWordFadeWidth(value = .5) {
5121
- super.setWordFadeWidth(value);
5122
- for (const el of this.currentLyricLineObjects) el.markMaskImageDirty("DomLyricPlayer setWordFadeWidth");
5123
- }
5124
- /**
5125
- * 设置当前播放歌词,要注意传入后这个数组内的信息不得修改,否则会发生错误
5126
- * @param lines 歌词数组
5127
- * @param initialTime 初始时间,默认为 0
5128
- */
5129
- setLyricLines(lines, initialTime = 0) {
5130
- super.setLyricLines(lines, initialTime);
5131
- if (this.hasDuetLine) this.element.classList.add(index_module_default.hasDuetLine);
5132
- else this.element.classList.remove(index_module_default.hasDuetLine);
5133
- for (const line of this.currentLyricLineObjects) {
5134
- line.removeMouseEventListener("click", this.onLineClickedHandler);
5135
- line.removeMouseEventListener("contextmenu", this.onLineClickedHandler);
5136
- line.dispose();
5137
- }
5138
- this.currentLyricLineObjects = this.processedLines.map((line, i) => {
5139
- const lineEl = new LyricLineEl(this, line);
5140
- lineEl.addMouseEventListener("click", this.onLineClickedHandler);
5141
- lineEl.addMouseEventListener("contextmenu", this.onLineClickedHandler);
5142
- this.element.appendChild(lineEl.getElement());
5143
- this.lyricLinesIndexes.set(lineEl, i);
5144
- lineEl.markMaskImageDirty("DomLyricPlayer setLyricLines");
5145
- return lineEl;
5146
- });
5147
- this.setLinePosXSpringParams({});
5148
- this.setLinePosYSpringParams({});
5149
- this.setLineScaleSpringParams({});
5150
- this.calcLayout(true).then(() => {
5151
- this.initialLayoutFinished = true;
5152
- });
5153
- }
5154
- pause() {
5155
- super.pause();
5156
- this.interludeDots.pause();
5157
- for (const line of this.currentLyricLineObjects) line.pause();
5158
- }
5159
- resume() {
5160
- super.resume();
5161
- this.interludeDots.resume();
5162
- for (const line of this.currentLyricLineObjects) line.resume();
5163
- }
5164
- update(delta = 0) {
5165
- if (!this.initialLayoutFinished) return;
5166
- super.update(delta);
5167
- if (!this.isPageVisible) return;
5168
- const deltaS = delta / 1e3;
5169
- for (const line of this.currentLyricLineObjects) line.update(deltaS);
5170
- }
5171
- async calcLayout(sync) {
5172
- await super.calcLayout(sync);
5173
- const curLine = this.currentLyricLineObjects[this.targetAlignIndex];
5174
- const curLineEl = curLine.getElement();
5175
- const curLineVisibility = curLineEl.checkVisibility({ contentVisibilityAuto: true });
5176
- const playerTop = this.element.getBoundingClientRect().top;
5177
- if (!curLineVisibility) curLineEl.scrollIntoView({
5178
- block: "center",
5179
- behavior: "instant"
5180
- });
5181
- const curLineHeight = curLineEl.clientHeight;
5182
- let scrollToPos = curLineEl.getBoundingClientRect().top - playerTop - this.size[1] * this.alignPosition;
5183
- if (curLine) switch (this.alignAnchor) {
5184
- case "bottom":
5185
- scrollToPos += curLineHeight;
5186
- break;
5187
- case "center":
5188
- scrollToPos += curLineHeight / 2;
5189
- break;
5190
- case "top": break;
5191
- }
5192
- this.element.scrollBy({
5193
- top: scrollToPos,
5194
- behavior: "smooth"
5195
- });
5196
- }
5197
- dispose() {
5198
- super.dispose();
5199
- this.element.remove();
5200
- for (const el of this.currentLyricLineObjects) el.dispose();
5201
- this.bottomLine.dispose();
5202
- this.interludeDots.dispose();
5203
- }
5204
- };
5205
- //#endregion
5206
- //#region src/lyric-player/index.ts
5207
- /**
5208
- * 歌词中不雅用语的掩码模式
5209
- */
5210
- var MaskObsceneWordsMode = /* @__PURE__ */ function(MaskObsceneWordsMode) {
5211
- /** 禁用任何不雅用语掩码 */
5212
- MaskObsceneWordsMode["Disabled"] = "";
5213
- /** 完全掩码所有不雅用语 */
5214
- MaskObsceneWordsMode["FullMask"] = "full-mask";
5215
- /** 保留首尾字符,屏蔽中间字符 */
5216
- MaskObsceneWordsMode["PartialMask"] = "partial-mask";
5217
- return MaskObsceneWordsMode;
5218
- }(MaskObsceneWordsMode || {});
5219
- /**
5220
- * 歌词行的渲染模式
5221
- * @internal
5222
- */
5223
- var LyricLineRenderMode = /* @__PURE__ */ function(LyricLineRenderMode) {
5224
- LyricLineRenderMode[LyricLineRenderMode["SOLID"] = 0] = "SOLID";
5225
- LyricLineRenderMode[LyricLineRenderMode["GRADIENT"] = 1] = "GRADIENT";
5226
- return LyricLineRenderMode;
5227
- }(LyricLineRenderMode || {});
5228
- //#endregion
5229
- export { AbstractBaseRenderer, BackgroundRender, BaseRenderer, DomLyricPlayer, DomLyricPlayer as LyricPlayer, DomSlimLyricPlayer, LyricLineMouseEvent, LyricLineRenderMode, LyricPlayerBase, MaskObsceneWordsMode, MeshGradientRenderer, PixiRenderer };
4552
+ export { AbstractBaseRenderer, BackgroundRender, BaseRenderer, DomLyricPlayer, DomLyricPlayer as LyricPlayer, LayoutAlignAnchor, LyricLineMouseEvent, LyricLineRenderMode, LyricPlayerBase, MaskObsceneWordsMode, MeshGradientRenderer, PixiRenderer };
5230
4553
 
5231
4554
  //# sourceMappingURL=amll-core.mjs.map