@shotstack/shotstack-canvas 2.1.3 → 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 +653 -575
- package/dist/entry.node.d.cts +41 -6
- package/dist/entry.node.d.ts +41 -6
- package/dist/entry.node.js +653 -575
- package/dist/entry.web.d.ts +41 -6
- package/dist/entry.web.js +2946 -2868
- 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,234 +2225,598 @@ 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
|
-
return {
|
|
2325
|
-
scale: 0.5,
|
|
2326
|
-
opacity: 0,
|
|
2327
|
-
isActive: false
|
|
2328
|
-
};
|
|
2329
|
-
}
|
|
2330
|
-
const adjustedDuration = ctx.animationDuration / speed;
|
|
2331
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2332
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2333
|
-
const easedProgress = easeOutBack(progress);
|
|
2334
|
-
const startScale = 0.5;
|
|
2335
|
-
const endScale = isWordActive(ctx) ? activeScale : 1;
|
|
2336
|
-
const scale = startScale + (endScale - startScale) * easedProgress;
|
|
2337
|
-
return {
|
|
2338
|
-
scale: Math.min(scale, activeScale),
|
|
2339
|
-
opacity: easedProgress,
|
|
2340
|
-
isActive: isWordActive(ctx)
|
|
2341
|
-
};
|
|
2340
|
+
function makeShapingKey(text, fontFamily, fontSize, fontWeight, letterSpacing = 0) {
|
|
2341
|
+
return `${text}\0${fontFamily}\0${fontSize}\0${fontWeight}\0${letterSpacing}`;
|
|
2342
2342
|
}
|
|
2343
|
-
function
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
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;
|
|
2349
2353
|
}
|
|
2350
|
-
const adjustedDuration = ctx.animationDuration / speed;
|
|
2351
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2352
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2353
|
-
const easedProgress = easeInOutQuad(progress);
|
|
2354
|
-
return {
|
|
2355
|
-
opacity: easedProgress,
|
|
2356
|
-
isActive: isWordActive(ctx)
|
|
2357
|
-
};
|
|
2358
2354
|
}
|
|
2359
|
-
function
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
return {
|
|
2364
|
-
translateX: offset2.x,
|
|
2365
|
-
translateY: offset2.y,
|
|
2366
|
-
opacity: 0,
|
|
2367
|
-
isActive: false
|
|
2368
|
-
};
|
|
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));
|
|
2369
2359
|
}
|
|
2370
|
-
|
|
2371
|
-
const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
|
|
2372
|
-
const progress = calculateAnimationProgress(adjustedCtx);
|
|
2373
|
-
const easedProgress = easeOutCirc(progress);
|
|
2374
|
-
const offset = getDirectionOffset(direction, slideDistance);
|
|
2375
|
-
const translateX = offset.x * (1 - easedProgress);
|
|
2376
|
-
const translateY = offset.y * (1 - easedProgress);
|
|
2377
|
-
return {
|
|
2378
|
-
translateX,
|
|
2379
|
-
translateY,
|
|
2380
|
-
opacity: easeOutQuad2(progress),
|
|
2381
|
-
isActive: isWordActive(ctx)
|
|
2382
|
-
};
|
|
2360
|
+
return chunks;
|
|
2383
2361
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
case "down":
|
|
2393
|
-
return { x: 0, y: distance };
|
|
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);
|
|
2394
2370
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
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;
|
|
2410
|
+
}
|
|
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
|
+
}
|
|
2399
2524
|
return {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2525
|
+
store,
|
|
2526
|
+
groups,
|
|
2527
|
+
shapedWords,
|
|
2528
|
+
paragraphDirection
|
|
2403
2529
|
};
|
|
2404
2530
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
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
|
+
}
|
|
2421
2554
|
return {
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
|
2425
2563
|
};
|
|
2426
2564
|
}
|
|
2427
|
-
|
|
2565
|
+
clearCache() {
|
|
2566
|
+
this.cache.clear();
|
|
2567
|
+
}
|
|
2568
|
+
getCacheStats() {
|
|
2428
2569
|
return {
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
isActive: false
|
|
2570
|
+
size: this.cache.size,
|
|
2571
|
+
calculatedSize: this.cache.calculatedSize
|
|
2432
2572
|
};
|
|
2433
2573
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
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
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
if (ctx.currentTime >= adjustedEnd) {
|
|
2798
|
+
return {
|
|
2799
|
+
visibleCharacters: charCount,
|
|
2800
|
+
opacity: 1,
|
|
2801
|
+
isActive: false,
|
|
2802
|
+
fillProgress: 0
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
const progress = calculateWordProgress(adjustedCtx);
|
|
2806
|
+
const visibleCharacters = Math.ceil(progress * charCount);
|
|
2807
|
+
const isActive = isWordActive(ctx);
|
|
2808
|
+
return {
|
|
2809
|
+
visibleCharacters: clamp(visibleCharacters, 0, charCount),
|
|
2810
|
+
opacity: 1,
|
|
2811
|
+
isActive,
|
|
2812
|
+
fillProgress: isActive ? 1 : 0
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
function calculateNoneState(_ctx) {
|
|
2816
|
+
return {
|
|
2817
|
+
opacity: 1,
|
|
2818
|
+
isActive: false,
|
|
2819
|
+
fillProgress: 0
|
|
2447
2820
|
};
|
|
2448
2821
|
}
|
|
2449
2822
|
function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
|
|
@@ -2517,28 +2890,35 @@ function getDefaultAnimationConfig() {
|
|
|
2517
2890
|
}
|
|
2518
2891
|
|
|
2519
2892
|
// src/core/rich-caption-generator.ts
|
|
2520
|
-
var ASCENT_RATIO = 0.8;
|
|
2521
|
-
var DESCENT_RATIO = 0.2;
|
|
2522
2893
|
var WORD_BG_OPACITY = 1;
|
|
2523
2894
|
var WORD_BG_BORDER_RADIUS = 4;
|
|
2524
2895
|
var WORD_BG_PADDING_RATIO = 0.12;
|
|
2525
2896
|
function extractFontConfig(asset) {
|
|
2526
2897
|
const font = asset.font;
|
|
2527
2898
|
const active = asset.active?.font;
|
|
2899
|
+
const hasExplicitActiveColor = active?.color !== void 0;
|
|
2528
2900
|
const baseColor = font?.color ?? "#ffffff";
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2901
|
+
const baseOpacity = font?.opacity ?? 1;
|
|
2902
|
+
let activeColor;
|
|
2903
|
+
let activeOpacity;
|
|
2904
|
+
if (!hasExplicitActiveColor) {
|
|
2905
|
+
activeColor = baseColor;
|
|
2906
|
+
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2907
|
+
} else {
|
|
2908
|
+
const animStyle = asset.wordAnimation?.style ?? "highlight";
|
|
2909
|
+
const isFillAnimation = animStyle === "karaoke" || animStyle === "highlight";
|
|
2910
|
+
const DEFAULT_ACTIVE_COLOR = "#ffff00";
|
|
2911
|
+
activeColor = active.color ?? (isFillAnimation ? DEFAULT_ACTIVE_COLOR : baseColor);
|
|
2912
|
+
activeOpacity = active?.opacity ?? baseOpacity;
|
|
2913
|
+
}
|
|
2534
2914
|
return {
|
|
2535
2915
|
family: font?.family ?? "Roboto",
|
|
2536
2916
|
size: font?.size ?? 24,
|
|
2537
2917
|
weight: String(font?.weight ?? "400"),
|
|
2538
2918
|
baseColor,
|
|
2539
2919
|
activeColor,
|
|
2540
|
-
baseOpacity
|
|
2541
|
-
activeOpacity
|
|
2920
|
+
baseOpacity,
|
|
2921
|
+
activeOpacity,
|
|
2542
2922
|
letterSpacing: asset.style?.letterSpacing ?? 0
|
|
2543
2923
|
};
|
|
2544
2924
|
}
|
|
@@ -2568,20 +2948,33 @@ function extractStrokeConfig(asset, isActive) {
|
|
|
2568
2948
|
return void 0;
|
|
2569
2949
|
}
|
|
2570
2950
|
function extractShadowConfig(asset, isActive) {
|
|
2571
|
-
|
|
2951
|
+
const baseShadow = asset.shadow;
|
|
2952
|
+
const activeShadow = asset.active?.shadow;
|
|
2953
|
+
if (!baseShadow && !activeShadow) {
|
|
2572
2954
|
return void 0;
|
|
2573
2955
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
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
|
+
};
|
|
2577
2967
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
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;
|
|
2585
2978
|
}
|
|
2586
2979
|
function extractBackgroundConfig(asset, isActive, fontSize) {
|
|
2587
2980
|
const fontBackground = asset.font?.background;
|
|
@@ -2636,6 +3029,17 @@ function extractCaptionBorder(asset) {
|
|
|
2636
3029
|
radius: border.radius ?? 0
|
|
2637
3030
|
};
|
|
2638
3031
|
}
|
|
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
|
+
}
|
|
2639
3043
|
function extractAnimationConfig(asset) {
|
|
2640
3044
|
const wordAnim = asset.wordAnimation;
|
|
2641
3045
|
if (!wordAnim) {
|
|
@@ -2678,7 +3082,8 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
|
|
|
2678
3082
|
letterSpacing: fontConfig.letterSpacing > 0 ? fontConfig.letterSpacing : void 0,
|
|
2679
3083
|
stroke: extractStrokeConfig(asset, isActive),
|
|
2680
3084
|
shadow: extractShadowConfig(asset, isActive),
|
|
2681
|
-
background: extractBackgroundConfig(asset, isActive, fontConfig.size)
|
|
3085
|
+
background: extractBackgroundConfig(asset, isActive, fontConfig.size),
|
|
3086
|
+
textDecoration: extractTextDecoration(asset, isActive)
|
|
2682
3087
|
};
|
|
2683
3088
|
}
|
|
2684
3089
|
function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, config) {
|
|
@@ -3182,7 +3587,8 @@ async function createNodePainter(opts) {
|
|
|
3182
3587
|
context.lineCap = "round";
|
|
3183
3588
|
context.strokeText(displayText, 0, 0);
|
|
3184
3589
|
}
|
|
3185
|
-
|
|
3590
|
+
const sameColor = wordOp.activeColor === wordOp.baseColor && wordOp.activeOpacity === wordOp.baseOpacity;
|
|
3591
|
+
if (wordOp.fillProgress <= 0 || sameColor) {
|
|
3186
3592
|
const baseC = parseHex6(wordOp.baseColor, wordOp.baseOpacity);
|
|
3187
3593
|
context.fillStyle = `rgba(${baseC.r},${baseC.g},${baseC.b},${baseC.a})`;
|
|
3188
3594
|
context.fillText(displayText, 0, 0);
|
|
@@ -3209,7 +3615,26 @@ async function createNodePainter(opts) {
|
|
|
3209
3615
|
context.fillText(displayText, 0, 0);
|
|
3210
3616
|
context.restore();
|
|
3211
3617
|
}
|
|
3212
|
-
|
|
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
|
+
}
|
|
3637
|
+
context.restore();
|
|
3213
3638
|
}
|
|
3214
3639
|
});
|
|
3215
3640
|
continue;
|
|
@@ -4668,353 +5093,6 @@ function extractSvgDimensions(svgString) {
|
|
|
4668
5093
|
return { width, height };
|
|
4669
5094
|
}
|
|
4670
5095
|
|
|
4671
|
-
// src/core/rich-caption-layout.ts
|
|
4672
|
-
import { LRUCache } from "lru-cache";
|
|
4673
|
-
function isRTLText(text) {
|
|
4674
|
-
return containsRTLCharacters(text);
|
|
4675
|
-
}
|
|
4676
|
-
var WordTimingStore = class {
|
|
4677
|
-
startTimes;
|
|
4678
|
-
endTimes;
|
|
4679
|
-
xPositions;
|
|
4680
|
-
yPositions;
|
|
4681
|
-
widths;
|
|
4682
|
-
words;
|
|
4683
|
-
length;
|
|
4684
|
-
constructor(words) {
|
|
4685
|
-
this.length = words.length;
|
|
4686
|
-
this.startTimes = new Uint32Array(this.length);
|
|
4687
|
-
this.endTimes = new Uint32Array(this.length);
|
|
4688
|
-
this.xPositions = new Float32Array(this.length);
|
|
4689
|
-
this.yPositions = new Float32Array(this.length);
|
|
4690
|
-
this.widths = new Float32Array(this.length);
|
|
4691
|
-
this.words = new Array(this.length);
|
|
4692
|
-
for (let i = 0; i < this.length; i++) {
|
|
4693
|
-
this.startTimes[i] = Math.floor(words[i].start);
|
|
4694
|
-
this.endTimes[i] = Math.floor(words[i].end);
|
|
4695
|
-
this.words[i] = words[i].text;
|
|
4696
|
-
}
|
|
4697
|
-
}
|
|
4698
|
-
};
|
|
4699
|
-
function findWordAtTime(store, timeMs) {
|
|
4700
|
-
let left = 0;
|
|
4701
|
-
let right = store.length - 1;
|
|
4702
|
-
while (left <= right) {
|
|
4703
|
-
const mid = left + right >>> 1;
|
|
4704
|
-
const start = store.startTimes[mid];
|
|
4705
|
-
const end = store.endTimes[mid];
|
|
4706
|
-
if (timeMs >= start && timeMs < end) {
|
|
4707
|
-
return mid;
|
|
4708
|
-
}
|
|
4709
|
-
if (timeMs < start) {
|
|
4710
|
-
right = mid - 1;
|
|
4711
|
-
} else {
|
|
4712
|
-
left = mid + 1;
|
|
4713
|
-
}
|
|
4714
|
-
}
|
|
4715
|
-
return -1;
|
|
4716
|
-
}
|
|
4717
|
-
function groupWordsByPause(store, pauseThreshold = 500) {
|
|
4718
|
-
if (store.length === 0) {
|
|
4719
|
-
return [];
|
|
4720
|
-
}
|
|
4721
|
-
const groups = [];
|
|
4722
|
-
let currentGroup = [];
|
|
4723
|
-
for (let i = 0; i < store.length; i++) {
|
|
4724
|
-
if (currentGroup.length === 0) {
|
|
4725
|
-
currentGroup.push(i);
|
|
4726
|
-
continue;
|
|
4727
|
-
}
|
|
4728
|
-
const prevEnd = store.endTimes[currentGroup[currentGroup.length - 1]];
|
|
4729
|
-
const currStart = store.startTimes[i];
|
|
4730
|
-
const gap = currStart - prevEnd;
|
|
4731
|
-
const prevText = store.words[currentGroup[currentGroup.length - 1]];
|
|
4732
|
-
const endsWithPunctuation = /[.!?]$/.test(prevText);
|
|
4733
|
-
if (gap >= pauseThreshold || endsWithPunctuation) {
|
|
4734
|
-
groups.push(currentGroup);
|
|
4735
|
-
currentGroup = [i];
|
|
4736
|
-
} else {
|
|
4737
|
-
currentGroup.push(i);
|
|
4738
|
-
}
|
|
4739
|
-
}
|
|
4740
|
-
if (currentGroup.length > 0) {
|
|
4741
|
-
groups.push(currentGroup);
|
|
4742
|
-
}
|
|
4743
|
-
return groups;
|
|
4744
|
-
}
|
|
4745
|
-
function breakIntoLines(wordWidths, maxWidth, spaceWidth) {
|
|
4746
|
-
const lines = [];
|
|
4747
|
-
let currentLine = [];
|
|
4748
|
-
let currentWidth = 0;
|
|
4749
|
-
for (let i = 0; i < wordWidths.length; i++) {
|
|
4750
|
-
const wordWidth = wordWidths[i];
|
|
4751
|
-
const spaceNeeded = currentLine.length > 0 ? spaceWidth : 0;
|
|
4752
|
-
if (currentWidth + spaceNeeded + wordWidth <= maxWidth) {
|
|
4753
|
-
currentLine.push(i);
|
|
4754
|
-
currentWidth += spaceNeeded + wordWidth;
|
|
4755
|
-
} else {
|
|
4756
|
-
if (currentLine.length > 0) {
|
|
4757
|
-
lines.push(currentLine);
|
|
4758
|
-
}
|
|
4759
|
-
currentLine = [i];
|
|
4760
|
-
currentWidth = wordWidth;
|
|
4761
|
-
}
|
|
4762
|
-
}
|
|
4763
|
-
if (currentLine.length > 0) {
|
|
4764
|
-
lines.push(currentLine);
|
|
4765
|
-
}
|
|
4766
|
-
return lines;
|
|
4767
|
-
}
|
|
4768
|
-
var GLYPH_SIZE_ESTIMATE = 64;
|
|
4769
|
-
function createShapedWordCache() {
|
|
4770
|
-
return new LRUCache({
|
|
4771
|
-
max: 5e4,
|
|
4772
|
-
maxSize: 50 * 1024 * 1024,
|
|
4773
|
-
maxEntrySize: 100 * 1024,
|
|
4774
|
-
sizeCalculation: (value, key) => {
|
|
4775
|
-
const keySize = key.length * 2;
|
|
4776
|
-
const glyphsSize = value.glyphs.length * GLYPH_SIZE_ESTIMATE;
|
|
4777
|
-
return keySize + glyphsSize + 100;
|
|
4778
|
-
}
|
|
4779
|
-
});
|
|
4780
|
-
}
|
|
4781
|
-
function makeShapingKey(text, fontFamily, fontSize, fontWeight, letterSpacing = 0) {
|
|
4782
|
-
return `${text}\0${fontFamily}\0${fontSize}\0${fontWeight}\0${letterSpacing}`;
|
|
4783
|
-
}
|
|
4784
|
-
function transformText(text, transform) {
|
|
4785
|
-
switch (transform) {
|
|
4786
|
-
case "uppercase":
|
|
4787
|
-
return text.toUpperCase();
|
|
4788
|
-
case "lowercase":
|
|
4789
|
-
return text.toLowerCase();
|
|
4790
|
-
case "capitalize":
|
|
4791
|
-
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
4792
|
-
default:
|
|
4793
|
-
return text;
|
|
4794
|
-
}
|
|
4795
|
-
}
|
|
4796
|
-
function splitIntoChunks(arr, chunkSize) {
|
|
4797
|
-
const chunks = [];
|
|
4798
|
-
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
4799
|
-
chunks.push(arr.slice(i, i + chunkSize));
|
|
4800
|
-
}
|
|
4801
|
-
return chunks;
|
|
4802
|
-
}
|
|
4803
|
-
var CaptionLayoutEngine = class {
|
|
4804
|
-
fontRegistry;
|
|
4805
|
-
cache;
|
|
4806
|
-
layoutEngine;
|
|
4807
|
-
constructor(fontRegistry) {
|
|
4808
|
-
this.fontRegistry = fontRegistry;
|
|
4809
|
-
this.cache = createShapedWordCache();
|
|
4810
|
-
this.layoutEngine = new LayoutEngine(fontRegistry);
|
|
4811
|
-
}
|
|
4812
|
-
async measureWord(text, config) {
|
|
4813
|
-
const transformedText = transformText(text, config.textTransform);
|
|
4814
|
-
const cacheKey = makeShapingKey(
|
|
4815
|
-
transformedText,
|
|
4816
|
-
config.fontFamily,
|
|
4817
|
-
config.fontSize,
|
|
4818
|
-
config.fontWeight,
|
|
4819
|
-
config.letterSpacing
|
|
4820
|
-
);
|
|
4821
|
-
const cached = this.cache.get(cacheKey);
|
|
4822
|
-
if (cached) {
|
|
4823
|
-
return cached;
|
|
4824
|
-
}
|
|
4825
|
-
const lines = await this.layoutEngine.layout({
|
|
4826
|
-
text: transformedText,
|
|
4827
|
-
width: 1e5,
|
|
4828
|
-
letterSpacing: config.letterSpacing,
|
|
4829
|
-
fontSize: config.fontSize,
|
|
4830
|
-
lineHeight: 1,
|
|
4831
|
-
desc: { family: config.fontFamily, weight: config.fontWeight },
|
|
4832
|
-
textTransform: "none"
|
|
4833
|
-
});
|
|
4834
|
-
const width = lines[0]?.width ?? 0;
|
|
4835
|
-
const glyphs = lines[0]?.glyphs ?? [];
|
|
4836
|
-
const isRTL = isRTLText(transformedText);
|
|
4837
|
-
const shaped = {
|
|
4838
|
-
text: transformedText,
|
|
4839
|
-
width,
|
|
4840
|
-
glyphs: glyphs.map((g) => ({
|
|
4841
|
-
id: g.id,
|
|
4842
|
-
xAdvance: g.xAdvance,
|
|
4843
|
-
xOffset: g.xOffset,
|
|
4844
|
-
yOffset: g.yOffset,
|
|
4845
|
-
cluster: g.cluster
|
|
4846
|
-
})),
|
|
4847
|
-
isRTL
|
|
4848
|
-
};
|
|
4849
|
-
this.cache.set(cacheKey, shaped);
|
|
4850
|
-
return shaped;
|
|
4851
|
-
}
|
|
4852
|
-
async layoutCaption(words, config) {
|
|
4853
|
-
const store = new WordTimingStore(words);
|
|
4854
|
-
const measurementConfig = {
|
|
4855
|
-
fontFamily: config.fontFamily,
|
|
4856
|
-
fontSize: config.fontSize,
|
|
4857
|
-
fontWeight: config.fontWeight,
|
|
4858
|
-
letterSpacing: config.letterSpacing,
|
|
4859
|
-
textTransform: config.textTransform
|
|
4860
|
-
};
|
|
4861
|
-
const shapedWords = await Promise.all(
|
|
4862
|
-
words.map((w) => this.measureWord(w.text, measurementConfig))
|
|
4863
|
-
);
|
|
4864
|
-
if (config.measureTextWidth) {
|
|
4865
|
-
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
4866
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
4867
|
-
store.widths[i] = config.measureTextWidth(shapedWords[i].text, fontString);
|
|
4868
|
-
}
|
|
4869
|
-
} else {
|
|
4870
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
4871
|
-
store.widths[i] = shapedWords[i].width;
|
|
4872
|
-
}
|
|
4873
|
-
}
|
|
4874
|
-
if (config.textTransform !== "none") {
|
|
4875
|
-
for (let i = 0; i < shapedWords.length; i++) {
|
|
4876
|
-
store.words[i] = shapedWords[i].text;
|
|
4877
|
-
}
|
|
4878
|
-
}
|
|
4879
|
-
const wordGroups = groupWordsByPause(store, config.pauseThreshold);
|
|
4880
|
-
const pixelMaxWidth = config.availableWidth;
|
|
4881
|
-
let spaceWidth;
|
|
4882
|
-
if (config.measureTextWidth) {
|
|
4883
|
-
const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
|
|
4884
|
-
spaceWidth = config.measureTextWidth(" ", fontString) + config.wordSpacing;
|
|
4885
|
-
} else {
|
|
4886
|
-
const spaceWord = await this.measureWord(" ", measurementConfig);
|
|
4887
|
-
spaceWidth = spaceWord.width + config.wordSpacing;
|
|
4888
|
-
}
|
|
4889
|
-
const groups = wordGroups.flatMap((indices) => {
|
|
4890
|
-
const groupWidths = indices.map((i) => store.widths[i]);
|
|
4891
|
-
const allLines = breakIntoLines(
|
|
4892
|
-
groupWidths,
|
|
4893
|
-
pixelMaxWidth,
|
|
4894
|
-
spaceWidth
|
|
4895
|
-
);
|
|
4896
|
-
const lineChunks = splitIntoChunks(allLines, config.maxLines);
|
|
4897
|
-
return lineChunks.map((chunkLines) => {
|
|
4898
|
-
const lines = chunkLines.map((lineWordIndices, lineIndex) => {
|
|
4899
|
-
const actualIndices = lineWordIndices.map((i) => indices[i]);
|
|
4900
|
-
const lineWidth = actualIndices.reduce((sum, idx) => sum + store.widths[idx], 0) + (actualIndices.length - 1) * spaceWidth;
|
|
4901
|
-
return {
|
|
4902
|
-
wordIndices: actualIndices,
|
|
4903
|
-
x: 0,
|
|
4904
|
-
y: lineIndex * config.fontSize * config.lineHeight,
|
|
4905
|
-
width: lineWidth,
|
|
4906
|
-
height: config.fontSize
|
|
4907
|
-
};
|
|
4908
|
-
});
|
|
4909
|
-
const allWordIndices = lines.flatMap((l) => l.wordIndices);
|
|
4910
|
-
if (allWordIndices.length === 0) {
|
|
4911
|
-
return null;
|
|
4912
|
-
}
|
|
4913
|
-
return {
|
|
4914
|
-
wordIndices: allWordIndices,
|
|
4915
|
-
startTime: store.startTimes[allWordIndices[0]],
|
|
4916
|
-
endTime: store.endTimes[allWordIndices[allWordIndices.length - 1]],
|
|
4917
|
-
lines
|
|
4918
|
-
};
|
|
4919
|
-
}).filter((g) => g !== null);
|
|
4920
|
-
});
|
|
4921
|
-
const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
|
|
4922
|
-
const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
|
|
4923
|
-
const ASCENT_RATIO2 = 0.8;
|
|
4924
|
-
const calculateGroupY = (group) => {
|
|
4925
|
-
const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
|
|
4926
|
-
switch (config.verticalAlign) {
|
|
4927
|
-
case "top":
|
|
4928
|
-
return config.padding.top + config.fontSize * ASCENT_RATIO2;
|
|
4929
|
-
case "bottom":
|
|
4930
|
-
return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO2;
|
|
4931
|
-
case "middle":
|
|
4932
|
-
default:
|
|
4933
|
-
return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO2;
|
|
4934
|
-
}
|
|
4935
|
-
};
|
|
4936
|
-
const allWordTexts = store.words.slice(0, store.length);
|
|
4937
|
-
const paragraphDirection = detectParagraphDirectionFromWords(allWordTexts);
|
|
4938
|
-
const calculateLineX = (lineWidth) => {
|
|
4939
|
-
switch (config.horizontalAlign) {
|
|
4940
|
-
case "left":
|
|
4941
|
-
return config.padding.left;
|
|
4942
|
-
case "right":
|
|
4943
|
-
return config.frameWidth - lineWidth - config.padding.right;
|
|
4944
|
-
case "center":
|
|
4945
|
-
default:
|
|
4946
|
-
return config.padding.left + (contentWidth - lineWidth) / 2;
|
|
4947
|
-
}
|
|
4948
|
-
};
|
|
4949
|
-
for (const group of groups) {
|
|
4950
|
-
const baseY = calculateGroupY(group);
|
|
4951
|
-
for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
|
|
4952
|
-
const line = group.lines[lineIdx];
|
|
4953
|
-
line.x = calculateLineX(line.width);
|
|
4954
|
-
line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
|
|
4955
|
-
const lineWordTexts = line.wordIndices.map((idx) => store.words[idx]);
|
|
4956
|
-
const visualOrder = reorderWordsForLine(lineWordTexts, paragraphDirection);
|
|
4957
|
-
let xCursor = line.x;
|
|
4958
|
-
for (const visualIdx of visualOrder) {
|
|
4959
|
-
const wordIdx = line.wordIndices[visualIdx];
|
|
4960
|
-
store.xPositions[wordIdx] = xCursor;
|
|
4961
|
-
store.yPositions[wordIdx] = line.y;
|
|
4962
|
-
xCursor += store.widths[wordIdx] + spaceWidth;
|
|
4963
|
-
}
|
|
4964
|
-
}
|
|
4965
|
-
}
|
|
4966
|
-
return {
|
|
4967
|
-
store,
|
|
4968
|
-
groups,
|
|
4969
|
-
shapedWords,
|
|
4970
|
-
paragraphDirection
|
|
4971
|
-
};
|
|
4972
|
-
}
|
|
4973
|
-
getVisibleWordsAtTime(layout, timeMs) {
|
|
4974
|
-
const activeGroup = layout.groups.find(
|
|
4975
|
-
(g) => timeMs >= g.startTime && timeMs <= g.endTime
|
|
4976
|
-
);
|
|
4977
|
-
if (!activeGroup) {
|
|
4978
|
-
return [];
|
|
4979
|
-
}
|
|
4980
|
-
return activeGroup.wordIndices.map((idx) => ({
|
|
4981
|
-
wordIndex: idx,
|
|
4982
|
-
text: layout.store.words[idx],
|
|
4983
|
-
x: layout.store.xPositions[idx],
|
|
4984
|
-
y: layout.store.yPositions[idx],
|
|
4985
|
-
width: layout.store.widths[idx],
|
|
4986
|
-
startTime: layout.store.startTimes[idx],
|
|
4987
|
-
endTime: layout.store.endTimes[idx],
|
|
4988
|
-
isRTL: layout.shapedWords[idx].isRTL
|
|
4989
|
-
}));
|
|
4990
|
-
}
|
|
4991
|
-
getActiveWordAtTime(layout, timeMs) {
|
|
4992
|
-
const wordIndex = findWordAtTime(layout.store, timeMs);
|
|
4993
|
-
if (wordIndex === -1) {
|
|
4994
|
-
return null;
|
|
4995
|
-
}
|
|
4996
|
-
return {
|
|
4997
|
-
wordIndex,
|
|
4998
|
-
text: layout.store.words[wordIndex],
|
|
4999
|
-
x: layout.store.xPositions[wordIndex],
|
|
5000
|
-
y: layout.store.yPositions[wordIndex],
|
|
5001
|
-
width: layout.store.widths[wordIndex],
|
|
5002
|
-
startTime: layout.store.startTimes[wordIndex],
|
|
5003
|
-
endTime: layout.store.endTimes[wordIndex],
|
|
5004
|
-
isRTL: layout.shapedWords[wordIndex].isRTL
|
|
5005
|
-
};
|
|
5006
|
-
}
|
|
5007
|
-
clearCache() {
|
|
5008
|
-
this.cache.clear();
|
|
5009
|
-
}
|
|
5010
|
-
getCacheStats() {
|
|
5011
|
-
return {
|
|
5012
|
-
size: this.cache.size,
|
|
5013
|
-
calculatedSize: this.cache.calculatedSize
|
|
5014
|
-
};
|
|
5015
|
-
}
|
|
5016
|
-
};
|
|
5017
|
-
|
|
5018
5096
|
// src/core/canvas-text-measurer.ts
|
|
5019
5097
|
async function createCanvasTextMeasurer() {
|
|
5020
5098
|
const canvasMod = await import("canvas");
|