@subvo/renderer 1.2.0 → 1.2.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/renderer.d.ts +4 -1
- package/dist/renderer.esm.js +196 -25
- package/dist/renderer.iife.js +196 -25
- package/package.json +1 -1
package/dist/renderer.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ type RenderMetadata = {
|
|
|
37
37
|
createdAt?: string;
|
|
38
38
|
tags?: string[];
|
|
39
39
|
};
|
|
40
|
-
type RenderPosition = "
|
|
40
|
+
type RenderPosition = "top_safe" | "center" | "bottom_safe";
|
|
41
41
|
type RenderPreset = "fire" | "clean" | "luxury" | "pop" | "ghost";
|
|
42
42
|
type RenderMode = "phrase_highlight" | "progressive_build";
|
|
43
43
|
type RenderStyle = {
|
|
@@ -60,6 +60,9 @@ type RenderStyle = {
|
|
|
60
60
|
offsetY: number;
|
|
61
61
|
fadeInMs: number;
|
|
62
62
|
fadeOutMs: number;
|
|
63
|
+
highlightBox?: boolean;
|
|
64
|
+
karaokeFill?: boolean;
|
|
65
|
+
bounce?: boolean;
|
|
63
66
|
preset: RenderPreset;
|
|
64
67
|
};
|
|
65
68
|
|
package/dist/renderer.esm.js
CHANGED
|
@@ -177,6 +177,33 @@ function buildLayoutRow(ctx, words) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// src/effects.ts
|
|
180
|
+
var PRESET_FEATURE_DEFAULTS = {
|
|
181
|
+
fire: {
|
|
182
|
+
highlightBox: true,
|
|
183
|
+
karaokeFill: true,
|
|
184
|
+
bounce: true
|
|
185
|
+
},
|
|
186
|
+
clean: {
|
|
187
|
+
highlightBox: false,
|
|
188
|
+
karaokeFill: true,
|
|
189
|
+
bounce: false
|
|
190
|
+
},
|
|
191
|
+
luxury: {
|
|
192
|
+
highlightBox: true,
|
|
193
|
+
karaokeFill: false,
|
|
194
|
+
bounce: false
|
|
195
|
+
},
|
|
196
|
+
pop: {
|
|
197
|
+
highlightBox: false,
|
|
198
|
+
karaokeFill: true,
|
|
199
|
+
bounce: true
|
|
200
|
+
},
|
|
201
|
+
ghost: {
|
|
202
|
+
highlightBox: false,
|
|
203
|
+
karaokeFill: false,
|
|
204
|
+
bounce: false
|
|
205
|
+
}
|
|
206
|
+
};
|
|
180
207
|
function clamp(value, min = 0, max = 1) {
|
|
181
208
|
return Math.min(max, Math.max(min, value));
|
|
182
209
|
}
|
|
@@ -188,46 +215,142 @@ function computeWordProgress(word, timeMs) {
|
|
|
188
215
|
function computePulseScale(style, progress) {
|
|
189
216
|
return 1 + (style.pulse - 1) * progress * style.highlightStrength;
|
|
190
217
|
}
|
|
218
|
+
function computeImpactProgress(word, timeMs, durationMs = 150) {
|
|
219
|
+
if (durationMs <= 0) return 0;
|
|
220
|
+
return clamp((timeMs - word.startMs) / durationMs);
|
|
221
|
+
}
|
|
222
|
+
function computeImpactScale(progress) {
|
|
223
|
+
const clampedProgress = clamp(progress);
|
|
224
|
+
if (clampedProgress <= 0 || clampedProgress >= 1) {
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
if (clampedProgress < 0.3) {
|
|
228
|
+
return 1 + clampedProgress * 0.8;
|
|
229
|
+
}
|
|
230
|
+
return Math.max(1, 1.25 - (clampedProgress - 0.3) * 0.35);
|
|
231
|
+
}
|
|
232
|
+
function resolveStyleFlag(style, key) {
|
|
233
|
+
const value = style[key];
|
|
234
|
+
if (typeof value === "boolean") {
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
return PRESET_FEATURE_DEFAULTS[style.preset][key];
|
|
238
|
+
}
|
|
191
239
|
|
|
192
240
|
// src/drawText.ts
|
|
193
|
-
|
|
241
|
+
var BOX_PADDING_X = 12;
|
|
242
|
+
var BOX_PADDING_Y = 6;
|
|
243
|
+
var BOX_RADIUS = 8;
|
|
244
|
+
function drawWord(ctx, rawText, text, x, y, style, scaleFactor, ascent, progress = 0, emphasis = 0) {
|
|
245
|
+
const activeProgress = Math.max(0, Math.min(1, progress));
|
|
194
246
|
const emphasisBoost = emphasis > 0.6 ? Math.min(1, (emphasis - 0.6) / 0.4) : 0;
|
|
195
247
|
const effectiveScale = scaleFactor * (1 + emphasisBoost * 0.06);
|
|
196
248
|
const invScale = 1 / effectiveScale;
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
249
|
+
const outlineMultiplier = emphasis >= 0.6 ? 1.4 : 1;
|
|
250
|
+
const strokeWidth = style.outlineWidth * outlineMultiplier + emphasisBoost * 1.2;
|
|
251
|
+
const glowIntensity = style.glowIntensity + (emphasis >= 0.75 ? 0.4 : 0);
|
|
252
|
+
const isActive = activeProgress > 0;
|
|
253
|
+
const highlightBoxActive = resolveStyleFlag(style, "highlightBox") && isActive && emphasis >= 0.7;
|
|
254
|
+
const karaokeFillActive = resolveStyleFlag(style, "karaokeFill") && isActive && activeProgress < 1;
|
|
255
|
+
const inactiveFillColor = !resolveStyleFlag(style, "karaokeFill") && style.preset !== "ghost" && emphasis >= 0.75 ? style.highlightColor : style.baseColor;
|
|
256
|
+
const baseFillColor = highlightBoxActive ? style.baseColor : isActive && karaokeFillActive ? style.baseColor : inactiveFillColor;
|
|
257
|
+
const activeFillColor = highlightBoxActive ? style.baseColor : style.highlightColor;
|
|
258
|
+
const textBounds = measureTextBounds(ctx, rawText, ascent);
|
|
200
259
|
ctx.save();
|
|
201
260
|
ctx.translate(x, y);
|
|
202
261
|
ctx.translate(0, -ascent);
|
|
203
262
|
ctx.scale(effectiveScale, effectiveScale);
|
|
204
263
|
ctx.translate(0, ascent);
|
|
205
|
-
|
|
264
|
+
if (highlightBoxActive) {
|
|
265
|
+
resetShadow(ctx);
|
|
266
|
+
drawHighlightBox(ctx, textBounds, style.highlightColor);
|
|
267
|
+
}
|
|
268
|
+
resetShadow(ctx);
|
|
206
269
|
if (strokeWidth > 0) {
|
|
207
270
|
ctx.lineWidth = strokeWidth * invScale;
|
|
208
271
|
ctx.strokeStyle = style.outlineColor;
|
|
209
272
|
ctx.strokeText(text, 0, 0);
|
|
210
273
|
}
|
|
211
|
-
|
|
212
|
-
ctx
|
|
274
|
+
applyTextEffects(
|
|
275
|
+
ctx,
|
|
276
|
+
style,
|
|
277
|
+
invScale,
|
|
278
|
+
glowIntensity,
|
|
279
|
+
emphasis >= 0.75 ? style.highlightColor : style.outlineColor
|
|
280
|
+
);
|
|
281
|
+
ctx.fillStyle = isActive && !karaokeFillActive ? activeFillColor : baseFillColor;
|
|
282
|
+
ctx.fillText(text, 0, 0);
|
|
283
|
+
if (karaokeFillActive) {
|
|
284
|
+
ctx.save();
|
|
285
|
+
applyTextEffects(ctx, style, invScale, glowIntensity, style.highlightColor);
|
|
286
|
+
ctx.beginPath();
|
|
287
|
+
ctx.rect(
|
|
288
|
+
textBounds.left - 1 * invScale,
|
|
289
|
+
textBounds.top - 2 * invScale,
|
|
290
|
+
(textBounds.width + 2 * invScale) * activeProgress,
|
|
291
|
+
textBounds.height + 4 * invScale
|
|
292
|
+
);
|
|
293
|
+
ctx.clip();
|
|
294
|
+
ctx.fillStyle = style.highlightColor;
|
|
295
|
+
ctx.fillText(text, 0, 0);
|
|
296
|
+
ctx.restore();
|
|
297
|
+
}
|
|
298
|
+
ctx.restore();
|
|
299
|
+
}
|
|
300
|
+
function measureTextBounds(ctx, rawText, fallbackAscent) {
|
|
301
|
+
const metrics = ctx.measureText(rawText);
|
|
302
|
+
const left = -metrics.actualBoundingBoxLeft;
|
|
303
|
+
const width = Math.max(
|
|
304
|
+
metrics.width,
|
|
305
|
+
metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight
|
|
306
|
+
);
|
|
307
|
+
const ascent = metrics.actualBoundingBoxAscent || fallbackAscent;
|
|
308
|
+
const descent = metrics.actualBoundingBoxDescent || Math.max(4, fallbackAscent * 0.28);
|
|
309
|
+
return {
|
|
310
|
+
left,
|
|
311
|
+
top: -ascent,
|
|
312
|
+
width,
|
|
313
|
+
height: ascent + descent
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function drawHighlightBox(ctx, textBounds, color) {
|
|
317
|
+
const boxX = textBounds.left - BOX_PADDING_X;
|
|
318
|
+
const boxY = textBounds.top - BOX_PADDING_Y;
|
|
319
|
+
const boxWidth = textBounds.width + BOX_PADDING_X * 2;
|
|
320
|
+
const boxHeight = textBounds.height + BOX_PADDING_Y * 2;
|
|
321
|
+
ctx.beginPath();
|
|
322
|
+
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, BOX_RADIUS);
|
|
323
|
+
ctx.fillStyle = color;
|
|
324
|
+
ctx.fill();
|
|
325
|
+
}
|
|
326
|
+
function applyTextEffects(ctx, style, invScale, glowIntensity, shadowColor) {
|
|
327
|
+
if (style.shadow || glowIntensity > 0) {
|
|
328
|
+
ctx.shadowColor = shadowColor;
|
|
213
329
|
ctx.shadowOffsetX = style.shadow ? style.shadowDepth * invScale : 0;
|
|
214
330
|
ctx.shadowOffsetY = style.shadow ? style.shadowDepth * invScale : 0;
|
|
215
|
-
ctx.shadowBlur =
|
|
216
|
-
|
|
217
|
-
ctx.shadowColor = "transparent";
|
|
218
|
-
ctx.shadowBlur = 0;
|
|
219
|
-
ctx.shadowOffsetX = 0;
|
|
220
|
-
ctx.shadowOffsetY = 0;
|
|
331
|
+
ctx.shadowBlur = style.fontSize * glowIntensity * invScale;
|
|
332
|
+
return;
|
|
221
333
|
}
|
|
222
|
-
ctx
|
|
223
|
-
|
|
334
|
+
resetShadow(ctx);
|
|
335
|
+
}
|
|
336
|
+
function resetShadow(ctx) {
|
|
337
|
+
ctx.shadowColor = "transparent";
|
|
338
|
+
ctx.shadowBlur = 0;
|
|
339
|
+
ctx.shadowOffsetX = 0;
|
|
340
|
+
ctx.shadowOffsetY = 0;
|
|
224
341
|
}
|
|
225
342
|
|
|
226
343
|
// src/renderFrame.ts
|
|
227
344
|
var DEFAULT_TRANSITION_MS = 150;
|
|
228
345
|
var MIN_TRANSITION_MS = 120;
|
|
229
346
|
var MAX_TRANSITION_MS = 180;
|
|
347
|
+
var TOP_UNSAFE_RATIO = 0.14;
|
|
348
|
+
var BOTTOM_UNSAFE_RATIO = 0.22;
|
|
349
|
+
var TOP_SAFE_ANCHOR_RATIO = 0.22;
|
|
350
|
+
var CENTER_ANCHOR_RATIO = 0.5;
|
|
351
|
+
var BOTTOM_SAFE_ANCHOR_RATIO = 0.7;
|
|
230
352
|
function renderFrame(ctx, renderJob, timeMs) {
|
|
353
|
+
var _a;
|
|
231
354
|
const { playRes, style } = renderJob;
|
|
232
355
|
const layout = computeLayout(ctx, renderJob);
|
|
233
356
|
const transitionMs = resolveTransitionMs(style);
|
|
@@ -249,36 +372,52 @@ function renderFrame(ctx, renderJob, timeMs) {
|
|
|
249
372
|
ctx.textBaseline = "alphabetic";
|
|
250
373
|
const metrics = ctx.measureText("Mg");
|
|
251
374
|
const ascent = metrics.actualBoundingBoxAscent;
|
|
375
|
+
const descent = metrics.actualBoundingBoxDescent || Math.max(4, style.fontSize * 0.28);
|
|
252
376
|
const centerX = playRes.width / 2;
|
|
253
|
-
|
|
377
|
+
const lineStep = style.fontSize * style.lineSpacing;
|
|
378
|
+
const safeTop = playRes.height * TOP_UNSAFE_RATIO;
|
|
379
|
+
const safeBottom = playRes.height * (1 - BOTTOM_UNSAFE_RATIO);
|
|
380
|
+
const anchorY = resolveAnchorY(playRes.height, style.position) + style.offsetY;
|
|
254
381
|
for (const { entry, alpha } of visibleLines) {
|
|
255
382
|
ctx.save();
|
|
256
383
|
ctx.globalAlpha = alpha;
|
|
257
|
-
|
|
258
|
-
|
|
384
|
+
const baselineY = clampBaselineY(
|
|
385
|
+
anchorY,
|
|
386
|
+
entry.rows.length,
|
|
387
|
+
ascent,
|
|
388
|
+
descent,
|
|
389
|
+
lineStep,
|
|
390
|
+
safeTop,
|
|
391
|
+
safeBottom
|
|
392
|
+
);
|
|
393
|
+
for (const [rowIndex, row] of entry.rows.entries()) {
|
|
259
394
|
let x = centerX - row.width / 2;
|
|
395
|
+
const rowY = baselineY + rowIndex * lineStep;
|
|
260
396
|
for (const { word, text, width } of row.words) {
|
|
261
397
|
const visibleWord = isWordVisible(entry.line, word, timeMs);
|
|
262
398
|
if (visibleWord) {
|
|
263
399
|
const activeWord = timeMs >= word.startMs && timeMs <= word.endMs;
|
|
400
|
+
const emphasis = (_a = word.emphasis) != null ? _a : 0;
|
|
264
401
|
const progress = activeWord ? computeWordProgress(word, timeMs) : 0;
|
|
265
|
-
const
|
|
266
|
-
const
|
|
402
|
+
const impactProgress = activeWord ? computeImpactProgress(word, timeMs) : 0;
|
|
403
|
+
const pulseScale = computePulseScale(style, progress);
|
|
404
|
+
const bounceScale = shouldBounceWord(style, emphasis) ? computeImpactScale(impactProgress) : 1;
|
|
405
|
+
const scaleFactor = pulseScale * bounceScale;
|
|
267
406
|
drawWord(
|
|
268
407
|
ctx,
|
|
408
|
+
word.text,
|
|
269
409
|
text,
|
|
270
410
|
x,
|
|
271
|
-
|
|
411
|
+
rowY,
|
|
272
412
|
style,
|
|
273
413
|
scaleFactor,
|
|
274
414
|
ascent,
|
|
275
|
-
|
|
276
|
-
|
|
415
|
+
progress,
|
|
416
|
+
emphasis
|
|
277
417
|
);
|
|
278
418
|
}
|
|
279
419
|
x += width;
|
|
280
420
|
}
|
|
281
|
-
lineY += style.fontSize * style.lineSpacing;
|
|
282
421
|
}
|
|
283
422
|
ctx.restore();
|
|
284
423
|
}
|
|
@@ -308,9 +447,41 @@ function isWordVisible(line, word, timeMs) {
|
|
|
308
447
|
}
|
|
309
448
|
return timeMs >= word.startMs;
|
|
310
449
|
}
|
|
450
|
+
function shouldBounceWord(style, emphasis) {
|
|
451
|
+
if (style.preset === "ghost") {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (resolveStyleFlag(style, "bounce")) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return emphasis >= 0.85;
|
|
458
|
+
}
|
|
459
|
+
function resolveAnchorY(playHeight, position) {
|
|
460
|
+
if (position === "top_safe") {
|
|
461
|
+
return playHeight * TOP_SAFE_ANCHOR_RATIO;
|
|
462
|
+
}
|
|
463
|
+
if (position === "center") {
|
|
464
|
+
return playHeight * CENTER_ANCHOR_RATIO;
|
|
465
|
+
}
|
|
466
|
+
return playHeight * BOTTOM_SAFE_ANCHOR_RATIO;
|
|
467
|
+
}
|
|
468
|
+
function clampBaselineY(baselineY, rowCount, ascent, descent, lineStep, safeTop, safeBottom) {
|
|
469
|
+
const rows = Math.max(1, rowCount);
|
|
470
|
+
const blockTop = baselineY - ascent;
|
|
471
|
+
const blockBottom = baselineY + (rows - 1) * lineStep + descent;
|
|
472
|
+
if (blockTop < safeTop) {
|
|
473
|
+
baselineY += safeTop - blockTop;
|
|
474
|
+
}
|
|
475
|
+
if (blockBottom > safeBottom) {
|
|
476
|
+
baselineY -= blockBottom - safeBottom;
|
|
477
|
+
}
|
|
478
|
+
const minBaselineY = safeTop + ascent;
|
|
479
|
+
const maxBaselineY = safeBottom - descent - (rows - 1) * lineStep;
|
|
480
|
+
return clamp(baselineY, minBaselineY, Math.max(minBaselineY, maxBaselineY));
|
|
481
|
+
}
|
|
311
482
|
|
|
312
483
|
// src/index.ts
|
|
313
|
-
var injectedVersion = "1.2.
|
|
484
|
+
var injectedVersion = "1.2.1".length > 0 ? "1.2.1" : "dev";
|
|
314
485
|
var VERSION = injectedVersion;
|
|
315
486
|
export {
|
|
316
487
|
VERSION,
|
package/dist/renderer.iife.js
CHANGED
|
@@ -204,6 +204,33 @@ var SubvoRenderer = (() => {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
// src/effects.ts
|
|
207
|
+
var PRESET_FEATURE_DEFAULTS = {
|
|
208
|
+
fire: {
|
|
209
|
+
highlightBox: true,
|
|
210
|
+
karaokeFill: true,
|
|
211
|
+
bounce: true
|
|
212
|
+
},
|
|
213
|
+
clean: {
|
|
214
|
+
highlightBox: false,
|
|
215
|
+
karaokeFill: true,
|
|
216
|
+
bounce: false
|
|
217
|
+
},
|
|
218
|
+
luxury: {
|
|
219
|
+
highlightBox: true,
|
|
220
|
+
karaokeFill: false,
|
|
221
|
+
bounce: false
|
|
222
|
+
},
|
|
223
|
+
pop: {
|
|
224
|
+
highlightBox: false,
|
|
225
|
+
karaokeFill: true,
|
|
226
|
+
bounce: true
|
|
227
|
+
},
|
|
228
|
+
ghost: {
|
|
229
|
+
highlightBox: false,
|
|
230
|
+
karaokeFill: false,
|
|
231
|
+
bounce: false
|
|
232
|
+
}
|
|
233
|
+
};
|
|
207
234
|
function clamp(value, min = 0, max = 1) {
|
|
208
235
|
return Math.min(max, Math.max(min, value));
|
|
209
236
|
}
|
|
@@ -215,46 +242,142 @@ var SubvoRenderer = (() => {
|
|
|
215
242
|
function computePulseScale(style, progress) {
|
|
216
243
|
return 1 + (style.pulse - 1) * progress * style.highlightStrength;
|
|
217
244
|
}
|
|
245
|
+
function computeImpactProgress(word, timeMs, durationMs = 150) {
|
|
246
|
+
if (durationMs <= 0) return 0;
|
|
247
|
+
return clamp((timeMs - word.startMs) / durationMs);
|
|
248
|
+
}
|
|
249
|
+
function computeImpactScale(progress) {
|
|
250
|
+
const clampedProgress = clamp(progress);
|
|
251
|
+
if (clampedProgress <= 0 || clampedProgress >= 1) {
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
if (clampedProgress < 0.3) {
|
|
255
|
+
return 1 + clampedProgress * 0.8;
|
|
256
|
+
}
|
|
257
|
+
return Math.max(1, 1.25 - (clampedProgress - 0.3) * 0.35);
|
|
258
|
+
}
|
|
259
|
+
function resolveStyleFlag(style, key) {
|
|
260
|
+
const value = style[key];
|
|
261
|
+
if (typeof value === "boolean") {
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
return PRESET_FEATURE_DEFAULTS[style.preset][key];
|
|
265
|
+
}
|
|
218
266
|
|
|
219
267
|
// src/drawText.ts
|
|
220
|
-
|
|
268
|
+
var BOX_PADDING_X = 12;
|
|
269
|
+
var BOX_PADDING_Y = 6;
|
|
270
|
+
var BOX_RADIUS = 8;
|
|
271
|
+
function drawWord(ctx, rawText, text, x, y, style, scaleFactor, ascent, progress = 0, emphasis = 0) {
|
|
272
|
+
const activeProgress = Math.max(0, Math.min(1, progress));
|
|
221
273
|
const emphasisBoost = emphasis > 0.6 ? Math.min(1, (emphasis - 0.6) / 0.4) : 0;
|
|
222
274
|
const effectiveScale = scaleFactor * (1 + emphasisBoost * 0.06);
|
|
223
275
|
const invScale = 1 / effectiveScale;
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
276
|
+
const outlineMultiplier = emphasis >= 0.6 ? 1.4 : 1;
|
|
277
|
+
const strokeWidth = style.outlineWidth * outlineMultiplier + emphasisBoost * 1.2;
|
|
278
|
+
const glowIntensity = style.glowIntensity + (emphasis >= 0.75 ? 0.4 : 0);
|
|
279
|
+
const isActive = activeProgress > 0;
|
|
280
|
+
const highlightBoxActive = resolveStyleFlag(style, "highlightBox") && isActive && emphasis >= 0.7;
|
|
281
|
+
const karaokeFillActive = resolveStyleFlag(style, "karaokeFill") && isActive && activeProgress < 1;
|
|
282
|
+
const inactiveFillColor = !resolveStyleFlag(style, "karaokeFill") && style.preset !== "ghost" && emphasis >= 0.75 ? style.highlightColor : style.baseColor;
|
|
283
|
+
const baseFillColor = highlightBoxActive ? style.baseColor : isActive && karaokeFillActive ? style.baseColor : inactiveFillColor;
|
|
284
|
+
const activeFillColor = highlightBoxActive ? style.baseColor : style.highlightColor;
|
|
285
|
+
const textBounds = measureTextBounds(ctx, rawText, ascent);
|
|
227
286
|
ctx.save();
|
|
228
287
|
ctx.translate(x, y);
|
|
229
288
|
ctx.translate(0, -ascent);
|
|
230
289
|
ctx.scale(effectiveScale, effectiveScale);
|
|
231
290
|
ctx.translate(0, ascent);
|
|
232
|
-
|
|
291
|
+
if (highlightBoxActive) {
|
|
292
|
+
resetShadow(ctx);
|
|
293
|
+
drawHighlightBox(ctx, textBounds, style.highlightColor);
|
|
294
|
+
}
|
|
295
|
+
resetShadow(ctx);
|
|
233
296
|
if (strokeWidth > 0) {
|
|
234
297
|
ctx.lineWidth = strokeWidth * invScale;
|
|
235
298
|
ctx.strokeStyle = style.outlineColor;
|
|
236
299
|
ctx.strokeText(text, 0, 0);
|
|
237
300
|
}
|
|
238
|
-
|
|
239
|
-
ctx
|
|
301
|
+
applyTextEffects(
|
|
302
|
+
ctx,
|
|
303
|
+
style,
|
|
304
|
+
invScale,
|
|
305
|
+
glowIntensity,
|
|
306
|
+
emphasis >= 0.75 ? style.highlightColor : style.outlineColor
|
|
307
|
+
);
|
|
308
|
+
ctx.fillStyle = isActive && !karaokeFillActive ? activeFillColor : baseFillColor;
|
|
309
|
+
ctx.fillText(text, 0, 0);
|
|
310
|
+
if (karaokeFillActive) {
|
|
311
|
+
ctx.save();
|
|
312
|
+
applyTextEffects(ctx, style, invScale, glowIntensity, style.highlightColor);
|
|
313
|
+
ctx.beginPath();
|
|
314
|
+
ctx.rect(
|
|
315
|
+
textBounds.left - 1 * invScale,
|
|
316
|
+
textBounds.top - 2 * invScale,
|
|
317
|
+
(textBounds.width + 2 * invScale) * activeProgress,
|
|
318
|
+
textBounds.height + 4 * invScale
|
|
319
|
+
);
|
|
320
|
+
ctx.clip();
|
|
321
|
+
ctx.fillStyle = style.highlightColor;
|
|
322
|
+
ctx.fillText(text, 0, 0);
|
|
323
|
+
ctx.restore();
|
|
324
|
+
}
|
|
325
|
+
ctx.restore();
|
|
326
|
+
}
|
|
327
|
+
function measureTextBounds(ctx, rawText, fallbackAscent) {
|
|
328
|
+
const metrics = ctx.measureText(rawText);
|
|
329
|
+
const left = -metrics.actualBoundingBoxLeft;
|
|
330
|
+
const width = Math.max(
|
|
331
|
+
metrics.width,
|
|
332
|
+
metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight
|
|
333
|
+
);
|
|
334
|
+
const ascent = metrics.actualBoundingBoxAscent || fallbackAscent;
|
|
335
|
+
const descent = metrics.actualBoundingBoxDescent || Math.max(4, fallbackAscent * 0.28);
|
|
336
|
+
return {
|
|
337
|
+
left,
|
|
338
|
+
top: -ascent,
|
|
339
|
+
width,
|
|
340
|
+
height: ascent + descent
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function drawHighlightBox(ctx, textBounds, color) {
|
|
344
|
+
const boxX = textBounds.left - BOX_PADDING_X;
|
|
345
|
+
const boxY = textBounds.top - BOX_PADDING_Y;
|
|
346
|
+
const boxWidth = textBounds.width + BOX_PADDING_X * 2;
|
|
347
|
+
const boxHeight = textBounds.height + BOX_PADDING_Y * 2;
|
|
348
|
+
ctx.beginPath();
|
|
349
|
+
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, BOX_RADIUS);
|
|
350
|
+
ctx.fillStyle = color;
|
|
351
|
+
ctx.fill();
|
|
352
|
+
}
|
|
353
|
+
function applyTextEffects(ctx, style, invScale, glowIntensity, shadowColor) {
|
|
354
|
+
if (style.shadow || glowIntensity > 0) {
|
|
355
|
+
ctx.shadowColor = shadowColor;
|
|
240
356
|
ctx.shadowOffsetX = style.shadow ? style.shadowDepth * invScale : 0;
|
|
241
357
|
ctx.shadowOffsetY = style.shadow ? style.shadowDepth * invScale : 0;
|
|
242
|
-
ctx.shadowBlur =
|
|
243
|
-
|
|
244
|
-
ctx.shadowColor = "transparent";
|
|
245
|
-
ctx.shadowBlur = 0;
|
|
246
|
-
ctx.shadowOffsetX = 0;
|
|
247
|
-
ctx.shadowOffsetY = 0;
|
|
358
|
+
ctx.shadowBlur = style.fontSize * glowIntensity * invScale;
|
|
359
|
+
return;
|
|
248
360
|
}
|
|
249
|
-
ctx
|
|
250
|
-
|
|
361
|
+
resetShadow(ctx);
|
|
362
|
+
}
|
|
363
|
+
function resetShadow(ctx) {
|
|
364
|
+
ctx.shadowColor = "transparent";
|
|
365
|
+
ctx.shadowBlur = 0;
|
|
366
|
+
ctx.shadowOffsetX = 0;
|
|
367
|
+
ctx.shadowOffsetY = 0;
|
|
251
368
|
}
|
|
252
369
|
|
|
253
370
|
// src/renderFrame.ts
|
|
254
371
|
var DEFAULT_TRANSITION_MS = 150;
|
|
255
372
|
var MIN_TRANSITION_MS = 120;
|
|
256
373
|
var MAX_TRANSITION_MS = 180;
|
|
374
|
+
var TOP_UNSAFE_RATIO = 0.14;
|
|
375
|
+
var BOTTOM_UNSAFE_RATIO = 0.22;
|
|
376
|
+
var TOP_SAFE_ANCHOR_RATIO = 0.22;
|
|
377
|
+
var CENTER_ANCHOR_RATIO = 0.5;
|
|
378
|
+
var BOTTOM_SAFE_ANCHOR_RATIO = 0.7;
|
|
257
379
|
function renderFrame(ctx, renderJob, timeMs) {
|
|
380
|
+
var _a;
|
|
258
381
|
const { playRes, style } = renderJob;
|
|
259
382
|
const layout = computeLayout(ctx, renderJob);
|
|
260
383
|
const transitionMs = resolveTransitionMs(style);
|
|
@@ -276,36 +399,52 @@ var SubvoRenderer = (() => {
|
|
|
276
399
|
ctx.textBaseline = "alphabetic";
|
|
277
400
|
const metrics = ctx.measureText("Mg");
|
|
278
401
|
const ascent = metrics.actualBoundingBoxAscent;
|
|
402
|
+
const descent = metrics.actualBoundingBoxDescent || Math.max(4, style.fontSize * 0.28);
|
|
279
403
|
const centerX = playRes.width / 2;
|
|
280
|
-
|
|
404
|
+
const lineStep = style.fontSize * style.lineSpacing;
|
|
405
|
+
const safeTop = playRes.height * TOP_UNSAFE_RATIO;
|
|
406
|
+
const safeBottom = playRes.height * (1 - BOTTOM_UNSAFE_RATIO);
|
|
407
|
+
const anchorY = resolveAnchorY(playRes.height, style.position) + style.offsetY;
|
|
281
408
|
for (const { entry, alpha } of visibleLines) {
|
|
282
409
|
ctx.save();
|
|
283
410
|
ctx.globalAlpha = alpha;
|
|
284
|
-
|
|
285
|
-
|
|
411
|
+
const baselineY = clampBaselineY(
|
|
412
|
+
anchorY,
|
|
413
|
+
entry.rows.length,
|
|
414
|
+
ascent,
|
|
415
|
+
descent,
|
|
416
|
+
lineStep,
|
|
417
|
+
safeTop,
|
|
418
|
+
safeBottom
|
|
419
|
+
);
|
|
420
|
+
for (const [rowIndex, row] of entry.rows.entries()) {
|
|
286
421
|
let x = centerX - row.width / 2;
|
|
422
|
+
const rowY = baselineY + rowIndex * lineStep;
|
|
287
423
|
for (const { word, text, width } of row.words) {
|
|
288
424
|
const visibleWord = isWordVisible(entry.line, word, timeMs);
|
|
289
425
|
if (visibleWord) {
|
|
290
426
|
const activeWord = timeMs >= word.startMs && timeMs <= word.endMs;
|
|
427
|
+
const emphasis = (_a = word.emphasis) != null ? _a : 0;
|
|
291
428
|
const progress = activeWord ? computeWordProgress(word, timeMs) : 0;
|
|
292
|
-
const
|
|
293
|
-
const
|
|
429
|
+
const impactProgress = activeWord ? computeImpactProgress(word, timeMs) : 0;
|
|
430
|
+
const pulseScale = computePulseScale(style, progress);
|
|
431
|
+
const bounceScale = shouldBounceWord(style, emphasis) ? computeImpactScale(impactProgress) : 1;
|
|
432
|
+
const scaleFactor = pulseScale * bounceScale;
|
|
294
433
|
drawWord(
|
|
295
434
|
ctx,
|
|
435
|
+
word.text,
|
|
296
436
|
text,
|
|
297
437
|
x,
|
|
298
|
-
|
|
438
|
+
rowY,
|
|
299
439
|
style,
|
|
300
440
|
scaleFactor,
|
|
301
441
|
ascent,
|
|
302
|
-
|
|
303
|
-
|
|
442
|
+
progress,
|
|
443
|
+
emphasis
|
|
304
444
|
);
|
|
305
445
|
}
|
|
306
446
|
x += width;
|
|
307
447
|
}
|
|
308
|
-
lineY += style.fontSize * style.lineSpacing;
|
|
309
448
|
}
|
|
310
449
|
ctx.restore();
|
|
311
450
|
}
|
|
@@ -335,9 +474,41 @@ var SubvoRenderer = (() => {
|
|
|
335
474
|
}
|
|
336
475
|
return timeMs >= word.startMs;
|
|
337
476
|
}
|
|
477
|
+
function shouldBounceWord(style, emphasis) {
|
|
478
|
+
if (style.preset === "ghost") {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
if (resolveStyleFlag(style, "bounce")) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
return emphasis >= 0.85;
|
|
485
|
+
}
|
|
486
|
+
function resolveAnchorY(playHeight, position) {
|
|
487
|
+
if (position === "top_safe") {
|
|
488
|
+
return playHeight * TOP_SAFE_ANCHOR_RATIO;
|
|
489
|
+
}
|
|
490
|
+
if (position === "center") {
|
|
491
|
+
return playHeight * CENTER_ANCHOR_RATIO;
|
|
492
|
+
}
|
|
493
|
+
return playHeight * BOTTOM_SAFE_ANCHOR_RATIO;
|
|
494
|
+
}
|
|
495
|
+
function clampBaselineY(baselineY, rowCount, ascent, descent, lineStep, safeTop, safeBottom) {
|
|
496
|
+
const rows = Math.max(1, rowCount);
|
|
497
|
+
const blockTop = baselineY - ascent;
|
|
498
|
+
const blockBottom = baselineY + (rows - 1) * lineStep + descent;
|
|
499
|
+
if (blockTop < safeTop) {
|
|
500
|
+
baselineY += safeTop - blockTop;
|
|
501
|
+
}
|
|
502
|
+
if (blockBottom > safeBottom) {
|
|
503
|
+
baselineY -= blockBottom - safeBottom;
|
|
504
|
+
}
|
|
505
|
+
const minBaselineY = safeTop + ascent;
|
|
506
|
+
const maxBaselineY = safeBottom - descent - (rows - 1) * lineStep;
|
|
507
|
+
return clamp(baselineY, minBaselineY, Math.max(minBaselineY, maxBaselineY));
|
|
508
|
+
}
|
|
338
509
|
|
|
339
510
|
// src/index.ts
|
|
340
|
-
var injectedVersion = "1.2.
|
|
511
|
+
var injectedVersion = "1.2.1".length > 0 ? "1.2.1" : "dev";
|
|
341
512
|
var VERSION = injectedVersion;
|
|
342
513
|
return __toCommonJS(src_exports);
|
|
343
514
|
})();
|