@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.
- package/dist/amll-core.cjs +313 -66
- package/dist/amll-core.cjs.map +1 -1
- package/dist/amll-core.d.cts +13 -1
- package/dist/amll-core.d.mts +13 -1
- package/dist/amll-core.mjs +311 -64
- package/dist/amll-core.mjs.map +1 -1
- package/dist/style.css +0 -3
- package/package.json +3 -3
package/dist/amll-core.d.cts
CHANGED
|
@@ -722,6 +722,15 @@ declare abstract class LyricLineBase extends EventTarget implements Disposable {
|
|
|
722
722
|
protected opacity: number;
|
|
723
723
|
protected delay: number;
|
|
724
724
|
readonly lineTransforms: LineTransforms;
|
|
725
|
+
/**
|
|
726
|
+
* 用于 CJK 词语边界检测的分词器
|
|
727
|
+
*/
|
|
728
|
+
static readonly wordSegmenter: Intl.Segmenter | null;
|
|
729
|
+
/**
|
|
730
|
+
* Unicode 标准的全局 Grapheme Cluster 分词器
|
|
731
|
+
* 用于正确处理 emoji、复合字符等
|
|
732
|
+
*/
|
|
733
|
+
static readonly graphemeSegmenter: Intl.Segmenter | null;
|
|
725
734
|
abstract getLine(): LyricLine;
|
|
726
735
|
abstract enable(time?: number, shouldPlay?: boolean): void;
|
|
727
736
|
abstract disable(): void;
|
|
@@ -775,7 +784,10 @@ declare class LyricLineEl$1 extends LyricLineBase {
|
|
|
775
784
|
private currentDarkAlpha;
|
|
776
785
|
private targetBrightAlpha;
|
|
777
786
|
private targetDarkAlpha;
|
|
778
|
-
|
|
787
|
+
/**
|
|
788
|
+
* 用于平衡换行、尽量减少各行长度差异的类
|
|
789
|
+
*/
|
|
790
|
+
private balancer?;
|
|
779
791
|
constructor(lyricPlayer: DomLyricPlayer, lyricLine?: LyricLine);
|
|
780
792
|
private listenersMap;
|
|
781
793
|
private readonly onMouseEvent;
|
package/dist/amll-core.d.mts
CHANGED
|
@@ -722,6 +722,15 @@ declare abstract class LyricLineBase extends EventTarget implements Disposable {
|
|
|
722
722
|
protected opacity: number;
|
|
723
723
|
protected delay: number;
|
|
724
724
|
readonly lineTransforms: LineTransforms;
|
|
725
|
+
/**
|
|
726
|
+
* 用于 CJK 词语边界检测的分词器
|
|
727
|
+
*/
|
|
728
|
+
static readonly wordSegmenter: Intl.Segmenter | null;
|
|
729
|
+
/**
|
|
730
|
+
* Unicode 标准的全局 Grapheme Cluster 分词器
|
|
731
|
+
* 用于正确处理 emoji、复合字符等
|
|
732
|
+
*/
|
|
733
|
+
static readonly graphemeSegmenter: Intl.Segmenter | null;
|
|
725
734
|
abstract getLine(): LyricLine;
|
|
726
735
|
abstract enable(time?: number, shouldPlay?: boolean): void;
|
|
727
736
|
abstract disable(): void;
|
|
@@ -775,7 +784,10 @@ declare class LyricLineEl$1 extends LyricLineBase {
|
|
|
775
784
|
private currentDarkAlpha;
|
|
776
785
|
private targetBrightAlpha;
|
|
777
786
|
private targetDarkAlpha;
|
|
778
|
-
|
|
787
|
+
/**
|
|
788
|
+
* 用于平衡换行、尽量减少各行长度差异的类
|
|
789
|
+
*/
|
|
790
|
+
private balancer?;
|
|
779
791
|
constructor(lyricPlayer: DomLyricPlayer, lyricLine?: LyricLine);
|
|
780
792
|
private listenersMap;
|
|
781
793
|
private readonly onMouseEvent;
|
package/dist/amll-core.mjs
CHANGED
|
@@ -2249,7 +2249,7 @@ var LyricPlayerBase = class extends EventTarget {
|
|
|
2249
2249
|
bottomLine = new BottomLineEl(this);
|
|
2250
2250
|
enableBlur = true;
|
|
2251
2251
|
enableScale = true;
|
|
2252
|
-
maskObsceneWords =
|
|
2252
|
+
maskObsceneWords = "";
|
|
2253
2253
|
maskObsceneWordChar = "*";
|
|
2254
2254
|
hidePassedLines = false;
|
|
2255
2255
|
scrollBoundary = [0, 0];
|
|
@@ -2539,7 +2539,7 @@ var LyricPlayerBase = class extends EventTarget {
|
|
|
2539
2539
|
const c = char.charAt(0) || "*";
|
|
2540
2540
|
if (this.maskObsceneWordChar === c) return;
|
|
2541
2541
|
this.maskObsceneWordChar = c;
|
|
2542
|
-
if (this.maskObsceneWords !==
|
|
2542
|
+
if (this.maskObsceneWords !== "") {
|
|
2543
2543
|
this.rebuildLyricLines();
|
|
2544
2544
|
this.calcLayout();
|
|
2545
2545
|
}
|
|
@@ -2554,10 +2554,10 @@ var LyricPlayerBase = class extends EventTarget {
|
|
|
2554
2554
|
*/
|
|
2555
2555
|
processObsceneWord(word) {
|
|
2556
2556
|
const text = word.word;
|
|
2557
|
-
if (!word.obscene || this.maskObsceneWords ===
|
|
2557
|
+
if (!word.obscene || this.maskObsceneWords === "") return text;
|
|
2558
2558
|
const maskChar = this.maskObsceneWordChar;
|
|
2559
|
-
if (this.maskObsceneWords ===
|
|
2560
|
-
if (this.maskObsceneWords ===
|
|
2559
|
+
if (this.maskObsceneWords === "full-mask") return text.replace(/\S/g, maskChar);
|
|
2560
|
+
if (this.maskObsceneWords === "partial-mask") {
|
|
2561
2561
|
const trimmed = text.trim();
|
|
2562
2562
|
if (trimmed.length <= 2) return text.replace(/\S/g, maskChar);
|
|
2563
2563
|
const startPos = text.indexOf(trimmed);
|
|
@@ -2918,7 +2918,7 @@ var LyricPlayerBase = class extends EventTarget {
|
|
|
2918
2918
|
let targetScale = 100;
|
|
2919
2919
|
if (!isActive && this.isPlaying) if (line.isBG) targetScale = 75;
|
|
2920
2920
|
else targetScale = SCALE_ASPECT;
|
|
2921
|
-
const renderMode = isActive ?
|
|
2921
|
+
const renderMode = isActive ? 1 : 0;
|
|
2922
2922
|
lineObj.setTransform(curPos, targetScale, targetOpacity, blurLevel, force, delay, renderMode);
|
|
2923
2923
|
if (line.isBG && (isActive || !this.isPlaying)) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
|
|
2924
2924
|
else if (!line.isBG) curPos += this.lyricLinesSize.get(lineObj)?.[1] ?? LINE_HEIGHT_FALLBACK;
|
|
@@ -3071,8 +3071,17 @@ var LyricLineBase = class extends EventTarget {
|
|
|
3071
3071
|
posY: new Spring(0),
|
|
3072
3072
|
scale: new Spring(100)
|
|
3073
3073
|
};
|
|
3074
|
+
/**
|
|
3075
|
+
* 用于 CJK 词语边界检测的分词器
|
|
3076
|
+
*/
|
|
3077
|
+
static wordSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
|
|
3078
|
+
/**
|
|
3079
|
+
* Unicode 标准的全局 Grapheme Cluster 分词器
|
|
3080
|
+
* 用于正确处理 emoji、复合字符等
|
|
3081
|
+
*/
|
|
3082
|
+
static graphemeSegmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
3074
3083
|
onLineSizeChange(_size) {}
|
|
3075
|
-
setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode =
|
|
3084
|
+
setTransform(top = this.top, scale = this.scale, opacity = this.opacity, blur = this.blur, _force = false, delay = 0, _mode = 0) {
|
|
3076
3085
|
this.top = top;
|
|
3077
3086
|
this.scale = scale;
|
|
3078
3087
|
this.opacity = opacity;
|
|
@@ -3097,8 +3106,251 @@ var LyricLineBase = class extends EventTarget {
|
|
|
3097
3106
|
dispose() {}
|
|
3098
3107
|
};
|
|
3099
3108
|
//#endregion
|
|
3109
|
+
//#region src/utils/lyric-line-break.ts
|
|
3110
|
+
/**
|
|
3111
|
+
* 单个词超过容器宽度时的大惩罚倍数
|
|
3112
|
+
*/
|
|
3113
|
+
const OVERFLOW_PENALTY_MULTIPLIER = 1e3;
|
|
3114
|
+
/**
|
|
3115
|
+
* 截断 CJK 词组边界的惩罚比例
|
|
3116
|
+
*
|
|
3117
|
+
* 相对于容器宽度
|
|
3118
|
+
*/
|
|
3119
|
+
const CJK_BREAK_PENALTY_RATIO = .15;
|
|
3120
|
+
/**
|
|
3121
|
+
* 截断普通文本(非空格、非 CJK 词界)的惩罚比例
|
|
3122
|
+
*/
|
|
3123
|
+
const NORMAL_BREAK_PENALTY_RATIO = .5;
|
|
3124
|
+
/**
|
|
3125
|
+
* 在空格处断开的奖励比例
|
|
3126
|
+
*/
|
|
3127
|
+
const SPACE_BREAK_REWARD_RATIO = .4;
|
|
3128
|
+
/**
|
|
3129
|
+
* 计算平均行长度的断点位置
|
|
3130
|
+
* @param children 子节点信息
|
|
3131
|
+
* @param containerWidth 容器可用内容宽度
|
|
3132
|
+
* @param fullText 完整的行文本
|
|
3133
|
+
* @param segmenter 预创建的 Intl.Segmenter 分词器
|
|
3134
|
+
* @returns 需要在其前面插入 `<br>` 的子节点索引数组,升序
|
|
3135
|
+
*/
|
|
3136
|
+
function calcBalancedBreaks(children, containerWidth, fullText, segmenter) {
|
|
3137
|
+
const n = children.length;
|
|
3138
|
+
if (n === 0 || containerWidth <= 0) return [];
|
|
3139
|
+
const cjkBoundaries = /* @__PURE__ */ new Set();
|
|
3140
|
+
let offset = 0;
|
|
3141
|
+
for (const { segment, isWordLike } of segmenter.segment(fullText)) {
|
|
3142
|
+
if (offset > 0 && isWordLike) {
|
|
3143
|
+
if ([...segment].some((ch) => isCJK(ch))) cjkBoundaries.add(offset);
|
|
3144
|
+
}
|
|
3145
|
+
offset += segment.length;
|
|
3146
|
+
}
|
|
3147
|
+
const charOffsets = new Int32Array(n + 1);
|
|
3148
|
+
const prefixWidth = new Float64Array(n + 1);
|
|
3149
|
+
for (let i = 0; i < n; i++) {
|
|
3150
|
+
charOffsets[i + 1] = charOffsets[i] + children[i].text.length;
|
|
3151
|
+
prefixWidth[i + 1] = prefixWidth[i] + children[i].width;
|
|
3152
|
+
}
|
|
3153
|
+
if (prefixWidth[n] <= containerWidth) return [];
|
|
3154
|
+
/**
|
|
3155
|
+
* dp[i] 表示将 index i 到 n-1 的节点进行排版的最小代价
|
|
3156
|
+
*/
|
|
3157
|
+
const dp = new Float64Array(n + 1).fill(Number.POSITIVE_INFINITY);
|
|
3158
|
+
const nextBreak = new Int32Array(n + 1).fill(-1);
|
|
3159
|
+
dp[n] = 0;
|
|
3160
|
+
const PENALTY_CJK = (containerWidth * CJK_BREAK_PENALTY_RATIO) ** 2;
|
|
3161
|
+
const PENALTY_NORMAL = (containerWidth * NORMAL_BREAK_PENALTY_RATIO) ** 2;
|
|
3162
|
+
for (let i = n - 1; i >= 0; i--) for (let j = i + 1; j <= n; j++) {
|
|
3163
|
+
const w = prefixWidth[j] - prefixWidth[i];
|
|
3164
|
+
let lineCost = 0;
|
|
3165
|
+
if (w > containerWidth) if (j === i + 1) lineCost = (w - containerWidth) ** 2 * OVERFLOW_PENALTY_MULTIPLIER;
|
|
3166
|
+
else continue;
|
|
3167
|
+
else lineCost = (containerWidth - w) ** 2;
|
|
3168
|
+
let breakPenalty = 0;
|
|
3169
|
+
if (j < n) if (children[j - 1].isSpace) breakPenalty = -((containerWidth * SPACE_BREAK_REWARD_RATIO) ** 2);
|
|
3170
|
+
else if (cjkBoundaries.has(charOffsets[j])) breakPenalty = PENALTY_CJK;
|
|
3171
|
+
else breakPenalty = PENALTY_NORMAL;
|
|
3172
|
+
const totalCost = lineCost + breakPenalty + dp[j];
|
|
3173
|
+
if (totalCost < dp[i]) {
|
|
3174
|
+
dp[i] = totalCost;
|
|
3175
|
+
nextBreak[i] = j;
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
const breaks = [];
|
|
3179
|
+
let curr = 0;
|
|
3180
|
+
while (curr < n) {
|
|
3181
|
+
curr = nextBreak[curr];
|
|
3182
|
+
if (curr > 0 && curr < n) breaks.push(curr);
|
|
3183
|
+
}
|
|
3184
|
+
return breaks;
|
|
3185
|
+
}
|
|
3186
|
+
//#endregion
|
|
3187
|
+
//#region src/utils/line-balancer.ts
|
|
3188
|
+
let sharedCanvasCtx = null;
|
|
3189
|
+
function getMeasurementContext() {
|
|
3190
|
+
if (!sharedCanvasCtx) sharedCanvasCtx = document.createElement("canvas").getContext("2d");
|
|
3191
|
+
return sharedCanvasCtx;
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* 用于平衡歌词行在换行后的各行长度
|
|
3195
|
+
*/
|
|
3196
|
+
var LineBalancer = class LineBalancer {
|
|
3197
|
+
isBalancing = false;
|
|
3198
|
+
lastBalancedContainerWidth = -1;
|
|
3199
|
+
/**
|
|
3200
|
+
* 防止误差导致的意外换行
|
|
3201
|
+
*/
|
|
3202
|
+
static SAFE_WIDTH_PADDING = 25;
|
|
3203
|
+
constructor(mainElement) {
|
|
3204
|
+
this.mainElement = mainElement;
|
|
3205
|
+
}
|
|
3206
|
+
balanceLineBreaks(isNonDynamic, hasSplittedWords, wordSegmenter) {
|
|
3207
|
+
if (this.isBalancing || !this.mainElement) return;
|
|
3208
|
+
const computedStyle = getComputedStyle(this.mainElement);
|
|
3209
|
+
const paddingLeft = Number.parseFloat(computedStyle.paddingLeft) || 0;
|
|
3210
|
+
const paddingRight = Number.parseFloat(computedStyle.paddingRight) || 0;
|
|
3211
|
+
const containerWidth = this.mainElement.clientWidth - paddingLeft - paddingRight;
|
|
3212
|
+
if (containerWidth <= 0) return;
|
|
3213
|
+
if (isNonDynamic) {
|
|
3214
|
+
this.balanceNonDynamicLineBreaks(containerWidth, computedStyle, wordSegmenter);
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
if (!hasSplittedWords) return;
|
|
3218
|
+
this.balanceDynamicLineBreaks(containerWidth, wordSegmenter);
|
|
3219
|
+
}
|
|
3220
|
+
reset() {
|
|
3221
|
+
this.lastBalancedContainerWidth = -1;
|
|
3222
|
+
}
|
|
3223
|
+
executeLineBalance(containerWidth, adapter, wordSegmenter) {
|
|
3224
|
+
const existingBrs = this.mainElement.querySelectorAll("br");
|
|
3225
|
+
if (containerWidth === this.lastBalancedContainerWidth && existingBrs.length > 0) return;
|
|
3226
|
+
adapter.resetDOM();
|
|
3227
|
+
const prevWhiteSpace = this.mainElement.style.whiteSpace;
|
|
3228
|
+
this.mainElement.style.whiteSpace = "nowrap";
|
|
3229
|
+
try {
|
|
3230
|
+
const range = document.createRange();
|
|
3231
|
+
range.selectNodeContents(this.mainElement);
|
|
3232
|
+
const lineWidth = range.getBoundingClientRect().width;
|
|
3233
|
+
const safeContainerWidth = Math.max(1, containerWidth - LineBalancer.SAFE_WIDTH_PADDING);
|
|
3234
|
+
if (lineWidth <= safeContainerWidth) {
|
|
3235
|
+
this.lastBalancedContainerWidth = containerWidth;
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
const { childInfos, fullText } = adapter.buildChildInfos();
|
|
3239
|
+
const measuredTotal = childInfos.reduce((sum, c) => sum + c.width, 0);
|
|
3240
|
+
if (measuredTotal > 0 && lineWidth > 0) {
|
|
3241
|
+
const scale = lineWidth / measuredTotal;
|
|
3242
|
+
for (const info of childInfos) info.width *= scale;
|
|
3243
|
+
}
|
|
3244
|
+
const breaks = calcBalancedBreaks(childInfos, safeContainerWidth, fullText, wordSegmenter);
|
|
3245
|
+
if (breaks.length === 0) {
|
|
3246
|
+
this.lastBalancedContainerWidth = containerWidth;
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
this.isBalancing = true;
|
|
3250
|
+
adapter.applyBreaks(breaks, childInfos);
|
|
3251
|
+
this.lastBalancedContainerWidth = containerWidth;
|
|
3252
|
+
this.isBalancing = false;
|
|
3253
|
+
} finally {
|
|
3254
|
+
this.mainElement.style.whiteSpace = prevWhiteSpace;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
balanceDynamicLineBreaks(containerWidth, wordSegmenter) {
|
|
3258
|
+
const infoToNode = [];
|
|
3259
|
+
this.executeLineBalance(containerWidth, {
|
|
3260
|
+
resetDOM: () => {
|
|
3261
|
+
this.mainElement.querySelectorAll("br").forEach((br) => {
|
|
3262
|
+
br.remove();
|
|
3263
|
+
});
|
|
3264
|
+
},
|
|
3265
|
+
buildChildInfos: () => {
|
|
3266
|
+
infoToNode.length = 0;
|
|
3267
|
+
const childNodes = Array.from(this.mainElement.childNodes);
|
|
3268
|
+
const childInfos = [];
|
|
3269
|
+
const range = document.createRange();
|
|
3270
|
+
for (const node of childNodes) if (node.nodeType === Node.TEXT_NODE) {
|
|
3271
|
+
const text = node.textContent ?? "";
|
|
3272
|
+
if (text.length === 0) continue;
|
|
3273
|
+
range.selectNodeContents(node);
|
|
3274
|
+
childInfos.push({
|
|
3275
|
+
width: range.getBoundingClientRect().width,
|
|
3276
|
+
text,
|
|
3277
|
+
isSpace: text.trim().length === 0
|
|
3278
|
+
});
|
|
3279
|
+
infoToNode.push(node);
|
|
3280
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
3281
|
+
const el = node;
|
|
3282
|
+
const rect = el.getBoundingClientRect();
|
|
3283
|
+
const elStyle = getComputedStyle(el);
|
|
3284
|
+
const marginLeft = Number.parseFloat(elStyle.marginLeft) || 0;
|
|
3285
|
+
const marginRight = Number.parseFloat(elStyle.marginRight) || 0;
|
|
3286
|
+
childInfos.push({
|
|
3287
|
+
width: Math.max(0, rect.width + marginLeft + marginRight),
|
|
3288
|
+
text: el.textContent ?? "",
|
|
3289
|
+
isSpace: false
|
|
3290
|
+
});
|
|
3291
|
+
infoToNode.push(node);
|
|
3292
|
+
}
|
|
3293
|
+
return {
|
|
3294
|
+
childInfos,
|
|
3295
|
+
fullText: childInfos.map((c) => c.text).join("")
|
|
3296
|
+
};
|
|
3297
|
+
},
|
|
3298
|
+
applyBreaks: (breaks) => {
|
|
3299
|
+
for (let i = breaks.length - 1; i >= 0; i--) {
|
|
3300
|
+
const breakIndex = breaks[i];
|
|
3301
|
+
if (breakIndex >= 0 && breakIndex < infoToNode.length) this.mainElement.insertBefore(document.createElement("br"), infoToNode[breakIndex]);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}, wordSegmenter);
|
|
3305
|
+
}
|
|
3306
|
+
balanceNonDynamicLineBreaks(containerWidth, computedStyle, wordSegmenter) {
|
|
3307
|
+
const fullText = this.mainElement.textContent ?? "";
|
|
3308
|
+
if (fullText.trim().length === 0) return;
|
|
3309
|
+
this.executeLineBalance(containerWidth, {
|
|
3310
|
+
resetDOM: () => {
|
|
3311
|
+
this.mainElement.innerHTML = "";
|
|
3312
|
+
this.mainElement.textContent = fullText;
|
|
3313
|
+
},
|
|
3314
|
+
buildChildInfos: () => {
|
|
3315
|
+
const ctx = getMeasurementContext();
|
|
3316
|
+
if (!ctx) {
|
|
3317
|
+
console.debug("Canvas 2D context is not supported, skipping line balancing");
|
|
3318
|
+
return {
|
|
3319
|
+
childInfos: [],
|
|
3320
|
+
fullText
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
ctx.font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
|
|
3324
|
+
if ("letterSpacing" in ctx) ctx.letterSpacing = computedStyle.letterSpacing !== "normal" ? computedStyle.letterSpacing : "0px";
|
|
3325
|
+
if ("wordSpacing" in ctx) ctx.wordSpacing = computedStyle.wordSpacing !== "normal" ? computedStyle.wordSpacing : "0px";
|
|
3326
|
+
const childInfos = [];
|
|
3327
|
+
for (const { segment } of wordSegmenter.segment(fullText)) childInfos.push({
|
|
3328
|
+
width: ctx.measureText(segment).width,
|
|
3329
|
+
text: segment,
|
|
3330
|
+
isSpace: segment.trim().length === 0
|
|
3331
|
+
});
|
|
3332
|
+
return {
|
|
3333
|
+
childInfos,
|
|
3334
|
+
fullText
|
|
3335
|
+
};
|
|
3336
|
+
},
|
|
3337
|
+
applyBreaks: (breaks, childInfos) => {
|
|
3338
|
+
this.mainElement.innerHTML = "";
|
|
3339
|
+
const breakSet = new Set(breaks);
|
|
3340
|
+
const fragment = document.createDocumentFragment();
|
|
3341
|
+
for (let i = 0; i < childInfos.length; i++) {
|
|
3342
|
+
if (breakSet.has(i)) fragment.appendChild(document.createElement("br"));
|
|
3343
|
+
fragment.appendChild(document.createTextNode(childInfos[i].text));
|
|
3344
|
+
}
|
|
3345
|
+
this.mainElement.appendChild(fragment);
|
|
3346
|
+
}
|
|
3347
|
+
}, wordSegmenter);
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
//#endregion
|
|
3100
3351
|
//#region src/utils/lyric-split-words.ts
|
|
3101
|
-
const
|
|
3352
|
+
const SPLIT_WHITESPACE_RE = /(\s+)/;
|
|
3353
|
+
const WHITESPACE_RE = /\s/g;
|
|
3102
3354
|
/**
|
|
3103
3355
|
* 将输入的单词重新分组,之间没有空格的单词将会组合成一个单词数组
|
|
3104
3356
|
*
|
|
@@ -3109,27 +3361,41 @@ const hasSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter !== "u
|
|
|
3109
3361
|
* @returns 重新分组后的单词数组
|
|
3110
3362
|
*/
|
|
3111
3363
|
function chunkAndSplitLyricWords(words) {
|
|
3112
|
-
const
|
|
3364
|
+
const result = [];
|
|
3365
|
+
let currentGroup = [];
|
|
3366
|
+
const flushGroup = () => {
|
|
3367
|
+
if (currentGroup.length > 0) {
|
|
3368
|
+
result.push(currentGroup.length === 1 ? currentGroup[0] : [...currentGroup]);
|
|
3369
|
+
currentGroup = [];
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
const processAtom = (atom) => {
|
|
3373
|
+
const isSpace = atom.word.trim().length === 0;
|
|
3374
|
+
const hasRuby = (atom.ruby?.length ?? 0) > 0;
|
|
3375
|
+
const isCJKChar = isCJK(atom.word);
|
|
3376
|
+
if (!isSpace && !hasRuby && !isCJKChar) currentGroup.push(atom);
|
|
3377
|
+
else {
|
|
3378
|
+
flushGroup();
|
|
3379
|
+
result.push(atom);
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3113
3382
|
for (const w of words) {
|
|
3114
3383
|
const isSpace = w.word.trim().length === 0;
|
|
3115
3384
|
const romanWord = w.romanWord ?? "";
|
|
3116
3385
|
const obscene = w.obscene ?? false;
|
|
3117
3386
|
const hasRuby = (w.ruby?.length ?? 0) > 0;
|
|
3118
|
-
if (isSpace) {
|
|
3119
|
-
|
|
3120
|
-
continue;
|
|
3121
|
-
}
|
|
3122
|
-
if (hasRuby) {
|
|
3123
|
-
atoms.push({ ...w });
|
|
3387
|
+
if (isSpace || hasRuby) {
|
|
3388
|
+
processAtom({ ...w });
|
|
3124
3389
|
continue;
|
|
3125
3390
|
}
|
|
3126
|
-
const parts = w.word.split(
|
|
3391
|
+
const parts = w.word.split(SPLIT_WHITESPACE_RE).filter((p) => p.length > 0);
|
|
3392
|
+
const totalLength = w.word.replace(WHITESPACE_RE, "").length || 1;
|
|
3393
|
+
const timePerUnit = (w.endTime - w.startTime) / totalLength;
|
|
3127
3394
|
let currentOffset = 0;
|
|
3128
|
-
const totalLength = w.word.replace(/\s/g, "").length || 1;
|
|
3129
3395
|
for (const part of parts) {
|
|
3130
3396
|
if (!part.trim()) {
|
|
3131
|
-
const startTime = w.startTime + currentOffset
|
|
3132
|
-
|
|
3397
|
+
const startTime = w.startTime + currentOffset * timePerUnit;
|
|
3398
|
+
processAtom({
|
|
3133
3399
|
word: part,
|
|
3134
3400
|
romanWord: "",
|
|
3135
3401
|
startTime,
|
|
@@ -3141,63 +3407,31 @@ function chunkAndSplitLyricWords(words) {
|
|
|
3141
3407
|
if (isCJK(part) && part.length > 1 && romanWord.trim().length === 0) {
|
|
3142
3408
|
const chars = part.split("");
|
|
3143
3409
|
for (const char of chars) {
|
|
3144
|
-
const
|
|
3145
|
-
|
|
3146
|
-
atoms.push({
|
|
3410
|
+
const startTime = w.startTime + currentOffset * timePerUnit;
|
|
3411
|
+
processAtom({
|
|
3147
3412
|
word: char,
|
|
3148
3413
|
romanWord: "",
|
|
3149
3414
|
startTime,
|
|
3150
|
-
endTime: startTime +
|
|
3415
|
+
endTime: startTime + timePerUnit,
|
|
3151
3416
|
obscene
|
|
3152
3417
|
});
|
|
3153
3418
|
currentOffset += 1;
|
|
3154
3419
|
}
|
|
3155
3420
|
} else {
|
|
3156
3421
|
const partRealLen = part.length;
|
|
3157
|
-
const
|
|
3158
|
-
|
|
3159
|
-
atoms.push({
|
|
3422
|
+
const startTime = w.startTime + currentOffset * timePerUnit;
|
|
3423
|
+
processAtom({
|
|
3160
3424
|
word: part,
|
|
3161
3425
|
romanWord,
|
|
3162
3426
|
startTime,
|
|
3163
|
-
endTime: startTime +
|
|
3427
|
+
endTime: startTime + partRealLen * timePerUnit,
|
|
3164
3428
|
obscene
|
|
3165
3429
|
});
|
|
3166
3430
|
currentOffset += partRealLen;
|
|
3167
3431
|
}
|
|
3168
3432
|
}
|
|
3169
3433
|
}
|
|
3170
|
-
|
|
3171
|
-
const fullText = atoms.map((a) => a.word).join("");
|
|
3172
|
-
const segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
|
|
3173
|
-
const segments = Array.from(segmenter.segment(fullText));
|
|
3174
|
-
const result = [];
|
|
3175
|
-
let atomIndex = 0;
|
|
3176
|
-
let expectedLength = 0;
|
|
3177
|
-
let actualLength = 0;
|
|
3178
|
-
let currentGroup = [];
|
|
3179
|
-
for (const segment of segments) {
|
|
3180
|
-
const segmentLen = segment.segment.length;
|
|
3181
|
-
expectedLength += segmentLen;
|
|
3182
|
-
while (actualLength < expectedLength && atomIndex < atoms.length) {
|
|
3183
|
-
const currentAtom = atoms[atomIndex];
|
|
3184
|
-
currentGroup.push(currentAtom);
|
|
3185
|
-
actualLength += currentAtom.word.length;
|
|
3186
|
-
atomIndex++;
|
|
3187
|
-
}
|
|
3188
|
-
if (actualLength === expectedLength) {
|
|
3189
|
-
while (currentGroup.length > 1 && !currentGroup[0].word.trim()) {
|
|
3190
|
-
const spaceAtom = currentGroup.shift();
|
|
3191
|
-
if (spaceAtom) result.push(spaceAtom);
|
|
3192
|
-
}
|
|
3193
|
-
if (currentGroup.length === 1) result.push(currentGroup[0]);
|
|
3194
|
-
else if (currentGroup.length > 1) result.push(currentGroup);
|
|
3195
|
-
currentGroup = [];
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
while (atomIndex < atoms.length) result.push(atoms[atomIndex++]);
|
|
3199
|
-
if (currentGroup.length > 0) if (currentGroup.length === 1) result.push(currentGroup[0]);
|
|
3200
|
-
else result.push(currentGroup);
|
|
3434
|
+
flushGroup();
|
|
3201
3435
|
return result;
|
|
3202
3436
|
}
|
|
3203
3437
|
//#endregion
|
|
@@ -3279,12 +3513,15 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3279
3513
|
splittedWords = [];
|
|
3280
3514
|
built = false;
|
|
3281
3515
|
lineSize = [0, 0];
|
|
3282
|
-
renderMode =
|
|
3516
|
+
renderMode = 0;
|
|
3283
3517
|
currentBrightAlpha = 1;
|
|
3284
3518
|
currentDarkAlpha = .2;
|
|
3285
3519
|
targetBrightAlpha = 1;
|
|
3286
3520
|
targetDarkAlpha = .2;
|
|
3287
|
-
|
|
3521
|
+
/**
|
|
3522
|
+
* 用于平衡换行、尽量减少各行长度差异的类
|
|
3523
|
+
*/
|
|
3524
|
+
balancer;
|
|
3288
3525
|
constructor(lyricPlayer, lyricLine = {
|
|
3289
3526
|
words: [],
|
|
3290
3527
|
translatedLyric: "",
|
|
@@ -3312,6 +3549,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3312
3549
|
main.setAttribute("class", lyric_player_module_default.lyricMainLine);
|
|
3313
3550
|
trans.setAttribute("class", lyric_player_module_default.lyricSubLine);
|
|
3314
3551
|
roman.setAttribute("class", lyric_player_module_default.lyricSubLine);
|
|
3552
|
+
if (LyricLineBase.wordSegmenter) this.balancer = new LineBalancer(main);
|
|
3315
3553
|
this.rebuildStyle();
|
|
3316
3554
|
}
|
|
3317
3555
|
listenersMap = /* @__PURE__ */ new Map();
|
|
@@ -3385,7 +3623,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3385
3623
|
disable() {
|
|
3386
3624
|
this.isEnabled = false;
|
|
3387
3625
|
this.element.classList.remove(lyric_player_module_default.active);
|
|
3388
|
-
this.renderMode =
|
|
3626
|
+
this.renderMode = 0;
|
|
3389
3627
|
const main = this.element.children[0];
|
|
3390
3628
|
for (const word of this.splittedWords) {
|
|
3391
3629
|
for (const a of word.elementAnimations) if (a.id === "float-word" || a.id.includes("emphasize-word-float-only")) {
|
|
@@ -3521,7 +3759,14 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3521
3759
|
const displayWord = this.lyricPlayer.processObsceneWord(word);
|
|
3522
3760
|
if (shouldEmphasize) {
|
|
3523
3761
|
mainWordEl.classList.add(lyric_player_module_default.emphasize);
|
|
3524
|
-
|
|
3762
|
+
const trimmedWord = displayWord.trim();
|
|
3763
|
+
if (LyricLineBase.graphemeSegmenter) for (const { segment } of LyricLineBase.graphemeSegmenter.segment(trimmedWord)) {
|
|
3764
|
+
const charEl = document.createElement("span");
|
|
3765
|
+
charEl.innerText = segment;
|
|
3766
|
+
subElements.push(charEl);
|
|
3767
|
+
wordContainer.appendChild(charEl);
|
|
3768
|
+
}
|
|
3769
|
+
else for (const segment of Array.from(trimmedWord)) {
|
|
3525
3770
|
const charEl = document.createElement("span");
|
|
3526
3771
|
charEl.innerText = segment;
|
|
3527
3772
|
subElements.push(charEl);
|
|
@@ -3703,6 +3948,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3703
3948
|
word.padding = 0;
|
|
3704
3949
|
}
|
|
3705
3950
|
}
|
|
3951
|
+
if (this.balancer && LyricLineBase.wordSegmenter) this.balancer.balanceLineBreaks(this.lyricPlayer._getIsNonDynamic(), this.splittedWords.length > 0, LyricLineBase.wordSegmenter);
|
|
3706
3952
|
if (this.lyricPlayer.supportMaskImage) this.generateWebAnimationBasedMaskImage();
|
|
3707
3953
|
else this.generateCalcBasedMaskImage();
|
|
3708
3954
|
if (this.isEnabled) {
|
|
@@ -3875,7 +4121,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3875
4121
|
const factor = Math.max(0, Math.min(1, (scale - .97) / .03));
|
|
3876
4122
|
const dynamicDarkAlpha = factor * .2 + .2;
|
|
3877
4123
|
const dynamicBrightAlpha = factor * .8 + .2;
|
|
3878
|
-
if (this.renderMode ===
|
|
4124
|
+
if (this.renderMode === 0) {
|
|
3879
4125
|
this.targetBrightAlpha = dynamicDarkAlpha;
|
|
3880
4126
|
this.targetDarkAlpha = dynamicDarkAlpha;
|
|
3881
4127
|
} else {
|
|
@@ -3897,7 +4143,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3897
4143
|
this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
|
|
3898
4144
|
this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
|
|
3899
4145
|
}
|
|
3900
|
-
setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode =
|
|
4146
|
+
setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = 0) {
|
|
3901
4147
|
super.setTransform(top, scale, opacity, blur, force, delay);
|
|
3902
4148
|
this.renderMode = mode;
|
|
3903
4149
|
const beforeInSight = this.isInSight;
|
|
@@ -3954,6 +4200,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3954
4200
|
return !(t > pb + h + ov || b < -h - ov);
|
|
3955
4201
|
}
|
|
3956
4202
|
disposeElements() {
|
|
4203
|
+
this.balancer?.reset();
|
|
3957
4204
|
for (const realWord of this.splittedWords) {
|
|
3958
4205
|
for (const a of realWord.elementAnimations) a.cancel();
|
|
3959
4206
|
for (const a of realWord.maskAnimations) a.cancel();
|