@applemusic-like-lyrics/core 0.4.0 → 0.4.2
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 +339 -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 +337 -64
- package/dist/amll-core.mjs.map +1 -1
- package/dist/style.css +6 -5
- 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,277 @@ 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
|
+
*
|
|
3131
|
+
* 比空格更高以便优先一点在标点处换行
|
|
3132
|
+
*/
|
|
3133
|
+
const PUNCTUATION_BREAK_REWARD_RATIO = .6;
|
|
3134
|
+
const PUNCTUATION_REGEX = /[,.;:!?,。;:!?、)】》」』’”)[\]}>~…]$/;
|
|
3135
|
+
/**
|
|
3136
|
+
* 计算平均行长度的断点位置
|
|
3137
|
+
* @param children 子节点信息
|
|
3138
|
+
* @param containerWidth 容器可用内容宽度
|
|
3139
|
+
* @param fullText 完整的行文本
|
|
3140
|
+
* @param segmenter 预创建的 Intl.Segmenter 分词器
|
|
3141
|
+
* @returns 需要在其前面插入 `<br>` 的子节点索引数组,升序
|
|
3142
|
+
*/
|
|
3143
|
+
function calcBalancedBreaks(children, containerWidth, fullText, segmenter) {
|
|
3144
|
+
const n = children.length;
|
|
3145
|
+
if (n === 0 || containerWidth <= 0) return [];
|
|
3146
|
+
const cjkBoundaries = /* @__PURE__ */ new Set();
|
|
3147
|
+
let offset = 0;
|
|
3148
|
+
for (const { segment, isWordLike } of segmenter.segment(fullText)) {
|
|
3149
|
+
if (offset > 0 && isWordLike) {
|
|
3150
|
+
if ([...segment].some((ch) => isCJK(ch))) cjkBoundaries.add(offset);
|
|
3151
|
+
}
|
|
3152
|
+
offset += segment.length;
|
|
3153
|
+
}
|
|
3154
|
+
const charOffsets = new Int32Array(n + 1);
|
|
3155
|
+
const prefixWidth = new Float64Array(n + 1);
|
|
3156
|
+
for (let i = 0; i < n; i++) {
|
|
3157
|
+
charOffsets[i + 1] = charOffsets[i] + children[i].text.length;
|
|
3158
|
+
prefixWidth[i + 1] = prefixWidth[i] + children[i].width;
|
|
3159
|
+
}
|
|
3160
|
+
if (prefixWidth[n] <= containerWidth) return [];
|
|
3161
|
+
/**
|
|
3162
|
+
* dp[i] 表示将 index i 到 n-1 的节点进行排版的最小代价
|
|
3163
|
+
*/
|
|
3164
|
+
const dp = new Float64Array(n + 1).fill(Number.POSITIVE_INFINITY);
|
|
3165
|
+
const nextBreak = new Int32Array(n + 1).fill(-1);
|
|
3166
|
+
dp[n] = 0;
|
|
3167
|
+
const PENALTY_CJK = (containerWidth * CJK_BREAK_PENALTY_RATIO) ** 2;
|
|
3168
|
+
const PENALTY_NORMAL = (containerWidth * NORMAL_BREAK_PENALTY_RATIO) ** 2;
|
|
3169
|
+
for (let i = n - 1; i >= 0; i--) for (let j = i + 1; j <= n; j++) {
|
|
3170
|
+
const w = prefixWidth[j] - prefixWidth[i];
|
|
3171
|
+
let lineCost = 0;
|
|
3172
|
+
if (w > containerWidth) if (j === i + 1) lineCost = (w - containerWidth) ** 2 * OVERFLOW_PENALTY_MULTIPLIER;
|
|
3173
|
+
else continue;
|
|
3174
|
+
else lineCost = (containerWidth - w) ** 2;
|
|
3175
|
+
let breakPenalty = 0;
|
|
3176
|
+
if (j < n) {
|
|
3177
|
+
const prevChild = children[j - 1];
|
|
3178
|
+
if (PUNCTUATION_REGEX.test(prevChild.text)) breakPenalty = -((containerWidth * PUNCTUATION_BREAK_REWARD_RATIO) ** 2);
|
|
3179
|
+
else if (prevChild.isSpace) breakPenalty = -((containerWidth * SPACE_BREAK_REWARD_RATIO) ** 2);
|
|
3180
|
+
else if (cjkBoundaries.has(charOffsets[j])) breakPenalty = PENALTY_CJK;
|
|
3181
|
+
else breakPenalty = PENALTY_NORMAL;
|
|
3182
|
+
}
|
|
3183
|
+
const totalCost = lineCost + breakPenalty + dp[j];
|
|
3184
|
+
if (totalCost < dp[i]) {
|
|
3185
|
+
dp[i] = totalCost;
|
|
3186
|
+
nextBreak[i] = j;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
const breaks = [];
|
|
3190
|
+
let curr = 0;
|
|
3191
|
+
while (curr < n) {
|
|
3192
|
+
curr = nextBreak[curr];
|
|
3193
|
+
if (curr > 0 && curr < n) breaks.push(curr);
|
|
3194
|
+
}
|
|
3195
|
+
return breaks;
|
|
3196
|
+
}
|
|
3197
|
+
//#endregion
|
|
3198
|
+
//#region src/utils/line-balancer.ts
|
|
3199
|
+
let sharedCanvasCtx = null;
|
|
3200
|
+
function getMeasurementContext() {
|
|
3201
|
+
if (!sharedCanvasCtx) sharedCanvasCtx = document.createElement("canvas").getContext("2d");
|
|
3202
|
+
return sharedCanvasCtx;
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* 用于平衡歌词行在换行后的各行长度
|
|
3206
|
+
*/
|
|
3207
|
+
var LineBalancer = class {
|
|
3208
|
+
isBalancing = false;
|
|
3209
|
+
lastBalancedContainerWidth = -1;
|
|
3210
|
+
constructor(mainElement) {
|
|
3211
|
+
this.mainElement = mainElement;
|
|
3212
|
+
}
|
|
3213
|
+
balanceLineBreaks(isNonDynamic, hasSplittedWords, wordSegmenter) {
|
|
3214
|
+
if (this.isBalancing || !this.mainElement) return;
|
|
3215
|
+
const computedStyle = getComputedStyle(this.mainElement);
|
|
3216
|
+
const paddingLeft = Number.parseFloat(computedStyle.paddingLeft) || 0;
|
|
3217
|
+
const paddingRight = Number.parseFloat(computedStyle.paddingRight) || 0;
|
|
3218
|
+
const containerWidth = this.mainElement.clientWidth - paddingLeft - paddingRight;
|
|
3219
|
+
if (containerWidth <= 0) return;
|
|
3220
|
+
if (isNonDynamic) {
|
|
3221
|
+
this.balanceNonDynamicLineBreaks(containerWidth, computedStyle, wordSegmenter);
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
if (!hasSplittedWords) return;
|
|
3225
|
+
this.balanceDynamicLineBreaks(containerWidth, wordSegmenter);
|
|
3226
|
+
}
|
|
3227
|
+
reset() {
|
|
3228
|
+
this.lastBalancedContainerWidth = -1;
|
|
3229
|
+
}
|
|
3230
|
+
executeLineBalance(containerWidth, adapter, wordSegmenter) {
|
|
3231
|
+
const existingBrs = this.mainElement.querySelectorAll("br");
|
|
3232
|
+
if (containerWidth === this.lastBalancedContainerWidth && existingBrs.length > 0) return;
|
|
3233
|
+
adapter.resetDOM();
|
|
3234
|
+
const prevWhiteSpace = this.mainElement.style.whiteSpace;
|
|
3235
|
+
this.mainElement.style.whiteSpace = "nowrap";
|
|
3236
|
+
const parentElement = this.mainElement.parentElement;
|
|
3237
|
+
let prevTransform = "";
|
|
3238
|
+
let transformChanged = false;
|
|
3239
|
+
if (parentElement) {
|
|
3240
|
+
prevTransform = parentElement.style.transform;
|
|
3241
|
+
if (prevTransform && prevTransform !== "none") {
|
|
3242
|
+
parentElement.style.transform = "none";
|
|
3243
|
+
transformChanged = true;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
let lockAcquired = false;
|
|
3247
|
+
try {
|
|
3248
|
+
const { childInfos, fullText } = adapter.buildChildInfos();
|
|
3249
|
+
let layoutWidth = childInfos.reduce((sum, c) => sum + c.width, 0);
|
|
3250
|
+
if (adapter.needsCalibration) {
|
|
3251
|
+
const range = document.createRange();
|
|
3252
|
+
range.selectNodeContents(this.mainElement);
|
|
3253
|
+
const visualWidth = range.getBoundingClientRect().width;
|
|
3254
|
+
if (layoutWidth > 0 && visualWidth > 0) {
|
|
3255
|
+
const scale = visualWidth / layoutWidth;
|
|
3256
|
+
for (const info of childInfos) info.width *= scale;
|
|
3257
|
+
}
|
|
3258
|
+
layoutWidth = visualWidth;
|
|
3259
|
+
}
|
|
3260
|
+
const safeContainerWidth = Math.max(1, containerWidth);
|
|
3261
|
+
if (layoutWidth <= safeContainerWidth) {
|
|
3262
|
+
this.lastBalancedContainerWidth = containerWidth;
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
const breaks = calcBalancedBreaks(childInfos, safeContainerWidth, fullText, wordSegmenter);
|
|
3266
|
+
if (breaks.length === 0) {
|
|
3267
|
+
this.lastBalancedContainerWidth = containerWidth;
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
this.isBalancing = true;
|
|
3271
|
+
lockAcquired = true;
|
|
3272
|
+
adapter.applyBreaks(breaks, childInfos);
|
|
3273
|
+
this.lastBalancedContainerWidth = containerWidth;
|
|
3274
|
+
this.isBalancing = false;
|
|
3275
|
+
} finally {
|
|
3276
|
+
this.mainElement.style.whiteSpace = prevWhiteSpace;
|
|
3277
|
+
if (transformChanged && parentElement) parentElement.style.transform = prevTransform;
|
|
3278
|
+
if (lockAcquired) this.isBalancing = false;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
balanceDynamicLineBreaks(containerWidth, wordSegmenter) {
|
|
3282
|
+
const infoToNode = [];
|
|
3283
|
+
this.executeLineBalance(containerWidth, {
|
|
3284
|
+
resetDOM: () => {
|
|
3285
|
+
this.mainElement.querySelectorAll("br").forEach((br) => {
|
|
3286
|
+
br.remove();
|
|
3287
|
+
});
|
|
3288
|
+
},
|
|
3289
|
+
buildChildInfos: () => {
|
|
3290
|
+
infoToNode.length = 0;
|
|
3291
|
+
const childNodes = Array.from(this.mainElement.childNodes);
|
|
3292
|
+
const childInfos = [];
|
|
3293
|
+
const range = document.createRange();
|
|
3294
|
+
for (const node of childNodes) if (node.nodeType === Node.TEXT_NODE) {
|
|
3295
|
+
const text = node.textContent ?? "";
|
|
3296
|
+
if (text.length === 0) continue;
|
|
3297
|
+
range.selectNodeContents(node);
|
|
3298
|
+
childInfos.push({
|
|
3299
|
+
width: range.getBoundingClientRect().width,
|
|
3300
|
+
text,
|
|
3301
|
+
isSpace: text.trim().length === 0
|
|
3302
|
+
});
|
|
3303
|
+
infoToNode.push(node);
|
|
3304
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
3305
|
+
const el = node;
|
|
3306
|
+
const rect = el.getBoundingClientRect();
|
|
3307
|
+
const elStyle = getComputedStyle(el);
|
|
3308
|
+
const marginLeft = Number.parseFloat(elStyle.marginLeft) || 0;
|
|
3309
|
+
const marginRight = Number.parseFloat(elStyle.marginRight) || 0;
|
|
3310
|
+
childInfos.push({
|
|
3311
|
+
width: Math.max(0, rect.width + marginLeft + marginRight),
|
|
3312
|
+
text: el.textContent ?? "",
|
|
3313
|
+
isSpace: false
|
|
3314
|
+
});
|
|
3315
|
+
infoToNode.push(node);
|
|
3316
|
+
}
|
|
3317
|
+
return {
|
|
3318
|
+
childInfos,
|
|
3319
|
+
fullText: childInfos.map((c) => c.text).join("")
|
|
3320
|
+
};
|
|
3321
|
+
},
|
|
3322
|
+
applyBreaks: (breaks) => {
|
|
3323
|
+
for (let i = breaks.length - 1; i >= 0; i--) {
|
|
3324
|
+
const breakIndex = breaks[i];
|
|
3325
|
+
if (breakIndex >= 0 && breakIndex < infoToNode.length) this.mainElement.insertBefore(document.createElement("br"), infoToNode[breakIndex]);
|
|
3326
|
+
}
|
|
3327
|
+
},
|
|
3328
|
+
needsCalibration: false
|
|
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
|
+
needsCalibration: true
|
|
3373
|
+
}, wordSegmenter);
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
//#endregion
|
|
3100
3377
|
//#region src/utils/lyric-split-words.ts
|
|
3101
|
-
const
|
|
3378
|
+
const SPLIT_WHITESPACE_RE = /(\s+)/;
|
|
3379
|
+
const WHITESPACE_RE = /\s/g;
|
|
3102
3380
|
/**
|
|
3103
3381
|
* 将输入的单词重新分组,之间没有空格的单词将会组合成一个单词数组
|
|
3104
3382
|
*
|
|
@@ -3109,27 +3387,41 @@ const hasSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter !== "u
|
|
|
3109
3387
|
* @returns 重新分组后的单词数组
|
|
3110
3388
|
*/
|
|
3111
3389
|
function chunkAndSplitLyricWords(words) {
|
|
3112
|
-
const
|
|
3390
|
+
const result = [];
|
|
3391
|
+
let currentGroup = [];
|
|
3392
|
+
const flushGroup = () => {
|
|
3393
|
+
if (currentGroup.length > 0) {
|
|
3394
|
+
result.push(currentGroup.length === 1 ? currentGroup[0] : [...currentGroup]);
|
|
3395
|
+
currentGroup = [];
|
|
3396
|
+
}
|
|
3397
|
+
};
|
|
3398
|
+
const processAtom = (atom) => {
|
|
3399
|
+
const isSpace = atom.word.trim().length === 0;
|
|
3400
|
+
const hasRuby = (atom.ruby?.length ?? 0) > 0;
|
|
3401
|
+
const isCJKChar = isCJK(atom.word);
|
|
3402
|
+
if (!isSpace && !hasRuby && !isCJKChar) currentGroup.push(atom);
|
|
3403
|
+
else {
|
|
3404
|
+
flushGroup();
|
|
3405
|
+
result.push(atom);
|
|
3406
|
+
}
|
|
3407
|
+
};
|
|
3113
3408
|
for (const w of words) {
|
|
3114
3409
|
const isSpace = w.word.trim().length === 0;
|
|
3115
3410
|
const romanWord = w.romanWord ?? "";
|
|
3116
3411
|
const obscene = w.obscene ?? false;
|
|
3117
3412
|
const hasRuby = (w.ruby?.length ?? 0) > 0;
|
|
3118
|
-
if (isSpace) {
|
|
3119
|
-
|
|
3413
|
+
if (isSpace || hasRuby) {
|
|
3414
|
+
processAtom({ ...w });
|
|
3120
3415
|
continue;
|
|
3121
3416
|
}
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
}
|
|
3126
|
-
const parts = w.word.split(/(\s+)/).filter((p) => p.length > 0);
|
|
3417
|
+
const parts = w.word.split(SPLIT_WHITESPACE_RE).filter((p) => p.length > 0);
|
|
3418
|
+
const totalLength = w.word.replace(WHITESPACE_RE, "").length || 1;
|
|
3419
|
+
const timePerUnit = (w.endTime - w.startTime) / totalLength;
|
|
3127
3420
|
let currentOffset = 0;
|
|
3128
|
-
const totalLength = w.word.replace(/\s/g, "").length || 1;
|
|
3129
3421
|
for (const part of parts) {
|
|
3130
3422
|
if (!part.trim()) {
|
|
3131
|
-
const startTime = w.startTime + currentOffset
|
|
3132
|
-
|
|
3423
|
+
const startTime = w.startTime + currentOffset * timePerUnit;
|
|
3424
|
+
processAtom({
|
|
3133
3425
|
word: part,
|
|
3134
3426
|
romanWord: "",
|
|
3135
3427
|
startTime,
|
|
@@ -3141,63 +3433,31 @@ function chunkAndSplitLyricWords(words) {
|
|
|
3141
3433
|
if (isCJK(part) && part.length > 1 && romanWord.trim().length === 0) {
|
|
3142
3434
|
const chars = part.split("");
|
|
3143
3435
|
for (const char of chars) {
|
|
3144
|
-
const
|
|
3145
|
-
|
|
3146
|
-
atoms.push({
|
|
3436
|
+
const startTime = w.startTime + currentOffset * timePerUnit;
|
|
3437
|
+
processAtom({
|
|
3147
3438
|
word: char,
|
|
3148
3439
|
romanWord: "",
|
|
3149
3440
|
startTime,
|
|
3150
|
-
endTime: startTime +
|
|
3441
|
+
endTime: startTime + timePerUnit,
|
|
3151
3442
|
obscene
|
|
3152
3443
|
});
|
|
3153
3444
|
currentOffset += 1;
|
|
3154
3445
|
}
|
|
3155
3446
|
} else {
|
|
3156
3447
|
const partRealLen = part.length;
|
|
3157
|
-
const
|
|
3158
|
-
|
|
3159
|
-
atoms.push({
|
|
3448
|
+
const startTime = w.startTime + currentOffset * timePerUnit;
|
|
3449
|
+
processAtom({
|
|
3160
3450
|
word: part,
|
|
3161
3451
|
romanWord,
|
|
3162
3452
|
startTime,
|
|
3163
|
-
endTime: startTime +
|
|
3453
|
+
endTime: startTime + partRealLen * timePerUnit,
|
|
3164
3454
|
obscene
|
|
3165
3455
|
});
|
|
3166
3456
|
currentOffset += partRealLen;
|
|
3167
3457
|
}
|
|
3168
3458
|
}
|
|
3169
3459
|
}
|
|
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);
|
|
3460
|
+
flushGroup();
|
|
3201
3461
|
return result;
|
|
3202
3462
|
}
|
|
3203
3463
|
//#endregion
|
|
@@ -3279,12 +3539,15 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3279
3539
|
splittedWords = [];
|
|
3280
3540
|
built = false;
|
|
3281
3541
|
lineSize = [0, 0];
|
|
3282
|
-
renderMode =
|
|
3542
|
+
renderMode = 0;
|
|
3283
3543
|
currentBrightAlpha = 1;
|
|
3284
3544
|
currentDarkAlpha = .2;
|
|
3285
3545
|
targetBrightAlpha = 1;
|
|
3286
3546
|
targetDarkAlpha = .2;
|
|
3287
|
-
|
|
3547
|
+
/**
|
|
3548
|
+
* 用于平衡换行、尽量减少各行长度差异的类
|
|
3549
|
+
*/
|
|
3550
|
+
balancer;
|
|
3288
3551
|
constructor(lyricPlayer, lyricLine = {
|
|
3289
3552
|
words: [],
|
|
3290
3553
|
translatedLyric: "",
|
|
@@ -3312,6 +3575,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3312
3575
|
main.setAttribute("class", lyric_player_module_default.lyricMainLine);
|
|
3313
3576
|
trans.setAttribute("class", lyric_player_module_default.lyricSubLine);
|
|
3314
3577
|
roman.setAttribute("class", lyric_player_module_default.lyricSubLine);
|
|
3578
|
+
if (LyricLineBase.wordSegmenter) this.balancer = new LineBalancer(main);
|
|
3315
3579
|
this.rebuildStyle();
|
|
3316
3580
|
}
|
|
3317
3581
|
listenersMap = /* @__PURE__ */ new Map();
|
|
@@ -3385,7 +3649,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3385
3649
|
disable() {
|
|
3386
3650
|
this.isEnabled = false;
|
|
3387
3651
|
this.element.classList.remove(lyric_player_module_default.active);
|
|
3388
|
-
this.renderMode =
|
|
3652
|
+
this.renderMode = 0;
|
|
3389
3653
|
const main = this.element.children[0];
|
|
3390
3654
|
for (const word of this.splittedWords) {
|
|
3391
3655
|
for (const a of word.elementAnimations) if (a.id === "float-word" || a.id.includes("emphasize-word-float-only")) {
|
|
@@ -3521,7 +3785,14 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3521
3785
|
const displayWord = this.lyricPlayer.processObsceneWord(word);
|
|
3522
3786
|
if (shouldEmphasize) {
|
|
3523
3787
|
mainWordEl.classList.add(lyric_player_module_default.emphasize);
|
|
3524
|
-
|
|
3788
|
+
const trimmedWord = displayWord.trim();
|
|
3789
|
+
if (LyricLineBase.graphemeSegmenter) for (const { segment } of LyricLineBase.graphemeSegmenter.segment(trimmedWord)) {
|
|
3790
|
+
const charEl = document.createElement("span");
|
|
3791
|
+
charEl.innerText = segment;
|
|
3792
|
+
subElements.push(charEl);
|
|
3793
|
+
wordContainer.appendChild(charEl);
|
|
3794
|
+
}
|
|
3795
|
+
else for (const segment of Array.from(trimmedWord)) {
|
|
3525
3796
|
const charEl = document.createElement("span");
|
|
3526
3797
|
charEl.innerText = segment;
|
|
3527
3798
|
subElements.push(charEl);
|
|
@@ -3703,6 +3974,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3703
3974
|
word.padding = 0;
|
|
3704
3975
|
}
|
|
3705
3976
|
}
|
|
3977
|
+
if (this.balancer && LyricLineBase.wordSegmenter) this.balancer.balanceLineBreaks(this.lyricPlayer._getIsNonDynamic(), this.splittedWords.length > 0, LyricLineBase.wordSegmenter);
|
|
3706
3978
|
if (this.lyricPlayer.supportMaskImage) this.generateWebAnimationBasedMaskImage();
|
|
3707
3979
|
else this.generateCalcBasedMaskImage();
|
|
3708
3980
|
if (this.isEnabled) {
|
|
@@ -3875,7 +4147,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3875
4147
|
const factor = Math.max(0, Math.min(1, (scale - .97) / .03));
|
|
3876
4148
|
const dynamicDarkAlpha = factor * .2 + .2;
|
|
3877
4149
|
const dynamicBrightAlpha = factor * .8 + .2;
|
|
3878
|
-
if (this.renderMode ===
|
|
4150
|
+
if (this.renderMode === 0) {
|
|
3879
4151
|
this.targetBrightAlpha = dynamicDarkAlpha;
|
|
3880
4152
|
this.targetDarkAlpha = dynamicDarkAlpha;
|
|
3881
4153
|
} else {
|
|
@@ -3897,7 +4169,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3897
4169
|
this.element.style.setProperty("--bright-mask-alpha", this.currentBrightAlpha.toFixed(3));
|
|
3898
4170
|
this.element.style.setProperty("--dark-mask-alpha", this.currentDarkAlpha.toFixed(3));
|
|
3899
4171
|
}
|
|
3900
|
-
setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode =
|
|
4172
|
+
setTransform(top = this.top, scale = this.scale, opacity = 1, blur = 0, force = false, delay = 0, mode = 0) {
|
|
3901
4173
|
super.setTransform(top, scale, opacity, blur, force, delay);
|
|
3902
4174
|
this.renderMode = mode;
|
|
3903
4175
|
const beforeInSight = this.isInSight;
|
|
@@ -3954,6 +4226,7 @@ var LyricLineEl$1 = class extends LyricLineBase {
|
|
|
3954
4226
|
return !(t > pb + h + ov || b < -h - ov);
|
|
3955
4227
|
}
|
|
3956
4228
|
disposeElements() {
|
|
4229
|
+
this.balancer?.reset();
|
|
3957
4230
|
for (const realWord of this.splittedWords) {
|
|
3958
4231
|
for (const a of realWord.elementAnimations) a.cancel();
|
|
3959
4232
|
for (const a of realWord.maskAnimations) a.cancel();
|