@shotstack/shotstack-canvas 2.1.4 → 2.1.5
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 +628 -572
- package/dist/entry.node.d.cts +41 -6
- package/dist/entry.node.d.ts +41 -6
- package/dist/entry.node.js +628 -572
- package/dist/entry.web.d.ts +41 -6
- package/dist/entry.web.js +2941 -2885
- package/package.json +2 -2
package/dist/entry.node.js
CHANGED
|
@@ -188,19 +188,28 @@ var richCaptionFontSchema = z.object({
|
|
|
188
188
|
weight: z.union([z.string(), z.number()]).default("400"),
|
|
189
189
|
color: z.string().regex(HEX6).default("#ffffff"),
|
|
190
190
|
opacity: z.number().min(0).max(1).default(1),
|
|
191
|
-
background: z.string().regex(HEX6).optional()
|
|
191
|
+
background: z.string().regex(HEX6).optional(),
|
|
192
|
+
textDecoration: z.enum(["none", "underline", "line-through"]).default("none")
|
|
192
193
|
});
|
|
193
194
|
var richCaptionActiveSchema = baseCaptionActiveSchema.extend({
|
|
194
195
|
font: z.object({
|
|
195
|
-
color: z.string().regex(HEX6).
|
|
196
|
+
color: z.string().regex(HEX6).optional(),
|
|
196
197
|
background: z.string().regex(HEX6).optional(),
|
|
197
|
-
opacity: z.number().min(0).max(1).
|
|
198
|
+
opacity: z.number().min(0).max(1).optional(),
|
|
199
|
+
textDecoration: z.enum(["none", "underline", "line-through"]).optional()
|
|
198
200
|
}).optional(),
|
|
199
201
|
stroke: z.object({
|
|
200
202
|
width: z.number().min(0).optional(),
|
|
201
203
|
color: z.string().regex(HEX6).optional(),
|
|
202
204
|
opacity: z.number().min(0).max(1).optional()
|
|
203
205
|
}).optional(),
|
|
206
|
+
shadow: z.object({
|
|
207
|
+
offsetX: z.number().optional(),
|
|
208
|
+
offsetY: z.number().optional(),
|
|
209
|
+
blur: z.number().min(0).optional(),
|
|
210
|
+
color: z.string().regex(HEX6).optional(),
|
|
211
|
+
opacity: z.number().min(0).max(1).optional()
|
|
212
|
+
}).optional(),
|
|
204
213
|
scale: z.number().min(0.5).max(2).default(1)
|
|
205
214
|
});
|
|
206
215
|
var richCaptionWordAnimationSchema = baseCaptionWordAnimationSchema.extend({
|
|
@@ -2216,239 +2225,591 @@ function parseHex6(hex, alpha = 1) {
|
|
|
2216
2225
|
return { r, g, b, a: alpha };
|
|
2217
2226
|
}
|
|
2218
2227
|
|
|
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));
|
|
2228
|
+
// src/core/rich-caption-layout.ts
|
|
2229
|
+
import { LRUCache } from "lru-cache";
|
|
2230
|
+
var ASCENT_RATIO = 0.8;
|
|
2231
|
+
var DESCENT_RATIO = 0.2;
|
|
2232
|
+
function isRTLText(text) {
|
|
2233
|
+
return containsRTLCharacters(text);
|
|
2252
2234
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2235
|
+
var WordTimingStore = class {
|
|
2236
|
+
startTimes;
|
|
2237
|
+
endTimes;
|
|
2238
|
+
xPositions;
|
|
2239
|
+
yPositions;
|
|
2240
|
+
widths;
|
|
2241
|
+
words;
|
|
2242
|
+
length;
|
|
2243
|
+
constructor(words) {
|
|
2244
|
+
this.length = words.length;
|
|
2245
|
+
this.startTimes = new Uint32Array(this.length);
|
|
2246
|
+
this.endTimes = new Uint32Array(this.length);
|
|
2247
|
+
this.xPositions = new Float32Array(this.length);
|
|
2248
|
+
this.yPositions = new Float32Array(this.length);
|
|
2249
|
+
this.widths = new Float32Array(this.length);
|
|
2250
|
+
this.words = new Array(this.length);
|
|
2251
|
+
for (let i = 0; i < this.length; i++) {
|
|
2252
|
+
this.startTimes[i] = Math.floor(words[i].start);
|
|
2253
|
+
this.endTimes[i] = Math.floor(words[i].end);
|
|
2254
|
+
this.words[i] = words[i].text;
|
|
2255
|
+
}
|
|
2261
2256
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2257
|
+
};
|
|
2258
|
+
function findWordAtTime(store, timeMs) {
|
|
2259
|
+
let left = 0;
|
|
2260
|
+
let right = store.length - 1;
|
|
2261
|
+
while (left <= right) {
|
|
2262
|
+
const mid = left + right >>> 1;
|
|
2263
|
+
const start = store.startTimes[mid];
|
|
2264
|
+
const end = store.endTimes[mid];
|
|
2265
|
+
if (timeMs >= start && timeMs < end) {
|
|
2266
|
+
return mid;
|
|
2267
|
+
}
|
|
2268
|
+
if (timeMs < start) {
|
|
2269
|
+
right = mid - 1;
|
|
2270
|
+
} else {
|
|
2271
|
+
left = mid + 1;
|
|
2272
|
+
}
|
|
2264
2273
|
}
|
|
2265
|
-
return
|
|
2266
|
-
}
|
|
2267
|
-
function clamp(value, min, max) {
|
|
2268
|
-
return Math.min(Math.max(value, min), max);
|
|
2274
|
+
return -1;
|
|
2269
2275
|
}
|
|
2270
|
-
function
|
|
2271
|
-
if (
|
|
2272
|
-
return
|
|
2276
|
+
function groupWordsByPause(store, pauseThreshold = 500) {
|
|
2277
|
+
if (store.length === 0) {
|
|
2278
|
+
return [];
|
|
2273
2279
|
}
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2280
|
+
const groups = [];
|
|
2281
|
+
let currentGroup = [];
|
|
2282
|
+
for (let i = 0; i < store.length; i++) {
|
|
2283
|
+
if (currentGroup.length === 0) {
|
|
2284
|
+
currentGroup.push(i);
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
const prevEnd = store.endTimes[currentGroup[currentGroup.length - 1]];
|
|
2288
|
+
const currStart = store.startTimes[i];
|
|
2289
|
+
const gap = currStart - prevEnd;
|
|
2290
|
+
const prevText = store.words[currentGroup[currentGroup.length - 1]];
|
|
2291
|
+
const endsWithPunctuation = /[.!?]$/.test(prevText);
|
|
2292
|
+
if (gap >= pauseThreshold || endsWithPunctuation) {
|
|
2293
|
+
groups.push(currentGroup);
|
|
2294
|
+
currentGroup = [i];
|
|
2295
|
+
} else {
|
|
2296
|
+
currentGroup.push(i);
|
|
2297
|
+
}
|
|
2281
2298
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
return ctx.currentTime >= ctx.wordStart && ctx.currentTime < ctx.wordEnd;
|
|
2299
|
+
if (currentGroup.length > 0) {
|
|
2300
|
+
groups.push(currentGroup);
|
|
2301
|
+
}
|
|
2302
|
+
return groups;
|
|
2287
2303
|
}
|
|
2288
|
-
function
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2304
|
+
function breakIntoLines(wordWidths, maxWidth, spaceWidth) {
|
|
2305
|
+
const lines = [];
|
|
2306
|
+
let currentLine = [];
|
|
2307
|
+
let currentWidth = 0;
|
|
2308
|
+
for (let i = 0; i < wordWidths.length; i++) {
|
|
2309
|
+
const wordWidth = wordWidths[i];
|
|
2310
|
+
const spaceNeeded = currentLine.length > 0 ? spaceWidth : 0;
|
|
2311
|
+
if (currentWidth + spaceNeeded + wordWidth <= maxWidth) {
|
|
2312
|
+
currentLine.push(i);
|
|
2313
|
+
currentWidth += spaceNeeded + wordWidth;
|
|
2314
|
+
} else {
|
|
2315
|
+
if (currentLine.length > 0) {
|
|
2316
|
+
lines.push(currentLine);
|
|
2317
|
+
}
|
|
2318
|
+
currentLine = [i];
|
|
2319
|
+
currentWidth = wordWidth;
|
|
2320
|
+
}
|
|
2300
2321
|
}
|
|
2301
|
-
if (
|
|
2302
|
-
|
|
2303
|
-
fillProgress: 1,
|
|
2304
|
-
isActive: false,
|
|
2305
|
-
opacity: 1
|
|
2306
|
-
};
|
|
2322
|
+
if (currentLine.length > 0) {
|
|
2323
|
+
lines.push(currentLine);
|
|
2307
2324
|
}
|
|
2308
|
-
return
|
|
2309
|
-
fillProgress: calculateWordProgress(adjustedCtx),
|
|
2310
|
-
isActive,
|
|
2311
|
-
opacity: 1
|
|
2312
|
-
};
|
|
2325
|
+
return lines;
|
|
2313
2326
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
return {
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2327
|
+
var GLYPH_SIZE_ESTIMATE = 64;
|
|
2328
|
+
function createShapedWordCache() {
|
|
2329
|
+
return new LRUCache({
|
|
2330
|
+
max: 5e4,
|
|
2331
|
+
maxSize: 50 * 1024 * 1024,
|
|
2332
|
+
maxEntrySize: 100 * 1024,
|
|
2333
|
+
sizeCalculation: (value, key) => {
|
|
2334
|
+
const keySize = key.length * 2;
|
|
2335
|
+
const glyphsSize = value.glyphs.length * GLYPH_SIZE_ESTIMATE;
|
|
2336
|
+
return keySize + glyphsSize + 100;
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2321
2339
|
}
|
|
2322
|
-
function
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2340
|
+
function makeShapingKey(text, fontFamily, fontSize, fontWeight, letterSpacing = 0) {
|
|
2341
|
+
return `${text}\0${fontFamily}\0${fontSize}\0${fontWeight}\0${letterSpacing}`;
|
|
2342
|
+
}
|
|
2343
|
+
function transformText(text, transform) {
|
|
2344
|
+
switch (transform) {
|
|
2345
|
+
case "uppercase":
|
|
2346
|
+
return text.toUpperCase();
|
|
2347
|
+
case "lowercase":
|
|
2348
|
+
return text.toLowerCase();
|
|
2349
|
+
case "capitalize":
|
|
2350
|
+
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2351
|
+
default:
|
|
2352
|
+
return text;
|
|
2330
2353
|
}
|
|
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
|
-
};
|
|
2345
2354
|
}
|
|
2346
|
-
function
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
isActive: false,
|
|
2351
|
-
fillProgress: 0
|
|
2352
|
-
};
|
|
2355
|
+
function splitIntoChunks(arr, chunkSize) {
|
|
2356
|
+
const chunks = [];
|
|
2357
|
+
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
2358
|
+
chunks.push(arr.slice(i, i + chunkSize));
|
|
2353
2359
|
}
|
|
2354
|
-
|
|
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
|
-
};
|
|
2360
|
+
return chunks;
|
|
2364
2361
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
isActive: false,
|
|
2374
|
-
fillProgress: 0
|
|
2375
|
-
};
|
|
2362
|
+
var CaptionLayoutEngine = class {
|
|
2363
|
+
fontRegistry;
|
|
2364
|
+
cache;
|
|
2365
|
+
layoutEngine;
|
|
2366
|
+
constructor(fontRegistry) {
|
|
2367
|
+
this.fontRegistry = fontRegistry;
|
|
2368
|
+
this.cache = createShapedWordCache();
|
|
2369
|
+
this.layoutEngine = new LayoutEngine(fontRegistry);
|
|
2376
2370
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2371
|
+
async measureWord(text, config) {
|
|
2372
|
+
const transformedText = transformText(text, config.textTransform);
|
|
2373
|
+
const cacheKey = makeShapingKey(
|
|
2374
|
+
transformedText,
|
|
2375
|
+
config.fontFamily,
|
|
2376
|
+
config.fontSize,
|
|
2377
|
+
config.fontWeight,
|
|
2378
|
+
config.letterSpacing
|
|
2379
|
+
);
|
|
2380
|
+
const cached = this.cache.get(cacheKey);
|
|
2381
|
+
if (cached) {
|
|
2382
|
+
return cached;
|
|
2383
|
+
}
|
|
2384
|
+
const lines = await this.layoutEngine.layout({
|
|
2385
|
+
text: transformedText,
|
|
2386
|
+
width: 1e5,
|
|
2387
|
+
letterSpacing: config.letterSpacing,
|
|
2388
|
+
fontSize: config.fontSize,
|
|
2389
|
+
lineHeight: 1,
|
|
2390
|
+
desc: { family: config.fontFamily, weight: config.fontWeight },
|
|
2391
|
+
textTransform: "none"
|
|
2392
|
+
});
|
|
2393
|
+
const width = lines[0]?.width ?? 0;
|
|
2394
|
+
const glyphs = lines[0]?.glyphs ?? [];
|
|
2395
|
+
const isRTL = isRTLText(transformedText);
|
|
2396
|
+
const shaped = {
|
|
2397
|
+
text: transformedText,
|
|
2398
|
+
width,
|
|
2399
|
+
glyphs: glyphs.map((g) => ({
|
|
2400
|
+
id: g.id,
|
|
2401
|
+
xAdvance: g.xAdvance,
|
|
2402
|
+
xOffset: g.xOffset,
|
|
2403
|
+
yOffset: g.yOffset,
|
|
2404
|
+
cluster: g.cluster
|
|
2405
|
+
})),
|
|
2406
|
+
isRTL
|
|
2407
|
+
};
|
|
2408
|
+
this.cache.set(cacheKey, shaped);
|
|
2409
|
+
return shaped;
|
|
2403
2410
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2411
|
+
async layoutCaption(words, config) {
|
|
2412
|
+
const store = new WordTimingStore(words);
|
|
2413
|
+
const measurementConfig = {
|
|
2414
|
+
fontFamily: config.fontFamily,
|
|
2415
|
+
fontSize: config.fontSize,
|
|
2416
|
+
fontWeight: config.fontWeight,
|
|
2417
|
+
letterSpacing: config.letterSpacing,
|
|
2418
|
+
textTransform: config.textTransform
|
|
2419
|
+
};
|
|
2420
|
+
const shapedWords = await Promise.all(
|
|
2421
|
+
words.map((w) => this.measureWord(w.text, measurementConfig))
|
|
2422
|
+
);
|
|
2423
|
+
if (config.measureTextWidth) {
|
|
2424
|
+
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
2425
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2426
|
+
store.widths[i] = config.measureTextWidth(shapedWords[i].text, fontString);
|
|
2427
|
+
}
|
|
2428
|
+
} else {
|
|
2429
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2430
|
+
store.widths[i] = shapedWords[i].width;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
if (config.textTransform !== "none") {
|
|
2434
|
+
for (let i = 0; i < shapedWords.length; i++) {
|
|
2435
|
+
store.words[i] = shapedWords[i].text;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
const wordGroups = groupWordsByPause(store, config.pauseThreshold);
|
|
2439
|
+
const pixelMaxWidth = config.availableWidth;
|
|
2440
|
+
let spaceWidth;
|
|
2441
|
+
if (config.measureTextWidth) {
|
|
2442
|
+
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
2443
|
+
spaceWidth = config.measureTextWidth(" ", fontString) + config.wordSpacing;
|
|
2444
|
+
} else {
|
|
2445
|
+
const spaceWord = await this.measureWord(" ", measurementConfig);
|
|
2446
|
+
spaceWidth = spaceWord.width + config.wordSpacing;
|
|
2447
|
+
}
|
|
2448
|
+
const groups = wordGroups.flatMap((indices) => {
|
|
2449
|
+
const groupWidths = indices.map((i) => store.widths[i]);
|
|
2450
|
+
const allLines = breakIntoLines(
|
|
2451
|
+
groupWidths,
|
|
2452
|
+
pixelMaxWidth,
|
|
2453
|
+
spaceWidth
|
|
2454
|
+
);
|
|
2455
|
+
const lineChunks = splitIntoChunks(allLines, config.maxLines);
|
|
2456
|
+
return lineChunks.map((chunkLines) => {
|
|
2457
|
+
const lines = chunkLines.map((lineWordIndices, lineIndex) => {
|
|
2458
|
+
const actualIndices = lineWordIndices.map((i) => indices[i]);
|
|
2459
|
+
const lineWidth = actualIndices.reduce((sum, idx) => sum + store.widths[idx], 0) + (actualIndices.length - 1) * spaceWidth;
|
|
2460
|
+
return {
|
|
2461
|
+
wordIndices: actualIndices,
|
|
2462
|
+
x: 0,
|
|
2463
|
+
y: lineIndex * config.fontSize * config.lineHeight,
|
|
2464
|
+
width: lineWidth,
|
|
2465
|
+
height: config.fontSize
|
|
2466
|
+
};
|
|
2467
|
+
});
|
|
2468
|
+
const allWordIndices = lines.flatMap((l) => l.wordIndices);
|
|
2469
|
+
if (allWordIndices.length === 0) {
|
|
2470
|
+
return null;
|
|
2471
|
+
}
|
|
2472
|
+
return {
|
|
2473
|
+
wordIndices: allWordIndices,
|
|
2474
|
+
startTime: store.startTimes[allWordIndices[0]],
|
|
2475
|
+
endTime: store.endTimes[allWordIndices[allWordIndices.length - 1]],
|
|
2476
|
+
lines
|
|
2477
|
+
};
|
|
2478
|
+
}).filter((g) => g !== null);
|
|
2479
|
+
});
|
|
2480
|
+
const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
|
|
2481
|
+
const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
|
|
2482
|
+
const calculateGroupY = (group) => {
|
|
2483
|
+
const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
|
|
2484
|
+
switch (config.verticalAlign) {
|
|
2485
|
+
case "top":
|
|
2486
|
+
return config.padding.top + config.fontSize * ASCENT_RATIO;
|
|
2487
|
+
case "bottom":
|
|
2488
|
+
return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO;
|
|
2489
|
+
case "middle":
|
|
2490
|
+
default:
|
|
2491
|
+
return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO;
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
const allWordTexts = store.words.slice(0, store.length);
|
|
2495
|
+
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
2496
|
+
const calculateLineX = (lineWidth) => {
|
|
2497
|
+
switch (config.horizontalAlign) {
|
|
2498
|
+
case "left":
|
|
2499
|
+
return config.padding.left;
|
|
2500
|
+
case "right":
|
|
2501
|
+
return config.frameWidth - lineWidth - config.padding.right;
|
|
2502
|
+
case "center":
|
|
2503
|
+
default:
|
|
2504
|
+
return config.padding.left + (contentWidth - lineWidth) / 2;
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
for (const group of groups) {
|
|
2508
|
+
const baseY = calculateGroupY(group);
|
|
2509
|
+
for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
|
|
2510
|
+
const line = group.lines[lineIdx];
|
|
2511
|
+
line.x = calculateLineX(line.width);
|
|
2512
|
+
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
2513
|
+
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
2514
|
+
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
2515
|
+
let xCursor = line.x;
|
|
2516
|
+
for (const visualIdx of visualOrder) {
|
|
2517
|
+
const wordIdx = line.wordIndices[visualIdx];
|
|
2518
|
+
store.xPositions[wordIdx] = xCursor;
|
|
2519
|
+
store.yPositions[wordIdx] = line.y;
|
|
2520
|
+
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2408
2524
|
return {
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2525
|
+
store,
|
|
2526
|
+
groups,
|
|
2527
|
+
shapedWords,
|
|
2528
|
+
paragraphDirection
|
|
2413
2529
|
};
|
|
2414
2530
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2531
|
+
getVisibleWordsAtTime(layout, timeMs) {
|
|
2532
|
+
const activeGroup = layout.groups.find(
|
|
2533
|
+
(g) => timeMs >= g.startTime && timeMs <= g.endTime
|
|
2534
|
+
);
|
|
2535
|
+
if (!activeGroup) {
|
|
2536
|
+
return [];
|
|
2537
|
+
}
|
|
2538
|
+
return activeGroup.wordIndices.map((idx) => ({
|
|
2539
|
+
wordIndex: idx,
|
|
2540
|
+
text: layout.store.words[idx],
|
|
2541
|
+
x: layout.store.xPositions[idx],
|
|
2542
|
+
y: layout.store.yPositions[idx],
|
|
2543
|
+
width: layout.store.widths[idx],
|
|
2544
|
+
startTime: layout.store.startTimes[idx],
|
|
2545
|
+
endTime: layout.store.endTimes[idx],
|
|
2546
|
+
isRTL: layout.shapedWords[idx].isRTL
|
|
2547
|
+
}));
|
|
2548
|
+
}
|
|
2549
|
+
getActiveWordAtTime(layout, timeMs) {
|
|
2550
|
+
const wordIndex = findWordAtTime(layout.store, timeMs);
|
|
2551
|
+
if (wordIndex === -1) {
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2433
2554
|
return {
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2555
|
+
wordIndex,
|
|
2556
|
+
text: layout.store.words[wordIndex],
|
|
2557
|
+
x: layout.store.xPositions[wordIndex],
|
|
2558
|
+
y: layout.store.yPositions[wordIndex],
|
|
2559
|
+
width: layout.store.widths[wordIndex],
|
|
2560
|
+
startTime: layout.store.startTimes[wordIndex],
|
|
2561
|
+
endTime: layout.store.endTimes[wordIndex],
|
|
2562
|
+
isRTL: layout.shapedWords[wordIndex].isRTL
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
clearCache() {
|
|
2566
|
+
this.cache.clear();
|
|
2567
|
+
}
|
|
2568
|
+
getCacheStats() {
|
|
2569
|
+
return {
|
|
2570
|
+
size: this.cache.size,
|
|
2571
|
+
calculatedSize: this.cache.calculatedSize
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
|
|
2576
|
+
// src/core/rich-caption-animator.ts
|
|
2577
|
+
var ANIMATION_DURATIONS = {
|
|
2578
|
+
karaoke: 0,
|
|
2579
|
+
highlight: 0,
|
|
2580
|
+
pop: 200,
|
|
2581
|
+
fade: 150,
|
|
2582
|
+
slide: 250,
|
|
2583
|
+
bounce: 400,
|
|
2584
|
+
typewriter: 0,
|
|
2585
|
+
none: 0
|
|
2586
|
+
};
|
|
2587
|
+
var DEFAULT_ANIMATION_STATE = {
|
|
2588
|
+
opacity: 1,
|
|
2589
|
+
scale: 1,
|
|
2590
|
+
translateX: 0,
|
|
2591
|
+
translateY: 0,
|
|
2592
|
+
fillProgress: 1,
|
|
2593
|
+
isActive: false,
|
|
2594
|
+
visibleCharacters: -1
|
|
2595
|
+
};
|
|
2596
|
+
function easeOutQuad2(t) {
|
|
2597
|
+
return t * (2 - t);
|
|
2598
|
+
}
|
|
2599
|
+
function easeInOutQuad(t) {
|
|
2600
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
2601
|
+
}
|
|
2602
|
+
function easeOutBack(t) {
|
|
2603
|
+
const c1 = 1.70158;
|
|
2604
|
+
const c3 = c1 + 1;
|
|
2605
|
+
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
|
2606
|
+
}
|
|
2607
|
+
function easeOutCirc(t) {
|
|
2608
|
+
return Math.sqrt(1 - Math.pow(t - 1, 2));
|
|
2609
|
+
}
|
|
2610
|
+
function easeOutBounce(t) {
|
|
2611
|
+
const n1 = 7.5625;
|
|
2612
|
+
const d1 = 2.75;
|
|
2613
|
+
if (t < 1 / d1) {
|
|
2614
|
+
return n1 * t * t;
|
|
2615
|
+
}
|
|
2616
|
+
if (t < 2 / d1) {
|
|
2617
|
+
return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
2618
|
+
}
|
|
2619
|
+
if (t < 2.5 / d1) {
|
|
2620
|
+
return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
2621
|
+
}
|
|
2622
|
+
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
2623
|
+
}
|
|
2624
|
+
function clamp(value, min, max) {
|
|
2625
|
+
return Math.min(Math.max(value, min), max);
|
|
2626
|
+
}
|
|
2627
|
+
function calculateAnimationProgress(ctx) {
|
|
2628
|
+
if (ctx.animationDuration <= 0) {
|
|
2629
|
+
return ctx.currentTime >= ctx.wordStart ? 1 : 0;
|
|
2630
|
+
}
|
|
2631
|
+
const elapsed = ctx.currentTime - ctx.wordStart;
|
|
2632
|
+
return clamp(elapsed / ctx.animationDuration, 0, 1);
|
|
2633
|
+
}
|
|
2634
|
+
function calculateWordProgress(ctx) {
|
|
2635
|
+
const duration = ctx.wordEnd - ctx.wordStart;
|
|
2636
|
+
if (duration <= 0) {
|
|
2637
|
+
return ctx.currentTime >= ctx.wordStart ? 1 : 0;
|
|
2638
|
+
}
|
|
2639
|
+
const elapsed = ctx.currentTime - ctx.wordStart;
|
|
2640
|
+
return clamp(elapsed / duration, 0, 1);
|
|
2641
|
+
}
|
|
2642
|
+
function isWordActive(ctx) {
|
|
2643
|
+
return ctx.currentTime >= ctx.wordStart && ctx.currentTime < ctx.wordEnd;
|
|
2644
|
+
}
|
|
2645
|
+
function calculateKaraokeState(ctx, speed) {
|
|
2646
|
+
const isActive = isWordActive(ctx);
|
|
2647
|
+
const wordDuration = ctx.wordEnd - ctx.wordStart;
|
|
2648
|
+
const adjustedDuration = wordDuration / speed;
|
|
2649
|
+
const adjustedEnd = ctx.wordStart + adjustedDuration;
|
|
2650
|
+
const adjustedCtx = { ...ctx, wordEnd: adjustedEnd };
|
|
2651
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2652
|
+
return {
|
|
2653
|
+
fillProgress: 0,
|
|
2654
|
+
isActive: false,
|
|
2655
|
+
opacity: 1
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
if (ctx.currentTime >= adjustedEnd) {
|
|
2659
|
+
return {
|
|
2660
|
+
fillProgress: 1,
|
|
2661
|
+
isActive: false,
|
|
2662
|
+
opacity: 1
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
return {
|
|
2666
|
+
fillProgress: calculateWordProgress(adjustedCtx),
|
|
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, speed) {
|
|
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 adjustedDuration = ctx.animationDuration / speed;
|
|
2689
|
+
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2690
|
+
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2691
|
+
const easedProgress = easeOutBack(progress);
|
|
2692
|
+
const startScale = 0.5;
|
|
2693
|
+
const isActive = isWordActive(ctx);
|
|
2694
|
+
const endScale = isActive ? activeScale : 1;
|
|
2695
|
+
const scale = startScale + (endScale - startScale) * easedProgress;
|
|
2696
|
+
return {
|
|
2697
|
+
scale: Math.min(scale, activeScale),
|
|
2698
|
+
opacity: easedProgress,
|
|
2699
|
+
isActive,
|
|
2700
|
+
fillProgress: isActive ? 1 : 0
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
function calculateFadeState(ctx, speed) {
|
|
2704
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2705
|
+
return {
|
|
2706
|
+
opacity: 0,
|
|
2707
|
+
isActive: false,
|
|
2708
|
+
fillProgress: 0
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
const adjustedDuration = ctx.animationDuration / speed;
|
|
2712
|
+
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2713
|
+
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2714
|
+
const easedProgress = easeInOutQuad(progress);
|
|
2715
|
+
const isActive = isWordActive(ctx);
|
|
2716
|
+
return {
|
|
2717
|
+
opacity: easedProgress,
|
|
2718
|
+
isActive,
|
|
2719
|
+
fillProgress: isActive ? 1 : 0
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
function calculateSlideState(ctx, direction, speed, fontSize) {
|
|
2723
|
+
const slideDistance = fontSize * 1.5;
|
|
2724
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2725
|
+
const offset2 = getDirectionOffset(direction, slideDistance);
|
|
2726
|
+
return {
|
|
2727
|
+
translateX: offset2.x,
|
|
2728
|
+
translateY: offset2.y,
|
|
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 = easeOutCirc(progress);
|
|
2738
|
+
const offset = getDirectionOffset(direction, slideDistance);
|
|
2739
|
+
const translateX = offset.x * (1 - easedProgress);
|
|
2740
|
+
const translateY = offset.y * (1 - easedProgress);
|
|
2741
|
+
const isActive = isWordActive(ctx);
|
|
2742
|
+
return {
|
|
2743
|
+
translateX,
|
|
2744
|
+
translateY,
|
|
2745
|
+
opacity: easeOutQuad2(progress),
|
|
2746
|
+
isActive,
|
|
2747
|
+
fillProgress: isActive ? 1 : 0
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
function getDirectionOffset(direction, distance) {
|
|
2751
|
+
switch (direction) {
|
|
2752
|
+
case "left":
|
|
2753
|
+
return { x: -distance, y: 0 };
|
|
2754
|
+
case "right":
|
|
2755
|
+
return { x: distance, y: 0 };
|
|
2756
|
+
case "up":
|
|
2757
|
+
return { x: 0, y: distance };
|
|
2758
|
+
case "down":
|
|
2759
|
+
return { x: 0, y: -distance };
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
function calculateBounceState(ctx, speed, fontSize) {
|
|
2763
|
+
const bounceDistance = fontSize * 0.8;
|
|
2764
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2765
|
+
return {
|
|
2766
|
+
translateY: -bounceDistance,
|
|
2767
|
+
opacity: 0,
|
|
2768
|
+
isActive: false,
|
|
2769
|
+
fillProgress: 0
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
const adjustedDuration = ctx.animationDuration / speed;
|
|
2773
|
+
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2774
|
+
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2775
|
+
const easedProgress = easeOutBounce(progress);
|
|
2776
|
+
const isActive = isWordActive(ctx);
|
|
2777
|
+
return {
|
|
2778
|
+
translateY: -bounceDistance * (1 - easedProgress),
|
|
2779
|
+
opacity: easeOutQuad2(progress),
|
|
2780
|
+
isActive,
|
|
2781
|
+
fillProgress: isActive ? 1 : 0
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
function calculateTypewriterState(ctx, charCount, speed) {
|
|
2785
|
+
const wordDuration = ctx.wordEnd - ctx.wordStart;
|
|
2786
|
+
const adjustedDuration = wordDuration / speed;
|
|
2787
|
+
const adjustedEnd = ctx.wordStart + adjustedDuration;
|
|
2788
|
+
const adjustedCtx = { ...ctx, wordEnd: adjustedEnd };
|
|
2789
|
+
if (ctx.currentTime < ctx.wordStart) {
|
|
2790
|
+
return {
|
|
2791
|
+
visibleCharacters: 0,
|
|
2792
|
+
opacity: 1,
|
|
2793
|
+
isActive: false,
|
|
2794
|
+
fillProgress: 0
|
|
2437
2795
|
};
|
|
2438
2796
|
}
|
|
2439
2797
|
if (ctx.currentTime >= adjustedEnd) {
|
|
2440
2798
|
return {
|
|
2441
2799
|
visibleCharacters: charCount,
|
|
2442
2800
|
opacity: 1,
|
|
2443
|
-
isActive: false
|
|
2801
|
+
isActive: false,
|
|
2802
|
+
fillProgress: 0
|
|
2444
2803
|
};
|
|
2445
2804
|
}
|
|
2446
2805
|
const progress = calculateWordProgress(adjustedCtx);
|
|
2447
2806
|
const visibleCharacters = Math.ceil(progress * charCount);
|
|
2807
|
+
const isActive = isWordActive(ctx);
|
|
2448
2808
|
return {
|
|
2449
2809
|
visibleCharacters: clamp(visibleCharacters, 0, charCount),
|
|
2450
2810
|
opacity: 1,
|
|
2451
|
-
isActive
|
|
2811
|
+
isActive,
|
|
2812
|
+
fillProgress: isActive ? 1 : 0
|
|
2452
2813
|
};
|
|
2453
2814
|
}
|
|
2454
2815
|
function calculateNoneState(_ctx) {
|
|
@@ -2529,28 +2890,25 @@ function getDefaultAnimationConfig() {
|
|
|
2529
2890
|
}
|
|
2530
2891
|
|
|
2531
2892
|
// src/core/rich-caption-generator.ts
|
|
2532
|
-
var ASCENT_RATIO = 0.8;
|
|
2533
|
-
var DESCENT_RATIO = 0.2;
|
|
2534
2893
|
var WORD_BG_OPACITY = 1;
|
|
2535
2894
|
var WORD_BG_BORDER_RADIUS = 4;
|
|
2536
2895
|
var WORD_BG_PADDING_RATIO = 0.12;
|
|
2537
2896
|
function extractFontConfig(asset) {
|
|
2538
2897
|
const font = asset.font;
|
|
2539
2898
|
const active = asset.active?.font;
|
|
2540
|
-
const
|
|
2899
|
+
const hasExplicitActiveColor = active?.color !== void 0;
|
|
2541
2900
|
const baseColor = font?.color ?? "#ffffff";
|
|
2542
2901
|
const baseOpacity = font?.opacity ?? 1;
|
|
2543
2902
|
let activeColor;
|
|
2544
2903
|
let activeOpacity;
|
|
2545
|
-
if (!
|
|
2904
|
+
if (!hasExplicitActiveColor) {
|
|
2546
2905
|
activeColor = baseColor;
|
|
2547
|
-
activeOpacity = baseOpacity;
|
|
2906
|
+
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2548
2907
|
} else {
|
|
2549
|
-
const explicitActiveColor = active?.color;
|
|
2550
2908
|
const animStyle = asset.wordAnimation?.style ?? "highlight";
|
|
2551
2909
|
const isFillAnimation = animStyle === "karaoke" || animStyle === "highlight";
|
|
2552
2910
|
const DEFAULT_ACTIVE_COLOR = "#ffff00";
|
|
2553
|
-
activeColor =
|
|
2911
|
+
activeColor = active.color ?? (isFillAnimation ? DEFAULT_ACTIVE_COLOR : baseColor);
|
|
2554
2912
|
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2555
2913
|
}
|
|
2556
2914
|
return {
|
|
@@ -2590,20 +2948,33 @@ function extractStrokeConfig(asset, isActive) {
|
|
|
2590
2948
|
return void 0;
|
|
2591
2949
|
}
|
|
2592
2950
|
function extractShadowConfig(asset, isActive) {
|
|
2593
|
-
|
|
2951
|
+
const baseShadow = asset.shadow;
|
|
2952
|
+
const activeShadow = asset.active?.shadow;
|
|
2953
|
+
if (!baseShadow && !activeShadow) {
|
|
2594
2954
|
return void 0;
|
|
2595
2955
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2956
|
+
if (isActive) {
|
|
2957
|
+
if (!activeShadow) {
|
|
2958
|
+
return void 0;
|
|
2959
|
+
}
|
|
2960
|
+
return {
|
|
2961
|
+
offsetX: activeShadow.offsetX ?? baseShadow?.offsetX ?? 0,
|
|
2962
|
+
offsetY: activeShadow.offsetY ?? baseShadow?.offsetY ?? 0,
|
|
2963
|
+
blur: activeShadow.blur ?? baseShadow?.blur ?? 0,
|
|
2964
|
+
color: activeShadow.color ?? baseShadow?.color ?? "#000000",
|
|
2965
|
+
opacity: activeShadow.opacity ?? baseShadow?.opacity ?? 0.5
|
|
2966
|
+
};
|
|
2599
2967
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2968
|
+
if (baseShadow) {
|
|
2969
|
+
return {
|
|
2970
|
+
offsetX: baseShadow.offsetX ?? 0,
|
|
2971
|
+
offsetY: baseShadow.offsetY ?? 0,
|
|
2972
|
+
blur: baseShadow.blur ?? 0,
|
|
2973
|
+
color: baseShadow.color ?? "#000000",
|
|
2974
|
+
opacity: baseShadow.opacity ?? 0.5
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
return void 0;
|
|
2607
2978
|
}
|
|
2608
2979
|
function extractBackgroundConfig(asset, isActive, fontSize) {
|
|
2609
2980
|
const fontBackground = asset.font?.background;
|
|
@@ -2658,9 +3029,20 @@ function extractCaptionBorder(asset) {
|
|
|
2658
3029
|
radius: border.radius ?? 0
|
|
2659
3030
|
};
|
|
2660
3031
|
}
|
|
2661
|
-
function
|
|
2662
|
-
const
|
|
2663
|
-
|
|
3032
|
+
function extractTextDecoration(asset, isActive) {
|
|
3033
|
+
const baseDecoration = asset.font?.textDecoration;
|
|
3034
|
+
const activeDecoration = asset.active?.font?.textDecoration;
|
|
3035
|
+
if (isActive && activeDecoration !== void 0) {
|
|
3036
|
+
return activeDecoration === "none" ? void 0 : activeDecoration;
|
|
3037
|
+
}
|
|
3038
|
+
if (!baseDecoration || baseDecoration === "none") {
|
|
3039
|
+
return void 0;
|
|
3040
|
+
}
|
|
3041
|
+
return baseDecoration;
|
|
3042
|
+
}
|
|
3043
|
+
function extractAnimationConfig(asset) {
|
|
3044
|
+
const wordAnim = asset.wordAnimation;
|
|
3045
|
+
if (!wordAnim) {
|
|
2664
3046
|
return getDefaultAnimationConfig();
|
|
2665
3047
|
}
|
|
2666
3048
|
return {
|
|
@@ -2700,7 +3082,8 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
|
|
|
2700
3082
|
letterSpacing: fontConfig.letterSpacing > 0 ? fontConfig.letterSpacing : void 0,
|
|
2701
3083
|
stroke: extractStrokeConfig(asset, isActive),
|
|
2702
3084
|
shadow: extractShadowConfig(asset, isActive),
|
|
2703
|
-
background: extractBackgroundConfig(asset, isActive, fontConfig.size)
|
|
3085
|
+
background: extractBackgroundConfig(asset, isActive, fontConfig.size),
|
|
3086
|
+
textDecoration: extractTextDecoration(asset, isActive)
|
|
2704
3087
|
};
|
|
2705
3088
|
}
|
|
2706
3089
|
function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, config) {
|
|
@@ -3204,7 +3587,8 @@ async function createNodePainter(opts) {
|
|
|
3204
3587
|
context.lineCap = "round";
|
|
3205
3588
|
context.strokeText(displayText, 0, 0);
|
|
3206
3589
|
}
|
|
3207
|
-
|
|
3590
|
+
const sameColor = wordOp.activeColor === wordOp.baseColor && wordOp.activeOpacity === wordOp.baseOpacity;
|
|
3591
|
+
if (wordOp.fillProgress <= 0 || sameColor) {
|
|
3208
3592
|
const baseC = parseHex6(wordOp.baseColor, wordOp.baseOpacity);
|
|
3209
3593
|
context.fillStyle = `rgba(${baseC.r},${baseC.g},${baseC.b},${baseC.a})`;
|
|
3210
3594
|
context.fillText(displayText, 0, 0);
|
|
@@ -3231,6 +3615,25 @@ async function createNodePainter(opts) {
|
|
|
3231
3615
|
context.fillText(displayText, 0, 0);
|
|
3232
3616
|
context.restore();
|
|
3233
3617
|
}
|
|
3618
|
+
if (wordOp.textDecoration) {
|
|
3619
|
+
const geo = decorationGeometry(wordOp.textDecoration, {
|
|
3620
|
+
baselineY: 0,
|
|
3621
|
+
fontSize: wordOp.fontSize,
|
|
3622
|
+
lineWidth: textWidth,
|
|
3623
|
+
xStart: 0
|
|
3624
|
+
});
|
|
3625
|
+
const sameC = wordOp.activeColor === wordOp.baseColor && wordOp.activeOpacity === wordOp.baseOpacity;
|
|
3626
|
+
const decoIsActive = wordOp.fillProgress >= 1 && !sameC;
|
|
3627
|
+
const decoColor = decoIsActive ? wordOp.activeColor : wordOp.baseColor;
|
|
3628
|
+
const decoOpacity = decoIsActive ? wordOp.activeOpacity : wordOp.baseOpacity;
|
|
3629
|
+
const dc = parseHex6(decoColor, decoOpacity);
|
|
3630
|
+
context.strokeStyle = `rgba(${dc.r},${dc.g},${dc.b},${dc.a})`;
|
|
3631
|
+
context.lineWidth = geo.width;
|
|
3632
|
+
context.beginPath();
|
|
3633
|
+
context.moveTo(geo.x1, geo.y);
|
|
3634
|
+
context.lineTo(geo.x2, geo.y);
|
|
3635
|
+
context.stroke();
|
|
3636
|
+
}
|
|
3234
3637
|
context.restore();
|
|
3235
3638
|
}
|
|
3236
3639
|
});
|
|
@@ -4690,353 +5093,6 @@ function extractSvgDimensions(svgString) {
|
|
|
4690
5093
|
return { width, height };
|
|
4691
5094
|
}
|
|
4692
5095
|
|
|
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;
|
|
4934
|
-
}
|
|
4935
|
-
return {
|
|
4936
|
-
wordIndices: allWordIndices,
|
|
4937
|
-
startTime: store.startTimes[allWordIndices[0]],
|
|
4938
|
-
endTime: store.endTimes[allWordIndices[allWordIndices.length - 1]],
|
|
4939
|
-
lines
|
|
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;
|
|
4985
|
-
}
|
|
4986
|
-
}
|
|
4987
|
-
}
|
|
4988
|
-
return {
|
|
4989
|
-
store,
|
|
4990
|
-
groups,
|
|
4991
|
-
shapedWords,
|
|
4992
|
-
paragraphDirection
|
|
4993
|
-
};
|
|
4994
|
-
}
|
|
4995
|
-
getVisibleWordsAtTime(layout, timeMs) {
|
|
4996
|
-
const activeGroup = layout.groups.find(
|
|
4997
|
-
(g) => timeMs >= g.startTime && timeMs <= g.endTime
|
|
4998
|
-
);
|
|
4999
|
-
if (!activeGroup) {
|
|
5000
|
-
return [];
|
|
5001
|
-
}
|
|
5002
|
-
return activeGroup.wordIndices.map((idx) => ({
|
|
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
|
-
}));
|
|
5012
|
-
}
|
|
5013
|
-
getActiveWordAtTime(layout, timeMs) {
|
|
5014
|
-
const wordIndex = findWordAtTime(layout.store, timeMs);
|
|
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
|
-
};
|
|
5028
|
-
}
|
|
5029
|
-
clearCache() {
|
|
5030
|
-
this.cache.clear();
|
|
5031
|
-
}
|
|
5032
|
-
getCacheStats() {
|
|
5033
|
-
return {
|
|
5034
|
-
size: this.cache.size,
|
|
5035
|
-
calculatedSize: this.cache.calculatedSize
|
|
5036
|
-
};
|
|
5037
|
-
}
|
|
5038
|
-
};
|
|
5039
|
-
|
|
5040
5096
|
// src/core/canvas-text-measurer.ts
|
|
5041
5097
|
async function createCanvasTextMeasurer() {
|
|
5042
5098
|
const canvasMod = await import("canvas");
|