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