@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.js
CHANGED
|
@@ -18,8 +18,7 @@ import {
|
|
|
18
18
|
svgShadowSchema,
|
|
19
19
|
svgTransformSchema,
|
|
20
20
|
svgGradientStopSchema,
|
|
21
|
-
richCaptionActiveSchema as baseCaptionActiveSchema
|
|
22
|
-
richCaptionWordAnimationSchema as baseCaptionWordAnimationSchema
|
|
21
|
+
richCaptionActiveSchema as baseCaptionActiveSchema
|
|
23
22
|
} from "@shotstack/schemas/zod";
|
|
24
23
|
|
|
25
24
|
// src/config/canvas-constants.ts
|
|
@@ -188,24 +187,38 @@ var richCaptionFontSchema = z.object({
|
|
|
188
187
|
weight: z.union([z.string(), z.number()]).default("400"),
|
|
189
188
|
color: z.string().regex(HEX6).default("#ffffff"),
|
|
190
189
|
opacity: z.number().min(0).max(1).default(1),
|
|
191
|
-
background: z.string().regex(HEX6).optional()
|
|
190
|
+
background: z.string().regex(HEX6).optional(),
|
|
191
|
+
textDecoration: z.enum(["none", "underline", "line-through"]).default("none")
|
|
192
192
|
});
|
|
193
193
|
var richCaptionActiveSchema = baseCaptionActiveSchema.extend({
|
|
194
194
|
font: z.object({
|
|
195
|
-
color: z.string().regex(HEX6).default("#ffffff"),
|
|
196
|
-
background: z.string().regex(HEX6).optional(),
|
|
197
|
-
opacity: z.number().min(0).max(1).default(1)
|
|
198
|
-
}).optional(),
|
|
199
|
-
stroke: z.object({
|
|
200
|
-
width: z.number().min(0).optional(),
|
|
201
195
|
color: z.string().regex(HEX6).optional(),
|
|
202
|
-
|
|
196
|
+
background: z.string().regex(HEX6).optional(),
|
|
197
|
+
opacity: z.number().min(0).max(1).optional(),
|
|
198
|
+
textDecoration: z.enum(["none", "underline", "line-through"]).optional()
|
|
203
199
|
}).optional(),
|
|
200
|
+
stroke: z.union([
|
|
201
|
+
z.object({
|
|
202
|
+
width: z.number().min(0).optional(),
|
|
203
|
+
color: z.string().regex(HEX6).optional(),
|
|
204
|
+
opacity: z.number().min(0).max(1).optional()
|
|
205
|
+
}),
|
|
206
|
+
z.literal("none")
|
|
207
|
+
]).optional(),
|
|
208
|
+
shadow: z.union([
|
|
209
|
+
z.object({
|
|
210
|
+
offsetX: z.number().optional(),
|
|
211
|
+
offsetY: z.number().optional(),
|
|
212
|
+
blur: z.number().min(0).optional(),
|
|
213
|
+
color: z.string().regex(HEX6).optional(),
|
|
214
|
+
opacity: z.number().min(0).max(1).optional()
|
|
215
|
+
}),
|
|
216
|
+
z.literal("none")
|
|
217
|
+
]).optional(),
|
|
204
218
|
scale: z.number().min(0.5).max(2).default(1)
|
|
205
219
|
});
|
|
206
|
-
var richCaptionWordAnimationSchema =
|
|
220
|
+
var richCaptionWordAnimationSchema = z.object({
|
|
207
221
|
style: z.enum(["karaoke", "highlight", "pop", "fade", "slide", "bounce", "typewriter", "none"]).default("highlight"),
|
|
208
|
-
speed: z.number().min(0.5).max(2).default(1),
|
|
209
222
|
direction: z.enum(["left", "right", "up", "down"]).default("up")
|
|
210
223
|
});
|
|
211
224
|
var richCaptionAssetSchema = z.object({
|
|
@@ -2216,281 +2229,616 @@ function parseHex6(hex, alpha = 1) {
|
|
|
2216
2229
|
return { r, g, b, a: alpha };
|
|
2217
2230
|
}
|
|
2218
2231
|
|
|
2219
|
-
// src/core/rich-caption-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
slide: 250,
|
|
2226
|
-
bounce: 400,
|
|
2227
|
-
typewriter: 0,
|
|
2228
|
-
none: 0
|
|
2229
|
-
};
|
|
2230
|
-
var DEFAULT_ANIMATION_STATE = {
|
|
2231
|
-
opacity: 1,
|
|
2232
|
-
scale: 1,
|
|
2233
|
-
translateX: 0,
|
|
2234
|
-
translateY: 0,
|
|
2235
|
-
fillProgress: 1,
|
|
2236
|
-
isActive: false,
|
|
2237
|
-
visibleCharacters: -1
|
|
2238
|
-
};
|
|
2239
|
-
function easeOutQuad2(t) {
|
|
2240
|
-
return t * (2 - t);
|
|
2241
|
-
}
|
|
2242
|
-
function easeInOutQuad(t) {
|
|
2243
|
-
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
2244
|
-
}
|
|
2245
|
-
function easeOutBack(t) {
|
|
2246
|
-
const c1 = 1.70158;
|
|
2247
|
-
const c3 = c1 + 1;
|
|
2248
|
-
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
2249
|
-
}
|
|
2250
|
-
function easeOutCirc(t) {
|
|
2251
|
-
return Math.sqrt(1 - Math.pow(t - 1, 2));
|
|
2232
|
+
// src/core/rich-caption-layout.ts
|
|
2233
|
+
import { LRUCache } from "lru-cache";
|
|
2234
|
+
var ASCENT_RATIO = 0.8;
|
|
2235
|
+
var DESCENT_RATIO = 0.2;
|
|
2236
|
+
function isRTLText(text) {
|
|
2237
|
+
return containsRTLCharacters(text);
|
|
2252
2238
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2239
|
+
var WordTimingStore = class {
|
|
2240
|
+
startTimes;
|
|
2241
|
+
endTimes;
|
|
2242
|
+
xPositions;
|
|
2243
|
+
yPositions;
|
|
2244
|
+
widths;
|
|
2245
|
+
words;
|
|
2246
|
+
length;
|
|
2247
|
+
constructor(words) {
|
|
2248
|
+
this.length = words.length;
|
|
2249
|
+
this.startTimes = new Uint32Array(this.length);
|
|
2250
|
+
this.endTimes = new Uint32Array(this.length);
|
|
2251
|
+
this.xPositions = new Float32Array(this.length);
|
|
2252
|
+
this.yPositions = new Float32Array(this.length);
|
|
2253
|
+
this.widths = new Float32Array(this.length);
|
|
2254
|
+
this.words = new Array(this.length);
|
|
2255
|
+
for (let i = 0; i < this.length; i++) {
|
|
2256
|
+
this.startTimes[i] = Math.floor(words[i].start);
|
|
2257
|
+
this.endTimes[i] = Math.floor(words[i].end);
|
|
2258
|
+
this.words[i] = words[i].text;
|
|
2259
|
+
}
|
|
2261
2260
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2261
|
+
};
|
|
2262
|
+
function findWordAtTime(store, timeMs) {
|
|
2263
|
+
let left = 0;
|
|
2264
|
+
let right = store.length - 1;
|
|
2265
|
+
while (left <= right) {
|
|
2266
|
+
const mid = left + right >>> 1;
|
|
2267
|
+
const start = store.startTimes[mid];
|
|
2268
|
+
const end = store.endTimes[mid];
|
|
2269
|
+
if (timeMs >= start && timeMs < end) {
|
|
2270
|
+
return mid;
|
|
2271
|
+
}
|
|
2272
|
+
if (timeMs < start) {
|
|
2273
|
+
right = mid - 1;
|
|
2274
|
+
} else {
|
|
2275
|
+
left = mid + 1;
|
|
2276
|
+
}
|
|
2264
2277
|
}
|
|
2265
|
-
return
|
|
2266
|
-
}
|
|
2267
|
-
function clamp(value, min, max) {
|
|
2268
|
-
return Math.min(Math.max(value, min), max);
|
|
2278
|
+
return -1;
|
|
2269
2279
|
}
|
|
2270
|
-
function
|
|
2271
|
-
if (
|
|
2272
|
-
return
|
|
2280
|
+
function groupWordsByPause(store, pauseThreshold = 500) {
|
|
2281
|
+
if (store.length === 0) {
|
|
2282
|
+
return [];
|
|
2273
2283
|
}
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2284
|
+
const groups = [];
|
|
2285
|
+
let currentGroup = [];
|
|
2286
|
+
for (let i = 0; i < store.length; i++) {
|
|
2287
|
+
if (currentGroup.length === 0) {
|
|
2288
|
+
currentGroup.push(i);
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
const prevEnd = store.endTimes[currentGroup[currentGroup.length - 1]];
|
|
2292
|
+
const currStart = store.startTimes[i];
|
|
2293
|
+
const gap = currStart - prevEnd;
|
|
2294
|
+
const prevText = store.words[currentGroup[currentGroup.length - 1]];
|
|
2295
|
+
const endsWithPunctuation = /[.!?]$/.test(prevText);
|
|
2296
|
+
if (gap >= pauseThreshold || endsWithPunctuation) {
|
|
2297
|
+
groups.push(currentGroup);
|
|
2298
|
+
currentGroup = [i];
|
|
2299
|
+
} else {
|
|
2300
|
+
currentGroup.push(i);
|
|
2301
|
+
}
|
|
2281
2302
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
return ctx.currentTime >= ctx.wordStart && ctx.currentTime < ctx.wordEnd;
|
|
2303
|
+
if (currentGroup.length > 0) {
|
|
2304
|
+
groups.push(currentGroup);
|
|
2305
|
+
}
|
|
2306
|
+
return groups;
|
|
2287
2307
|
}
|
|
2288
|
-
function
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2308
|
+
function breakIntoLines(wordWidths, maxWidth, spaceWidth) {
|
|
2309
|
+
const lines = [];
|
|
2310
|
+
let currentLine = [];
|
|
2311
|
+
let currentWidth = 0;
|
|
2312
|
+
for (let i = 0; i < wordWidths.length; i++) {
|
|
2313
|
+
const wordWidth = wordWidths[i];
|
|
2314
|
+
const spaceNeeded = currentLine.length > 0 ? spaceWidth : 0;
|
|
2315
|
+
if (currentWidth + spaceNeeded + wordWidth <= maxWidth) {
|
|
2316
|
+
currentLine.push(i);
|
|
2317
|
+
currentWidth += spaceNeeded + wordWidth;
|
|
2318
|
+
} else {
|
|
2319
|
+
if (currentLine.length > 0) {
|
|
2320
|
+
lines.push(currentLine);
|
|
2321
|
+
}
|
|
2322
|
+
currentLine = [i];
|
|
2323
|
+
currentWidth = wordWidth;
|
|
2324
|
+
}
|
|
2300
2325
|
}
|
|
2301
|
-
if (
|
|
2302
|
-
|
|
2303
|
-
fillProgress: 1,
|
|
2304
|
-
isActive: false,
|
|
2305
|
-
opacity: 1
|
|
2306
|
-
};
|
|
2326
|
+
if (currentLine.length > 0) {
|
|
2327
|
+
lines.push(currentLine);
|
|
2307
2328
|
}
|
|
2308
|
-
return
|
|
2309
|
-
fillProgress: calculateWordProgress(adjustedCtx),
|
|
2310
|
-
isActive,
|
|
2311
|
-
opacity: 1
|
|
2312
|
-
};
|
|
2329
|
+
return lines;
|
|
2313
2330
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
return {
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2331
|
+
var GLYPH_SIZE_ESTIMATE = 64;
|
|
2332
|
+
function createShapedWordCache() {
|
|
2333
|
+
return new LRUCache({
|
|
2334
|
+
max: 5e4,
|
|
2335
|
+
maxSize: 50 * 1024 * 1024,
|
|
2336
|
+
maxEntrySize: 100 * 1024,
|
|
2337
|
+
sizeCalculation: (value, key) => {
|
|
2338
|
+
const keySize = key.length * 2;
|
|
2339
|
+
const glyphsSize = value.glyphs.length * GLYPH_SIZE_ESTIMATE;
|
|
2340
|
+
return keySize + glyphsSize + 100;
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2321
2343
|
}
|
|
2322
|
-
function
|
|
2323
|
-
|
|
2324
|
-
return {
|
|
2325
|
-
scale: 0.5,
|
|
2326
|
-
opacity: 0,
|
|
2327
|
-
isActive: false,
|
|
2328
|
-
fillProgress: 0
|
|
2329
|
-
};
|
|
2330
|
-
}
|
|
2331
|
-
const adjustedDuration = ctx.animationDuration / speed;
|
|
2332
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2333
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2334
|
-
const easedProgress = easeOutBack(progress);
|
|
2335
|
-
const startScale = 0.5;
|
|
2336
|
-
const isActive = isWordActive(ctx);
|
|
2337
|
-
const endScale = isActive ? activeScale : 1;
|
|
2338
|
-
const scale = startScale + (endScale - startScale) * easedProgress;
|
|
2339
|
-
return {
|
|
2340
|
-
scale: Math.min(scale, activeScale),
|
|
2341
|
-
opacity: easedProgress,
|
|
2342
|
-
isActive,
|
|
2343
|
-
fillProgress: isActive ? 1 : 0
|
|
2344
|
-
};
|
|
2344
|
+
function makeShapingKey(text, fontFamily, fontSize, fontWeight, letterSpacing = 0) {
|
|
2345
|
+
return `${text}\0${fontFamily}\0${fontSize}\0${fontWeight}\0${letterSpacing}`;
|
|
2345
2346
|
}
|
|
2346
|
-
function
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2347
|
+
function transformText(text, transform) {
|
|
2348
|
+
switch (transform) {
|
|
2349
|
+
case "uppercase":
|
|
2350
|
+
return text.toUpperCase();
|
|
2351
|
+
case "lowercase":
|
|
2352
|
+
return text.toLowerCase();
|
|
2353
|
+
case "capitalize":
|
|
2354
|
+
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2355
|
+
default:
|
|
2356
|
+
return text;
|
|
2353
2357
|
}
|
|
2354
|
-
const adjustedDuration = ctx.animationDuration / speed;
|
|
2355
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2356
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2357
|
-
const easedProgress = easeInOutQuad(progress);
|
|
2358
|
-
const isActive = isWordActive(ctx);
|
|
2359
|
-
return {
|
|
2360
|
-
opacity: easedProgress,
|
|
2361
|
-
isActive,
|
|
2362
|
-
fillProgress: isActive ? 1 : 0
|
|
2363
|
-
};
|
|
2364
2358
|
}
|
|
2365
|
-
function
|
|
2366
|
-
const
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
return {
|
|
2370
|
-
translateX: offset2.x,
|
|
2371
|
-
translateY: offset2.y,
|
|
2372
|
-
opacity: 0,
|
|
2373
|
-
isActive: false,
|
|
2374
|
-
fillProgress: 0
|
|
2375
|
-
};
|
|
2359
|
+
function splitIntoChunks(arr, chunkSize) {
|
|
2360
|
+
const chunks = [];
|
|
2361
|
+
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
2362
|
+
chunks.push(arr.slice(i, i + chunkSize));
|
|
2376
2363
|
}
|
|
2377
|
-
|
|
2378
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2379
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2380
|
-
const easedProgress = easeOutCirc(progress);
|
|
2381
|
-
const offset = getDirectionOffset(direction, slideDistance);
|
|
2382
|
-
const translateX = offset.x * (1 - easedProgress);
|
|
2383
|
-
const translateY = offset.y * (1 - easedProgress);
|
|
2384
|
-
const isActive = isWordActive(ctx);
|
|
2385
|
-
return {
|
|
2386
|
-
translateX,
|
|
2387
|
-
translateY,
|
|
2388
|
-
opacity: easeOutQuad2(progress),
|
|
2389
|
-
isActive,
|
|
2390
|
-
fillProgress: isActive ? 1 : 0
|
|
2391
|
-
};
|
|
2364
|
+
return chunks;
|
|
2392
2365
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
case "down":
|
|
2402
|
-
return { x: 0, y: distance };
|
|
2366
|
+
var CaptionLayoutEngine = class {
|
|
2367
|
+
fontRegistry;
|
|
2368
|
+
cache;
|
|
2369
|
+
layoutEngine;
|
|
2370
|
+
constructor(fontRegistry) {
|
|
2371
|
+
this.fontRegistry = fontRegistry;
|
|
2372
|
+
this.cache = createShapedWordCache();
|
|
2373
|
+
this.layoutEngine = new LayoutEngine(fontRegistry);
|
|
2403
2374
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2375
|
+
async measureWord(text, config) {
|
|
2376
|
+
const transformedText = transformText(text, config.textTransform);
|
|
2377
|
+
const cacheKey = makeShapingKey(
|
|
2378
|
+
transformedText,
|
|
2379
|
+
config.fontFamily,
|
|
2380
|
+
config.fontSize,
|
|
2381
|
+
config.fontWeight,
|
|
2382
|
+
config.letterSpacing
|
|
2383
|
+
);
|
|
2384
|
+
const cached = this.cache.get(cacheKey);
|
|
2385
|
+
if (cached) {
|
|
2386
|
+
return cached;
|
|
2387
|
+
}
|
|
2388
|
+
const lines = await this.layoutEngine.layout({
|
|
2389
|
+
text: transformedText,
|
|
2390
|
+
width: 1e5,
|
|
2391
|
+
letterSpacing: config.letterSpacing,
|
|
2392
|
+
fontSize: config.fontSize,
|
|
2393
|
+
lineHeight: 1,
|
|
2394
|
+
desc: { family: config.fontFamily, weight: config.fontWeight },
|
|
2395
|
+
textTransform: "none"
|
|
2396
|
+
});
|
|
2397
|
+
const width = lines[0]?.width ?? 0;
|
|
2398
|
+
const glyphs = lines[0]?.glyphs ?? [];
|
|
2399
|
+
const isRTL = isRTLText(transformedText);
|
|
2400
|
+
const shaped = {
|
|
2401
|
+
text: transformedText,
|
|
2402
|
+
width,
|
|
2403
|
+
glyphs: glyphs.map((g) => ({
|
|
2404
|
+
id: g.id,
|
|
2405
|
+
xAdvance: g.xAdvance,
|
|
2406
|
+
xOffset: g.xOffset,
|
|
2407
|
+
yOffset: g.yOffset,
|
|
2408
|
+
cluster: g.cluster
|
|
2409
|
+
})),
|
|
2410
|
+
isRTL
|
|
2411
|
+
};
|
|
2412
|
+
this.cache.set(cacheKey, shaped);
|
|
2413
|
+
return shaped;
|
|
2414
|
+
}
|
|
2415
|
+
async layoutCaption(words, config) {
|
|
2416
|
+
const store = new WordTimingStore(words);
|
|
2417
|
+
const measurementConfig = {
|
|
2418
|
+
fontFamily: config.fontFamily,
|
|
2419
|
+
fontSize: config.fontSize,
|
|
2420
|
+
fontWeight: config.fontWeight,
|
|
2421
|
+
letterSpacing: config.letterSpacing,
|
|
2422
|
+
textTransform: config.textTransform
|
|
2423
|
+
};
|
|
2424
|
+
const shapedWords = await Promise.all(
|
|
2425
|
+
words.map((w) => this.measureWord(w.text, measurementConfig))
|
|
2426
|
+
);
|
|
2427
|
+
if (config.measureTextWidth) {
|
|
2428
|
+
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
2429
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2430
|
+
store.widths[i] = config.measureTextWidth(shapedWords[i].text, fontString);
|
|
2431
|
+
}
|
|
2432
|
+
} else {
|
|
2433
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2434
|
+
store.widths[i] = shapedWords[i].width;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (config.textTransform !== "none") {
|
|
2438
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2439
|
+
store.words[i] = shapedWords[i].text;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
const wordGroups = groupWordsByPause(store, config.pauseThreshold);
|
|
2443
|
+
const pixelMaxWidth = config.availableWidth;
|
|
2444
|
+
let spaceWidth;
|
|
2445
|
+
if (config.measureTextWidth) {
|
|
2446
|
+
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
2447
|
+
spaceWidth = config.measureTextWidth(" ", fontString) + config.wordSpacing;
|
|
2448
|
+
} else {
|
|
2449
|
+
const spaceWord = await this.measureWord(" ", measurementConfig);
|
|
2450
|
+
spaceWidth = spaceWord.width + config.wordSpacing;
|
|
2451
|
+
}
|
|
2452
|
+
const groups = wordGroups.flatMap((indices) => {
|
|
2453
|
+
const groupWidths = indices.map((i) => store.widths[i]);
|
|
2454
|
+
const allLines = breakIntoLines(
|
|
2455
|
+
groupWidths,
|
|
2456
|
+
pixelMaxWidth,
|
|
2457
|
+
spaceWidth
|
|
2458
|
+
);
|
|
2459
|
+
const lineChunks = splitIntoChunks(allLines, config.maxLines);
|
|
2460
|
+
return lineChunks.map((chunkLines) => {
|
|
2461
|
+
const lines = chunkLines.map((lineWordIndices, lineIndex) => {
|
|
2462
|
+
const actualIndices = lineWordIndices.map((i) => indices[i]);
|
|
2463
|
+
const lineWidth = actualIndices.reduce((sum, idx) => sum + store.widths[idx], 0) + (actualIndices.length - 1) * spaceWidth;
|
|
2464
|
+
return {
|
|
2465
|
+
wordIndices: actualIndices,
|
|
2466
|
+
x: 0,
|
|
2467
|
+
y: lineIndex * config.fontSize * config.lineHeight,
|
|
2468
|
+
width: lineWidth,
|
|
2469
|
+
height: config.fontSize
|
|
2470
|
+
};
|
|
2471
|
+
});
|
|
2472
|
+
const allWordIndices = lines.flatMap((l) => l.wordIndices);
|
|
2473
|
+
if (allWordIndices.length === 0) {
|
|
2474
|
+
return null;
|
|
2475
|
+
}
|
|
2476
|
+
return {
|
|
2477
|
+
wordIndices: allWordIndices,
|
|
2478
|
+
startTime: store.startTimes[allWordIndices[0]],
|
|
2479
|
+
endTime: store.endTimes[allWordIndices[allWordIndices.length - 1]],
|
|
2480
|
+
lines
|
|
2481
|
+
};
|
|
2482
|
+
}).filter((g) => g !== null);
|
|
2483
|
+
});
|
|
2484
|
+
const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
|
|
2485
|
+
const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
|
|
2486
|
+
const calculateGroupY = (group) => {
|
|
2487
|
+
const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
|
|
2488
|
+
switch (config.verticalAlign) {
|
|
2489
|
+
case "top":
|
|
2490
|
+
return config.padding.top + config.fontSize * ASCENT_RATIO;
|
|
2491
|
+
case "bottom":
|
|
2492
|
+
return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO;
|
|
2493
|
+
case "middle":
|
|
2494
|
+
default:
|
|
2495
|
+
return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO;
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
const allWordTexts = store.words.slice(0, store.length);
|
|
2499
|
+
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
2500
|
+
const calculateLineX = (lineWidth) => {
|
|
2501
|
+
switch (config.horizontalAlign) {
|
|
2502
|
+
case "left":
|
|
2503
|
+
return config.padding.left;
|
|
2504
|
+
case "right":
|
|
2505
|
+
return config.frameWidth - lineWidth - config.padding.right;
|
|
2506
|
+
case "center":
|
|
2507
|
+
default:
|
|
2508
|
+
return config.padding.left + (contentWidth - lineWidth) / 2;
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
for (const group of groups) {
|
|
2512
|
+
const baseY = calculateGroupY(group);
|
|
2513
|
+
for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
|
|
2514
|
+
const line = group.lines[lineIdx];
|
|
2515
|
+
line.x = calculateLineX(line.width);
|
|
2516
|
+
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
2517
|
+
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
2518
|
+
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
2519
|
+
let xCursor = line.x;
|
|
2520
|
+
for (const visualIdx of visualOrder) {
|
|
2521
|
+
const wordIdx = line.wordIndices[visualIdx];
|
|
2522
|
+
store.xPositions[wordIdx] = xCursor;
|
|
2523
|
+
store.yPositions[wordIdx] = line.y;
|
|
2524
|
+
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2408
2528
|
return {
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2529
|
+
store,
|
|
2530
|
+
groups,
|
|
2531
|
+
shapedWords,
|
|
2532
|
+
paragraphDirection
|
|
2413
2533
|
};
|
|
2414
2534
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2535
|
+
getVisibleWordsAtTime(layout, timeMs) {
|
|
2536
|
+
const activeGroup = layout.groups.find(
|
|
2537
|
+
(g) => timeMs >= g.startTime && timeMs <= g.endTime
|
|
2538
|
+
);
|
|
2539
|
+
if (!activeGroup) {
|
|
2540
|
+
return [];
|
|
2541
|
+
}
|
|
2542
|
+
return activeGroup.wordIndices.map((idx) => ({
|
|
2543
|
+
wordIndex: idx,
|
|
2544
|
+
text: layout.store.words[idx],
|
|
2545
|
+
x: layout.store.xPositions[idx],
|
|
2546
|
+
y: layout.store.yPositions[idx],
|
|
2547
|
+
width: layout.store.widths[idx],
|
|
2548
|
+
startTime: layout.store.startTimes[idx],
|
|
2549
|
+
endTime: layout.store.endTimes[idx],
|
|
2550
|
+
isRTL: layout.shapedWords[idx].isRTL
|
|
2551
|
+
}));
|
|
2552
|
+
}
|
|
2553
|
+
getActiveWordAtTime(layout, timeMs) {
|
|
2554
|
+
const wordIndex = findWordAtTime(layout.store, timeMs);
|
|
2555
|
+
if (wordIndex === -1) {
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2433
2558
|
return {
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2559
|
+
wordIndex,
|
|
2560
|
+
text: layout.store.words[wordIndex],
|
|
2561
|
+
x: layout.store.xPositions[wordIndex],
|
|
2562
|
+
y: layout.store.yPositions[wordIndex],
|
|
2563
|
+
width: layout.store.widths[wordIndex],
|
|
2564
|
+
startTime: layout.store.startTimes[wordIndex],
|
|
2565
|
+
endTime: layout.store.endTimes[wordIndex],
|
|
2566
|
+
isRTL: layout.shapedWords[wordIndex].isRTL
|
|
2437
2567
|
};
|
|
2438
2568
|
}
|
|
2439
|
-
|
|
2569
|
+
clearCache() {
|
|
2570
|
+
this.cache.clear();
|
|
2571
|
+
}
|
|
2572
|
+
getCacheStats() {
|
|
2440
2573
|
return {
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
isActive: false
|
|
2574
|
+
size: this.cache.size,
|
|
2575
|
+
calculatedSize: this.cache.calculatedSize
|
|
2444
2576
|
};
|
|
2445
2577
|
}
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// src/core/rich-caption-animator.ts
|
|
2581
|
+
var ANIMATION_DURATIONS = {
|
|
2582
|
+
karaoke: 0,
|
|
2583
|
+
highlight: 0,
|
|
2584
|
+
pop: 200,
|
|
2585
|
+
fade: 150,
|
|
2586
|
+
slide: 250,
|
|
2587
|
+
bounce: 400,
|
|
2588
|
+
typewriter: 0,
|
|
2589
|
+
none: 0
|
|
2590
|
+
};
|
|
2591
|
+
var DEFAULT_ANIMATION_STATE = {
|
|
2592
|
+
opacity: 1,
|
|
2593
|
+
scale: 1,
|
|
2594
|
+
translateX: 0,
|
|
2595
|
+
translateY: 0,
|
|
2596
|
+
fillProgress: 1,
|
|
2597
|
+
isActive: false,
|
|
2598
|
+
visibleCharacters: -1
|
|
2599
|
+
};
|
|
2600
|
+
function easeOutQuad2(t) {
|
|
2601
|
+
return t * (2 - t);
|
|
2602
|
+
}
|
|
2603
|
+
function easeInOutQuad(t) {
|
|
2604
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
2605
|
+
}
|
|
2606
|
+
function easeOutBack(t) {
|
|
2607
|
+
const c1 = 1.70158;
|
|
2608
|
+
const c3 = c1 + 1;
|
|
2609
|
+
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
2610
|
+
}
|
|
2611
|
+
function easeOutCirc(t) {
|
|
2612
|
+
return Math.sqrt(1 - Math.pow(t - 1, 2));
|
|
2613
|
+
}
|
|
2614
|
+
function easeOutBounce(t) {
|
|
2615
|
+
const n1 = 7.5625;
|
|
2616
|
+
const d1 = 2.75;
|
|
2617
|
+
if (t < 1 / d1) {
|
|
2618
|
+
return n1 * t * t;
|
|
2619
|
+
}
|
|
2620
|
+
if (t < 2 / d1) {
|
|
2621
|
+
return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
2622
|
+
}
|
|
2623
|
+
if (t < 2.5 / d1) {
|
|
2624
|
+
return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
2625
|
+
}
|
|
2626
|
+
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
2627
|
+
}
|
|
2628
|
+
function clamp(value, min, max) {
|
|
2629
|
+
return Math.min(Math.max(value, min), max);
|
|
2630
|
+
}
|
|
2631
|
+
function calculateAnimationProgress(ctx) {
|
|
2632
|
+
if (ctx.animationDuration <= 0) {
|
|
2633
|
+
return ctx.currentTime >= ctx.wordStart ? 1 : 0;
|
|
2634
|
+
}
|
|
2635
|
+
const elapsed = ctx.currentTime - ctx.wordStart;
|
|
2636
|
+
return clamp(elapsed / ctx.animationDuration, 0, 1);
|
|
2637
|
+
}
|
|
2638
|
+
function calculateWordProgress(ctx) {
|
|
2639
|
+
const duration = ctx.wordEnd - ctx.wordStart;
|
|
2640
|
+
if (duration <= 0) {
|
|
2641
|
+
return ctx.currentTime >= ctx.wordStart ? 1 : 0;
|
|
2642
|
+
}
|
|
2643
|
+
const elapsed = ctx.currentTime - ctx.wordStart;
|
|
2644
|
+
return clamp(elapsed / duration, 0, 1);
|
|
2645
|
+
}
|
|
2646
|
+
function isWordActive(ctx) {
|
|
2647
|
+
return ctx.currentTime >= ctx.wordStart && ctx.currentTime < ctx.wordEnd;
|
|
2648
|
+
}
|
|
2649
|
+
function calculateKaraokeState(ctx) {
|
|
2650
|
+
const isActive = isWordActive(ctx);
|
|
2651
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2652
|
+
return {
|
|
2653
|
+
fillProgress: 0,
|
|
2654
|
+
isActive: false,
|
|
2655
|
+
opacity: 1
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
if (ctx.currentTime >= ctx.wordEnd) {
|
|
2659
|
+
return {
|
|
2660
|
+
fillProgress: 1,
|
|
2661
|
+
isActive: false,
|
|
2662
|
+
opacity: 1
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
return {
|
|
2666
|
+
fillProgress: calculateWordProgress(ctx),
|
|
2667
|
+
isActive,
|
|
2668
|
+
opacity: 1
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
function calculateHighlightState(ctx) {
|
|
2672
|
+
const isActive = isWordActive(ctx);
|
|
2673
|
+
return {
|
|
2674
|
+
isActive,
|
|
2675
|
+
fillProgress: isActive ? 1 : 0,
|
|
2676
|
+
opacity: 1
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
function calculatePopState(ctx, activeScale) {
|
|
2680
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2681
|
+
return {
|
|
2682
|
+
scale: 0.5,
|
|
2683
|
+
opacity: 0,
|
|
2684
|
+
isActive: false,
|
|
2685
|
+
fillProgress: 0
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
const progress = calculateAnimationProgress(ctx);
|
|
2689
|
+
const easedProgress = easeOutBack(progress);
|
|
2690
|
+
const startScale = 0.5;
|
|
2691
|
+
const isActive = isWordActive(ctx);
|
|
2692
|
+
const endScale = isActive ? activeScale : 1;
|
|
2693
|
+
const scale = startScale + (endScale - startScale) * easedProgress;
|
|
2694
|
+
return {
|
|
2695
|
+
scale: Math.min(scale, activeScale),
|
|
2696
|
+
opacity: easedProgress,
|
|
2697
|
+
isActive,
|
|
2698
|
+
fillProgress: isActive ? 1 : 0
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function calculateFadeState(ctx) {
|
|
2702
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2703
|
+
return {
|
|
2704
|
+
opacity: 0,
|
|
2705
|
+
isActive: false,
|
|
2706
|
+
fillProgress: 0
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
const progress = calculateAnimationProgress(ctx);
|
|
2710
|
+
const easedProgress = easeInOutQuad(progress);
|
|
2711
|
+
const isActive = isWordActive(ctx);
|
|
2712
|
+
return {
|
|
2713
|
+
opacity: easedProgress,
|
|
2714
|
+
isActive,
|
|
2715
|
+
fillProgress: isActive ? 1 : 0
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
function calculateSlideState(ctx, direction, fontSize) {
|
|
2719
|
+
const slideDistance = fontSize * 1.5;
|
|
2720
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2721
|
+
const offset2 = getDirectionOffset(direction, slideDistance);
|
|
2722
|
+
return {
|
|
2723
|
+
translateX: offset2.x,
|
|
2724
|
+
translateY: offset2.y,
|
|
2725
|
+
opacity: 0,
|
|
2726
|
+
isActive: false,
|
|
2727
|
+
fillProgress: 0
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
const progress = calculateAnimationProgress(ctx);
|
|
2731
|
+
const easedProgress = easeOutCirc(progress);
|
|
2732
|
+
const offset = getDirectionOffset(direction, slideDistance);
|
|
2733
|
+
const translateX = offset.x * (1 - easedProgress);
|
|
2734
|
+
const translateY = offset.y * (1 - easedProgress);
|
|
2735
|
+
const isActive = isWordActive(ctx);
|
|
2736
|
+
return {
|
|
2737
|
+
translateX,
|
|
2738
|
+
translateY,
|
|
2739
|
+
opacity: easeOutQuad2(progress),
|
|
2740
|
+
isActive,
|
|
2741
|
+
fillProgress: isActive ? 1 : 0
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
function getDirectionOffset(direction, distance) {
|
|
2745
|
+
switch (direction) {
|
|
2746
|
+
case "left":
|
|
2747
|
+
return { x: -distance, y: 0 };
|
|
2748
|
+
case "right":
|
|
2749
|
+
return { x: distance, y: 0 };
|
|
2750
|
+
case "up":
|
|
2751
|
+
return { x: 0, y: distance };
|
|
2752
|
+
case "down":
|
|
2753
|
+
return { x: 0, y: -distance };
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function calculateBounceState(ctx, fontSize) {
|
|
2757
|
+
const bounceDistance = fontSize * 0.8;
|
|
2758
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2759
|
+
return {
|
|
2760
|
+
translateY: -bounceDistance,
|
|
2761
|
+
opacity: 0,
|
|
2762
|
+
isActive: false,
|
|
2763
|
+
fillProgress: 0
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
const progress = calculateAnimationProgress(ctx);
|
|
2767
|
+
const easedProgress = easeOutBounce(progress);
|
|
2768
|
+
const isActive = isWordActive(ctx);
|
|
2769
|
+
return {
|
|
2770
|
+
translateY: -bounceDistance * (1 - easedProgress),
|
|
2771
|
+
opacity: easeOutQuad2(progress),
|
|
2772
|
+
isActive,
|
|
2773
|
+
fillProgress: isActive ? 1 : 0
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
function calculateTypewriterState(ctx, charCount) {
|
|
2777
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2778
|
+
return {
|
|
2779
|
+
visibleCharacters: 0,
|
|
2780
|
+
opacity: 1,
|
|
2781
|
+
isActive: false,
|
|
2782
|
+
fillProgress: 0
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
if (ctx.currentTime >= ctx.wordEnd) {
|
|
2786
|
+
return {
|
|
2787
|
+
visibleCharacters: charCount,
|
|
2788
|
+
opacity: 1,
|
|
2789
|
+
isActive: false,
|
|
2790
|
+
fillProgress: 0
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
const progress = calculateWordProgress(ctx);
|
|
2794
|
+
const visibleCharacters = Math.ceil(progress * charCount);
|
|
2795
|
+
const isActive = isWordActive(ctx);
|
|
2796
|
+
return {
|
|
2797
|
+
visibleCharacters: clamp(visibleCharacters, 0, charCount),
|
|
2798
|
+
opacity: 1,
|
|
2799
|
+
isActive,
|
|
2800
|
+
fillProgress: isActive ? 1 : 0
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
function calculateNoneState(_ctx) {
|
|
2804
|
+
return {
|
|
2805
|
+
opacity: 1,
|
|
2806
|
+
isActive: false,
|
|
2807
|
+
fillProgress: 0
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
|
|
2811
|
+
const ctx = {
|
|
2812
|
+
wordStart,
|
|
2813
|
+
wordEnd,
|
|
2814
|
+
currentTime,
|
|
2815
|
+
animationDuration: ANIMATION_DURATIONS[config.style]
|
|
2816
|
+
};
|
|
2817
|
+
const baseState = { ...DEFAULT_ANIMATION_STATE };
|
|
2470
2818
|
let partialState;
|
|
2471
2819
|
switch (config.style) {
|
|
2472
2820
|
case "karaoke":
|
|
2473
|
-
partialState = calculateKaraokeState(ctx
|
|
2821
|
+
partialState = calculateKaraokeState(ctx);
|
|
2474
2822
|
break;
|
|
2475
2823
|
case "highlight":
|
|
2476
2824
|
partialState = calculateHighlightState(ctx);
|
|
2477
2825
|
break;
|
|
2478
2826
|
case "pop":
|
|
2479
|
-
partialState = calculatePopState(ctx, activeScale
|
|
2827
|
+
partialState = calculatePopState(ctx, activeScale);
|
|
2480
2828
|
break;
|
|
2481
2829
|
case "fade":
|
|
2482
|
-
partialState = calculateFadeState(ctx
|
|
2830
|
+
partialState = calculateFadeState(ctx);
|
|
2483
2831
|
break;
|
|
2484
2832
|
case "slide": {
|
|
2485
2833
|
const slideDir = mirrorAnimationDirection(config.direction, isRTL);
|
|
2486
|
-
partialState = calculateSlideState(ctx, slideDir,
|
|
2834
|
+
partialState = calculateSlideState(ctx, slideDir, fontSize);
|
|
2487
2835
|
break;
|
|
2488
2836
|
}
|
|
2489
2837
|
case "bounce":
|
|
2490
|
-
partialState = calculateBounceState(ctx,
|
|
2838
|
+
partialState = calculateBounceState(ctx, fontSize);
|
|
2491
2839
|
break;
|
|
2492
2840
|
case "typewriter":
|
|
2493
|
-
partialState = calculateTypewriterState(ctx, charCount
|
|
2841
|
+
partialState = calculateTypewriterState(ctx, charCount);
|
|
2494
2842
|
break;
|
|
2495
2843
|
case "none":
|
|
2496
2844
|
default:
|
|
@@ -2523,34 +2871,30 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
|
|
|
2523
2871
|
function getDefaultAnimationConfig() {
|
|
2524
2872
|
return {
|
|
2525
2873
|
style: "highlight",
|
|
2526
|
-
speed: 1,
|
|
2527
2874
|
direction: "up"
|
|
2528
2875
|
};
|
|
2529
2876
|
}
|
|
2530
2877
|
|
|
2531
2878
|
// src/core/rich-caption-generator.ts
|
|
2532
|
-
var ASCENT_RATIO = 0.8;
|
|
2533
|
-
var DESCENT_RATIO = 0.2;
|
|
2534
2879
|
var WORD_BG_OPACITY = 1;
|
|
2535
2880
|
var WORD_BG_BORDER_RADIUS = 4;
|
|
2536
2881
|
var WORD_BG_PADDING_RATIO = 0.12;
|
|
2537
2882
|
function extractFontConfig(asset) {
|
|
2538
2883
|
const font = asset.font;
|
|
2539
2884
|
const active = asset.active?.font;
|
|
2540
|
-
const
|
|
2885
|
+
const hasExplicitActiveColor = active?.color !== void 0;
|
|
2541
2886
|
const baseColor = font?.color ?? "#ffffff";
|
|
2542
2887
|
const baseOpacity = font?.opacity ?? 1;
|
|
2543
2888
|
let activeColor;
|
|
2544
2889
|
let activeOpacity;
|
|
2545
|
-
if (!
|
|
2890
|
+
if (!hasExplicitActiveColor) {
|
|
2546
2891
|
activeColor = baseColor;
|
|
2547
|
-
activeOpacity = baseOpacity;
|
|
2892
|
+
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2548
2893
|
} else {
|
|
2549
|
-
const explicitActiveColor = active?.color;
|
|
2550
2894
|
const animStyle = asset.wordAnimation?.style ?? "highlight";
|
|
2551
2895
|
const isFillAnimation = animStyle === "karaoke" || animStyle === "highlight";
|
|
2552
2896
|
const DEFAULT_ACTIVE_COLOR = "#ffff00";
|
|
2553
|
-
activeColor =
|
|
2897
|
+
activeColor = active.color ?? (isFillAnimation ? DEFAULT_ACTIVE_COLOR : baseColor);
|
|
2554
2898
|
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2555
2899
|
}
|
|
2556
2900
|
return {
|
|
@@ -2567,18 +2911,17 @@ function extractFontConfig(asset) {
|
|
|
2567
2911
|
function extractStrokeConfig(asset, isActive) {
|
|
2568
2912
|
const baseStroke = asset.stroke;
|
|
2569
2913
|
const activeStroke = asset.active?.stroke;
|
|
2570
|
-
if (!baseStroke && !activeStroke) {
|
|
2571
|
-
return void 0;
|
|
2572
|
-
}
|
|
2573
2914
|
if (isActive) {
|
|
2574
|
-
if (
|
|
2915
|
+
if (activeStroke === "none") {
|
|
2575
2916
|
return void 0;
|
|
2576
2917
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2918
|
+
if (activeStroke && typeof activeStroke === "object") {
|
|
2919
|
+
return {
|
|
2920
|
+
width: activeStroke.width ?? baseStroke?.width ?? 0,
|
|
2921
|
+
color: activeStroke.color ?? baseStroke?.color ?? "#000000",
|
|
2922
|
+
opacity: activeStroke.opacity ?? baseStroke?.opacity ?? 1
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2582
2925
|
}
|
|
2583
2926
|
if (baseStroke) {
|
|
2584
2927
|
return {
|
|
@@ -2590,25 +2933,42 @@ function extractStrokeConfig(asset, isActive) {
|
|
|
2590
2933
|
return void 0;
|
|
2591
2934
|
}
|
|
2592
2935
|
function extractShadowConfig(asset, isActive) {
|
|
2936
|
+
const baseShadow = asset.shadow;
|
|
2937
|
+
const activeShadow = asset.active?.shadow;
|
|
2593
2938
|
if (isActive) {
|
|
2594
|
-
|
|
2939
|
+
if (activeShadow === "none") {
|
|
2940
|
+
return void 0;
|
|
2941
|
+
}
|
|
2942
|
+
if (activeShadow && typeof activeShadow === "object") {
|
|
2943
|
+
return {
|
|
2944
|
+
offsetX: activeShadow.offsetX ?? baseShadow?.offsetX ?? 0,
|
|
2945
|
+
offsetY: activeShadow.offsetY ?? baseShadow?.offsetY ?? 0,
|
|
2946
|
+
blur: activeShadow.blur ?? baseShadow?.blur ?? 0,
|
|
2947
|
+
color: activeShadow.color ?? baseShadow?.color ?? "#000000",
|
|
2948
|
+
opacity: activeShadow.opacity ?? baseShadow?.opacity ?? 0.5
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2595
2951
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2952
|
+
if (baseShadow) {
|
|
2953
|
+
return {
|
|
2954
|
+
offsetX: baseShadow.offsetX ?? 0,
|
|
2955
|
+
offsetY: baseShadow.offsetY ?? 0,
|
|
2956
|
+
blur: baseShadow.blur ?? 0,
|
|
2957
|
+
color: baseShadow.color ?? "#000000",
|
|
2958
|
+
opacity: baseShadow.opacity ?? 0.5
|
|
2959
|
+
};
|
|
2599
2960
|
}
|
|
2600
|
-
return
|
|
2601
|
-
offsetX: shadow.offsetX ?? 0,
|
|
2602
|
-
offsetY: shadow.offsetY ?? 0,
|
|
2603
|
-
blur: shadow.blur ?? 0,
|
|
2604
|
-
color: shadow.color ?? "#000000",
|
|
2605
|
-
opacity: shadow.opacity ?? 0.5
|
|
2606
|
-
};
|
|
2961
|
+
return void 0;
|
|
2607
2962
|
}
|
|
2608
2963
|
function extractBackgroundConfig(asset, isActive, fontSize) {
|
|
2609
2964
|
const fontBackground = asset.font?.background;
|
|
2610
2965
|
const activeBackground = asset.active?.font?.background;
|
|
2611
|
-
|
|
2966
|
+
let bgColor;
|
|
2967
|
+
if (isActive) {
|
|
2968
|
+
bgColor = activeBackground ?? fontBackground;
|
|
2969
|
+
} else {
|
|
2970
|
+
bgColor = fontBackground;
|
|
2971
|
+
}
|
|
2612
2972
|
if (!bgColor) {
|
|
2613
2973
|
return void 0;
|
|
2614
2974
|
}
|
|
@@ -2658,6 +3018,17 @@ function extractCaptionBorder(asset) {
|
|
|
2658
3018
|
radius: border.radius ?? 0
|
|
2659
3019
|
};
|
|
2660
3020
|
}
|
|
3021
|
+
function extractTextDecoration(asset, isActive) {
|
|
3022
|
+
const baseDecoration = asset.font?.textDecoration;
|
|
3023
|
+
const activeDecoration = asset.active?.font?.textDecoration;
|
|
3024
|
+
if (isActive && activeDecoration !== void 0) {
|
|
3025
|
+
return activeDecoration === "none" ? void 0 : activeDecoration;
|
|
3026
|
+
}
|
|
3027
|
+
if (!baseDecoration || baseDecoration === "none") {
|
|
3028
|
+
return void 0;
|
|
3029
|
+
}
|
|
3030
|
+
return baseDecoration;
|
|
3031
|
+
}
|
|
2661
3032
|
function extractAnimationConfig(asset) {
|
|
2662
3033
|
const wordAnim = asset.wordAnimation;
|
|
2663
3034
|
if (!wordAnim) {
|
|
@@ -2665,7 +3036,6 @@ function extractAnimationConfig(asset) {
|
|
|
2665
3036
|
}
|
|
2666
3037
|
return {
|
|
2667
3038
|
style: wordAnim.style ?? "highlight",
|
|
2668
|
-
speed: wordAnim.speed ?? 1,
|
|
2669
3039
|
direction: wordAnim.direction ?? "up"
|
|
2670
3040
|
};
|
|
2671
3041
|
}
|
|
@@ -2700,7 +3070,8 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
|
|
|
2700
3070
|
letterSpacing: fontConfig.letterSpacing > 0 ? fontConfig.letterSpacing : void 0,
|
|
2701
3071
|
stroke: extractStrokeConfig(asset, isActive),
|
|
2702
3072
|
shadow: extractShadowConfig(asset, isActive),
|
|
2703
|
-
background: extractBackgroundConfig(asset, isActive, fontConfig.size)
|
|
3073
|
+
background: extractBackgroundConfig(asset, isActive, fontConfig.size),
|
|
3074
|
+
textDecoration: extractTextDecoration(asset, isActive)
|
|
2704
3075
|
};
|
|
2705
3076
|
}
|
|
2706
3077
|
function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, config) {
|
|
@@ -3204,7 +3575,8 @@ async function createNodePainter(opts) {
|
|
|
3204
3575
|
context.lineCap = "round";
|
|
3205
3576
|
context.strokeText(displayText, 0, 0);
|
|
3206
3577
|
}
|
|
3207
|
-
|
|
3578
|
+
const sameColor = wordOp.activeColor === wordOp.baseColor && wordOp.activeOpacity === wordOp.baseOpacity;
|
|
3579
|
+
if (wordOp.fillProgress <= 0 || sameColor) {
|
|
3208
3580
|
const baseC = parseHex6(wordOp.baseColor, wordOp.baseOpacity);
|
|
3209
3581
|
context.fillStyle = `rgba(${baseC.r},${baseC.g},${baseC.b},${baseC.a})`;
|
|
3210
3582
|
context.fillText(displayText, 0, 0);
|
|
@@ -3231,7 +3603,26 @@ async function createNodePainter(opts) {
|
|
|
3231
3603
|
context.fillText(displayText, 0, 0);
|
|
3232
3604
|
context.restore();
|
|
3233
3605
|
}
|
|
3234
|
-
|
|
3606
|
+
if (wordOp.textDecoration) {
|
|
3607
|
+
const geo = decorationGeometry(wordOp.textDecoration, {
|
|
3608
|
+
baselineY: 0,
|
|
3609
|
+
fontSize: wordOp.fontSize,
|
|
3610
|
+
lineWidth: textWidth,
|
|
3611
|
+
xStart: 0
|
|
3612
|
+
});
|
|
3613
|
+
const sameC = wordOp.activeColor === wordOp.baseColor && wordOp.activeOpacity === wordOp.baseOpacity;
|
|
3614
|
+
const decoIsActive = wordOp.fillProgress >= 1 && !sameC;
|
|
3615
|
+
const decoColor = decoIsActive ? wordOp.activeColor : wordOp.baseColor;
|
|
3616
|
+
const decoOpacity = decoIsActive ? wordOp.activeOpacity : wordOp.baseOpacity;
|
|
3617
|
+
const dc = parseHex6(decoColor, decoOpacity);
|
|
3618
|
+
context.strokeStyle = `rgba(${dc.r},${dc.g},${dc.b},${dc.a})`;
|
|
3619
|
+
context.lineWidth = geo.width;
|
|
3620
|
+
context.beginPath();
|
|
3621
|
+
context.moveTo(geo.x1, geo.y);
|
|
3622
|
+
context.lineTo(geo.x2, geo.y);
|
|
3623
|
+
context.stroke();
|
|
3624
|
+
}
|
|
3625
|
+
context.restore();
|
|
3235
3626
|
}
|
|
3236
3627
|
});
|
|
3237
3628
|
continue;
|
|
@@ -4522,520 +4913,173 @@ function computeSimplePathBounds(d) {
|
|
|
4522
4913
|
minX = Math.min(minX, currentX);
|
|
4523
4914
|
maxX = Math.max(maxX, currentX);
|
|
4524
4915
|
}
|
|
4525
|
-
break;
|
|
4526
|
-
case "h":
|
|
4527
|
-
if (numIndex < numbers.length) {
|
|
4528
|
-
currentX += numbers[numIndex++];
|
|
4529
|
-
minX = Math.min(minX, currentX);
|
|
4530
|
-
maxX = Math.max(maxX, currentX);
|
|
4531
|
-
}
|
|
4532
|
-
break;
|
|
4533
|
-
case "V":
|
|
4534
|
-
if (numIndex < numbers.length) {
|
|
4535
|
-
currentY = numbers[numIndex++];
|
|
4536
|
-
minY = Math.min(minY, currentY);
|
|
4537
|
-
maxY = Math.max(maxY, currentY);
|
|
4538
|
-
}
|
|
4539
|
-
break;
|
|
4540
|
-
case "v":
|
|
4541
|
-
if (numIndex < numbers.length) {
|
|
4542
|
-
currentY += numbers[numIndex++];
|
|
4543
|
-
minY = Math.min(minY, currentY);
|
|
4544
|
-
maxY = Math.max(maxY, currentY);
|
|
4545
|
-
}
|
|
4546
|
-
break;
|
|
4547
|
-
case "C":
|
|
4548
|
-
if (numIndex + 5 < numbers.length) {
|
|
4549
|
-
for (let j = 0; j < 3; j++) {
|
|
4550
|
-
const x = numbers[numIndex++];
|
|
4551
|
-
const y = numbers[numIndex++];
|
|
4552
|
-
minX = Math.min(minX, x);
|
|
4553
|
-
maxX = Math.max(maxX, x);
|
|
4554
|
-
minY = Math.min(minY, y);
|
|
4555
|
-
maxY = Math.max(maxY, y);
|
|
4556
|
-
if (j === 2) {
|
|
4557
|
-
currentX = x;
|
|
4558
|
-
currentY = y;
|
|
4559
|
-
}
|
|
4560
|
-
}
|
|
4561
|
-
}
|
|
4562
|
-
break;
|
|
4563
|
-
case "c":
|
|
4564
|
-
if (numIndex + 5 < numbers.length) {
|
|
4565
|
-
for (let j = 0; j < 3; j++) {
|
|
4566
|
-
const x = currentX + numbers[numIndex++];
|
|
4567
|
-
const y = currentY + numbers[numIndex++];
|
|
4568
|
-
minX = Math.min(minX, x);
|
|
4569
|
-
maxX = Math.max(maxX, x);
|
|
4570
|
-
minY = Math.min(minY, y);
|
|
4571
|
-
maxY = Math.max(maxY, y);
|
|
4572
|
-
if (j === 2) {
|
|
4573
|
-
currentX = x;
|
|
4574
|
-
currentY = y;
|
|
4575
|
-
}
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
break;
|
|
4579
|
-
case "S":
|
|
4580
|
-
case "Q":
|
|
4581
|
-
if (numIndex + 3 < numbers.length) {
|
|
4582
|
-
for (let j = 0; j < 2; j++) {
|
|
4583
|
-
const x = numbers[numIndex++];
|
|
4584
|
-
const y = numbers[numIndex++];
|
|
4585
|
-
minX = Math.min(minX, x);
|
|
4586
|
-
maxX = Math.max(maxX, x);
|
|
4587
|
-
minY = Math.min(minY, y);
|
|
4588
|
-
maxY = Math.max(maxY, y);
|
|
4589
|
-
if (j === 1) {
|
|
4590
|
-
currentX = x;
|
|
4591
|
-
currentY = y;
|
|
4592
|
-
}
|
|
4593
|
-
}
|
|
4594
|
-
}
|
|
4595
|
-
break;
|
|
4596
|
-
case "s":
|
|
4597
|
-
case "q":
|
|
4598
|
-
if (numIndex + 3 < numbers.length) {
|
|
4599
|
-
for (let j = 0; j < 2; j++) {
|
|
4600
|
-
const x = currentX + numbers[numIndex++];
|
|
4601
|
-
const y = currentY + numbers[numIndex++];
|
|
4602
|
-
minX = Math.min(minX, x);
|
|
4603
|
-
maxX = Math.max(maxX, x);
|
|
4604
|
-
minY = Math.min(minY, y);
|
|
4605
|
-
maxY = Math.max(maxY, y);
|
|
4606
|
-
if (j === 1) {
|
|
4607
|
-
currentX = x;
|
|
4608
|
-
currentY = y;
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
|
-
break;
|
|
4613
|
-
case "A":
|
|
4614
|
-
if (numIndex + 6 < numbers.length) {
|
|
4615
|
-
numIndex += 5;
|
|
4616
|
-
currentX = numbers[numIndex++];
|
|
4617
|
-
currentY = numbers[numIndex++];
|
|
4618
|
-
minX = Math.min(minX, currentX);
|
|
4619
|
-
maxX = Math.max(maxX, currentX);
|
|
4620
|
-
minY = Math.min(minY, currentY);
|
|
4621
|
-
maxY = Math.max(maxY, currentY);
|
|
4622
|
-
}
|
|
4623
|
-
break;
|
|
4624
|
-
case "a":
|
|
4625
|
-
if (numIndex + 6 < numbers.length) {
|
|
4626
|
-
numIndex += 5;
|
|
4627
|
-
currentX += numbers[numIndex++];
|
|
4628
|
-
currentY += numbers[numIndex++];
|
|
4629
|
-
minX = Math.min(minX, currentX);
|
|
4630
|
-
maxX = Math.max(maxX, currentX);
|
|
4631
|
-
minY = Math.min(minY, currentY);
|
|
4632
|
-
maxY = Math.max(maxY, currentY);
|
|
4633
|
-
}
|
|
4634
|
-
break;
|
|
4635
|
-
case "Z":
|
|
4636
|
-
case "z":
|
|
4637
|
-
break;
|
|
4638
|
-
}
|
|
4639
|
-
cmdIndex++;
|
|
4640
|
-
}
|
|
4641
|
-
if (minX === Infinity) {
|
|
4642
|
-
return { x: 0, y: 0, w: 0, h: 0 };
|
|
4643
|
-
}
|
|
4644
|
-
return {
|
|
4645
|
-
x: minX,
|
|
4646
|
-
y: minY,
|
|
4647
|
-
w: maxX - minX,
|
|
4648
|
-
h: maxY - minY
|
|
4649
|
-
};
|
|
4650
|
-
}
|
|
4651
|
-
async function renderSvgAssetToPng(asset, options = {}) {
|
|
4652
|
-
const defaultWidth = options.defaultWidth ?? 1920;
|
|
4653
|
-
const defaultHeight = options.defaultHeight ?? 1080;
|
|
4654
|
-
let svgString;
|
|
4655
|
-
let targetWidth;
|
|
4656
|
-
let targetHeight;
|
|
4657
|
-
if (asset.src) {
|
|
4658
|
-
svgString = asset.src;
|
|
4659
|
-
const dimensions = extractSvgDimensions(svgString);
|
|
4660
|
-
targetWidth = dimensions.width || defaultWidth;
|
|
4661
|
-
targetHeight = dimensions.height || defaultHeight;
|
|
4662
|
-
} else if (asset.shape) {
|
|
4663
|
-
targetWidth = toNumber(asset.width, defaultWidth);
|
|
4664
|
-
targetHeight = toNumber(asset.height, defaultHeight);
|
|
4665
|
-
svgString = shapeToSvgString(asset, targetWidth, targetHeight);
|
|
4666
|
-
} else {
|
|
4667
|
-
throw new Error("Either 'src' or 'shape' must be provided");
|
|
4668
|
-
}
|
|
4669
|
-
return renderSvgToPng(svgString, {
|
|
4670
|
-
width: targetWidth,
|
|
4671
|
-
height: targetHeight,
|
|
4672
|
-
background: options.background
|
|
4673
|
-
});
|
|
4674
|
-
}
|
|
4675
|
-
function extractSvgDimensions(svgString) {
|
|
4676
|
-
const widthMatch = svgString.match(/width\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
4677
|
-
const heightMatch = svgString.match(/height\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
4678
|
-
let width = widthMatch ? parseFloat(widthMatch[1]) : 0;
|
|
4679
|
-
let height = heightMatch ? parseFloat(heightMatch[1]) : 0;
|
|
4680
|
-
if (!width || !height) {
|
|
4681
|
-
const viewBoxMatch = svgString.match(/viewBox\s*=\s*["']([^"']+)["']/i);
|
|
4682
|
-
if (viewBoxMatch) {
|
|
4683
|
-
const parts = viewBoxMatch[1].trim().split(/[\s,]+/).map(parseFloat);
|
|
4684
|
-
if (parts.length === 4) {
|
|
4685
|
-
width = width || parts[2];
|
|
4686
|
-
height = height || parts[3];
|
|
4687
|
-
}
|
|
4688
|
-
}
|
|
4689
|
-
}
|
|
4690
|
-
return { width, height };
|
|
4691
|
-
}
|
|
4692
|
-
|
|
4693
|
-
// src/core/rich-caption-layout.ts
|
|
4694
|
-
import { LRUCache } from "lru-cache";
|
|
4695
|
-
function isRTLText(text) {
|
|
4696
|
-
return containsRTLCharacters(text);
|
|
4697
|
-
}
|
|
4698
|
-
var WordTimingStore = class {
|
|
4699
|
-
startTimes;
|
|
4700
|
-
endTimes;
|
|
4701
|
-
xPositions;
|
|
4702
|
-
yPositions;
|
|
4703
|
-
widths;
|
|
4704
|
-
words;
|
|
4705
|
-
length;
|
|
4706
|
-
constructor(words) {
|
|
4707
|
-
this.length = words.length;
|
|
4708
|
-
this.startTimes = new Uint32Array(this.length);
|
|
4709
|
-
this.endTimes = new Uint32Array(this.length);
|
|
4710
|
-
this.xPositions = new Float32Array(this.length);
|
|
4711
|
-
this.yPositions = new Float32Array(this.length);
|
|
4712
|
-
this.widths = new Float32Array(this.length);
|
|
4713
|
-
this.words = new Array(this.length);
|
|
4714
|
-
for (let i = 0; i < this.length; i++) {
|
|
4715
|
-
this.startTimes[i] = Math.floor(words[i].start);
|
|
4716
|
-
this.endTimes[i] = Math.floor(words[i].end);
|
|
4717
|
-
this.words[i] = words[i].text;
|
|
4718
|
-
}
|
|
4719
|
-
}
|
|
4720
|
-
};
|
|
4721
|
-
function findWordAtTime(store, timeMs) {
|
|
4722
|
-
let left = 0;
|
|
4723
|
-
let right = store.length - 1;
|
|
4724
|
-
while (left <= right) {
|
|
4725
|
-
const mid = left + right >>> 1;
|
|
4726
|
-
const start = store.startTimes[mid];
|
|
4727
|
-
const end = store.endTimes[mid];
|
|
4728
|
-
if (timeMs >= start && timeMs < end) {
|
|
4729
|
-
return mid;
|
|
4730
|
-
}
|
|
4731
|
-
if (timeMs < start) {
|
|
4732
|
-
right = mid - 1;
|
|
4733
|
-
} else {
|
|
4734
|
-
left = mid + 1;
|
|
4735
|
-
}
|
|
4736
|
-
}
|
|
4737
|
-
return -1;
|
|
4738
|
-
}
|
|
4739
|
-
function groupWordsByPause(store, pauseThreshold = 500) {
|
|
4740
|
-
if (store.length === 0) {
|
|
4741
|
-
return [];
|
|
4742
|
-
}
|
|
4743
|
-
const groups = [];
|
|
4744
|
-
let currentGroup = [];
|
|
4745
|
-
for (let i = 0; i < store.length; i++) {
|
|
4746
|
-
if (currentGroup.length === 0) {
|
|
4747
|
-
currentGroup.push(i);
|
|
4748
|
-
continue;
|
|
4749
|
-
}
|
|
4750
|
-
const prevEnd = store.endTimes[currentGroup[currentGroup.length - 1]];
|
|
4751
|
-
const currStart = store.startTimes[i];
|
|
4752
|
-
const gap = currStart - prevEnd;
|
|
4753
|
-
const prevText = store.words[currentGroup[currentGroup.length - 1]];
|
|
4754
|
-
const endsWithPunctuation = /[.!?]$/.test(prevText);
|
|
4755
|
-
if (gap >= pauseThreshold || endsWithPunctuation) {
|
|
4756
|
-
groups.push(currentGroup);
|
|
4757
|
-
currentGroup = [i];
|
|
4758
|
-
} else {
|
|
4759
|
-
currentGroup.push(i);
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
4762
|
-
if (currentGroup.length > 0) {
|
|
4763
|
-
groups.push(currentGroup);
|
|
4764
|
-
}
|
|
4765
|
-
return groups;
|
|
4766
|
-
}
|
|
4767
|
-
function breakIntoLines(wordWidths, maxWidth, spaceWidth) {
|
|
4768
|
-
const lines = [];
|
|
4769
|
-
let currentLine = [];
|
|
4770
|
-
let currentWidth = 0;
|
|
4771
|
-
for (let i = 0; i < wordWidths.length; i++) {
|
|
4772
|
-
const wordWidth = wordWidths[i];
|
|
4773
|
-
const spaceNeeded = currentLine.length > 0 ? spaceWidth : 0;
|
|
4774
|
-
if (currentWidth + spaceNeeded + wordWidth <= maxWidth) {
|
|
4775
|
-
currentLine.push(i);
|
|
4776
|
-
currentWidth += spaceNeeded + wordWidth;
|
|
4777
|
-
} else {
|
|
4778
|
-
if (currentLine.length > 0) {
|
|
4779
|
-
lines.push(currentLine);
|
|
4780
|
-
}
|
|
4781
|
-
currentLine = [i];
|
|
4782
|
-
currentWidth = wordWidth;
|
|
4783
|
-
}
|
|
4784
|
-
}
|
|
4785
|
-
if (currentLine.length > 0) {
|
|
4786
|
-
lines.push(currentLine);
|
|
4787
|
-
}
|
|
4788
|
-
return lines;
|
|
4789
|
-
}
|
|
4790
|
-
var GLYPH_SIZE_ESTIMATE = 64;
|
|
4791
|
-
function createShapedWordCache() {
|
|
4792
|
-
return new LRUCache({
|
|
4793
|
-
max: 5e4,
|
|
4794
|
-
maxSize: 50 * 1024 * 1024,
|
|
4795
|
-
maxEntrySize: 100 * 1024,
|
|
4796
|
-
sizeCalculation: (value, key) => {
|
|
4797
|
-
const keySize = key.length * 2;
|
|
4798
|
-
const glyphsSize = value.glyphs.length * GLYPH_SIZE_ESTIMATE;
|
|
4799
|
-
return keySize + glyphsSize + 100;
|
|
4800
|
-
}
|
|
4801
|
-
});
|
|
4802
|
-
}
|
|
4803
|
-
function makeShapingKey(text, fontFamily, fontSize, fontWeight, letterSpacing = 0) {
|
|
4804
|
-
return `${text}\0${fontFamily}\0${fontSize}\0${fontWeight}\0${letterSpacing}`;
|
|
4805
|
-
}
|
|
4806
|
-
function transformText(text, transform) {
|
|
4807
|
-
switch (transform) {
|
|
4808
|
-
case "uppercase":
|
|
4809
|
-
return text.toUpperCase();
|
|
4810
|
-
case "lowercase":
|
|
4811
|
-
return text.toLowerCase();
|
|
4812
|
-
case "capitalize":
|
|
4813
|
-
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
4814
|
-
default:
|
|
4815
|
-
return text;
|
|
4816
|
-
}
|
|
4817
|
-
}
|
|
4818
|
-
function splitIntoChunks(arr, chunkSize) {
|
|
4819
|
-
const chunks = [];
|
|
4820
|
-
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
4821
|
-
chunks.push(arr.slice(i, i + chunkSize));
|
|
4822
|
-
}
|
|
4823
|
-
return chunks;
|
|
4824
|
-
}
|
|
4825
|
-
var CaptionLayoutEngine = class {
|
|
4826
|
-
fontRegistry;
|
|
4827
|
-
cache;
|
|
4828
|
-
layoutEngine;
|
|
4829
|
-
constructor(fontRegistry) {
|
|
4830
|
-
this.fontRegistry = fontRegistry;
|
|
4831
|
-
this.cache = createShapedWordCache();
|
|
4832
|
-
this.layoutEngine = new LayoutEngine(fontRegistry);
|
|
4833
|
-
}
|
|
4834
|
-
async measureWord(text, config) {
|
|
4835
|
-
const transformedText = transformText(text, config.textTransform);
|
|
4836
|
-
const cacheKey = makeShapingKey(
|
|
4837
|
-
transformedText,
|
|
4838
|
-
config.fontFamily,
|
|
4839
|
-
config.fontSize,
|
|
4840
|
-
config.fontWeight,
|
|
4841
|
-
config.letterSpacing
|
|
4842
|
-
);
|
|
4843
|
-
const cached = this.cache.get(cacheKey);
|
|
4844
|
-
if (cached) {
|
|
4845
|
-
return cached;
|
|
4846
|
-
}
|
|
4847
|
-
const lines = await this.layoutEngine.layout({
|
|
4848
|
-
text: transformedText,
|
|
4849
|
-
width: 1e5,
|
|
4850
|
-
letterSpacing: config.letterSpacing,
|
|
4851
|
-
fontSize: config.fontSize,
|
|
4852
|
-
lineHeight: 1,
|
|
4853
|
-
desc: { family: config.fontFamily, weight: config.fontWeight },
|
|
4854
|
-
textTransform: "none"
|
|
4855
|
-
});
|
|
4856
|
-
const width = lines[0]?.width ?? 0;
|
|
4857
|
-
const glyphs = lines[0]?.glyphs ?? [];
|
|
4858
|
-
const isRTL = isRTLText(transformedText);
|
|
4859
|
-
const shaped = {
|
|
4860
|
-
text: transformedText,
|
|
4861
|
-
width,
|
|
4862
|
-
glyphs: glyphs.map((g) => ({
|
|
4863
|
-
id: g.id,
|
|
4864
|
-
xAdvance: g.xAdvance,
|
|
4865
|
-
xOffset: g.xOffset,
|
|
4866
|
-
yOffset: g.yOffset,
|
|
4867
|
-
cluster: g.cluster
|
|
4868
|
-
})),
|
|
4869
|
-
isRTL
|
|
4870
|
-
};
|
|
4871
|
-
this.cache.set(cacheKey, shaped);
|
|
4872
|
-
return shaped;
|
|
4873
|
-
}
|
|
4874
|
-
async layoutCaption(words, config) {
|
|
4875
|
-
const store = new WordTimingStore(words);
|
|
4876
|
-
const measurementConfig = {
|
|
4877
|
-
fontFamily: config.fontFamily,
|
|
4878
|
-
fontSize: config.fontSize,
|
|
4879
|
-
fontWeight: config.fontWeight,
|
|
4880
|
-
letterSpacing: config.letterSpacing,
|
|
4881
|
-
textTransform: config.textTransform
|
|
4882
|
-
};
|
|
4883
|
-
const shapedWords = await Promise.all(
|
|
4884
|
-
words.map((w) => this.measureWord(w.text, measurementConfig))
|
|
4885
|
-
);
|
|
4886
|
-
if (config.measureTextWidth) {
|
|
4887
|
-
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
4888
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
4889
|
-
store.widths[i] = config.measureTextWidth(shapedWords[i].text, fontString);
|
|
4890
|
-
}
|
|
4891
|
-
} else {
|
|
4892
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
4893
|
-
store.widths[i] = shapedWords[i].width;
|
|
4894
|
-
}
|
|
4895
|
-
}
|
|
4896
|
-
if (config.textTransform !== "none") {
|
|
4897
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
4898
|
-
store.words[i] = shapedWords[i].text;
|
|
4899
|
-
}
|
|
4900
|
-
}
|
|
4901
|
-
const wordGroups = groupWordsByPause(store, config.pauseThreshold);
|
|
4902
|
-
const pixelMaxWidth = config.availableWidth;
|
|
4903
|
-
let spaceWidth;
|
|
4904
|
-
if (config.measureTextWidth) {
|
|
4905
|
-
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
4906
|
-
spaceWidth = config.measureTextWidth(" ", fontString) + config.wordSpacing;
|
|
4907
|
-
} else {
|
|
4908
|
-
const spaceWord = await this.measureWord(" ", measurementConfig);
|
|
4909
|
-
spaceWidth = spaceWord.width + config.wordSpacing;
|
|
4910
|
-
}
|
|
4911
|
-
const groups = wordGroups.flatMap((indices) => {
|
|
4912
|
-
const groupWidths = indices.map((i) => store.widths[i]);
|
|
4913
|
-
const allLines = breakIntoLines(
|
|
4914
|
-
groupWidths,
|
|
4915
|
-
pixelMaxWidth,
|
|
4916
|
-
spaceWidth
|
|
4917
|
-
);
|
|
4918
|
-
const lineChunks = splitIntoChunks(allLines, config.maxLines);
|
|
4919
|
-
return lineChunks.map((chunkLines) => {
|
|
4920
|
-
const lines = chunkLines.map((lineWordIndices, lineIndex) => {
|
|
4921
|
-
const actualIndices = lineWordIndices.map((i) => indices[i]);
|
|
4922
|
-
const lineWidth = actualIndices.reduce((sum, idx) => sum + store.widths[idx], 0) + (actualIndices.length - 1) * spaceWidth;
|
|
4923
|
-
return {
|
|
4924
|
-
wordIndices: actualIndices,
|
|
4925
|
-
x: 0,
|
|
4926
|
-
y: lineIndex * config.fontSize * config.lineHeight,
|
|
4927
|
-
width: lineWidth,
|
|
4928
|
-
height: config.fontSize
|
|
4929
|
-
};
|
|
4930
|
-
});
|
|
4931
|
-
const allWordIndices = lines.flatMap((l) => l.wordIndices);
|
|
4932
|
-
if (allWordIndices.length === 0) {
|
|
4933
|
-
return null;
|
|
4916
|
+
break;
|
|
4917
|
+
case "h":
|
|
4918
|
+
if (numIndex < numbers.length) {
|
|
4919
|
+
currentX += numbers[numIndex++];
|
|
4920
|
+
minX = Math.min(minX, currentX);
|
|
4921
|
+
maxX = Math.max(maxX, currentX);
|
|
4934
4922
|
}
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
}).filter((g) => g !== null);
|
|
4942
|
-
});
|
|
4943
|
-
const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
|
|
4944
|
-
const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
|
|
4945
|
-
const ASCENT_RATIO2 = 0.8;
|
|
4946
|
-
const calculateGroupY = (group) => {
|
|
4947
|
-
const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
|
|
4948
|
-
switch (config.verticalAlign) {
|
|
4949
|
-
case "top":
|
|
4950
|
-
return config.padding.top + config.fontSize * ASCENT_RATIO2;
|
|
4951
|
-
case "bottom":
|
|
4952
|
-
return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO2;
|
|
4953
|
-
case "middle":
|
|
4954
|
-
default:
|
|
4955
|
-
return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO2;
|
|
4956
|
-
}
|
|
4957
|
-
};
|
|
4958
|
-
const allWordTexts = store.words.slice(0, store.length);
|
|
4959
|
-
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
4960
|
-
const calculateLineX = (lineWidth) => {
|
|
4961
|
-
switch (config.horizontalAlign) {
|
|
4962
|
-
case "left":
|
|
4963
|
-
return config.padding.left;
|
|
4964
|
-
case "right":
|
|
4965
|
-
return config.frameWidth - lineWidth - config.padding.right;
|
|
4966
|
-
case "center":
|
|
4967
|
-
default:
|
|
4968
|
-
return config.padding.left + (contentWidth - lineWidth) / 2;
|
|
4969
|
-
}
|
|
4970
|
-
};
|
|
4971
|
-
for (const group of groups) {
|
|
4972
|
-
const baseY = calculateGroupY(group);
|
|
4973
|
-
for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
|
|
4974
|
-
const line = group.lines[lineIdx];
|
|
4975
|
-
line.x = calculateLineX(line.width);
|
|
4976
|
-
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
4977
|
-
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
4978
|
-
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
4979
|
-
let xCursor = line.x;
|
|
4980
|
-
for (const visualIdx of visualOrder) {
|
|
4981
|
-
const wordIdx = line.wordIndices[visualIdx];
|
|
4982
|
-
store.xPositions[wordIdx] = xCursor;
|
|
4983
|
-
store.yPositions[wordIdx] = line.y;
|
|
4984
|
-
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
4923
|
+
break;
|
|
4924
|
+
case "V":
|
|
4925
|
+
if (numIndex < numbers.length) {
|
|
4926
|
+
currentY = numbers[numIndex++];
|
|
4927
|
+
minY = Math.min(minY, currentY);
|
|
4928
|
+
maxY = Math.max(maxY, currentY);
|
|
4985
4929
|
}
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
4930
|
+
break;
|
|
4931
|
+
case "v":
|
|
4932
|
+
if (numIndex < numbers.length) {
|
|
4933
|
+
currentY += numbers[numIndex++];
|
|
4934
|
+
minY = Math.min(minY, currentY);
|
|
4935
|
+
maxY = Math.max(maxY, currentY);
|
|
4936
|
+
}
|
|
4937
|
+
break;
|
|
4938
|
+
case "C":
|
|
4939
|
+
if (numIndex + 5 < numbers.length) {
|
|
4940
|
+
for (let j = 0; j < 3; j++) {
|
|
4941
|
+
const x = numbers[numIndex++];
|
|
4942
|
+
const y = numbers[numIndex++];
|
|
4943
|
+
minX = Math.min(minX, x);
|
|
4944
|
+
maxX = Math.max(maxX, x);
|
|
4945
|
+
minY = Math.min(minY, y);
|
|
4946
|
+
maxY = Math.max(maxY, y);
|
|
4947
|
+
if (j === 2) {
|
|
4948
|
+
currentX = x;
|
|
4949
|
+
currentY = y;
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
break;
|
|
4954
|
+
case "c":
|
|
4955
|
+
if (numIndex + 5 < numbers.length) {
|
|
4956
|
+
for (let j = 0; j < 3; j++) {
|
|
4957
|
+
const x = currentX + numbers[numIndex++];
|
|
4958
|
+
const y = currentY + numbers[numIndex++];
|
|
4959
|
+
minX = Math.min(minX, x);
|
|
4960
|
+
maxX = Math.max(maxX, x);
|
|
4961
|
+
minY = Math.min(minY, y);
|
|
4962
|
+
maxY = Math.max(maxY, y);
|
|
4963
|
+
if (j === 2) {
|
|
4964
|
+
currentX = x;
|
|
4965
|
+
currentY = y;
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
break;
|
|
4970
|
+
case "S":
|
|
4971
|
+
case "Q":
|
|
4972
|
+
if (numIndex + 3 < numbers.length) {
|
|
4973
|
+
for (let j = 0; j < 2; j++) {
|
|
4974
|
+
const x = numbers[numIndex++];
|
|
4975
|
+
const y = numbers[numIndex++];
|
|
4976
|
+
minX = Math.min(minX, x);
|
|
4977
|
+
maxX = Math.max(maxX, x);
|
|
4978
|
+
minY = Math.min(minY, y);
|
|
4979
|
+
maxY = Math.max(maxY, y);
|
|
4980
|
+
if (j === 1) {
|
|
4981
|
+
currentX = x;
|
|
4982
|
+
currentY = y;
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
break;
|
|
4987
|
+
case "s":
|
|
4988
|
+
case "q":
|
|
4989
|
+
if (numIndex + 3 < numbers.length) {
|
|
4990
|
+
for (let j = 0; j < 2; j++) {
|
|
4991
|
+
const x = currentX + numbers[numIndex++];
|
|
4992
|
+
const y = currentY + numbers[numIndex++];
|
|
4993
|
+
minX = Math.min(minX, x);
|
|
4994
|
+
maxX = Math.max(maxX, x);
|
|
4995
|
+
minY = Math.min(minY, y);
|
|
4996
|
+
maxY = Math.max(maxY, y);
|
|
4997
|
+
if (j === 1) {
|
|
4998
|
+
currentX = x;
|
|
4999
|
+
currentY = y;
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
break;
|
|
5004
|
+
case "A":
|
|
5005
|
+
if (numIndex + 6 < numbers.length) {
|
|
5006
|
+
numIndex += 5;
|
|
5007
|
+
currentX = numbers[numIndex++];
|
|
5008
|
+
currentY = numbers[numIndex++];
|
|
5009
|
+
minX = Math.min(minX, currentX);
|
|
5010
|
+
maxX = Math.max(maxX, currentX);
|
|
5011
|
+
minY = Math.min(minY, currentY);
|
|
5012
|
+
maxY = Math.max(maxY, currentY);
|
|
5013
|
+
}
|
|
5014
|
+
break;
|
|
5015
|
+
case "a":
|
|
5016
|
+
if (numIndex + 6 < numbers.length) {
|
|
5017
|
+
numIndex += 5;
|
|
5018
|
+
currentX += numbers[numIndex++];
|
|
5019
|
+
currentY += numbers[numIndex++];
|
|
5020
|
+
minX = Math.min(minX, currentX);
|
|
5021
|
+
maxX = Math.max(maxX, currentX);
|
|
5022
|
+
minY = Math.min(minY, currentY);
|
|
5023
|
+
maxY = Math.max(maxY, currentY);
|
|
5024
|
+
}
|
|
5025
|
+
break;
|
|
5026
|
+
case "Z":
|
|
5027
|
+
case "z":
|
|
5028
|
+
break;
|
|
5001
5029
|
}
|
|
5002
|
-
|
|
5003
|
-
wordIndex: idx,
|
|
5004
|
-
text: layout.store.words[idx],
|
|
5005
|
-
x: layout.store.xPositions[idx],
|
|
5006
|
-
y: layout.store.yPositions[idx],
|
|
5007
|
-
width: layout.store.widths[idx],
|
|
5008
|
-
startTime: layout.store.startTimes[idx],
|
|
5009
|
-
endTime: layout.store.endTimes[idx],
|
|
5010
|
-
isRTL: layout.shapedWords[idx].isRTL
|
|
5011
|
-
}));
|
|
5030
|
+
cmdIndex++;
|
|
5012
5031
|
}
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
if (wordIndex === -1) {
|
|
5016
|
-
return null;
|
|
5017
|
-
}
|
|
5018
|
-
return {
|
|
5019
|
-
wordIndex,
|
|
5020
|
-
text: layout.store.words[wordIndex],
|
|
5021
|
-
x: layout.store.xPositions[wordIndex],
|
|
5022
|
-
y: layout.store.yPositions[wordIndex],
|
|
5023
|
-
width: layout.store.widths[wordIndex],
|
|
5024
|
-
startTime: layout.store.startTimes[wordIndex],
|
|
5025
|
-
endTime: layout.store.endTimes[wordIndex],
|
|
5026
|
-
isRTL: layout.shapedWords[wordIndex].isRTL
|
|
5027
|
-
};
|
|
5032
|
+
if (minX === Infinity) {
|
|
5033
|
+
return { x: 0, y: 0, w: 0, h: 0 };
|
|
5028
5034
|
}
|
|
5029
|
-
|
|
5030
|
-
|
|
5035
|
+
return {
|
|
5036
|
+
x: minX,
|
|
5037
|
+
y: minY,
|
|
5038
|
+
w: maxX - minX,
|
|
5039
|
+
h: maxY - minY
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
5042
|
+
async function renderSvgAssetToPng(asset, options = {}) {
|
|
5043
|
+
const defaultWidth = options.defaultWidth ?? 1920;
|
|
5044
|
+
const defaultHeight = options.defaultHeight ?? 1080;
|
|
5045
|
+
let svgString;
|
|
5046
|
+
let targetWidth;
|
|
5047
|
+
let targetHeight;
|
|
5048
|
+
if (asset.src) {
|
|
5049
|
+
svgString = asset.src;
|
|
5050
|
+
const dimensions = extractSvgDimensions(svgString);
|
|
5051
|
+
targetWidth = dimensions.width || defaultWidth;
|
|
5052
|
+
targetHeight = dimensions.height || defaultHeight;
|
|
5053
|
+
} else if (asset.shape) {
|
|
5054
|
+
targetWidth = toNumber(asset.width, defaultWidth);
|
|
5055
|
+
targetHeight = toNumber(asset.height, defaultHeight);
|
|
5056
|
+
svgString = shapeToSvgString(asset, targetWidth, targetHeight);
|
|
5057
|
+
} else {
|
|
5058
|
+
throw new Error("Either 'src' or 'shape' must be provided");
|
|
5031
5059
|
}
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5060
|
+
return renderSvgToPng(svgString, {
|
|
5061
|
+
width: targetWidth,
|
|
5062
|
+
height: targetHeight,
|
|
5063
|
+
background: options.background
|
|
5064
|
+
});
|
|
5065
|
+
}
|
|
5066
|
+
function extractSvgDimensions(svgString) {
|
|
5067
|
+
const widthMatch = svgString.match(/width\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
5068
|
+
const heightMatch = svgString.match(/height\s*=\s*["']?(\d+(?:\.\d+)?)/i);
|
|
5069
|
+
let width = widthMatch ? parseFloat(widthMatch[1]) : 0;
|
|
5070
|
+
let height = heightMatch ? parseFloat(heightMatch[1]) : 0;
|
|
5071
|
+
if (!width || !height) {
|
|
5072
|
+
const viewBoxMatch = svgString.match(/viewBox\s*=\s*["']([^"']+)["']/i);
|
|
5073
|
+
if (viewBoxMatch) {
|
|
5074
|
+
const parts = viewBoxMatch[1].trim().split(/[\s,]+/).map(parseFloat);
|
|
5075
|
+
if (parts.length === 4) {
|
|
5076
|
+
width = width || parts[2];
|
|
5077
|
+
height = height || parts[3];
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5037
5080
|
}
|
|
5038
|
-
};
|
|
5081
|
+
return { width, height };
|
|
5082
|
+
}
|
|
5039
5083
|
|
|
5040
5084
|
// src/core/canvas-text-measurer.ts
|
|
5041
5085
|
async function createCanvasTextMeasurer() {
|
|
@@ -5411,7 +5455,7 @@ function findActiveWordIndex(store, groupWordIndices, timeMs) {
|
|
|
5411
5455
|
}
|
|
5412
5456
|
return -1;
|
|
5413
5457
|
}
|
|
5414
|
-
function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle
|
|
5458
|
+
function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle) {
|
|
5415
5459
|
if (groupWordIndices.length === 0) {
|
|
5416
5460
|
return "idle";
|
|
5417
5461
|
}
|
|
@@ -5428,7 +5472,7 @@ function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle, spee
|
|
|
5428
5472
|
return "after";
|
|
5429
5473
|
}
|
|
5430
5474
|
if (TRANSITION_ANIMATION_STYLES.has(animationStyle)) {
|
|
5431
|
-
const transitionDurationMs =
|
|
5475
|
+
const transitionDurationMs = ANIMATION_DURATION_MS[animationStyle] ?? 200;
|
|
5432
5476
|
for (const idx of groupWordIndices) {
|
|
5433
5477
|
const wordStart = store.startTimes[idx];
|
|
5434
5478
|
if (timeMs >= wordStart && timeMs < wordStart + transitionDurationMs) {
|
|
@@ -5450,7 +5494,7 @@ function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle, spee
|
|
|
5450
5494
|
}
|
|
5451
5495
|
return "before";
|
|
5452
5496
|
}
|
|
5453
|
-
function computeStateSignature(layout, timeMs, animationStyle
|
|
5497
|
+
function computeStateSignature(layout, timeMs, animationStyle) {
|
|
5454
5498
|
const groupIndex = findGroupIndexAtTime(layout.groups, timeMs);
|
|
5455
5499
|
if (groupIndex === -1) {
|
|
5456
5500
|
return { groupIndex: -1, activeWordIndex: -1, animationPhase: "idle" };
|
|
@@ -5461,21 +5505,20 @@ function computeStateSignature(layout, timeMs, animationStyle, speed) {
|
|
|
5461
5505
|
layout.store,
|
|
5462
5506
|
group.wordIndices,
|
|
5463
5507
|
timeMs,
|
|
5464
|
-
animationStyle
|
|
5465
|
-
speed
|
|
5508
|
+
animationStyle
|
|
5466
5509
|
);
|
|
5467
5510
|
return { groupIndex, activeWordIndex, animationPhase };
|
|
5468
5511
|
}
|
|
5469
5512
|
function signaturesMatch(a, b) {
|
|
5470
5513
|
return a.groupIndex === b.groupIndex && a.activeWordIndex === b.activeWordIndex && a.animationPhase === b.animationPhase;
|
|
5471
5514
|
}
|
|
5472
|
-
function createFrameSchedule(layout, durationMs, fps, animationStyle = "highlight"
|
|
5515
|
+
function createFrameSchedule(layout, durationMs, fps, animationStyle = "highlight") {
|
|
5473
5516
|
const totalFrames = Math.max(2, Math.round(durationMs / 1e3 * fps) + 1);
|
|
5474
5517
|
const renderFrames = [];
|
|
5475
5518
|
let previousSignature = null;
|
|
5476
5519
|
for (let frame = 0; frame < totalFrames; frame++) {
|
|
5477
5520
|
const timeMs = frame / (totalFrames - 1) * durationMs;
|
|
5478
|
-
const signature = computeStateSignature(layout, timeMs, animationStyle
|
|
5521
|
+
const signature = computeStateSignature(layout, timeMs, animationStyle);
|
|
5479
5522
|
const isAnimating = signature.animationPhase === "animating";
|
|
5480
5523
|
if (isAnimating || previousSignature === null || !signaturesMatch(signature, previousSignature)) {
|
|
5481
5524
|
renderFrames.push({
|
|
@@ -5877,14 +5920,12 @@ var RichCaptionRenderer = class {
|
|
|
5877
5920
|
throw new Error("No asset loaded. Call loadAsset() first.");
|
|
5878
5921
|
}
|
|
5879
5922
|
const animationStyle = this.extractAnimationStyle();
|
|
5880
|
-
const animationSpeed = this.extractAnimationSpeed();
|
|
5881
5923
|
const durationMs = duration * 1e3;
|
|
5882
5924
|
const schedule = createFrameSchedule(
|
|
5883
5925
|
this.currentLayout,
|
|
5884
5926
|
durationMs,
|
|
5885
5927
|
this.fps,
|
|
5886
|
-
animationStyle
|
|
5887
|
-
animationSpeed
|
|
5928
|
+
animationStyle
|
|
5888
5929
|
);
|
|
5889
5930
|
const bgColor = options?.bgColor;
|
|
5890
5931
|
const hasAlpha = !bgColor;
|
|
@@ -6045,13 +6086,11 @@ var RichCaptionRenderer = class {
|
|
|
6045
6086
|
throw new Error("No asset loaded. Call loadAsset() first.");
|
|
6046
6087
|
}
|
|
6047
6088
|
const animationStyle = this.extractAnimationStyle();
|
|
6048
|
-
const animationSpeed = this.extractAnimationSpeed();
|
|
6049
6089
|
return createFrameSchedule(
|
|
6050
6090
|
this.currentLayout,
|
|
6051
6091
|
duration * 1e3,
|
|
6052
6092
|
this.fps,
|
|
6053
|
-
animationStyle
|
|
6054
|
-
animationSpeed
|
|
6093
|
+
animationStyle
|
|
6055
6094
|
);
|
|
6056
6095
|
}
|
|
6057
6096
|
getStats() {
|
|
@@ -6089,10 +6128,6 @@ var RichCaptionRenderer = class {
|
|
|
6089
6128
|
const wordAnim = this.currentAsset?.wordAnimation;
|
|
6090
6129
|
return wordAnim?.style ?? "highlight";
|
|
6091
6130
|
}
|
|
6092
|
-
extractAnimationSpeed() {
|
|
6093
|
-
const wordAnim = this.currentAsset?.wordAnimation;
|
|
6094
|
-
return wordAnim?.speed ?? 1;
|
|
6095
|
-
}
|
|
6096
6131
|
logProgress(pct, framesProcessed, totalFrames, uniqueProcessed, uniqueTotal, fps, eta) {
|
|
6097
6132
|
if (typeof process !== "undefined" && process.stderr) {
|
|
6098
6133
|
process.stderr.write(
|