@applemusic-like-lyrics/core 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,9 +30,9 @@ let _pixi_filter_bulge_pinch = require("@pixi/filter-bulge-pinch");
30
30
  let _pixi_filter_color_matrix = require("@pixi/filter-color-matrix");
31
31
  let _pixi_sprite = require("@pixi/sprite");
32
32
  let _ungap_structured_clone = require("@ungap/structured-clone");
33
- _ungap_structured_clone = __toESM(_ungap_structured_clone);
33
+ _ungap_structured_clone = __toESM(_ungap_structured_clone, 1);
34
34
  let bezier_easing = require("bezier-easing");
35
- bezier_easing = __toESM(bezier_easing);
35
+ bezier_easing = __toESM(bezier_easing, 1);
36
36
  //#region src/bg-render/base.ts
37
37
  var AbstractBaseRenderer = class {};
38
38
  var BaseRenderer = class extends AbstractBaseRenderer {
@@ -2274,7 +2274,7 @@ var LyricPlayerBase = class extends EventTarget {
2274
2274
  bottomLine = new BottomLineEl(this);
2275
2275
  enableBlur = true;
2276
2276
  enableScale = true;
2277
- maskObsceneWords = MaskObsceneWordsMode.Disabled;
2277
+ maskObsceneWords = "";
2278
2278
  maskObsceneWordChar = "*";
2279
2279
  hidePassedLines = false;
2280
2280
  scrollBoundary = [0, 0];
@@ -2564,7 +2564,7 @@ var LyricPlayerBase = class extends EventTarget {
2564
2564
  const c = char.charAt(0) || "*";
2565
2565
  if (this.maskObsceneWordChar === c) return;
2566
2566
  this.maskObsceneWordChar = c;
2567
- if (this.maskObsceneWords !== MaskObsceneWordsMode.Disabled) {
2567
+ if (this.maskObsceneWords !== "") {
2568
2568
  this.rebuildLyricLines();
2569
2569
  this.calcLayout();
2570
2570
  }
@@ -2579,10 +2579,10 @@ var LyricPlayerBase = class extends EventTarget {
2579
2579
  */
2580
2580
  processObsceneWord(word) {
2581
2581
  const text = word.word;
2582
- if (!word.obscene || this.maskObsceneWords === MaskObsceneWordsMode.Disabled) return text;
2582
+ if (!word.obscene || this.maskObsceneWords === "") return text;
2583
2583
  const maskChar = this.maskObsceneWordChar;
2584
- if (this.maskObsceneWords === MaskObsceneWordsMode.FullMask) return text.replace(/\S/g, maskChar);
2585
- if (this.maskObsceneWords === MaskObsceneWordsMode.PartialMask) {
2584
+ if (this.maskObsceneWords === "full-mask") return text.replace(/\S/g, maskChar);
2585
+ if (this.maskObsceneWords === "partial-mask") {
2586
2586
  const trimmed = text.trim();
2587
2587
  if (trimmed.length <= 2) return text.replace(/\S/g, maskChar);
2588
2588
  const startPos = text.indexOf(trimmed);
@@ -2943,7 +2943,7 @@ var LyricPlayerBase = class extends EventTarget {
2943
2943
  let targetScale = 100;
2944
2944
  if (!isActive && this.isPlaying) if (line.isBG) targetScale = 75;
2945
2945
  else targetScale = SCALE_ASPECT;
2946
- const renderMode = isActive ? LyricLineRenderMode.GRADIENT : LyricLineRenderMode.SOLID;
2946
+ const renderMode = isActive ? 1 : 0;
2947
2947
  lineObj.setTransform(curPos, targetScale, targetOpacity, blurLevel, force, delay, renderMode);
2948
2948
  if (line.isBG && (isActive || !this.isPlaying)) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
2949
2949
  else if (!line.isBG) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
@@ -3096,8 +3096,17 @@ var LyricLineBase = class extends EventTarget {
3096
3096
  posY: new Spring(0),
3097
3097
  scale: new Spring(100)
3098
3098
  };
3099
+ /**
3100
+ * 用于 CJK 词语边界检测的分词器
3101
+ */
3102
+ static wordSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
3103
+ /**
3104
+ * Unicode 标准的全局 Grapheme Cluster 分词器
3105
+ * 用于正确处理 emoji、复合字符等
3106
+ */
3107
+ static graphemeSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
3099
3108
  onLineSizeChange(_size) {}
3100
- setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = LyricLineRenderMode.SOLID) {
3109
+ setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = 0) {
3101
3110
  this.top = top;
3102
3111
  this.scale = scale;
3103
3112
  this.opacity = opacity;
@@ -3122,8 +3131,251 @@ var LyricLineBase = class extends EventTarget {
3122
3131
  dispose() {}
3123
3132
  };
3124
3133
  //#endregion
3134
+ //#region src/utils/lyric-line-break.ts
3135
+ /**
3136
+ * 单个词超过容器宽度时的大惩罚倍数
3137
+ */
3138
+ const OVERFLOW_PENALTY_MULTIPLIER = 1e3;
3139
+ /**
3140
+ * 截断 CJK 词组边界的惩罚比例
3141
+ *
3142
+ * 相对于容器宽度
3143
+ */
3144
+ const CJK_BREAK_PENALTY_RATIO = .15;
3145
+ /**
3146
+ * 截断普通文本(非空格、非 CJK 词界)的惩罚比例
3147
+ */
3148
+ const NORMAL_BREAK_PENALTY_RATIO = .5;
3149
+ /**
3150
+ * 在空格处断开的奖励比例
3151
+ */
3152
+ const SPACE_BREAK_REWARD_RATIO = .4;
3153
+ /**
3154
+ * 计算平均行长度的断点位置
3155
+ * @param children 子节点信息
3156
+ * @param containerWidth 容器可用内容宽度
3157
+ * @param fullText 完整的行文本
3158
+ * @param segmenter 预创建的 Intl.Segmenter 分词器
3159
+ * @returns 需要在其前面插入 `<br>` 的子节点索引数组,升序
3160
+ */
3161
+ function calcBalancedBreaks(children, containerWidth, fullText, segmenter) {
3162
+ const n = children.length;
3163
+ if (n === 0 || containerWidth <= 0) return [];
3164
+ const cjkBoundaries = /* @__PURE__ */ new Set();
3165
+ let offset = 0;
3166
+ for (const { segment, isWordLike } of segmenter.segment(fullText)) {
3167
+ if (offset > 0 && isWordLike) {
3168
+ if ([...segment].some((ch) => isCJK(ch))) cjkBoundaries.add(offset);
3169
+ }
3170
+ offset += segment.length;
3171
+ }
3172
+ const charOffsets = new Int32Array(n + 1);
3173
+ const prefixWidth = new Float64Array(n + 1);
3174
+ for (let i = 0; i < n; i++) {
3175
+ charOffsets[i + 1] = charOffsets[i] + children[i].text.length;
3176
+ prefixWidth[i + 1] = prefixWidth[i] + children[i].width;
3177
+ }
3178
+ if (prefixWidth[n] <= containerWidth) return [];
3179
+ /**
3180
+ * dp[i] 表示将 index i 到 n-1 的节点进行排版的最小代价
3181
+ */
3182
+ const dp = new Float64Array(n + 1).fill(Number.POSITIVE_INFINITY);
3183
+ const nextBreak = new Int32Array(n + 1).fill(-1);
3184
+ dp[n] = 0;
3185
+ const PENALTY_CJK = (containerWidth * CJK_BREAK_PENALTY_RATIO) ** 2;
3186
+ const PENALTY_NORMAL = (containerWidth * NORMAL_BREAK_PENALTY_RATIO) ** 2;
3187
+ for (let i = n - 1; i >= 0; i--) for (let j = i + 1; j <= n; j++) {
3188
+ const w = prefixWidth[j] - prefixWidth[i];
3189
+ let lineCost = 0;
3190
+ if (w > containerWidth) if (j === i + 1) lineCost = (w - containerWidth) ** 2 * OVERFLOW_PENALTY_MULTIPLIER;
3191
+ else continue;
3192
+ else lineCost = (containerWidth - w) ** 2;
3193
+ 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;
3197
+ const totalCost = lineCost + breakPenalty + dp[j];
3198
+ if (totalCost < dp[i]) {
3199
+ dp[i] = totalCost;
3200
+ nextBreak[i] = j;
3201
+ }
3202
+ }
3203
+ const breaks = [];
3204
+ let curr = 0;
3205
+ while (curr < n) {
3206
+ curr = nextBreak[curr];
3207
+ if (curr > 0 && curr < n) breaks.push(curr);
3208
+ }
3209
+ return breaks;
3210
+ }
3211
+ //#endregion
3212
+ //#region src/utils/line-balancer.ts
3213
+ let sharedCanvasCtx = null;
3214
+ function getMeasurementContext() {
3215
+ if (!sharedCanvasCtx) sharedCanvasCtx = document.createElement("canvas").getContext("2d");
3216
+ return sharedCanvasCtx;
3217
+ }
3218
+ /**
3219
+ * 用于平衡歌词行在换行后的各行长度
3220
+ */
3221
+ var LineBalancer = class LineBalancer {
3222
+ isBalancing = false;
3223
+ lastBalancedContainerWidth = -1;
3224
+ /**
3225
+ * 防止误差导致的意外换行
3226
+ */
3227
+ static SAFE_WIDTH_PADDING = 25;
3228
+ constructor(mainElement) {
3229
+ this.mainElement = mainElement;
3230
+ }
3231
+ balanceLineBreaks(isNonDynamic, hasSplittedWords, wordSegmenter) {
3232
+ if (this.isBalancing || !this.mainElement) return;
3233
+ const computedStyle = getComputedStyle(this.mainElement);
3234
+ const paddingLeft = Number.parseFloat(computedStyle.paddingLeft) || 0;
3235
+ const paddingRight = Number.parseFloat(computedStyle.paddingRight) || 0;
3236
+ const containerWidth = this.mainElement.clientWidth - paddingLeft - paddingRight;
3237
+ if (containerWidth <= 0) return;
3238
+ if (isNonDynamic) {
3239
+ this.balanceNonDynamicLineBreaks(containerWidth, computedStyle, wordSegmenter);
3240
+ return;
3241
+ }
3242
+ if (!hasSplittedWords) return;
3243
+ this.balanceDynamicLineBreaks(containerWidth, wordSegmenter);
3244
+ }
3245
+ reset() {
3246
+ this.lastBalancedContainerWidth = -1;
3247
+ }
3248
+ executeLineBalance(containerWidth, adapter, wordSegmenter) {
3249
+ const existingBrs = this.mainElement.querySelectorAll("br");
3250
+ if (containerWidth === this.lastBalancedContainerWidth && existingBrs.length > 0) return;
3251
+ adapter.resetDOM();
3252
+ const prevWhiteSpace = this.mainElement.style.whiteSpace;
3253
+ this.mainElement.style.whiteSpace = "nowrap";
3254
+ 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) {
3260
+ this.lastBalancedContainerWidth = containerWidth;
3261
+ return;
3262
+ }
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
+ const breaks = calcBalancedBreaks(childInfos, safeContainerWidth, fullText, wordSegmenter);
3270
+ if (breaks.length === 0) {
3271
+ this.lastBalancedContainerWidth = containerWidth;
3272
+ return;
3273
+ }
3274
+ this.isBalancing = true;
3275
+ adapter.applyBreaks(breaks, childInfos);
3276
+ this.lastBalancedContainerWidth = containerWidth;
3277
+ this.isBalancing = false;
3278
+ } finally {
3279
+ this.mainElement.style.whiteSpace = prevWhiteSpace;
3280
+ }
3281
+ }
3282
+ balanceDynamicLineBreaks(containerWidth, wordSegmenter) {
3283
+ const infoToNode = [];
3284
+ this.executeLineBalance(containerWidth, {
3285
+ resetDOM: () => {
3286
+ this.mainElement.querySelectorAll("br").forEach((br) => {
3287
+ br.remove();
3288
+ });
3289
+ },
3290
+ buildChildInfos: () => {
3291
+ infoToNode.length = 0;
3292
+ const childNodes = Array.from(this.mainElement.childNodes);
3293
+ const childInfos = [];
3294
+ const range = document.createRange();
3295
+ for (const node of childNodes) if (node.nodeType === Node.TEXT_NODE) {
3296
+ const text = node.textContent ?? "";
3297
+ if (text.length === 0) continue;
3298
+ range.selectNodeContents(node);
3299
+ childInfos.push({
3300
+ width: range.getBoundingClientRect().width,
3301
+ text,
3302
+ isSpace: text.trim().length === 0
3303
+ });
3304
+ infoToNode.push(node);
3305
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
3306
+ const el = node;
3307
+ const rect = el.getBoundingClientRect();
3308
+ const elStyle = getComputedStyle(el);
3309
+ const marginLeft = Number.parseFloat(elStyle.marginLeft) || 0;
3310
+ const marginRight = Number.parseFloat(elStyle.marginRight) || 0;
3311
+ childInfos.push({
3312
+ width: Math.max(0, rect.width + marginLeft + marginRight),
3313
+ text: el.textContent ?? "",
3314
+ isSpace: false
3315
+ });
3316
+ infoToNode.push(node);
3317
+ }
3318
+ return {
3319
+ childInfos,
3320
+ fullText: childInfos.map((c) => c.text).join("")
3321
+ };
3322
+ },
3323
+ applyBreaks: (breaks) => {
3324
+ for (let i = breaks.length - 1; i >= 0; i--) {
3325
+ const breakIndex = breaks[i];
3326
+ if (breakIndex >= 0 && breakIndex < infoToNode.length) this.mainElement.insertBefore(document.createElement("br"), infoToNode[breakIndex]);
3327
+ }
3328
+ }
3329
+ }, wordSegmenter);
3330
+ }
3331
+ balanceNonDynamicLineBreaks(containerWidth, computedStyle, wordSegmenter) {
3332
+ const fullText = this.mainElement.textContent ?? "";
3333
+ if (fullText.trim().length === 0) return;
3334
+ this.executeLineBalance(containerWidth, {
3335
+ resetDOM: () => {
3336
+ this.mainElement.innerHTML = "";
3337
+ this.mainElement.textContent = fullText;
3338
+ },
3339
+ buildChildInfos: () => {
3340
+ const ctx = getMeasurementContext();
3341
+ if (!ctx) {
3342
+ console.debug("Canvas 2D context is not supported, skipping line balancing");
3343
+ return {
3344
+ childInfos: [],
3345
+ fullText
3346
+ };
3347
+ }
3348
+ ctx.font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
3349
+ if ("letterSpacing" in ctx) ctx.letterSpacing = computedStyle.letterSpacing !== "normal" ? computedStyle.letterSpacing : "0px";
3350
+ if ("wordSpacing" in ctx) ctx.wordSpacing = computedStyle.wordSpacing !== "normal" ? computedStyle.wordSpacing : "0px";
3351
+ const childInfos = [];
3352
+ for (const { segment } of wordSegmenter.segment(fullText)) childInfos.push({
3353
+ width: ctx.measureText(segment).width,
3354
+ text: segment,
3355
+ isSpace: segment.trim().length === 0
3356
+ });
3357
+ return {
3358
+ childInfos,
3359
+ fullText
3360
+ };
3361
+ },
3362
+ applyBreaks: (breaks, childInfos) => {
3363
+ this.mainElement.innerHTML = "";
3364
+ const breakSet = new Set(breaks);
3365
+ const fragment = document.createDocumentFragment();
3366
+ for (let i = 0; i < childInfos.length; i++) {
3367
+ if (breakSet.has(i)) fragment.appendChild(document.createElement("br"));
3368
+ fragment.appendChild(document.createTextNode(childInfos[i].text));
3369
+ }
3370
+ this.mainElement.appendChild(fragment);
3371
+ }
3372
+ }, wordSegmenter);
3373
+ }
3374
+ };
3375
+ //#endregion
3125
3376
  //#region src/utils/lyric-split-words.ts
3126
- const hasSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter !== "undefined";
3377
+ const SPLIT_WHITESPACE_RE = /(\s+)/;
3378
+ const WHITESPACE_RE = /\s/g;
3127
3379
  /**
3128
3380
  * 将输入的单词重新分组,之间没有空格的单词将会组合成一个单词数组
3129
3381
  *
@@ -3134,27 +3386,41 @@ const hasSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter !== "u
3134
3386
  * @returns 重新分组后的单词数组
3135
3387
  */
3136
3388
  function chunkAndSplitLyricWords(words) {
3137
- const atoms = [];
3389
+ const result = [];
3390
+ let currentGroup = [];
3391
+ const flushGroup = () => {
3392
+ if (currentGroup.length > 0) {
3393
+ result.push(currentGroup.length === 1 ? currentGroup[0] : [...currentGroup]);
3394
+ currentGroup = [];
3395
+ }
3396
+ };
3397
+ const processAtom = (atom) => {
3398
+ const isSpace = atom.word.trim().length === 0;
3399
+ const hasRuby = (atom.ruby?.length ?? 0) > 0;
3400
+ const isCJKChar = isCJK(atom.word);
3401
+ if (!isSpace && !hasRuby && !isCJKChar) currentGroup.push(atom);
3402
+ else {
3403
+ flushGroup();
3404
+ result.push(atom);
3405
+ }
3406
+ };
3138
3407
  for (const w of words) {
3139
3408
  const isSpace = w.word.trim().length === 0;
3140
3409
  const romanWord = w.romanWord ?? "";
3141
3410
  const obscene = w.obscene ?? false;
3142
3411
  const hasRuby = (w.ruby?.length ?? 0) > 0;
3143
- if (isSpace) {
3144
- atoms.push({ ...w });
3145
- continue;
3146
- }
3147
- if (hasRuby) {
3148
- atoms.push({ ...w });
3412
+ if (isSpace || hasRuby) {
3413
+ processAtom({ ...w });
3149
3414
  continue;
3150
3415
  }
3151
- const parts = w.word.split(/(\s+)/).filter((p) => p.length > 0);
3416
+ const parts = w.word.split(SPLIT_WHITESPACE_RE).filter((p) => p.length > 0);
3417
+ const totalLength = w.word.replace(WHITESPACE_RE, "").length || 1;
3418
+ const timePerUnit = (w.endTime - w.startTime) / totalLength;
3152
3419
  let currentOffset = 0;
3153
- const totalLength = w.word.replace(/\s/g, "").length || 1;
3154
3420
  for (const part of parts) {
3155
3421
  if (!part.trim()) {
3156
- const startTime = w.startTime + currentOffset / totalLength * (w.endTime - w.startTime);
3157
- atoms.push({
3422
+ const startTime = w.startTime + currentOffset * timePerUnit;
3423
+ processAtom({
3158
3424
  word: part,
3159
3425
  romanWord: "",
3160
3426
  startTime,
@@ -3166,63 +3432,31 @@ function chunkAndSplitLyricWords(words) {
3166
3432
  if (isCJK(part) && part.length > 1 && romanWord.trim().length === 0) {
3167
3433
  const chars = part.split("");
3168
3434
  for (const char of chars) {
3169
- const charDuration = 1 / totalLength * (w.endTime - w.startTime);
3170
- const startTime = w.startTime + currentOffset / totalLength * (w.endTime - w.startTime);
3171
- atoms.push({
3435
+ const startTime = w.startTime + currentOffset * timePerUnit;
3436
+ processAtom({
3172
3437
  word: char,
3173
3438
  romanWord: "",
3174
3439
  startTime,
3175
- endTime: startTime + charDuration,
3440
+ endTime: startTime + timePerUnit,
3176
3441
  obscene
3177
3442
  });
3178
3443
  currentOffset += 1;
3179
3444
  }
3180
3445
  } else {
3181
3446
  const partRealLen = part.length;
3182
- const duration = partRealLen / totalLength * (w.endTime - w.startTime);
3183
- const startTime = w.startTime + currentOffset / totalLength * (w.endTime - w.startTime);
3184
- atoms.push({
3447
+ const startTime = w.startTime + currentOffset * timePerUnit;
3448
+ processAtom({
3185
3449
  word: part,
3186
3450
  romanWord,
3187
3451
  startTime,
3188
- endTime: startTime + duration,
3452
+ endTime: startTime + partRealLen * timePerUnit,
3189
3453
  obscene
3190
3454
  });
3191
3455
  currentOffset += partRealLen;
3192
3456
  }
3193
3457
  }
3194
3458
  }
3195
- if (!hasSegmenter) return atoms;
3196
- const fullText = atoms.map((a) => a.word).join("");
3197
- const segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
3198
- const segments = Array.from(segmenter.segment(fullText));
3199
- const result = [];
3200
- let atomIndex = 0;
3201
- let expectedLength = 0;
3202
- let actualLength = 0;
3203
- let currentGroup = [];
3204
- for (const segment of segments) {
3205
- const segmentLen = segment.segment.length;
3206
- expectedLength += segmentLen;
3207
- while (actualLength < expectedLength && atomIndex < atoms.length) {
3208
- const currentAtom = atoms[atomIndex];
3209
- currentGroup.push(currentAtom);
3210
- actualLength += currentAtom.word.length;
3211
- atomIndex++;
3212
- }
3213
- if (actualLength === expectedLength) {
3214
- while (currentGroup.length > 1 && !currentGroup[0].word.trim()) {
3215
- const spaceAtom = currentGroup.shift();
3216
- if (spaceAtom) result.push(spaceAtom);
3217
- }
3218
- if (currentGroup.length === 1) result.push(currentGroup[0]);
3219
- else if (currentGroup.length > 1) result.push(currentGroup);
3220
- currentGroup = [];
3221
- }
3222
- }
3223
- while (atomIndex < atoms.length) result.push(atoms[atomIndex++]);
3224
- if (currentGroup.length > 0) if (currentGroup.length === 1) result.push(currentGroup[0]);
3225
- else result.push(currentGroup);
3459
+ flushGroup();
3226
3460
  return result;
3227
3461
  }
3228
3462
  //#endregion
@@ -3304,12 +3538,15 @@ var LyricLineEl$1 = class extends LyricLineBase {
3304
3538
  splittedWords = [];
3305
3539
  built = false;
3306
3540
  lineSize = [0, 0];
3307
- renderMode = LyricLineRenderMode.SOLID;
3541
+ renderMode = 0;
3308
3542
  currentBrightAlpha = 1;
3309
3543
  currentDarkAlpha = .2;
3310
3544
  targetBrightAlpha = 1;
3311
3545
  targetDarkAlpha = .2;
3312
- segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
3546
+ /**
3547
+ * 用于平衡换行、尽量减少各行长度差异的类
3548
+ */
3549
+ balancer;
3313
3550
  constructor(lyricPlayer, lyricLine = {
3314
3551
  words: [],
3315
3552
  translatedLyric: "",
@@ -3337,6 +3574,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3337
3574
  main.setAttribute("class", lyric_player_module_default.lyricMainLine);
3338
3575
  trans.setAttribute("class", lyric_player_module_default.lyricSubLine);
3339
3576
  roman.setAttribute("class", lyric_player_module_default.lyricSubLine);
3577
+ if (LyricLineBase.wordSegmenter) this.balancer = new LineBalancer(main);
3340
3578
  this.rebuildStyle();
3341
3579
  }
3342
3580
  listenersMap = /* @__PURE__ */ new Map();
@@ -3410,7 +3648,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3410
3648
  disable() {
3411
3649
  this.isEnabled = false;
3412
3650
  this.element.classList.remove(lyric_player_module_default.active);
3413
- this.renderMode = LyricLineRenderMode.SOLID;
3651
+ this.renderMode = 0;
3414
3652
  const main = this.element.children[0];
3415
3653
  for (const word of this.splittedWords) {
3416
3654
  for (const a of word.elementAnimations) if (a.id === "float-word" || a.id.includes("emphasize-word-float-only")) {
@@ -3546,7 +3784,14 @@ var LyricLineEl$1 = class extends LyricLineBase {
3546
3784
  const displayWord = this.lyricPlayer.processObsceneWord(word);
3547
3785
  if (shouldEmphasize) {
3548
3786
  mainWordEl.classList.add(lyric_player_module_default.emphasize);
3549
- for (const { segment } of this.segmenter.segment(displayWord.trim())) {
3787
+ const trimmedWord = displayWord.trim();
3788
+ if (LyricLineBase.graphemeSegmenter) for (const { segment } of LyricLineBase.graphemeSegmenter.segment(trimmedWord)) {
3789
+ const charEl = document.createElement("span");
3790
+ charEl.innerText = segment;
3791
+ subElements.push(charEl);
3792
+ wordContainer.appendChild(charEl);
3793
+ }
3794
+ else for (const segment of Array.from(trimmedWord)) {
3550
3795
  const charEl = document.createElement("span");
3551
3796
  charEl.innerText = segment;
3552
3797
  subElements.push(charEl);
@@ -3728,6 +3973,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3728
3973
  word.padding = 0;
3729
3974
  }
3730
3975
  }
3976
+ if (this.balancer && LyricLineBase.wordSegmenter) this.balancer.balanceLineBreaks(this.lyricPlayer._getIsNonDynamic(), this.splittedWords.length > 0, LyricLineBase.wordSegmenter);
3731
3977
  if (this.lyricPlayer.supportMaskImage) this.generateWebAnimationBasedMaskImage();
3732
3978
  else this.generateCalcBasedMaskImage();
3733
3979
  if (this.isEnabled) {
@@ -3900,7 +4146,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3900
4146
  const factor = Math.max(0, Math.min(1, (scale - .97) / .03));
3901
4147
  const dynamicDarkAlpha = factor * .2 + .2;
3902
4148
  const dynamicBrightAlpha = factor * .8 + .2;
3903
- if (this.renderMode === LyricLineRenderMode.SOLID) {
4149
+ if (this.renderMode === 0) {
3904
4150
  this.targetBrightAlpha = dynamicDarkAlpha;
3905
4151
  this.targetDarkAlpha = dynamicDarkAlpha;
3906
4152
  } else {
@@ -3922,7 +4168,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3922
4168
  this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
3923
4169
  this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
3924
4170
  }
3925
- setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = LyricLineRenderMode.SOLID) {
4171
+ setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = 0) {
3926
4172
  super.setTransform(top, scale, opacity, blur, force, delay);
3927
4173
  this.renderMode = mode;
3928
4174
  const beforeInSight = this.isInSight;
@@ -3979,6 +4225,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
3979
4225
  return !(t > pb + h + ov || b < -h - ov);
3980
4226
  }
3981
4227
  disposeElements() {
4228
+ this.balancer?.reset();
3982
4229
  for (const realWord of this.splittedWords) {
3983
4230
  for (const a of realWord.elementAnimations) a.cancel();
3984
4231
  for (const a of realWord.maskAnimations) a.cancel();