@shotstack/shotstack-canvas 2.1.4 → 2.1.6
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/entry.node.cjs +834 -798
- package/dist/entry.node.d.cts +50 -19
- package/dist/entry.node.d.ts +50 -19
- package/dist/entry.node.js +835 -800
- package/dist/entry.web.d.ts +50 -18
- package/dist/entry.web.js +2948 -2905
- package/package.json +2 -2
package/dist/entry.node.cjs
CHANGED
|
@@ -590,24 +590,38 @@ var richCaptionFontSchema = import_zod.z.object({
|
|
|
590
590
|
weight: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).default("400"),
|
|
591
591
|
color: import_zod.z.string().regex(HEX6).default("#ffffff"),
|
|
592
592
|
opacity: import_zod.z.number().min(0).max(1).default(1),
|
|
593
|
-
background: import_zod.z.string().regex(HEX6).optional()
|
|
593
|
+
background: import_zod.z.string().regex(HEX6).optional(),
|
|
594
|
+
textDecoration: import_zod.z.enum(["none", "underline", "line-through"]).default("none")
|
|
594
595
|
});
|
|
595
596
|
var richCaptionActiveSchema = import_zod2.richCaptionActiveSchema.extend({
|
|
596
597
|
font: import_zod.z.object({
|
|
597
|
-
color: import_zod.z.string().regex(HEX6).default("#ffffff"),
|
|
598
|
-
background: import_zod.z.string().regex(HEX6).optional(),
|
|
599
|
-
opacity: import_zod.z.number().min(0).max(1).default(1)
|
|
600
|
-
}).optional(),
|
|
601
|
-
stroke: import_zod.z.object({
|
|
602
|
-
width: import_zod.z.number().min(0).optional(),
|
|
603
598
|
color: import_zod.z.string().regex(HEX6).optional(),
|
|
604
|
-
|
|
599
|
+
background: import_zod.z.string().regex(HEX6).optional(),
|
|
600
|
+
opacity: import_zod.z.number().min(0).max(1).optional(),
|
|
601
|
+
textDecoration: import_zod.z.enum(["none", "underline", "line-through"]).optional()
|
|
605
602
|
}).optional(),
|
|
603
|
+
stroke: import_zod.z.union([
|
|
604
|
+
import_zod.z.object({
|
|
605
|
+
width: import_zod.z.number().min(0).optional(),
|
|
606
|
+
color: import_zod.z.string().regex(HEX6).optional(),
|
|
607
|
+
opacity: import_zod.z.number().min(0).max(1).optional()
|
|
608
|
+
}),
|
|
609
|
+
import_zod.z.literal("none")
|
|
610
|
+
]).optional(),
|
|
611
|
+
shadow: import_zod.z.union([
|
|
612
|
+
import_zod.z.object({
|
|
613
|
+
offsetX: import_zod.z.number().optional(),
|
|
614
|
+
offsetY: import_zod.z.number().optional(),
|
|
615
|
+
blur: import_zod.z.number().min(0).optional(),
|
|
616
|
+
color: import_zod.z.string().regex(HEX6).optional(),
|
|
617
|
+
opacity: import_zod.z.number().min(0).max(1).optional()
|
|
618
|
+
}),
|
|
619
|
+
import_zod.z.literal("none")
|
|
620
|
+
]).optional(),
|
|
606
621
|
scale: import_zod.z.number().min(0.5).max(2).default(1)
|
|
607
622
|
});
|
|
608
|
-
var richCaptionWordAnimationSchema =
|
|
623
|
+
var richCaptionWordAnimationSchema = import_zod.z.object({
|
|
609
624
|
style: import_zod.z.enum(["karaoke", "highlight", "pop", "fade", "slide", "bounce", "typewriter", "none"]).default("highlight"),
|
|
610
|
-
speed: import_zod.z.number().min(0.5).max(2).default(1),
|
|
611
625
|
direction: import_zod.z.enum(["left", "right", "up", "down"]).default("up")
|
|
612
626
|
});
|
|
613
627
|
var richCaptionAssetSchema = import_zod.z.object({
|
|
@@ -2619,281 +2633,616 @@ function parseHex6(hex, alpha = 1) {
|
|
|
2619
2633
|
return { r, g, b, a: alpha };
|
|
2620
2634
|
}
|
|
2621
2635
|
|
|
2622
|
-
// src/core/rich-caption-
|
|
2623
|
-
var
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
slide: 250,
|
|
2629
|
-
bounce: 400,
|
|
2630
|
-
typewriter: 0,
|
|
2631
|
-
none: 0
|
|
2632
|
-
};
|
|
2633
|
-
var DEFAULT_ANIMATION_STATE = {
|
|
2634
|
-
opacity: 1,
|
|
2635
|
-
scale: 1,
|
|
2636
|
-
translateX: 0,
|
|
2637
|
-
translateY: 0,
|
|
2638
|
-
fillProgress: 1,
|
|
2639
|
-
isActive: false,
|
|
2640
|
-
visibleCharacters: -1
|
|
2641
|
-
};
|
|
2642
|
-
function easeOutQuad2(t) {
|
|
2643
|
-
return t * (2 - t);
|
|
2644
|
-
}
|
|
2645
|
-
function easeInOutQuad(t) {
|
|
2646
|
-
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
2647
|
-
}
|
|
2648
|
-
function easeOutBack(t) {
|
|
2649
|
-
const c1 = 1.70158;
|
|
2650
|
-
const c3 = c1 + 1;
|
|
2651
|
-
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
2652
|
-
}
|
|
2653
|
-
function easeOutCirc(t) {
|
|
2654
|
-
return Math.sqrt(1 - Math.pow(t - 1, 2));
|
|
2636
|
+
// src/core/rich-caption-layout.ts
|
|
2637
|
+
var import_lru_cache = require("lru-cache");
|
|
2638
|
+
var ASCENT_RATIO = 0.8;
|
|
2639
|
+
var DESCENT_RATIO = 0.2;
|
|
2640
|
+
function isRTLText(text) {
|
|
2641
|
+
return containsRTLCharacters(text);
|
|
2655
2642
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2643
|
+
var WordTimingStore = class {
|
|
2644
|
+
startTimes;
|
|
2645
|
+
endTimes;
|
|
2646
|
+
xPositions;
|
|
2647
|
+
yPositions;
|
|
2648
|
+
widths;
|
|
2649
|
+
words;
|
|
2650
|
+
length;
|
|
2651
|
+
constructor(words) {
|
|
2652
|
+
this.length = words.length;
|
|
2653
|
+
this.startTimes = new Uint32Array(this.length);
|
|
2654
|
+
this.endTimes = new Uint32Array(this.length);
|
|
2655
|
+
this.xPositions = new Float32Array(this.length);
|
|
2656
|
+
this.yPositions = new Float32Array(this.length);
|
|
2657
|
+
this.widths = new Float32Array(this.length);
|
|
2658
|
+
this.words = new Array(this.length);
|
|
2659
|
+
for (let i = 0; i < this.length; i++) {
|
|
2660
|
+
this.startTimes[i] = Math.floor(words[i].start);
|
|
2661
|
+
this.endTimes[i] = Math.floor(words[i].end);
|
|
2662
|
+
this.words[i] = words[i].text;
|
|
2663
|
+
}
|
|
2664
2664
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2665
|
+
};
|
|
2666
|
+
function findWordAtTime(store, timeMs) {
|
|
2667
|
+
let left = 0;
|
|
2668
|
+
let right = store.length - 1;
|
|
2669
|
+
while (left <= right) {
|
|
2670
|
+
const mid = left + right >>> 1;
|
|
2671
|
+
const start = store.startTimes[mid];
|
|
2672
|
+
const end = store.endTimes[mid];
|
|
2673
|
+
if (timeMs >= start && timeMs < end) {
|
|
2674
|
+
return mid;
|
|
2675
|
+
}
|
|
2676
|
+
if (timeMs < start) {
|
|
2677
|
+
right = mid - 1;
|
|
2678
|
+
} else {
|
|
2679
|
+
left = mid + 1;
|
|
2680
|
+
}
|
|
2667
2681
|
}
|
|
2668
|
-
return
|
|
2669
|
-
}
|
|
2670
|
-
function clamp(value, min, max) {
|
|
2671
|
-
return Math.min(Math.max(value, min), max);
|
|
2682
|
+
return -1;
|
|
2672
2683
|
}
|
|
2673
|
-
function
|
|
2674
|
-
if (
|
|
2675
|
-
return
|
|
2684
|
+
function groupWordsByPause(store, pauseThreshold = 500) {
|
|
2685
|
+
if (store.length === 0) {
|
|
2686
|
+
return [];
|
|
2676
2687
|
}
|
|
2677
|
-
const
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2688
|
+
const groups = [];
|
|
2689
|
+
let currentGroup = [];
|
|
2690
|
+
for (let i = 0; i < store.length; i++) {
|
|
2691
|
+
if (currentGroup.length === 0) {
|
|
2692
|
+
currentGroup.push(i);
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
const prevEnd = store.endTimes[currentGroup[currentGroup.length - 1]];
|
|
2696
|
+
const currStart = store.startTimes[i];
|
|
2697
|
+
const gap = currStart - prevEnd;
|
|
2698
|
+
const prevText = store.words[currentGroup[currentGroup.length - 1]];
|
|
2699
|
+
const endsWithPunctuation = /[.!?]$/.test(prevText);
|
|
2700
|
+
if (gap >= pauseThreshold || endsWithPunctuation) {
|
|
2701
|
+
groups.push(currentGroup);
|
|
2702
|
+
currentGroup = [i];
|
|
2703
|
+
} else {
|
|
2704
|
+
currentGroup.push(i);
|
|
2705
|
+
}
|
|
2684
2706
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
return ctx.currentTime >= ctx.wordStart && ctx.currentTime < ctx.wordEnd;
|
|
2707
|
+
if (currentGroup.length > 0) {
|
|
2708
|
+
groups.push(currentGroup);
|
|
2709
|
+
}
|
|
2710
|
+
return groups;
|
|
2690
2711
|
}
|
|
2691
|
-
function
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2712
|
+
function breakIntoLines(wordWidths, maxWidth, spaceWidth) {
|
|
2713
|
+
const lines = [];
|
|
2714
|
+
let currentLine = [];
|
|
2715
|
+
let currentWidth = 0;
|
|
2716
|
+
for (let i = 0; i < wordWidths.length; i++) {
|
|
2717
|
+
const wordWidth = wordWidths[i];
|
|
2718
|
+
const spaceNeeded = currentLine.length > 0 ? spaceWidth : 0;
|
|
2719
|
+
if (currentWidth + spaceNeeded + wordWidth <= maxWidth) {
|
|
2720
|
+
currentLine.push(i);
|
|
2721
|
+
currentWidth += spaceNeeded + wordWidth;
|
|
2722
|
+
} else {
|
|
2723
|
+
if (currentLine.length > 0) {
|
|
2724
|
+
lines.push(currentLine);
|
|
2725
|
+
}
|
|
2726
|
+
currentLine = [i];
|
|
2727
|
+
currentWidth = wordWidth;
|
|
2728
|
+
}
|
|
2703
2729
|
}
|
|
2704
|
-
if (
|
|
2705
|
-
|
|
2706
|
-
fillProgress: 1,
|
|
2707
|
-
isActive: false,
|
|
2708
|
-
opacity: 1
|
|
2709
|
-
};
|
|
2730
|
+
if (currentLine.length > 0) {
|
|
2731
|
+
lines.push(currentLine);
|
|
2710
2732
|
}
|
|
2711
|
-
return
|
|
2712
|
-
fillProgress: calculateWordProgress(adjustedCtx),
|
|
2713
|
-
isActive,
|
|
2714
|
-
opacity: 1
|
|
2715
|
-
};
|
|
2733
|
+
return lines;
|
|
2716
2734
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
return {
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2735
|
+
var GLYPH_SIZE_ESTIMATE = 64;
|
|
2736
|
+
function createShapedWordCache() {
|
|
2737
|
+
return new import_lru_cache.LRUCache({
|
|
2738
|
+
max: 5e4,
|
|
2739
|
+
maxSize: 50 * 1024 * 1024,
|
|
2740
|
+
maxEntrySize: 100 * 1024,
|
|
2741
|
+
sizeCalculation: (value, key) => {
|
|
2742
|
+
const keySize = key.length * 2;
|
|
2743
|
+
const glyphsSize = value.glyphs.length * GLYPH_SIZE_ESTIMATE;
|
|
2744
|
+
return keySize + glyphsSize + 100;
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2724
2747
|
}
|
|
2725
|
-
function
|
|
2726
|
-
|
|
2727
|
-
return {
|
|
2728
|
-
scale: 0.5,
|
|
2729
|
-
opacity: 0,
|
|
2730
|
-
isActive: false,
|
|
2731
|
-
fillProgress: 0
|
|
2732
|
-
};
|
|
2733
|
-
}
|
|
2734
|
-
const adjustedDuration = ctx.animationDuration / speed;
|
|
2735
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2736
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2737
|
-
const easedProgress = easeOutBack(progress);
|
|
2738
|
-
const startScale = 0.5;
|
|
2739
|
-
const isActive = isWordActive(ctx);
|
|
2740
|
-
const endScale = isActive ? activeScale : 1;
|
|
2741
|
-
const scale = startScale + (endScale - startScale) * easedProgress;
|
|
2742
|
-
return {
|
|
2743
|
-
scale: Math.min(scale, activeScale),
|
|
2744
|
-
opacity: easedProgress,
|
|
2745
|
-
isActive,
|
|
2746
|
-
fillProgress: isActive ? 1 : 0
|
|
2747
|
-
};
|
|
2748
|
+
function makeShapingKey(text, fontFamily, fontSize, fontWeight, letterSpacing = 0) {
|
|
2749
|
+
return `${text}\0${fontFamily}\0${fontSize}\0${fontWeight}\0${letterSpacing}`;
|
|
2748
2750
|
}
|
|
2749
|
-
function
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2751
|
+
function transformText(text, transform) {
|
|
2752
|
+
switch (transform) {
|
|
2753
|
+
case "uppercase":
|
|
2754
|
+
return text.toUpperCase();
|
|
2755
|
+
case "lowercase":
|
|
2756
|
+
return text.toLowerCase();
|
|
2757
|
+
case "capitalize":
|
|
2758
|
+
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2759
|
+
default:
|
|
2760
|
+
return text;
|
|
2756
2761
|
}
|
|
2757
|
-
const adjustedDuration = ctx.animationDuration / speed;
|
|
2758
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2759
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2760
|
-
const easedProgress = easeInOutQuad(progress);
|
|
2761
|
-
const isActive = isWordActive(ctx);
|
|
2762
|
-
return {
|
|
2763
|
-
opacity: easedProgress,
|
|
2764
|
-
isActive,
|
|
2765
|
-
fillProgress: isActive ? 1 : 0
|
|
2766
|
-
};
|
|
2767
2762
|
}
|
|
2768
|
-
function
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
return {
|
|
2773
|
-
translateX: offset2.x,
|
|
2774
|
-
translateY: offset2.y,
|
|
2775
|
-
opacity: 0,
|
|
2776
|
-
isActive: false,
|
|
2777
|
-
fillProgress: 0
|
|
2778
|
-
};
|
|
2763
|
+
function splitIntoChunks(arr, chunkSize) {
|
|
2764
|
+
const chunks = [];
|
|
2765
|
+
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
2766
|
+
chunks.push(arr.slice(i, i + chunkSize));
|
|
2779
2767
|
}
|
|
2780
|
-
|
|
2781
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2782
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2783
|
-
const easedProgress = easeOutCirc(progress);
|
|
2784
|
-
const offset = getDirectionOffset(direction, slideDistance);
|
|
2785
|
-
const translateX = offset.x * (1 - easedProgress);
|
|
2786
|
-
const translateY = offset.y * (1 - easedProgress);
|
|
2787
|
-
const isActive = isWordActive(ctx);
|
|
2788
|
-
return {
|
|
2789
|
-
translateX,
|
|
2790
|
-
translateY,
|
|
2791
|
-
opacity: easeOutQuad2(progress),
|
|
2792
|
-
isActive,
|
|
2793
|
-
fillProgress: isActive ? 1 : 0
|
|
2794
|
-
};
|
|
2768
|
+
return chunks;
|
|
2795
2769
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
case "down":
|
|
2805
|
-
return { x: 0, y: distance };
|
|
2770
|
+
var CaptionLayoutEngine = class {
|
|
2771
|
+
fontRegistry;
|
|
2772
|
+
cache;
|
|
2773
|
+
layoutEngine;
|
|
2774
|
+
constructor(fontRegistry) {
|
|
2775
|
+
this.fontRegistry = fontRegistry;
|
|
2776
|
+
this.cache = createShapedWordCache();
|
|
2777
|
+
this.layoutEngine = new LayoutEngine(fontRegistry);
|
|
2806
2778
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2779
|
+
async measureWord(text, config) {
|
|
2780
|
+
const transformedText = transformText(text, config.textTransform);
|
|
2781
|
+
const cacheKey = makeShapingKey(
|
|
2782
|
+
transformedText,
|
|
2783
|
+
config.fontFamily,
|
|
2784
|
+
config.fontSize,
|
|
2785
|
+
config.fontWeight,
|
|
2786
|
+
config.letterSpacing
|
|
2787
|
+
);
|
|
2788
|
+
const cached = this.cache.get(cacheKey);
|
|
2789
|
+
if (cached) {
|
|
2790
|
+
return cached;
|
|
2791
|
+
}
|
|
2792
|
+
const lines = await this.layoutEngine.layout({
|
|
2793
|
+
text: transformedText,
|
|
2794
|
+
width: 1e5,
|
|
2795
|
+
letterSpacing: config.letterSpacing,
|
|
2796
|
+
fontSize: config.fontSize,
|
|
2797
|
+
lineHeight: 1,
|
|
2798
|
+
desc: { family: config.fontFamily, weight: config.fontWeight },
|
|
2799
|
+
textTransform: "none"
|
|
2800
|
+
});
|
|
2801
|
+
const width = lines[0]?.width ?? 0;
|
|
2802
|
+
const glyphs = lines[0]?.glyphs ?? [];
|
|
2803
|
+
const isRTL = isRTLText(transformedText);
|
|
2804
|
+
const shaped = {
|
|
2805
|
+
text: transformedText,
|
|
2806
|
+
width,
|
|
2807
|
+
glyphs: glyphs.map((g) => ({
|
|
2808
|
+
id: g.id,
|
|
2809
|
+
xAdvance: g.xAdvance,
|
|
2810
|
+
xOffset: g.xOffset,
|
|
2811
|
+
yOffset: g.yOffset,
|
|
2812
|
+
cluster: g.cluster
|
|
2813
|
+
})),
|
|
2814
|
+
isRTL
|
|
2815
|
+
};
|
|
2816
|
+
this.cache.set(cacheKey, shaped);
|
|
2817
|
+
return shaped;
|
|
2818
|
+
}
|
|
2819
|
+
async layoutCaption(words, config) {
|
|
2820
|
+
const store = new WordTimingStore(words);
|
|
2821
|
+
const measurementConfig = {
|
|
2822
|
+
fontFamily: config.fontFamily,
|
|
2823
|
+
fontSize: config.fontSize,
|
|
2824
|
+
fontWeight: config.fontWeight,
|
|
2825
|
+
letterSpacing: config.letterSpacing,
|
|
2826
|
+
textTransform: config.textTransform
|
|
2827
|
+
};
|
|
2828
|
+
const shapedWords = await Promise.all(
|
|
2829
|
+
words.map((w) => this.measureWord(w.text, measurementConfig))
|
|
2830
|
+
);
|
|
2831
|
+
if (config.measureTextWidth) {
|
|
2832
|
+
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
2833
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2834
|
+
store.widths[i] = config.measureTextWidth(shapedWords[i].text, fontString);
|
|
2835
|
+
}
|
|
2836
|
+
} else {
|
|
2837
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2838
|
+
store.widths[i] = shapedWords[i].width;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
if (config.textTransform !== "none") {
|
|
2842
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2843
|
+
store.words[i] = shapedWords[i].text;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
const wordGroups = groupWordsByPause(store, config.pauseThreshold);
|
|
2847
|
+
const pixelMaxWidth = config.availableWidth;
|
|
2848
|
+
let spaceWidth;
|
|
2849
|
+
if (config.measureTextWidth) {
|
|
2850
|
+
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
2851
|
+
spaceWidth = config.measureTextWidth(" ", fontString) + config.wordSpacing;
|
|
2852
|
+
} else {
|
|
2853
|
+
const spaceWord = await this.measureWord(" ", measurementConfig);
|
|
2854
|
+
spaceWidth = spaceWord.width + config.wordSpacing;
|
|
2855
|
+
}
|
|
2856
|
+
const groups = wordGroups.flatMap((indices) => {
|
|
2857
|
+
const groupWidths = indices.map((i) => store.widths[i]);
|
|
2858
|
+
const allLines = breakIntoLines(
|
|
2859
|
+
groupWidths,
|
|
2860
|
+
pixelMaxWidth,
|
|
2861
|
+
spaceWidth
|
|
2862
|
+
);
|
|
2863
|
+
const lineChunks = splitIntoChunks(allLines, config.maxLines);
|
|
2864
|
+
return lineChunks.map((chunkLines) => {
|
|
2865
|
+
const lines = chunkLines.map((lineWordIndices, lineIndex) => {
|
|
2866
|
+
const actualIndices = lineWordIndices.map((i) => indices[i]);
|
|
2867
|
+
const lineWidth = actualIndices.reduce((sum, idx) => sum + store.widths[idx], 0) + (actualIndices.length - 1) * spaceWidth;
|
|
2868
|
+
return {
|
|
2869
|
+
wordIndices: actualIndices,
|
|
2870
|
+
x: 0,
|
|
2871
|
+
y: lineIndex * config.fontSize * config.lineHeight,
|
|
2872
|
+
width: lineWidth,
|
|
2873
|
+
height: config.fontSize
|
|
2874
|
+
};
|
|
2875
|
+
});
|
|
2876
|
+
const allWordIndices = lines.flatMap((l) => l.wordIndices);
|
|
2877
|
+
if (allWordIndices.length === 0) {
|
|
2878
|
+
return null;
|
|
2879
|
+
}
|
|
2880
|
+
return {
|
|
2881
|
+
wordIndices: allWordIndices,
|
|
2882
|
+
startTime: store.startTimes[allWordIndices[0]],
|
|
2883
|
+
endTime: store.endTimes[allWordIndices[allWordIndices.length - 1]],
|
|
2884
|
+
lines
|
|
2885
|
+
};
|
|
2886
|
+
}).filter((g) => g !== null);
|
|
2887
|
+
});
|
|
2888
|
+
const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
|
|
2889
|
+
const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
|
|
2890
|
+
const calculateGroupY = (group) => {
|
|
2891
|
+
const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
|
|
2892
|
+
switch (config.verticalAlign) {
|
|
2893
|
+
case "top":
|
|
2894
|
+
return config.padding.top + config.fontSize * ASCENT_RATIO;
|
|
2895
|
+
case "bottom":
|
|
2896
|
+
return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO;
|
|
2897
|
+
case "middle":
|
|
2898
|
+
default:
|
|
2899
|
+
return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO;
|
|
2900
|
+
}
|
|
2901
|
+
};
|
|
2902
|
+
const allWordTexts = store.words.slice(0, store.length);
|
|
2903
|
+
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
2904
|
+
const calculateLineX = (lineWidth) => {
|
|
2905
|
+
switch (config.horizontalAlign) {
|
|
2906
|
+
case "left":
|
|
2907
|
+
return config.padding.left;
|
|
2908
|
+
case "right":
|
|
2909
|
+
return config.frameWidth - lineWidth - config.padding.right;
|
|
2910
|
+
case "center":
|
|
2911
|
+
default:
|
|
2912
|
+
return config.padding.left + (contentWidth - lineWidth) / 2;
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
for (const group of groups) {
|
|
2916
|
+
const baseY = calculateGroupY(group);
|
|
2917
|
+
for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
|
|
2918
|
+
const line = group.lines[lineIdx];
|
|
2919
|
+
line.x = calculateLineX(line.width);
|
|
2920
|
+
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
2921
|
+
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
2922
|
+
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
2923
|
+
let xCursor = line.x;
|
|
2924
|
+
for (const visualIdx of visualOrder) {
|
|
2925
|
+
const wordIdx = line.wordIndices[visualIdx];
|
|
2926
|
+
store.xPositions[wordIdx] = xCursor;
|
|
2927
|
+
store.yPositions[wordIdx] = line.y;
|
|
2928
|
+
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2811
2932
|
return {
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2933
|
+
store,
|
|
2934
|
+
groups,
|
|
2935
|
+
shapedWords,
|
|
2936
|
+
paragraphDirection
|
|
2816
2937
|
};
|
|
2817
2938
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2939
|
+
getVisibleWordsAtTime(layout, timeMs) {
|
|
2940
|
+
const activeGroup = layout.groups.find(
|
|
2941
|
+
(g) => timeMs >= g.startTime && timeMs <= g.endTime
|
|
2942
|
+
);
|
|
2943
|
+
if (!activeGroup) {
|
|
2944
|
+
return [];
|
|
2945
|
+
}
|
|
2946
|
+
return activeGroup.wordIndices.map((idx) => ({
|
|
2947
|
+
wordIndex: idx,
|
|
2948
|
+
text: layout.store.words[idx],
|
|
2949
|
+
x: layout.store.xPositions[idx],
|
|
2950
|
+
y: layout.store.yPositions[idx],
|
|
2951
|
+
width: layout.store.widths[idx],
|
|
2952
|
+
startTime: layout.store.startTimes[idx],
|
|
2953
|
+
endTime: layout.store.endTimes[idx],
|
|
2954
|
+
isRTL: layout.shapedWords[idx].isRTL
|
|
2955
|
+
}));
|
|
2956
|
+
}
|
|
2957
|
+
getActiveWordAtTime(layout, timeMs) {
|
|
2958
|
+
const wordIndex = findWordAtTime(layout.store, timeMs);
|
|
2959
|
+
if (wordIndex === -1) {
|
|
2960
|
+
return null;
|
|
2961
|
+
}
|
|
2836
2962
|
return {
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2963
|
+
wordIndex,
|
|
2964
|
+
text: layout.store.words[wordIndex],
|
|
2965
|
+
x: layout.store.xPositions[wordIndex],
|
|
2966
|
+
y: layout.store.yPositions[wordIndex],
|
|
2967
|
+
width: layout.store.widths[wordIndex],
|
|
2968
|
+
startTime: layout.store.startTimes[wordIndex],
|
|
2969
|
+
endTime: layout.store.endTimes[wordIndex],
|
|
2970
|
+
isRTL: layout.shapedWords[wordIndex].isRTL
|
|
2840
2971
|
};
|
|
2841
2972
|
}
|
|
2842
|
-
|
|
2973
|
+
clearCache() {
|
|
2974
|
+
this.cache.clear();
|
|
2975
|
+
}
|
|
2976
|
+
getCacheStats() {
|
|
2843
2977
|
return {
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
isActive: false
|
|
2978
|
+
size: this.cache.size,
|
|
2979
|
+
calculatedSize: this.cache.calculatedSize
|
|
2847
2980
|
};
|
|
2848
2981
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2982
|
+
};
|
|
2983
|
+
|
|
2984
|
+
// src/core/rich-caption-animator.ts
|
|
2985
|
+
var ANIMATION_DURATIONS = {
|
|
2986
|
+
karaoke: 0,
|
|
2987
|
+
highlight: 0,
|
|
2988
|
+
pop: 200,
|
|
2989
|
+
fade: 150,
|
|
2990
|
+
slide: 250,
|
|
2991
|
+
bounce: 400,
|
|
2992
|
+
typewriter: 0,
|
|
2993
|
+
none: 0
|
|
2994
|
+
};
|
|
2995
|
+
var DEFAULT_ANIMATION_STATE = {
|
|
2996
|
+
opacity: 1,
|
|
2997
|
+
scale: 1,
|
|
2998
|
+
translateX: 0,
|
|
2999
|
+
translateY: 0,
|
|
3000
|
+
fillProgress: 1,
|
|
3001
|
+
isActive: false,
|
|
3002
|
+
visibleCharacters: -1
|
|
3003
|
+
};
|
|
3004
|
+
function easeOutQuad2(t) {
|
|
3005
|
+
return t * (2 - t);
|
|
3006
|
+
}
|
|
3007
|
+
function easeInOutQuad(t) {
|
|
3008
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
3009
|
+
}
|
|
3010
|
+
function easeOutBack(t) {
|
|
3011
|
+
const c1 = 1.70158;
|
|
3012
|
+
const c3 = c1 + 1;
|
|
3013
|
+
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
3014
|
+
}
|
|
3015
|
+
function easeOutCirc(t) {
|
|
3016
|
+
return Math.sqrt(1 - Math.pow(t - 1, 2));
|
|
3017
|
+
}
|
|
3018
|
+
function easeOutBounce(t) {
|
|
3019
|
+
const n1 = 7.5625;
|
|
3020
|
+
const d1 = 2.75;
|
|
3021
|
+
if (t < 1 / d1) {
|
|
3022
|
+
return n1 * t * t;
|
|
3023
|
+
}
|
|
3024
|
+
if (t < 2 / d1) {
|
|
3025
|
+
return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
3026
|
+
}
|
|
3027
|
+
if (t < 2.5 / d1) {
|
|
3028
|
+
return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
3029
|
+
}
|
|
3030
|
+
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
3031
|
+
}
|
|
3032
|
+
function clamp(value, min, max) {
|
|
3033
|
+
return Math.min(Math.max(value, min), max);
|
|
3034
|
+
}
|
|
3035
|
+
function calculateAnimationProgress(ctx) {
|
|
3036
|
+
if (ctx.animationDuration <= 0) {
|
|
3037
|
+
return ctx.currentTime >= ctx.wordStart ? 1 : 0;
|
|
3038
|
+
}
|
|
3039
|
+
const elapsed = ctx.currentTime - ctx.wordStart;
|
|
3040
|
+
return clamp(elapsed / ctx.animationDuration, 0, 1);
|
|
3041
|
+
}
|
|
3042
|
+
function calculateWordProgress(ctx) {
|
|
3043
|
+
const duration = ctx.wordEnd - ctx.wordStart;
|
|
3044
|
+
if (duration <= 0) {
|
|
3045
|
+
return ctx.currentTime >= ctx.wordStart ? 1 : 0;
|
|
3046
|
+
}
|
|
3047
|
+
const elapsed = ctx.currentTime - ctx.wordStart;
|
|
3048
|
+
return clamp(elapsed / duration, 0, 1);
|
|
3049
|
+
}
|
|
3050
|
+
function isWordActive(ctx) {
|
|
3051
|
+
return ctx.currentTime >= ctx.wordStart && ctx.currentTime < ctx.wordEnd;
|
|
3052
|
+
}
|
|
3053
|
+
function calculateKaraokeState(ctx) {
|
|
3054
|
+
const isActive = isWordActive(ctx);
|
|
3055
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
3056
|
+
return {
|
|
3057
|
+
fillProgress: 0,
|
|
3058
|
+
isActive: false,
|
|
3059
|
+
opacity: 1
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
if (ctx.currentTime >= ctx.wordEnd) {
|
|
3063
|
+
return {
|
|
3064
|
+
fillProgress: 1,
|
|
3065
|
+
isActive: false,
|
|
3066
|
+
opacity: 1
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
return {
|
|
3070
|
+
fillProgress: calculateWordProgress(ctx),
|
|
3071
|
+
isActive,
|
|
3072
|
+
opacity: 1
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
function calculateHighlightState(ctx) {
|
|
3076
|
+
const isActive = isWordActive(ctx);
|
|
3077
|
+
return {
|
|
3078
|
+
isActive,
|
|
3079
|
+
fillProgress: isActive ? 1 : 0,
|
|
3080
|
+
opacity: 1
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
function calculatePopState(ctx, activeScale) {
|
|
3084
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
3085
|
+
return {
|
|
3086
|
+
scale: 0.5,
|
|
3087
|
+
opacity: 0,
|
|
3088
|
+
isActive: false,
|
|
3089
|
+
fillProgress: 0
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
const progress = calculateAnimationProgress(ctx);
|
|
3093
|
+
const easedProgress = easeOutBack(progress);
|
|
3094
|
+
const startScale = 0.5;
|
|
3095
|
+
const isActive = isWordActive(ctx);
|
|
3096
|
+
const endScale = isActive ? activeScale : 1;
|
|
3097
|
+
const scale = startScale + (endScale - startScale) * easedProgress;
|
|
3098
|
+
return {
|
|
3099
|
+
scale: Math.min(scale, activeScale),
|
|
3100
|
+
opacity: easedProgress,
|
|
3101
|
+
isActive,
|
|
3102
|
+
fillProgress: isActive ? 1 : 0
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
function calculateFadeState(ctx) {
|
|
3106
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
3107
|
+
return {
|
|
3108
|
+
opacity: 0,
|
|
3109
|
+
isActive: false,
|
|
3110
|
+
fillProgress: 0
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
const progress = calculateAnimationProgress(ctx);
|
|
3114
|
+
const easedProgress = easeInOutQuad(progress);
|
|
3115
|
+
const isActive = isWordActive(ctx);
|
|
3116
|
+
return {
|
|
3117
|
+
opacity: easedProgress,
|
|
3118
|
+
isActive,
|
|
3119
|
+
fillProgress: isActive ? 1 : 0
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
function calculateSlideState(ctx, direction, fontSize) {
|
|
3123
|
+
const slideDistance = fontSize * 1.5;
|
|
3124
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
3125
|
+
const offset2 = getDirectionOffset(direction, slideDistance);
|
|
3126
|
+
return {
|
|
3127
|
+
translateX: offset2.x,
|
|
3128
|
+
translateY: offset2.y,
|
|
3129
|
+
opacity: 0,
|
|
3130
|
+
isActive: false,
|
|
3131
|
+
fillProgress: 0
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
const progress = calculateAnimationProgress(ctx);
|
|
3135
|
+
const easedProgress = easeOutCirc(progress);
|
|
3136
|
+
const offset = getDirectionOffset(direction, slideDistance);
|
|
3137
|
+
const translateX = offset.x * (1 - easedProgress);
|
|
3138
|
+
const translateY = offset.y * (1 - easedProgress);
|
|
3139
|
+
const isActive = isWordActive(ctx);
|
|
3140
|
+
return {
|
|
3141
|
+
translateX,
|
|
3142
|
+
translateY,
|
|
3143
|
+
opacity: easeOutQuad2(progress),
|
|
3144
|
+
isActive,
|
|
3145
|
+
fillProgress: isActive ? 1 : 0
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
function getDirectionOffset(direction, distance) {
|
|
3149
|
+
switch (direction) {
|
|
3150
|
+
case "left":
|
|
3151
|
+
return { x: -distance, y: 0 };
|
|
3152
|
+
case "right":
|
|
3153
|
+
return { x: distance, y: 0 };
|
|
3154
|
+
case "up":
|
|
3155
|
+
return { x: 0, y: distance };
|
|
3156
|
+
case "down":
|
|
3157
|
+
return { x: 0, y: -distance };
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
function calculateBounceState(ctx, fontSize) {
|
|
3161
|
+
const bounceDistance = fontSize * 0.8;
|
|
3162
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
3163
|
+
return {
|
|
3164
|
+
translateY: -bounceDistance,
|
|
3165
|
+
opacity: 0,
|
|
3166
|
+
isActive: false,
|
|
3167
|
+
fillProgress: 0
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
const progress = calculateAnimationProgress(ctx);
|
|
3171
|
+
const easedProgress = easeOutBounce(progress);
|
|
3172
|
+
const isActive = isWordActive(ctx);
|
|
3173
|
+
return {
|
|
3174
|
+
translateY: -bounceDistance * (1 - easedProgress),
|
|
3175
|
+
opacity: easeOutQuad2(progress),
|
|
3176
|
+
isActive,
|
|
3177
|
+
fillProgress: isActive ? 1 : 0
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
function calculateTypewriterState(ctx, charCount) {
|
|
3181
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
3182
|
+
return {
|
|
3183
|
+
visibleCharacters: 0,
|
|
3184
|
+
opacity: 1,
|
|
3185
|
+
isActive: false,
|
|
3186
|
+
fillProgress: 0
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
if (ctx.currentTime >= ctx.wordEnd) {
|
|
3190
|
+
return {
|
|
3191
|
+
visibleCharacters: charCount,
|
|
3192
|
+
opacity: 1,
|
|
3193
|
+
isActive: false,
|
|
3194
|
+
fillProgress: 0
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
const progress = calculateWordProgress(ctx);
|
|
3198
|
+
const visibleCharacters = Math.ceil(progress * charCount);
|
|
3199
|
+
const isActive = isWordActive(ctx);
|
|
3200
|
+
return {
|
|
3201
|
+
visibleCharacters: clamp(visibleCharacters, 0, charCount),
|
|
3202
|
+
opacity: 1,
|
|
3203
|
+
isActive,
|
|
3204
|
+
fillProgress: isActive ? 1 : 0
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
function calculateNoneState(_ctx) {
|
|
3208
|
+
return {
|
|
3209
|
+
opacity: 1,
|
|
3210
|
+
isActive: false,
|
|
3211
|
+
fillProgress: 0
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
|
|
3215
|
+
const ctx = {
|
|
3216
|
+
wordStart,
|
|
3217
|
+
wordEnd,
|
|
3218
|
+
currentTime,
|
|
3219
|
+
animationDuration: ANIMATION_DURATIONS[config.style]
|
|
3220
|
+
};
|
|
3221
|
+
const baseState = { ...DEFAULT_ANIMATION_STATE };
|
|
2873
3222
|
let partialState;
|
|
2874
3223
|
switch (config.style) {
|
|
2875
3224
|
case "karaoke":
|
|
2876
|
-
partialState = calculateKaraokeState(ctx
|
|
3225
|
+
partialState = calculateKaraokeState(ctx);
|
|
2877
3226
|
break;
|
|
2878
3227
|
case "highlight":
|
|
2879
3228
|
partialState = calculateHighlightState(ctx);
|
|
2880
3229
|
break;
|
|
2881
3230
|
case "pop":
|
|
2882
|
-
partialState = calculatePopState(ctx, activeScale
|
|
3231
|
+
partialState = calculatePopState(ctx, activeScale);
|
|
2883
3232
|
break;
|
|
2884
3233
|
case "fade":
|
|
2885
|
-
partialState = calculateFadeState(ctx
|
|
3234
|
+
partialState = calculateFadeState(ctx);
|
|
2886
3235
|
break;
|
|
2887
3236
|
case "slide": {
|
|
2888
3237
|
const slideDir = mirrorAnimationDirection(config.direction, isRTL);
|
|
2889
|
-
partialState = calculateSlideState(ctx, slideDir,
|
|
3238
|
+
partialState = calculateSlideState(ctx, slideDir, fontSize);
|
|
2890
3239
|
break;
|
|
2891
3240
|
}
|
|
2892
3241
|
case "bounce":
|
|
2893
|
-
partialState = calculateBounceState(ctx,
|
|
3242
|
+
partialState = calculateBounceState(ctx, fontSize);
|
|
2894
3243
|
break;
|
|
2895
3244
|
case "typewriter":
|
|
2896
|
-
partialState = calculateTypewriterState(ctx, charCount
|
|
3245
|
+
partialState = calculateTypewriterState(ctx, charCount);
|
|
2897
3246
|
break;
|
|
2898
3247
|
case "none":
|
|
2899
3248
|
default:
|
|
@@ -2926,34 +3275,30 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
|
|
|
2926
3275
|
function getDefaultAnimationConfig() {
|
|
2927
3276
|
return {
|
|
2928
3277
|
style: "highlight",
|
|
2929
|
-
speed: 1,
|
|
2930
3278
|
direction: "up"
|
|
2931
3279
|
};
|
|
2932
3280
|
}
|
|
2933
3281
|
|
|
2934
3282
|
// src/core/rich-caption-generator.ts
|
|
2935
|
-
var ASCENT_RATIO = 0.8;
|
|
2936
|
-
var DESCENT_RATIO = 0.2;
|
|
2937
3283
|
var WORD_BG_OPACITY = 1;
|
|
2938
3284
|
var WORD_BG_BORDER_RADIUS = 4;
|
|
2939
3285
|
var WORD_BG_PADDING_RATIO = 0.12;
|
|
2940
3286
|
function extractFontConfig(asset) {
|
|
2941
3287
|
const font = asset.font;
|
|
2942
3288
|
const active = asset.active?.font;
|
|
2943
|
-
const
|
|
3289
|
+
const hasExplicitActiveColor = active?.color !== void 0;
|
|
2944
3290
|
const baseColor = font?.color ?? "#ffffff";
|
|
2945
3291
|
const baseOpacity = font?.opacity ?? 1;
|
|
2946
3292
|
let activeColor;
|
|
2947
3293
|
let activeOpacity;
|
|
2948
|
-
if (!
|
|
3294
|
+
if (!hasExplicitActiveColor) {
|
|
2949
3295
|
activeColor = baseColor;
|
|
2950
|
-
activeOpacity = baseOpacity;
|
|
3296
|
+
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2951
3297
|
} else {
|
|
2952
|
-
const explicitActiveColor = active?.color;
|
|
2953
3298
|
const animStyle = asset.wordAnimation?.style ?? "highlight";
|
|
2954
3299
|
const isFillAnimation = animStyle === "karaoke" || animStyle === "highlight";
|
|
2955
3300
|
const DEFAULT_ACTIVE_COLOR = "#ffff00";
|
|
2956
|
-
activeColor =
|
|
3301
|
+
activeColor = active.color ?? (isFillAnimation ? DEFAULT_ACTIVE_COLOR : baseColor);
|
|
2957
3302
|
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2958
3303
|
}
|
|
2959
3304
|
return {
|
|
@@ -2970,18 +3315,17 @@ function extractFontConfig(asset) {
|
|
|
2970
3315
|
function extractStrokeConfig(asset, isActive) {
|
|
2971
3316
|
const baseStroke = asset.stroke;
|
|
2972
3317
|
const activeStroke = asset.active?.stroke;
|
|
2973
|
-
if (!baseStroke && !activeStroke) {
|
|
2974
|
-
return void 0;
|
|
2975
|
-
}
|
|
2976
3318
|
if (isActive) {
|
|
2977
|
-
if (
|
|
3319
|
+
if (activeStroke === "none") {
|
|
2978
3320
|
return void 0;
|
|
2979
3321
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
3322
|
+
if (activeStroke && typeof activeStroke === "object") {
|
|
3323
|
+
return {
|
|
3324
|
+
width: activeStroke.width ?? baseStroke?.width ?? 0,
|
|
3325
|
+
color: activeStroke.color ?? baseStroke?.color ?? "#000000",
|
|
3326
|
+
opacity: activeStroke.opacity ?? baseStroke?.opacity ?? 1
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
2985
3329
|
}
|
|
2986
3330
|
if (baseStroke) {
|
|
2987
3331
|
return {
|
|
@@ -2993,25 +3337,42 @@ function extractStrokeConfig(asset, isActive) {
|
|
|
2993
3337
|
return void 0;
|
|
2994
3338
|
}
|
|
2995
3339
|
function extractShadowConfig(asset, isActive) {
|
|
3340
|
+
const baseShadow = asset.shadow;
|
|
3341
|
+
const activeShadow = asset.active?.shadow;
|
|
2996
3342
|
if (isActive) {
|
|
2997
|
-
|
|
3343
|
+
if (activeShadow === "none") {
|
|
3344
|
+
return void 0;
|
|
3345
|
+
}
|
|
3346
|
+
if (activeShadow && typeof activeShadow === "object") {
|
|
3347
|
+
return {
|
|
3348
|
+
offsetX: activeShadow.offsetX ?? baseShadow?.offsetX ?? 0,
|
|
3349
|
+
offsetY: activeShadow.offsetY ?? baseShadow?.offsetY ?? 0,
|
|
3350
|
+
blur: activeShadow.blur ?? baseShadow?.blur ?? 0,
|
|
3351
|
+
color: activeShadow.color ?? baseShadow?.color ?? "#000000",
|
|
3352
|
+
opacity: activeShadow.opacity ?? baseShadow?.opacity ?? 0.5
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
2998
3355
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3356
|
+
if (baseShadow) {
|
|
3357
|
+
return {
|
|
3358
|
+
offsetX: baseShadow.offsetX ?? 0,
|
|
3359
|
+
offsetY: baseShadow.offsetY ?? 0,
|
|
3360
|
+
blur: baseShadow.blur ?? 0,
|
|
3361
|
+
color: baseShadow.color ?? "#000000",
|
|
3362
|
+
opacity: baseShadow.opacity ?? 0.5
|
|
3363
|
+
};
|
|
3002
3364
|
}
|
|
3003
|
-
return
|
|
3004
|
-
offsetX: shadow.offsetX ?? 0,
|
|
3005
|
-
offsetY: shadow.offsetY ?? 0,
|
|
3006
|
-
blur: shadow.blur ?? 0,
|
|
3007
|
-
color: shadow.color ?? "#000000",
|
|
3008
|
-
opacity: shadow.opacity ?? 0.5
|
|
3009
|
-
};
|
|
3365
|
+
return void 0;
|
|
3010
3366
|
}
|
|
3011
3367
|
function extractBackgroundConfig(asset, isActive, fontSize) {
|
|
3012
3368
|
const fontBackground = asset.font?.background;
|
|
3013
3369
|
const activeBackground = asset.active?.font?.background;
|
|
3014
|
-
|
|
3370
|
+
let bgColor;
|
|
3371
|
+
if (isActive) {
|
|
3372
|
+
bgColor = activeBackground ?? fontBackground;
|
|
3373
|
+
} else {
|
|
3374
|
+
bgColor = fontBackground;
|
|
3375
|
+
}
|
|
3015
3376
|
if (!bgColor) {
|
|
3016
3377
|
return void 0;
|
|
3017
3378
|
}
|
|
@@ -3061,6 +3422,17 @@ function extractCaptionBorder(asset) {
|
|
|
3061
3422
|
radius: border.radius ?? 0
|
|
3062
3423
|
};
|
|
3063
3424
|
}
|
|
3425
|
+
function extractTextDecoration(asset, isActive) {
|
|
3426
|
+
const baseDecoration = asset.font?.textDecoration;
|
|
3427
|
+
const activeDecoration = asset.active?.font?.textDecoration;
|
|
3428
|
+
if (isActive && activeDecoration !== void 0) {
|
|
3429
|
+
return activeDecoration === "none" ? void 0 : activeDecoration;
|
|
3430
|
+
}
|
|
3431
|
+
if (!baseDecoration || baseDecoration === "none") {
|
|
3432
|
+
return void 0;
|
|
3433
|
+
}
|
|
3434
|
+
return baseDecoration;
|
|
3435
|
+
}
|
|
3064
3436
|
function extractAnimationConfig(asset) {
|
|
3065
3437
|
const wordAnim = asset.wordAnimation;
|
|
3066
3438
|
if (!wordAnim) {
|
|
@@ -3068,7 +3440,6 @@ function extractAnimationConfig(asset) {
|
|
|
3068
3440
|
}
|
|
3069
3441
|
return {
|
|
3070
3442
|
style: wordAnim.style ?? "highlight",
|
|
3071
|
-
speed: wordAnim.speed ?? 1,
|
|
3072
3443
|
direction: wordAnim.direction ?? "up"
|
|
3073
3444
|
};
|
|
3074
3445
|
}
|
|
@@ -3103,7 +3474,8 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
|
|
|
3103
3474
|
letterSpacing: fontConfig.letterSpacing > 0 ? fontConfig.letterSpacing : void 0,
|
|
3104
3475
|
stroke: extractStrokeConfig(asset, isActive),
|
|
3105
3476
|
shadow: extractShadowConfig(asset, isActive),
|
|
3106
|
-
background: extractBackgroundConfig(asset, isActive, fontConfig.size)
|
|
3477
|
+
background: extractBackgroundConfig(asset, isActive, fontConfig.size),
|
|
3478
|
+
textDecoration: extractTextDecoration(asset, isActive)
|
|
3107
3479
|
};
|
|
3108
3480
|
}
|
|
3109
3481
|
function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, config) {
|
|
@@ -3607,7 +3979,8 @@ async function createNodePainter(opts) {
|
|
|
3607
3979
|
context.lineCap = "round";
|
|
3608
3980
|
context.strokeText(displayText, 0, 0);
|
|
3609
3981
|
}
|
|
3610
|
-
|
|
3982
|
+
const sameColor = wordOp.activeColor === wordOp.baseColor && wordOp.activeOpacity === wordOp.baseOpacity;
|
|
3983
|
+
if (wordOp.fillProgress <= 0 || sameColor) {
|
|
3611
3984
|
const baseC = parseHex6(wordOp.baseColor, wordOp.baseOpacity);
|
|
3612
3985
|
context.fillStyle = `rgba(${baseC.r},${baseC.g},${baseC.b},${baseC.a})`;
|
|
3613
3986
|
context.fillText(displayText, 0, 0);
|
|
@@ -3634,7 +4007,26 @@ async function createNodePainter(opts) {
|
|
|
3634
4007
|
context.fillText(displayText, 0, 0);
|
|
3635
4008
|
context.restore();
|
|
3636
4009
|
}
|
|
3637
|
-
|
|
4010
|
+
if (wordOp.textDecoration) {
|
|
4011
|
+
const geo = decorationGeometry(wordOp.textDecoration, {
|
|
4012
|
+
baselineY: 0,
|
|
4013
|
+
fontSize: wordOp.fontSize,
|
|
4014
|
+
lineWidth: textWidth,
|
|
4015
|
+
xStart: 0
|
|
4016
|
+
});
|
|
4017
|
+
const sameC = wordOp.activeColor === wordOp.baseColor && wordOp.activeOpacity === wordOp.baseOpacity;
|
|
4018
|
+
const decoIsActive = wordOp.fillProgress >= 1 && !sameC;
|
|
4019
|
+
const decoColor = decoIsActive ? wordOp.activeColor : wordOp.baseColor;
|
|
4020
|
+
const decoOpacity = decoIsActive ? wordOp.activeOpacity : wordOp.baseOpacity;
|
|
4021
|
+
const dc = parseHex6(decoColor, decoOpacity);
|
|
4022
|
+
context.strokeStyle = `rgba(${dc.r},${dc.g},${dc.b},${dc.a})`;
|
|
4023
|
+
context.lineWidth = geo.width;
|
|
4024
|
+
context.beginPath();
|
|
4025
|
+
context.moveTo(geo.x1, geo.y);
|
|
4026
|
+
context.lineTo(geo.x2, geo.y);
|
|
4027
|
+
context.stroke();
|
|
4028
|
+
}
|
|
4029
|
+
context.restore();
|
|
3638
4030
|
}
|
|
3639
4031
|
});
|
|
3640
4032
|
continue;
|
|
@@ -4925,520 +5317,173 @@ function computeSimplePathBounds(d) {
|
|
|
4925
5317
|
minX = Math.min(minX, currentX);
|
|
4926
5318
|
maxX = Math.max(maxX, currentX);
|
|
4927
5319
|
}
|
|
4928
|
-
break;
|
|
4929
|
-
case "h":
|
|
4930
|
-
if (numIndex < numbers.length) {
|
|
4931
|
-
currentX += numbers[numIndex++];
|
|
4932
|
-
minX = Math.min(minX, currentX);
|
|
4933
|
-
maxX = Math.max(maxX, currentX);
|
|
4934
|
-
}
|
|
4935
|
-
break;
|
|
4936
|
-
case "V":
|
|
4937
|
-
if (numIndex < numbers.length) {
|
|
4938
|
-
currentY = numbers[numIndex++];
|
|
4939
|
-
minY = Math.min(minY, currentY);
|
|
4940
|
-
maxY = Math.max(maxY, currentY);
|
|
4941
|
-
}
|
|
4942
|
-
break;
|
|
4943
|
-
case "v":
|
|
4944
|
-
if (numIndex < numbers.length) {
|
|
4945
|
-
currentY += numbers[numIndex++];
|
|
4946
|
-
minY = Math.min(minY, currentY);
|
|
4947
|
-
maxY = Math.max(maxY, currentY);
|
|
4948
|
-
}
|
|
4949
|
-
break;
|
|
4950
|
-
case "C":
|
|
4951
|
-
if (numIndex + 5 < numbers.length) {
|
|
4952
|
-
for (let j = 0; j < 3; j++) {
|
|
4953
|
-
const x = numbers[numIndex++];
|
|
4954
|
-
const y = numbers[numIndex++];
|
|
4955
|
-
minX = Math.min(minX, x);
|
|
4956
|
-
maxX = Math.max(maxX, x);
|
|
4957
|
-
minY = Math.min(minY, y);
|
|
4958
|
-
maxY = Math.max(maxY, y);
|
|
4959
|
-
if (j === 2) {
|
|
4960
|
-
currentX = x;
|
|
4961
|
-
currentY = y;
|
|
4962
|
-
}
|
|
4963
|
-
}
|
|
4964
|
-
}
|
|
4965
|
-
break;
|
|
4966
|
-
case "c":
|
|
4967
|
-
if (numIndex + 5 < numbers.length) {
|
|
4968
|
-
for (let j = 0; j < 3; j++) {
|
|
4969
|
-
const x = currentX + numbers[numIndex++];
|
|
4970
|
-
const y = currentY + numbers[numIndex++];
|
|
4971
|
-
minX = Math.min(minX, x);
|
|
4972
|
-
maxX = Math.max(maxX, x);
|
|
4973
|
-
minY = Math.min(minY, y);
|
|
4974
|
-
maxY = Math.max(maxY, y);
|
|
4975
|
-
if (j === 2) {
|
|
4976
|
-
currentX = x;
|
|
4977
|
-
currentY = y;
|
|
4978
|
-
}
|
|
4979
|
-
}
|
|
4980
|
-
}
|
|
4981
|
-
break;
|
|
4982
|
-
case "S":
|
|
4983
|
-
case "Q":
|
|
4984
|
-
if (numIndex + 3 < numbers.length) {
|
|
4985
|
-
for (let j = 0; j < 2; j++) {
|
|
4986
|
-
const x = numbers[numIndex++];
|
|
4987
|
-
const y = numbers[numIndex++];
|
|
4988
|
-
minX = Math.min(minX, x);
|
|
4989
|
-
maxX = Math.max(maxX, x);
|
|
4990
|
-
minY = Math.min(minY, y);
|
|
4991
|
-
maxY = Math.max(maxY, y);
|
|
4992
|
-
if (j === 1) {
|
|
4993
|
-
currentX = x;
|
|
4994
|
-
currentY = y;
|
|
4995
|
-
}
|
|
4996
|
-
}
|
|
4997
|
-
}
|
|
4998
|
-
break;
|
|
4999
|
-
case "s":
|
|
5000
|
-
case "q":
|
|
5001
|
-
if (numIndex + 3 < numbers.length) {
|
|
5002
|
-
for (let j = 0; j < 2; j++) {
|
|
5003
|
-
const x = currentX + numbers[numIndex++];
|
|
5004
|
-
const y = currentY + numbers[numIndex++];
|
|
5005
|
-
minX = Math.min(minX, x);
|
|
5006
|
-
maxX = Math.max(maxX, x);
|
|
5007
|
-
minY = Math.min(minY, y);
|
|
5008
|
-
maxY = Math.max(maxY, y);
|
|
5009
|
-
if (j === 1) {
|
|
5010
|
-
currentX = x;
|
|
5011
|
-
currentY = y;
|
|
5012
|
-
}
|
|
5013
|
-
}
|
|
5014
|
-
}
|
|
5015
|
-
break;
|
|
5016
|
-
case "A":
|
|
5017
|
-
if (numIndex + 6 < numbers.length) {
|
|
5018
|
-
numIndex += 5;
|
|
5019
|
-
currentX = numbers[numIndex++];
|
|
5020
|
-
currentY = numbers[numIndex++];
|
|
5021
|
-
minX = Math.min(minX, currentX);
|
|
5022
|
-
maxX = Math.max(maxX, currentX);
|
|
5023
|
-
minY = Math.min(minY, currentY);
|
|
5024
|
-
maxY = Math.max(maxY, currentY);
|
|
5025
|
-
}
|
|
5026
|
-
break;
|
|
5027
|
-
case "a":
|
|
5028
|
-
if (numIndex + 6 < numbers.length) {
|
|
5029
|
-
numIndex += 5;
|
|
5030
|
-
currentX += numbers[numIndex++];
|
|
5031
|
-
currentY += numbers[numIndex++];
|
|
5032
|
-
minX = Math.min(minX, currentX);
|
|
5033
|
-
maxX = Math.max(maxX, currentX);
|
|
5034
|
-
minY = Math.min(minY, currentY);
|
|
5035
|
-
maxY = Math.max(maxY, currentY);
|
|
5036
|
-
}
|
|
5037
|
-
break;
|
|
5038
|
-
case "Z":
|
|
5039
|
-
case "z":
|
|
5040
|
-
break;
|
|
5041
|
-
}
|
|
5042
|
-
cmdIndex++;
|
|
5043
|
-
}
|
|
5044
|
-
if (minX === Infinity) {
|
|
5045
|
-
return { x: 0, y: 0, w: 0, h: 0 };
|
|
5046
|
-
}
|
|
5047
|
-
return {
|
|
5048
|
-
x: minX,
|
|
5049
|
-
y: minY,
|
|
5050
|
-
w: maxX - minX,
|
|
5051
|
-
h: maxY - minY
|
|
5052
|
-
};
|
|
5053
|
-
}
|
|
5054
|
-
async function renderSvgAssetToPng(asset, options = {}) {
|
|
5055
|
-
const defaultWidth = options.defaultWidth ?? 1920;
|
|
5056
|
-
const defaultHeight = options.defaultHeight ?? 1080;
|
|
5057
|
-
let svgString;
|
|
5058
|
-
let targetWidth;
|
|
5059
|
-
let targetHeight;
|
|
5060
|
-
if (asset.src) {
|
|
5061
|
-
svgString = asset.src;
|
|
5062
|
-
const dimensions = extractSvgDimensions(svgString);
|
|
5063
|
-
targetWidth = dimensions.width || defaultWidth;
|
|
5064
|
-
targetHeight = dimensions.height || defaultHeight;
|
|
5065
|
-
} else if (asset.shape) {
|
|
5066
|
-
targetWidth = toNumber(asset.width, defaultWidth);
|
|
5067
|
-
targetHeight = toNumber(asset.height, defaultHeight);
|
|
5068
|
-
svgString = shapeToSvgString(asset, targetWidth, targetHeight);
|
|
5069
|
-
} else {
|
|
5070
|
-
throw new Error("Either 'src' or 'shape' must be provided");
|
|
5071
|
-
}
|
|
5072
|
-
return renderSvgToPng(svgString, {
|
|
5073
|
-
width: targetWidth,
|
|
5074
|
-
height: targetHeight,
|
|
5075
|
-
background: options.background
|
|
5076
|
-
});
|
|
5077
|
-
}
|
|
5078
|
-
function extractSvgDimensions(svgString) {
|
|
5079
|
-
const widthMatch = svgString.match(/width\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
5080
|
-
const heightMatch = svgString.match(/height\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
5081
|
-
let width = widthMatch ? parseFloat(widthMatch[1]) : 0;
|
|
5082
|
-
let height = heightMatch ? parseFloat(heightMatch[1]) : 0;
|
|
5083
|
-
if (!width || !height) {
|
|
5084
|
-
const viewBoxMatch = svgString.match(/viewBox\s*=\s*["']([^"']+)["']/i);
|
|
5085
|
-
if (viewBoxMatch) {
|
|
5086
|
-
const parts = viewBoxMatch[1].trim().split(/[\s,]+/).map(parseFloat);
|
|
5087
|
-
if (parts.length === 4) {
|
|
5088
|
-
width = width || parts[2];
|
|
5089
|
-
height = height || parts[3];
|
|
5090
|
-
}
|
|
5091
|
-
}
|
|
5092
|
-
}
|
|
5093
|
-
return { width, height };
|
|
5094
|
-
}
|
|
5095
|
-
|
|
5096
|
-
// src/core/rich-caption-layout.ts
|
|
5097
|
-
var import_lru_cache = require("lru-cache");
|
|
5098
|
-
function isRTLText(text) {
|
|
5099
|
-
return containsRTLCharacters(text);
|
|
5100
|
-
}
|
|
5101
|
-
var WordTimingStore = class {
|
|
5102
|
-
startTimes;
|
|
5103
|
-
endTimes;
|
|
5104
|
-
xPositions;
|
|
5105
|
-
yPositions;
|
|
5106
|
-
widths;
|
|
5107
|
-
words;
|
|
5108
|
-
length;
|
|
5109
|
-
constructor(words) {
|
|
5110
|
-
this.length = words.length;
|
|
5111
|
-
this.startTimes = new Uint32Array(this.length);
|
|
5112
|
-
this.endTimes = new Uint32Array(this.length);
|
|
5113
|
-
this.xPositions = new Float32Array(this.length);
|
|
5114
|
-
this.yPositions = new Float32Array(this.length);
|
|
5115
|
-
this.widths = new Float32Array(this.length);
|
|
5116
|
-
this.words = new Array(this.length);
|
|
5117
|
-
for (let i = 0; i < this.length; i++) {
|
|
5118
|
-
this.startTimes[i] = Math.floor(words[i].start);
|
|
5119
|
-
this.endTimes[i] = Math.floor(words[i].end);
|
|
5120
|
-
this.words[i] = words[i].text;
|
|
5121
|
-
}
|
|
5122
|
-
}
|
|
5123
|
-
};
|
|
5124
|
-
function findWordAtTime(store, timeMs) {
|
|
5125
|
-
let left = 0;
|
|
5126
|
-
let right = store.length - 1;
|
|
5127
|
-
while (left <= right) {
|
|
5128
|
-
const mid = left + right >>> 1;
|
|
5129
|
-
const start = store.startTimes[mid];
|
|
5130
|
-
const end = store.endTimes[mid];
|
|
5131
|
-
if (timeMs >= start && timeMs < end) {
|
|
5132
|
-
return mid;
|
|
5133
|
-
}
|
|
5134
|
-
if (timeMs < start) {
|
|
5135
|
-
right = mid - 1;
|
|
5136
|
-
} else {
|
|
5137
|
-
left = mid + 1;
|
|
5138
|
-
}
|
|
5139
|
-
}
|
|
5140
|
-
return -1;
|
|
5141
|
-
}
|
|
5142
|
-
function groupWordsByPause(store, pauseThreshold = 500) {
|
|
5143
|
-
if (store.length === 0) {
|
|
5144
|
-
return [];
|
|
5145
|
-
}
|
|
5146
|
-
const groups = [];
|
|
5147
|
-
let currentGroup = [];
|
|
5148
|
-
for (let i = 0; i < store.length; i++) {
|
|
5149
|
-
if (currentGroup.length === 0) {
|
|
5150
|
-
currentGroup.push(i);
|
|
5151
|
-
continue;
|
|
5152
|
-
}
|
|
5153
|
-
const prevEnd = store.endTimes[currentGroup[currentGroup.length - 1]];
|
|
5154
|
-
const currStart = store.startTimes[i];
|
|
5155
|
-
const gap = currStart - prevEnd;
|
|
5156
|
-
const prevText = store.words[currentGroup[currentGroup.length - 1]];
|
|
5157
|
-
const endsWithPunctuation = /[.!?]$/.test(prevText);
|
|
5158
|
-
if (gap >= pauseThreshold || endsWithPunctuation) {
|
|
5159
|
-
groups.push(currentGroup);
|
|
5160
|
-
currentGroup = [i];
|
|
5161
|
-
} else {
|
|
5162
|
-
currentGroup.push(i);
|
|
5163
|
-
}
|
|
5164
|
-
}
|
|
5165
|
-
if (currentGroup.length > 0) {
|
|
5166
|
-
groups.push(currentGroup);
|
|
5167
|
-
}
|
|
5168
|
-
return groups;
|
|
5169
|
-
}
|
|
5170
|
-
function breakIntoLines(wordWidths, maxWidth, spaceWidth) {
|
|
5171
|
-
const lines = [];
|
|
5172
|
-
let currentLine = [];
|
|
5173
|
-
let currentWidth = 0;
|
|
5174
|
-
for (let i = 0; i < wordWidths.length; i++) {
|
|
5175
|
-
const wordWidth = wordWidths[i];
|
|
5176
|
-
const spaceNeeded = currentLine.length > 0 ? spaceWidth : 0;
|
|
5177
|
-
if (currentWidth + spaceNeeded + wordWidth <= maxWidth) {
|
|
5178
|
-
currentLine.push(i);
|
|
5179
|
-
currentWidth += spaceNeeded + wordWidth;
|
|
5180
|
-
} else {
|
|
5181
|
-
if (currentLine.length > 0) {
|
|
5182
|
-
lines.push(currentLine);
|
|
5183
|
-
}
|
|
5184
|
-
currentLine = [i];
|
|
5185
|
-
currentWidth = wordWidth;
|
|
5186
|
-
}
|
|
5187
|
-
}
|
|
5188
|
-
if (currentLine.length > 0) {
|
|
5189
|
-
lines.push(currentLine);
|
|
5190
|
-
}
|
|
5191
|
-
return lines;
|
|
5192
|
-
}
|
|
5193
|
-
var GLYPH_SIZE_ESTIMATE = 64;
|
|
5194
|
-
function createShapedWordCache() {
|
|
5195
|
-
return new import_lru_cache.LRUCache({
|
|
5196
|
-
max: 5e4,
|
|
5197
|
-
maxSize: 50 * 1024 * 1024,
|
|
5198
|
-
maxEntrySize: 100 * 1024,
|
|
5199
|
-
sizeCalculation: (value, key) => {
|
|
5200
|
-
const keySize = key.length * 2;
|
|
5201
|
-
const glyphsSize = value.glyphs.length * GLYPH_SIZE_ESTIMATE;
|
|
5202
|
-
return keySize + glyphsSize + 100;
|
|
5203
|
-
}
|
|
5204
|
-
});
|
|
5205
|
-
}
|
|
5206
|
-
function makeShapingKey(text, fontFamily, fontSize, fontWeight, letterSpacing = 0) {
|
|
5207
|
-
return `${text}\0${fontFamily}\0${fontSize}\0${fontWeight}\0${letterSpacing}`;
|
|
5208
|
-
}
|
|
5209
|
-
function transformText(text, transform) {
|
|
5210
|
-
switch (transform) {
|
|
5211
|
-
case "uppercase":
|
|
5212
|
-
return text.toUpperCase();
|
|
5213
|
-
case "lowercase":
|
|
5214
|
-
return text.toLowerCase();
|
|
5215
|
-
case "capitalize":
|
|
5216
|
-
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5217
|
-
default:
|
|
5218
|
-
return text;
|
|
5219
|
-
}
|
|
5220
|
-
}
|
|
5221
|
-
function splitIntoChunks(arr, chunkSize) {
|
|
5222
|
-
const chunks = [];
|
|
5223
|
-
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
5224
|
-
chunks.push(arr.slice(i, i + chunkSize));
|
|
5225
|
-
}
|
|
5226
|
-
return chunks;
|
|
5227
|
-
}
|
|
5228
|
-
var CaptionLayoutEngine = class {
|
|
5229
|
-
fontRegistry;
|
|
5230
|
-
cache;
|
|
5231
|
-
layoutEngine;
|
|
5232
|
-
constructor(fontRegistry) {
|
|
5233
|
-
this.fontRegistry = fontRegistry;
|
|
5234
|
-
this.cache = createShapedWordCache();
|
|
5235
|
-
this.layoutEngine = new LayoutEngine(fontRegistry);
|
|
5236
|
-
}
|
|
5237
|
-
async measureWord(text, config) {
|
|
5238
|
-
const transformedText = transformText(text, config.textTransform);
|
|
5239
|
-
const cacheKey = makeShapingKey(
|
|
5240
|
-
transformedText,
|
|
5241
|
-
config.fontFamily,
|
|
5242
|
-
config.fontSize,
|
|
5243
|
-
config.fontWeight,
|
|
5244
|
-
config.letterSpacing
|
|
5245
|
-
);
|
|
5246
|
-
const cached = this.cache.get(cacheKey);
|
|
5247
|
-
if (cached) {
|
|
5248
|
-
return cached;
|
|
5249
|
-
}
|
|
5250
|
-
const lines = await this.layoutEngine.layout({
|
|
5251
|
-
text: transformedText,
|
|
5252
|
-
width: 1e5,
|
|
5253
|
-
letterSpacing: config.letterSpacing,
|
|
5254
|
-
fontSize: config.fontSize,
|
|
5255
|
-
lineHeight: 1,
|
|
5256
|
-
desc: { family: config.fontFamily, weight: config.fontWeight },
|
|
5257
|
-
textTransform: "none"
|
|
5258
|
-
});
|
|
5259
|
-
const width = lines[0]?.width ?? 0;
|
|
5260
|
-
const glyphs = lines[0]?.glyphs ?? [];
|
|
5261
|
-
const isRTL = isRTLText(transformedText);
|
|
5262
|
-
const shaped = {
|
|
5263
|
-
text: transformedText,
|
|
5264
|
-
width,
|
|
5265
|
-
glyphs: glyphs.map((g) => ({
|
|
5266
|
-
id: g.id,
|
|
5267
|
-
xAdvance: g.xAdvance,
|
|
5268
|
-
xOffset: g.xOffset,
|
|
5269
|
-
yOffset: g.yOffset,
|
|
5270
|
-
cluster: g.cluster
|
|
5271
|
-
})),
|
|
5272
|
-
isRTL
|
|
5273
|
-
};
|
|
5274
|
-
this.cache.set(cacheKey, shaped);
|
|
5275
|
-
return shaped;
|
|
5276
|
-
}
|
|
5277
|
-
async layoutCaption(words, config) {
|
|
5278
|
-
const store = new WordTimingStore(words);
|
|
5279
|
-
const measurementConfig = {
|
|
5280
|
-
fontFamily: config.fontFamily,
|
|
5281
|
-
fontSize: config.fontSize,
|
|
5282
|
-
fontWeight: config.fontWeight,
|
|
5283
|
-
letterSpacing: config.letterSpacing,
|
|
5284
|
-
textTransform: config.textTransform
|
|
5285
|
-
};
|
|
5286
|
-
const shapedWords = await Promise.all(
|
|
5287
|
-
words.map((w) => this.measureWord(w.text, measurementConfig))
|
|
5288
|
-
);
|
|
5289
|
-
if (config.measureTextWidth) {
|
|
5290
|
-
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
5291
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
5292
|
-
store.widths[i] = config.measureTextWidth(shapedWords[i].text, fontString);
|
|
5293
|
-
}
|
|
5294
|
-
} else {
|
|
5295
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
5296
|
-
store.widths[i] = shapedWords[i].width;
|
|
5297
|
-
}
|
|
5298
|
-
}
|
|
5299
|
-
if (config.textTransform !== "none") {
|
|
5300
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
5301
|
-
store.words[i] = shapedWords[i].text;
|
|
5302
|
-
}
|
|
5303
|
-
}
|
|
5304
|
-
const wordGroups = groupWordsByPause(store, config.pauseThreshold);
|
|
5305
|
-
const pixelMaxWidth = config.availableWidth;
|
|
5306
|
-
let spaceWidth;
|
|
5307
|
-
if (config.measureTextWidth) {
|
|
5308
|
-
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
5309
|
-
spaceWidth = config.measureTextWidth(" ", fontString) + config.wordSpacing;
|
|
5310
|
-
} else {
|
|
5311
|
-
const spaceWord = await this.measureWord(" ", measurementConfig);
|
|
5312
|
-
spaceWidth = spaceWord.width + config.wordSpacing;
|
|
5313
|
-
}
|
|
5314
|
-
const groups = wordGroups.flatMap((indices) => {
|
|
5315
|
-
const groupWidths = indices.map((i) => store.widths[i]);
|
|
5316
|
-
const allLines = breakIntoLines(
|
|
5317
|
-
groupWidths,
|
|
5318
|
-
pixelMaxWidth,
|
|
5319
|
-
spaceWidth
|
|
5320
|
-
);
|
|
5321
|
-
const lineChunks = splitIntoChunks(allLines, config.maxLines);
|
|
5322
|
-
return lineChunks.map((chunkLines) => {
|
|
5323
|
-
const lines = chunkLines.map((lineWordIndices, lineIndex) => {
|
|
5324
|
-
const actualIndices = lineWordIndices.map((i) => indices[i]);
|
|
5325
|
-
const lineWidth = actualIndices.reduce((sum, idx) => sum + store.widths[idx], 0) + (actualIndices.length - 1) * spaceWidth;
|
|
5326
|
-
return {
|
|
5327
|
-
wordIndices: actualIndices,
|
|
5328
|
-
x: 0,
|
|
5329
|
-
y: lineIndex * config.fontSize * config.lineHeight,
|
|
5330
|
-
width: lineWidth,
|
|
5331
|
-
height: config.fontSize
|
|
5332
|
-
};
|
|
5333
|
-
});
|
|
5334
|
-
const allWordIndices = lines.flatMap((l) => l.wordIndices);
|
|
5335
|
-
if (allWordIndices.length === 0) {
|
|
5336
|
-
return null;
|
|
5320
|
+
break;
|
|
5321
|
+
case "h":
|
|
5322
|
+
if (numIndex < numbers.length) {
|
|
5323
|
+
currentX += numbers[numIndex++];
|
|
5324
|
+
minX = Math.min(minX, currentX);
|
|
5325
|
+
maxX = Math.max(maxX, currentX);
|
|
5337
5326
|
}
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
}).filter((g) => g !== null);
|
|
5345
|
-
});
|
|
5346
|
-
const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
|
|
5347
|
-
const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
|
|
5348
|
-
const ASCENT_RATIO2 = 0.8;
|
|
5349
|
-
const calculateGroupY = (group) => {
|
|
5350
|
-
const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
|
|
5351
|
-
switch (config.verticalAlign) {
|
|
5352
|
-
case "top":
|
|
5353
|
-
return config.padding.top + config.fontSize * ASCENT_RATIO2;
|
|
5354
|
-
case "bottom":
|
|
5355
|
-
return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO2;
|
|
5356
|
-
case "middle":
|
|
5357
|
-
default:
|
|
5358
|
-
return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO2;
|
|
5359
|
-
}
|
|
5360
|
-
};
|
|
5361
|
-
const allWordTexts = store.words.slice(0, store.length);
|
|
5362
|
-
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
5363
|
-
const calculateLineX = (lineWidth) => {
|
|
5364
|
-
switch (config.horizontalAlign) {
|
|
5365
|
-
case "left":
|
|
5366
|
-
return config.padding.left;
|
|
5367
|
-
case "right":
|
|
5368
|
-
return config.frameWidth - lineWidth - config.padding.right;
|
|
5369
|
-
case "center":
|
|
5370
|
-
default:
|
|
5371
|
-
return config.padding.left + (contentWidth - lineWidth) / 2;
|
|
5372
|
-
}
|
|
5373
|
-
};
|
|
5374
|
-
for (const group of groups) {
|
|
5375
|
-
const baseY = calculateGroupY(group);
|
|
5376
|
-
for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
|
|
5377
|
-
const line = group.lines[lineIdx];
|
|
5378
|
-
line.x = calculateLineX(line.width);
|
|
5379
|
-
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
5380
|
-
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
5381
|
-
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
5382
|
-
let xCursor = line.x;
|
|
5383
|
-
for (const visualIdx of visualOrder) {
|
|
5384
|
-
const wordIdx = line.wordIndices[visualIdx];
|
|
5385
|
-
store.xPositions[wordIdx] = xCursor;
|
|
5386
|
-
store.yPositions[wordIdx] = line.y;
|
|
5387
|
-
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
5327
|
+
break;
|
|
5328
|
+
case "V":
|
|
5329
|
+
if (numIndex < numbers.length) {
|
|
5330
|
+
currentY = numbers[numIndex++];
|
|
5331
|
+
minY = Math.min(minY, currentY);
|
|
5332
|
+
maxY = Math.max(maxY, currentY);
|
|
5388
5333
|
}
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5334
|
+
break;
|
|
5335
|
+
case "v":
|
|
5336
|
+
if (numIndex < numbers.length) {
|
|
5337
|
+
currentY += numbers[numIndex++];
|
|
5338
|
+
minY = Math.min(minY, currentY);
|
|
5339
|
+
maxY = Math.max(maxY, currentY);
|
|
5340
|
+
}
|
|
5341
|
+
break;
|
|
5342
|
+
case "C":
|
|
5343
|
+
if (numIndex + 5 < numbers.length) {
|
|
5344
|
+
for (let j = 0; j < 3; j++) {
|
|
5345
|
+
const x = numbers[numIndex++];
|
|
5346
|
+
const y = numbers[numIndex++];
|
|
5347
|
+
minX = Math.min(minX, x);
|
|
5348
|
+
maxX = Math.max(maxX, x);
|
|
5349
|
+
minY = Math.min(minY, y);
|
|
5350
|
+
maxY = Math.max(maxY, y);
|
|
5351
|
+
if (j === 2) {
|
|
5352
|
+
currentX = x;
|
|
5353
|
+
currentY = y;
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
break;
|
|
5358
|
+
case "c":
|
|
5359
|
+
if (numIndex + 5 < numbers.length) {
|
|
5360
|
+
for (let j = 0; j < 3; j++) {
|
|
5361
|
+
const x = currentX + numbers[numIndex++];
|
|
5362
|
+
const y = currentY + numbers[numIndex++];
|
|
5363
|
+
minX = Math.min(minX, x);
|
|
5364
|
+
maxX = Math.max(maxX, x);
|
|
5365
|
+
minY = Math.min(minY, y);
|
|
5366
|
+
maxY = Math.max(maxY, y);
|
|
5367
|
+
if (j === 2) {
|
|
5368
|
+
currentX = x;
|
|
5369
|
+
currentY = y;
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
break;
|
|
5374
|
+
case "S":
|
|
5375
|
+
case "Q":
|
|
5376
|
+
if (numIndex + 3 < numbers.length) {
|
|
5377
|
+
for (let j = 0; j < 2; j++) {
|
|
5378
|
+
const x = numbers[numIndex++];
|
|
5379
|
+
const y = numbers[numIndex++];
|
|
5380
|
+
minX = Math.min(minX, x);
|
|
5381
|
+
maxX = Math.max(maxX, x);
|
|
5382
|
+
minY = Math.min(minY, y);
|
|
5383
|
+
maxY = Math.max(maxY, y);
|
|
5384
|
+
if (j === 1) {
|
|
5385
|
+
currentX = x;
|
|
5386
|
+
currentY = y;
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
break;
|
|
5391
|
+
case "s":
|
|
5392
|
+
case "q":
|
|
5393
|
+
if (numIndex + 3 < numbers.length) {
|
|
5394
|
+
for (let j = 0; j < 2; j++) {
|
|
5395
|
+
const x = currentX + numbers[numIndex++];
|
|
5396
|
+
const y = currentY + numbers[numIndex++];
|
|
5397
|
+
minX = Math.min(minX, x);
|
|
5398
|
+
maxX = Math.max(maxX, x);
|
|
5399
|
+
minY = Math.min(minY, y);
|
|
5400
|
+
maxY = Math.max(maxY, y);
|
|
5401
|
+
if (j === 1) {
|
|
5402
|
+
currentX = x;
|
|
5403
|
+
currentY = y;
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
break;
|
|
5408
|
+
case "A":
|
|
5409
|
+
if (numIndex + 6 < numbers.length) {
|
|
5410
|
+
numIndex += 5;
|
|
5411
|
+
currentX = numbers[numIndex++];
|
|
5412
|
+
currentY = numbers[numIndex++];
|
|
5413
|
+
minX = Math.min(minX, currentX);
|
|
5414
|
+
maxX = Math.max(maxX, currentX);
|
|
5415
|
+
minY = Math.min(minY, currentY);
|
|
5416
|
+
maxY = Math.max(maxY, currentY);
|
|
5417
|
+
}
|
|
5418
|
+
break;
|
|
5419
|
+
case "a":
|
|
5420
|
+
if (numIndex + 6 < numbers.length) {
|
|
5421
|
+
numIndex += 5;
|
|
5422
|
+
currentX += numbers[numIndex++];
|
|
5423
|
+
currentY += numbers[numIndex++];
|
|
5424
|
+
minX = Math.min(minX, currentX);
|
|
5425
|
+
maxX = Math.max(maxX, currentX);
|
|
5426
|
+
minY = Math.min(minY, currentY);
|
|
5427
|
+
maxY = Math.max(maxY, currentY);
|
|
5428
|
+
}
|
|
5429
|
+
break;
|
|
5430
|
+
case "Z":
|
|
5431
|
+
case "z":
|
|
5432
|
+
break;
|
|
5404
5433
|
}
|
|
5405
|
-
|
|
5406
|
-
wordIndex: idx,
|
|
5407
|
-
text: layout.store.words[idx],
|
|
5408
|
-
x: layout.store.xPositions[idx],
|
|
5409
|
-
y: layout.store.yPositions[idx],
|
|
5410
|
-
width: layout.store.widths[idx],
|
|
5411
|
-
startTime: layout.store.startTimes[idx],
|
|
5412
|
-
endTime: layout.store.endTimes[idx],
|
|
5413
|
-
isRTL: layout.shapedWords[idx].isRTL
|
|
5414
|
-
}));
|
|
5434
|
+
cmdIndex++;
|
|
5415
5435
|
}
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
if (wordIndex === -1) {
|
|
5419
|
-
return null;
|
|
5420
|
-
}
|
|
5421
|
-
return {
|
|
5422
|
-
wordIndex,
|
|
5423
|
-
text: layout.store.words[wordIndex],
|
|
5424
|
-
x: layout.store.xPositions[wordIndex],
|
|
5425
|
-
y: layout.store.yPositions[wordIndex],
|
|
5426
|
-
width: layout.store.widths[wordIndex],
|
|
5427
|
-
startTime: layout.store.startTimes[wordIndex],
|
|
5428
|
-
endTime: layout.store.endTimes[wordIndex],
|
|
5429
|
-
isRTL: layout.shapedWords[wordIndex].isRTL
|
|
5430
|
-
};
|
|
5436
|
+
if (minX === Infinity) {
|
|
5437
|
+
return { x: 0, y: 0, w: 0, h: 0 };
|
|
5431
5438
|
}
|
|
5432
|
-
|
|
5433
|
-
|
|
5439
|
+
return {
|
|
5440
|
+
x: minX,
|
|
5441
|
+
y: minY,
|
|
5442
|
+
w: maxX - minX,
|
|
5443
|
+
h: maxY - minY
|
|
5444
|
+
};
|
|
5445
|
+
}
|
|
5446
|
+
async function renderSvgAssetToPng(asset, options = {}) {
|
|
5447
|
+
const defaultWidth = options.defaultWidth ?? 1920;
|
|
5448
|
+
const defaultHeight = options.defaultHeight ?? 1080;
|
|
5449
|
+
let svgString;
|
|
5450
|
+
let targetWidth;
|
|
5451
|
+
let targetHeight;
|
|
5452
|
+
if (asset.src) {
|
|
5453
|
+
svgString = asset.src;
|
|
5454
|
+
const dimensions = extractSvgDimensions(svgString);
|
|
5455
|
+
targetWidth = dimensions.width || defaultWidth;
|
|
5456
|
+
targetHeight = dimensions.height || defaultHeight;
|
|
5457
|
+
} else if (asset.shape) {
|
|
5458
|
+
targetWidth = toNumber(asset.width, defaultWidth);
|
|
5459
|
+
targetHeight = toNumber(asset.height, defaultHeight);
|
|
5460
|
+
svgString = shapeToSvgString(asset, targetWidth, targetHeight);
|
|
5461
|
+
} else {
|
|
5462
|
+
throw new Error("Either 'src' or 'shape' must be provided");
|
|
5434
5463
|
}
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5464
|
+
return renderSvgToPng(svgString, {
|
|
5465
|
+
width: targetWidth,
|
|
5466
|
+
height: targetHeight,
|
|
5467
|
+
background: options.background
|
|
5468
|
+
});
|
|
5469
|
+
}
|
|
5470
|
+
function extractSvgDimensions(svgString) {
|
|
5471
|
+
const widthMatch = svgString.match(/width\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
5472
|
+
const heightMatch = svgString.match(/height\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
5473
|
+
let width = widthMatch ? parseFloat(widthMatch[1]) : 0;
|
|
5474
|
+
let height = heightMatch ? parseFloat(heightMatch[1]) : 0;
|
|
5475
|
+
if (!width || !height) {
|
|
5476
|
+
const viewBoxMatch = svgString.match(/viewBox\s*=\s*["']([^"']+)["']/i);
|
|
5477
|
+
if (viewBoxMatch) {
|
|
5478
|
+
const parts = viewBoxMatch[1].trim().split(/[\s,]+/).map(parseFloat);
|
|
5479
|
+
if (parts.length === 4) {
|
|
5480
|
+
width = width || parts[2];
|
|
5481
|
+
height = height || parts[3];
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5440
5484
|
}
|
|
5441
|
-
};
|
|
5485
|
+
return { width, height };
|
|
5486
|
+
}
|
|
5442
5487
|
|
|
5443
5488
|
// src/core/canvas-text-measurer.ts
|
|
5444
5489
|
async function createCanvasTextMeasurer() {
|
|
@@ -5814,7 +5859,7 @@ function findActiveWordIndex(store, groupWordIndices, timeMs) {
|
|
|
5814
5859
|
}
|
|
5815
5860
|
return -1;
|
|
5816
5861
|
}
|
|
5817
|
-
function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle
|
|
5862
|
+
function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle) {
|
|
5818
5863
|
if (groupWordIndices.length === 0) {
|
|
5819
5864
|
return "idle";
|
|
5820
5865
|
}
|
|
@@ -5831,7 +5876,7 @@ function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle, spee
|
|
|
5831
5876
|
return "after";
|
|
5832
5877
|
}
|
|
5833
5878
|
if (TRANSITION_ANIMATION_STYLES.has(animationStyle)) {
|
|
5834
|
-
const transitionDurationMs =
|
|
5879
|
+
const transitionDurationMs = ANIMATION_DURATION_MS[animationStyle] ?? 200;
|
|
5835
5880
|
for (const idx of groupWordIndices) {
|
|
5836
5881
|
const wordStart = store.startTimes[idx];
|
|
5837
5882
|
if (timeMs >= wordStart && timeMs < wordStart + transitionDurationMs) {
|
|
@@ -5853,7 +5898,7 @@ function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle, spee
|
|
|
5853
5898
|
}
|
|
5854
5899
|
return "before";
|
|
5855
5900
|
}
|
|
5856
|
-
function computeStateSignature(layout, timeMs, animationStyle
|
|
5901
|
+
function computeStateSignature(layout, timeMs, animationStyle) {
|
|
5857
5902
|
const groupIndex = findGroupIndexAtTime(layout.groups, timeMs);
|
|
5858
5903
|
if (groupIndex === -1) {
|
|
5859
5904
|
return { groupIndex: -1, activeWordIndex: -1, animationPhase: "idle" };
|
|
@@ -5864,21 +5909,20 @@ function computeStateSignature(layout, timeMs, animationStyle, speed) {
|
|
|
5864
5909
|
layout.store,
|
|
5865
5910
|
group.wordIndices,
|
|
5866
5911
|
timeMs,
|
|
5867
|
-
animationStyle
|
|
5868
|
-
speed
|
|
5912
|
+
animationStyle
|
|
5869
5913
|
);
|
|
5870
5914
|
return { groupIndex, activeWordIndex, animationPhase };
|
|
5871
5915
|
}
|
|
5872
5916
|
function signaturesMatch(a, b) {
|
|
5873
5917
|
return a.groupIndex === b.groupIndex && a.activeWordIndex === b.activeWordIndex && a.animationPhase === b.animationPhase;
|
|
5874
5918
|
}
|
|
5875
|
-
function createFrameSchedule(layout, durationMs, fps, animationStyle = "highlight"
|
|
5919
|
+
function createFrameSchedule(layout, durationMs, fps, animationStyle = "highlight") {
|
|
5876
5920
|
const totalFrames = Math.max(2, Math.round(durationMs / 1e3 * fps) + 1);
|
|
5877
5921
|
const renderFrames = [];
|
|
5878
5922
|
let previousSignature = null;
|
|
5879
5923
|
for (let frame = 0; frame < totalFrames; frame++) {
|
|
5880
5924
|
const timeMs = frame / (totalFrames - 1) * durationMs;
|
|
5881
|
-
const signature = computeStateSignature(layout, timeMs, animationStyle
|
|
5925
|
+
const signature = computeStateSignature(layout, timeMs, animationStyle);
|
|
5882
5926
|
const isAnimating = signature.animationPhase === "animating";
|
|
5883
5927
|
if (isAnimating || previousSignature === null || !signaturesMatch(signature, previousSignature)) {
|
|
5884
5928
|
renderFrames.push({
|
|
@@ -6280,14 +6324,12 @@ var RichCaptionRenderer = class {
|
|
|
6280
6324
|
throw new Error("No asset loaded. Call loadAsset() first.");
|
|
6281
6325
|
}
|
|
6282
6326
|
const animationStyle = this.extractAnimationStyle();
|
|
6283
|
-
const animationSpeed = this.extractAnimationSpeed();
|
|
6284
6327
|
const durationMs = duration * 1e3;
|
|
6285
6328
|
const schedule = createFrameSchedule(
|
|
6286
6329
|
this.currentLayout,
|
|
6287
6330
|
durationMs,
|
|
6288
6331
|
this.fps,
|
|
6289
|
-
animationStyle
|
|
6290
|
-
animationSpeed
|
|
6332
|
+
animationStyle
|
|
6291
6333
|
);
|
|
6292
6334
|
const bgColor = options?.bgColor;
|
|
6293
6335
|
const hasAlpha = !bgColor;
|
|
@@ -6448,13 +6490,11 @@ var RichCaptionRenderer = class {
|
|
|
6448
6490
|
throw new Error("No asset loaded. Call loadAsset() first.");
|
|
6449
6491
|
}
|
|
6450
6492
|
const animationStyle = this.extractAnimationStyle();
|
|
6451
|
-
const animationSpeed = this.extractAnimationSpeed();
|
|
6452
6493
|
return createFrameSchedule(
|
|
6453
6494
|
this.currentLayout,
|
|
6454
6495
|
duration * 1e3,
|
|
6455
6496
|
this.fps,
|
|
6456
|
-
animationStyle
|
|
6457
|
-
animationSpeed
|
|
6497
|
+
animationStyle
|
|
6458
6498
|
);
|
|
6459
6499
|
}
|
|
6460
6500
|
getStats() {
|
|
@@ -6492,10 +6532,6 @@ var RichCaptionRenderer = class {
|
|
|
6492
6532
|
const wordAnim = this.currentAsset?.wordAnimation;
|
|
6493
6533
|
return wordAnim?.style ?? "highlight";
|
|
6494
6534
|
}
|
|
6495
|
-
extractAnimationSpeed() {
|
|
6496
|
-
const wordAnim = this.currentAsset?.wordAnimation;
|
|
6497
|
-
return wordAnim?.speed ?? 1;
|
|
6498
|
-
}
|
|
6499
6535
|
logProgress(pct, framesProcessed, totalFrames, uniqueProcessed, uniqueTotal, fps, eta) {
|
|
6500
6536
|
if (typeof process !== "undefined" && process.stderr) {
|
|
6501
6537
|
process.stderr.write(
|