@meframe/core 0.0.19 → 0.0.21

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.
Files changed (58) hide show
  1. package/dist/config/types.d.ts +4 -0
  2. package/dist/config/types.d.ts.map +1 -1
  3. package/dist/model/CompositionModel.d.ts.map +1 -1
  4. package/dist/model/CompositionModel.js +21 -1
  5. package/dist/model/CompositionModel.js.map +1 -1
  6. package/dist/model/types.d.ts +31 -0
  7. package/dist/model/types.d.ts.map +1 -1
  8. package/dist/model/types.js.map +1 -1
  9. package/dist/orchestrator/CompositionPlanner.d.ts +1 -0
  10. package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
  11. package/dist/orchestrator/CompositionPlanner.js +36 -10
  12. package/dist/orchestrator/CompositionPlanner.js.map +1 -1
  13. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  14. package/dist/orchestrator/Orchestrator.js +2 -1
  15. package/dist/orchestrator/Orchestrator.js.map +1 -1
  16. package/dist/stages/compose/LayerRenderer.d.ts +4 -7
  17. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  18. package/dist/stages/compose/VideoComposer.d.ts +1 -0
  19. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  20. package/dist/stages/compose/font-system/FontManager.d.ts +11 -0
  21. package/dist/stages/compose/font-system/FontManager.d.ts.map +1 -0
  22. package/dist/stages/compose/font-system/FontManager.js +69 -0
  23. package/dist/stages/compose/font-system/FontManager.js.map +1 -0
  24. package/dist/stages/compose/font-system/font-templates.d.ts +12 -0
  25. package/dist/stages/compose/font-system/font-templates.d.ts.map +1 -0
  26. package/dist/stages/compose/font-system/font-templates.js +384 -0
  27. package/dist/stages/compose/font-system/font-templates.js.map +1 -0
  28. package/dist/stages/compose/font-system/index.d.ts +5 -0
  29. package/dist/stages/compose/font-system/index.d.ts.map +1 -0
  30. package/dist/stages/compose/font-system/types.d.ts +60 -0
  31. package/dist/stages/compose/font-system/types.d.ts.map +1 -0
  32. package/dist/stages/compose/instructions.d.ts +50 -0
  33. package/dist/stages/compose/instructions.d.ts.map +1 -1
  34. package/dist/stages/compose/text-renderers/animation-utils.d.ts +16 -0
  35. package/dist/stages/compose/text-renderers/animation-utils.d.ts.map +1 -0
  36. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +5 -0
  37. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -0
  38. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +4 -0
  39. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -0
  40. package/dist/stages/compose/text-renderers/index.d.ts +6 -0
  41. package/dist/stages/compose/text-renderers/index.d.ts.map +1 -0
  42. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +4 -0
  43. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -0
  44. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +4 -0
  45. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -0
  46. package/dist/stages/compose/text-utils/index.d.ts +4 -0
  47. package/dist/stages/compose/text-utils/index.d.ts.map +1 -0
  48. package/dist/stages/compose/text-utils/locale-detector.d.ts +5 -0
  49. package/dist/stages/compose/text-utils/locale-detector.d.ts.map +1 -0
  50. package/dist/stages/compose/text-utils/text-metrics.d.ts +3 -0
  51. package/dist/stages/compose/text-utils/text-metrics.d.ts.map +1 -0
  52. package/dist/stages/compose/text-utils/text-wrapper.d.ts +4 -0
  53. package/dist/stages/compose/text-utils/text-wrapper.d.ts.map +1 -0
  54. package/dist/stages/compose/types.d.ts +51 -1
  55. package/dist/stages/compose/types.d.ts.map +1 -1
  56. package/dist/workers/stages/compose/video-compose.worker.js +845 -77
  57. package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
  58. package/package.json +1 -1
@@ -12,16 +12,794 @@ function frameDurationFromFps(fps) {
12
12
  const duration = MICROSECONDS_PER_SECOND / normalized;
13
13
  return Math.max(Math.round(duration), 1);
14
14
  }
15
+ function measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight = 400) {
16
+ ctx.save();
17
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
18
+ const metrics = ctx.measureText(text);
19
+ ctx.restore();
20
+ return metrics.width;
21
+ }
22
+ function getLetterCaseText(text, letterCase) {
23
+ if (letterCase === "upper") {
24
+ return text.toUpperCase();
25
+ }
26
+ if (letterCase === "lower") {
27
+ return text.toLowerCase();
28
+ }
29
+ return text;
30
+ }
31
+ function findAllBreakPoints(text) {
32
+ const breakPoints = [0];
33
+ const chars = Array.from(text);
34
+ for (let i = 1; i < chars.length - 1; i++) {
35
+ if (/[、。!?,,!?;:]/.test(chars[i])) {
36
+ breakPoints.push(i + 1);
37
+ } else if (/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(chars[i]) && /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(chars[i + 1])) {
38
+ breakPoints.push(i + 1);
39
+ } else if (/[\s\-–—,.!?;:]/.test(chars[i])) {
40
+ breakPoints.push(i + 1);
41
+ } else if (/\s/.test(chars[i]) && /[a-zA-Z]/.test(chars[i + 1])) {
42
+ breakPoints.push(i + 1);
43
+ }
44
+ }
45
+ breakPoints.push(chars.length);
46
+ return breakPoints;
47
+ }
48
+ function evaluateBalance(lines, ctx, fontSize, fontFamily, fontWeight) {
49
+ if (lines.length <= 1) return 0;
50
+ const lengths = lines.map(
51
+ (line) => measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight)
52
+ );
53
+ const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
54
+ return lengths.reduce((sum, len) => sum + Math.abs(len - avgLength), 0);
55
+ }
56
+ function tryBreakPointsForMultipleLines(ctx, text, start, remainingLines, currentLines, maxWidth, fontSize, fontFamily, fontWeight, breakPoints) {
57
+ let bestLines = [];
58
+ let bestBalance = Infinity;
59
+ if (remainingLines === 1) {
60
+ const lastLine = text.slice(start).trim();
61
+ const lastLineWidth = measureTextWidth(ctx, lastLine, fontSize, fontFamily, fontWeight);
62
+ if (lastLineWidth <= maxWidth) {
63
+ const allLines = [...currentLines, lastLine];
64
+ const balance = evaluateBalance(allLines, ctx, fontSize, fontFamily, fontWeight);
65
+ if (balance < bestBalance) {
66
+ bestBalance = balance;
67
+ bestLines = allLines;
68
+ }
69
+ } else {
70
+ const words = lastLine.split(/\s+/);
71
+ let currentLine = "";
72
+ let tempLines = [...currentLines];
73
+ for (const word of words) {
74
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
75
+ const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
76
+ if (lineWidth <= maxWidth) {
77
+ currentLine = testLine;
78
+ } else {
79
+ if (currentLine) {
80
+ tempLines.push(currentLine);
81
+ currentLine = word;
82
+ } else {
83
+ tempLines.push(word);
84
+ currentLine = "";
85
+ }
86
+ }
87
+ }
88
+ if (currentLine) {
89
+ tempLines.push(currentLine);
90
+ }
91
+ const balance = evaluateBalance(tempLines, ctx, fontSize, fontFamily, fontWeight);
92
+ if (balance < bestBalance) {
93
+ bestBalance = balance;
94
+ bestLines = tempLines;
95
+ }
96
+ }
97
+ return { bestLines, bestBalance };
98
+ }
99
+ let foundValidBreak = false;
100
+ for (let i = 0; i < breakPoints.length; i++) {
101
+ const bp = breakPoints[i];
102
+ if (bp <= start || bp >= text.length) continue;
103
+ const line = text.slice(start, bp).trim();
104
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
105
+ if (lineWidth <= maxWidth) {
106
+ foundValidBreak = true;
107
+ const result = tryBreakPointsForMultipleLines(
108
+ ctx,
109
+ text,
110
+ bp,
111
+ remainingLines - 1,
112
+ [...currentLines, line],
113
+ maxWidth,
114
+ fontSize,
115
+ fontFamily,
116
+ fontWeight,
117
+ breakPoints
118
+ );
119
+ if (result.bestBalance < bestBalance) {
120
+ bestBalance = result.bestBalance;
121
+ bestLines = result.bestLines;
122
+ }
123
+ }
124
+ }
125
+ if (!foundValidBreak) {
126
+ const textPortion = text.slice(start);
127
+ const words = textPortion.split(/\s+/);
128
+ let currentLine = "";
129
+ let tempLines = [...currentLines];
130
+ for (const word of words) {
131
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
132
+ const lineWidth = measureTextWidth(ctx, testLine, fontSize, fontFamily, fontWeight);
133
+ if (lineWidth <= maxWidth) {
134
+ currentLine = testLine;
135
+ } else {
136
+ if (currentLine) {
137
+ tempLines.push(currentLine);
138
+ currentLine = word;
139
+ } else {
140
+ tempLines.push(word);
141
+ }
142
+ }
143
+ }
144
+ if (currentLine) {
145
+ tempLines.push(currentLine);
146
+ }
147
+ bestLines = tempLines;
148
+ }
149
+ return { bestLines, bestBalance };
150
+ }
151
+ function wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight = 400) {
152
+ const textWidth = measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight);
153
+ if (textWidth <= maxWidth) {
154
+ return [text];
155
+ }
156
+ const estimatedLines = Math.ceil(textWidth / maxWidth);
157
+ const breakPoints = findAllBreakPoints(text);
158
+ const { bestLines } = tryBreakPointsForMultipleLines(
159
+ ctx,
160
+ text,
161
+ 0,
162
+ estimatedLines,
163
+ [],
164
+ maxWidth,
165
+ fontSize,
166
+ fontFamily,
167
+ fontWeight,
168
+ breakPoints
169
+ );
170
+ return bestLines.length > 0 ? bestLines : [text];
171
+ }
172
+ function formLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
173
+ const result = [];
174
+ let accumulatedWidth = 0;
175
+ const spaceWidth = measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight);
176
+ let currentLine = "";
177
+ for (const word of words) {
178
+ let wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
179
+ if (needsSpace) {
180
+ wordWidth += spaceWidth;
181
+ }
182
+ if (wordWidth + accumulatedWidth <= maxWidth) {
183
+ currentLine += word + (needsSpace ? " " : "");
184
+ accumulatedWidth += wordWidth;
185
+ } else {
186
+ if (currentLine) {
187
+ result.push(currentLine);
188
+ }
189
+ currentLine = word + (needsSpace ? " " : "");
190
+ accumulatedWidth = wordWidth;
191
+ }
192
+ }
193
+ if (currentLine !== "") {
194
+ result.push(currentLine);
195
+ }
196
+ return result;
197
+ }
198
+ function formEvenLinesWithWords(ctx, words, maxWidth, fontSize, needsSpace, fontFamily, fontWeight = 400) {
199
+ let minWidth = maxWidth / 2;
200
+ for (const word of words) {
201
+ const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
202
+ if (wordWidth > minWidth) {
203
+ minWidth = wordWidth;
204
+ }
205
+ }
206
+ const leastLineNum = formLinesWithWords(
207
+ ctx,
208
+ words,
209
+ maxWidth,
210
+ fontSize,
211
+ needsSpace,
212
+ fontFamily,
213
+ fontWeight
214
+ ).length;
215
+ let bestDelta = maxWidth;
216
+ let bestWidth = minWidth;
217
+ for (let width = maxWidth; width >= minWidth; width -= 1) {
218
+ const lines = formLinesWithWords(
219
+ ctx,
220
+ words,
221
+ width,
222
+ fontSize,
223
+ needsSpace,
224
+ fontFamily,
225
+ fontWeight
226
+ );
227
+ if (lines.length > leastLineNum) {
228
+ break;
229
+ }
230
+ let minLineWidth = Infinity;
231
+ let maxLineWidth = 0;
232
+ for (const line of lines) {
233
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
234
+ if (lineWidth < minLineWidth) {
235
+ minLineWidth = lineWidth;
236
+ }
237
+ if (lineWidth > maxLineWidth) {
238
+ maxLineWidth = lineWidth;
239
+ }
240
+ }
241
+ const delta = maxLineWidth - minLineWidth;
242
+ if (delta < bestDelta) {
243
+ bestDelta = delta;
244
+ bestWidth = width;
245
+ }
246
+ }
247
+ return formLinesWithWords(ctx, words, bestWidth, fontSize, needsSpace, fontFamily, fontWeight);
248
+ }
249
+ function springEasing(frame, config) {
250
+ const { damping, mass, stiffness, overshootClamping = false } = config;
251
+ if (frame <= 0) return 0;
252
+ const zeta = damping / (2 * Math.sqrt(mass * stiffness));
253
+ const omega = Math.sqrt(stiffness / mass);
254
+ const t = frame / 60;
255
+ let value;
256
+ if (zeta < 1) {
257
+ const omegaD = omega * Math.sqrt(1 - zeta * zeta);
258
+ const A = 1;
259
+ const B = zeta * omega / omegaD;
260
+ value = 1 - Math.exp(-zeta * omega * t) * (A * Math.cos(omegaD * t) + B * Math.sin(omegaD * t));
261
+ } else if (zeta === 1) {
262
+ value = 1 - Math.exp(-omega * t) * (1 + omega * t);
263
+ } else {
264
+ const r1 = -omega * (zeta - Math.sqrt(zeta * zeta - 1));
265
+ const r2 = -omega * (zeta + Math.sqrt(zeta * zeta - 1));
266
+ const A = r2 / (r2 - r1);
267
+ const B = 1 - A;
268
+ value = 1 - A * Math.exp(r1 * t) - B * Math.exp(r2 * t);
269
+ }
270
+ if (overshootClamping && value > 1) {
271
+ return 1;
272
+ }
273
+ return Math.max(0, value);
274
+ }
275
+ function interpolate(value, inputRange, outputRange, options) {
276
+ const { extrapolateLeft = "extend", extrapolateRight = "extend" } = options || {};
277
+ const [inputMin, inputMax] = inputRange;
278
+ const [outputMin, outputMax] = outputRange;
279
+ if (value < inputMin) {
280
+ if (extrapolateLeft === "clamp") {
281
+ return outputMin;
282
+ }
283
+ }
284
+ if (value > inputMax) {
285
+ if (extrapolateRight === "clamp") {
286
+ return outputMax;
287
+ }
288
+ }
289
+ const inputDelta = inputMax - inputMin;
290
+ if (inputDelta === 0) {
291
+ return outputMin;
292
+ }
293
+ const progress = (value - inputMin) / inputDelta;
294
+ const outputDelta = outputMax - outputMin;
295
+ return outputMin + progress * outputDelta;
296
+ }
297
+ function parseRgb(color) {
298
+ const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
299
+ if (match) {
300
+ return {
301
+ r: parseInt(match[1], 10),
302
+ g: parseInt(match[2], 10),
303
+ b: parseInt(match[3], 10)
304
+ };
305
+ }
306
+ return null;
307
+ }
308
+ function interpolateColor(color1, color2, progress) {
309
+ const rgb1 = parseRgb(color1);
310
+ const rgb2 = parseRgb(color2);
311
+ if (!rgb1 || !rgb2) {
312
+ return color1;
313
+ }
314
+ const r = Math.round(interpolate(progress, [0, 1], [rgb1.r, rgb2.r]));
315
+ const g = Math.round(interpolate(progress, [0, 1], [rgb1.g, rgb2.g]));
316
+ const b = Math.round(interpolate(progress, [0, 1], [rgb1.b, rgb2.b]));
317
+ return `rgb(${r}, ${g}, ${b})`;
318
+ }
319
+ const CJK_REGEX = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\u3400-\u4dbf\uac00-\ud7af]/g;
320
+ function needsSpaceBetweenWords(locale, text) {
321
+ if (text) {
322
+ const cjkMatches = text.match(CJK_REGEX);
323
+ const cjkCount = cjkMatches ? cjkMatches.length : 0;
324
+ if (cjkCount > 0 && cjkCount / text.length >= 0.6) {
325
+ return false;
326
+ }
327
+ return true;
328
+ }
329
+ return !["zh-CN", "ja-JP", "ko-KR"].includes(locale);
330
+ }
331
+ function calculateYPosition$3(canvasHeight, totalHeight, globalPosition) {
332
+ if (!globalPosition) {
333
+ return canvasHeight / 2 - totalHeight / 2;
334
+ }
335
+ if (globalPosition.top) {
336
+ const topPercent = parseFloat(globalPosition.top) / 100;
337
+ return canvasHeight * topPercent;
338
+ }
339
+ if (globalPosition.bottom) {
340
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
341
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
342
+ }
343
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
344
+ return canvasHeight / 2 - totalHeight / 2;
345
+ }
346
+ return canvasHeight / 2 - totalHeight / 2;
347
+ }
348
+ function renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame) {
349
+ const fontConfig = layer.fontConfig?.textStyle;
350
+ if (!fontConfig) return;
351
+ const fontSize = fontConfig.fontSize;
352
+ const fontFamily = fontConfig.fontFamily;
353
+ const fontWeight = fontConfig.fontWeight;
354
+ const fill = fontConfig.fill;
355
+ const stroke = fontConfig.stroke;
356
+ const strokeWidth = fontConfig.strokeWidth || 0;
357
+ const lineHeight = fontConfig.lineHeight || 1.2;
358
+ const maxWidth = canvasWidth * 0.64;
359
+ const text = getLetterCaseText(layer.text, layer.letterCase);
360
+ let lines;
361
+ if (layer.wordTimings && layer.wordTimings.length > 0) {
362
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
363
+ const words = text.split(needsSpace ? /\s+/ : "");
364
+ lines = formEvenLinesWithWords(
365
+ ctx,
366
+ words,
367
+ maxWidth,
368
+ fontSize,
369
+ needsSpace,
370
+ fontFamily,
371
+ fontWeight
372
+ );
373
+ } else {
374
+ lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
375
+ }
376
+ ctx.save();
377
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
378
+ ctx.textAlign = "center";
379
+ ctx.textBaseline = "middle";
380
+ ctx.lineJoin = "round";
381
+ ctx.lineCap = "round";
382
+ const totalHeight = lines.length * fontSize * lineHeight;
383
+ const startY = calculateYPosition$3(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
384
+ for (let i = 0; i < lines.length; i++) {
385
+ const line = lines[i];
386
+ const y = startY + i * fontSize * lineHeight + fontSize / 2;
387
+ if (stroke && strokeWidth > 0) {
388
+ ctx.strokeStyle = stroke;
389
+ ctx.lineWidth = strokeWidth;
390
+ ctx.strokeText(line, canvasWidth / 2, y);
391
+ }
392
+ ctx.fillStyle = fill;
393
+ ctx.fillText(line, canvasWidth / 2, y);
394
+ }
395
+ ctx.restore();
396
+ }
397
+ function renderTextWithEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame) {
398
+ const fontConfig = layer.fontConfig?.textStyle;
399
+ if (!fontConfig) return;
400
+ const entrance = springEasing(relativeFrame, {
401
+ damping: 200,
402
+ mass: 1,
403
+ stiffness: 300
404
+ });
405
+ const opacity = interpolate(entrance, [0, 1], [0, 1]);
406
+ const scale = interpolate(entrance, [0, 1], [0.9, 1]);
407
+ ctx.save();
408
+ ctx.globalAlpha = opacity;
409
+ ctx.translate(canvasWidth / 2, canvasHeight / 2);
410
+ ctx.scale(scale, scale);
411
+ ctx.translate(-canvasWidth / 2, -canvasHeight / 2);
412
+ renderBasicText(ctx, layer, canvasWidth, canvasHeight);
413
+ ctx.restore();
414
+ }
415
+ function usToFrame$2(us, fps) {
416
+ return Math.floor(us / (1e6 / fps));
417
+ }
418
+ function calculateYPosition$2(canvasHeight, totalHeight, globalPosition) {
419
+ if (!globalPosition) {
420
+ return canvasHeight / 2 - totalHeight / 2;
421
+ }
422
+ if (globalPosition.top) {
423
+ const topPercent = parseFloat(globalPosition.top) / 100;
424
+ return canvasHeight * topPercent;
425
+ }
426
+ if (globalPosition.bottom) {
427
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
428
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
429
+ }
430
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
431
+ return canvasHeight / 2 - totalHeight / 2;
432
+ }
433
+ return canvasHeight / 2 - totalHeight / 2;
434
+ }
435
+ function renderWordByWord(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
436
+ const fontConfig = layer.fontConfig?.textStyle;
437
+ if (!fontConfig) return;
438
+ const fontSize = fontConfig.fontSize;
439
+ const fontFamily = fontConfig.fontFamily;
440
+ const fontWeight = fontConfig.fontWeight;
441
+ const fill = fontConfig.fill;
442
+ const stroke = fontConfig.stroke;
443
+ const strokeWidth = fontConfig.strokeWidth || 0;
444
+ const lineHeight = fontConfig.lineHeight || 1.2;
445
+ const highlightFill = layer.animation?.highlightTextStyle?.fill || "rgb(255, 215, 0)";
446
+ const highlightStroke = layer.animation?.highlightTextStyle?.stroke || stroke;
447
+ const maxWidth = canvasWidth * 0.64;
448
+ const text = getLetterCaseText(layer.text, layer.letterCase);
449
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
450
+ const words = text.split(needsSpace ? /\s+/ : "");
451
+ const lines = formEvenLinesWithWords(
452
+ ctx,
453
+ words,
454
+ maxWidth,
455
+ fontSize,
456
+ needsSpace,
457
+ fontFamily,
458
+ fontWeight
459
+ );
460
+ const wordPositions = [];
461
+ let wordIndex = 0;
462
+ const totalHeight = lines.length * fontSize * lineHeight;
463
+ const startY = calculateYPosition$2(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
464
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
465
+ const line = lines[lineIndex];
466
+ const lineWords = line.split(needsSpace ? /\s+/ : "");
467
+ const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
468
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
469
+ let currentX = canvasWidth / 2 - lineWidth / 2;
470
+ for (const word of lineWords) {
471
+ const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
472
+ const wordTimingUs = layer.wordTimings?.[wordIndex];
473
+ const wordTiming = wordTimingUs ? {
474
+ startFrame: usToFrame$2(wordTimingUs.startUs, fps),
475
+ endFrame: usToFrame$2(wordTimingUs.endUs, fps)
476
+ } : void 0;
477
+ wordPositions.push({
478
+ text: word,
479
+ x: currentX + wordWidth / 2,
480
+ y,
481
+ lineIndex,
482
+ wordIndex,
483
+ timing: wordTiming
484
+ });
485
+ currentX += wordWidth + (needsSpace ? measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight) : 0);
486
+ wordIndex++;
487
+ }
488
+ }
489
+ ctx.save();
490
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
491
+ ctx.textAlign = "center";
492
+ ctx.textBaseline = "middle";
493
+ ctx.lineJoin = "round";
494
+ ctx.lineCap = "round";
495
+ for (const wordPos of wordPositions) {
496
+ let currentFill = fill;
497
+ let currentStroke = stroke;
498
+ if (wordPos.timing) {
499
+ const { startFrame, endFrame } = wordPos.timing;
500
+ if (relativeFrame >= startFrame && relativeFrame <= endFrame) {
501
+ const transitionProgressIn = interpolate(
502
+ relativeFrame,
503
+ [startFrame, startFrame + 3],
504
+ [0, 1],
505
+ {
506
+ extrapolateLeft: "clamp",
507
+ extrapolateRight: "clamp"
508
+ }
509
+ );
510
+ currentFill = interpolateColor(fill, highlightFill, transitionProgressIn);
511
+ if (stroke && highlightStroke) {
512
+ currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressIn);
513
+ }
514
+ } else if (relativeFrame > endFrame) {
515
+ const transitionProgressOut = interpolate(relativeFrame, [endFrame, endFrame + 3], [1, 0], {
516
+ extrapolateLeft: "clamp",
517
+ extrapolateRight: "clamp"
518
+ });
519
+ currentFill = interpolateColor(fill, highlightFill, transitionProgressOut);
520
+ if (stroke && highlightStroke) {
521
+ currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressOut);
522
+ }
523
+ }
524
+ }
525
+ if (currentStroke && strokeWidth > 0) {
526
+ ctx.strokeStyle = currentStroke;
527
+ ctx.lineWidth = strokeWidth;
528
+ ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);
529
+ }
530
+ ctx.fillStyle = currentFill;
531
+ ctx.fillText(wordPos.text, wordPos.x, wordPos.y);
532
+ }
533
+ ctx.restore();
534
+ }
535
+ function usToFrame$1(us, fps) {
536
+ return Math.floor(us / (1e6 / fps));
537
+ }
538
+ function calculateYPosition$1(canvasHeight, totalHeight, globalPosition) {
539
+ if (!globalPosition) {
540
+ return canvasHeight / 2 - totalHeight / 2;
541
+ }
542
+ if (globalPosition.top) {
543
+ const topPercent = parseFloat(globalPosition.top) / 100;
544
+ return canvasHeight * topPercent;
545
+ }
546
+ if (globalPosition.bottom) {
547
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
548
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
549
+ }
550
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
551
+ return canvasHeight / 2 - totalHeight / 2;
552
+ }
553
+ return canvasHeight / 2 - totalHeight / 2;
554
+ }
555
+ function renderCharacterKTV(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
556
+ const fontConfig = layer.fontConfig?.textStyle;
557
+ if (!fontConfig) return;
558
+ const fontSize = fontConfig.fontSize;
559
+ const fontFamily = fontConfig.fontFamily;
560
+ const fontWeight = fontConfig.fontWeight;
561
+ const baseFill = fontConfig.fill;
562
+ const stroke = fontConfig.stroke;
563
+ const strokeWidth = fontConfig.strokeWidth || 0;
564
+ const lineHeight = fontConfig.lineHeight || 1.2;
565
+ const highlightFill = layer.animation?.highlightTextStyle?.fill || "rgb(255, 215, 0)";
566
+ const glowColor = layer.animation?.glowColor || "#ffffff";
567
+ const glowIntensity = layer.animation?.glowIntensity || 3;
568
+ const transitionFrames = layer.animation?.transitionFrames || 10;
569
+ const maxWidth = canvasWidth * 0.9;
570
+ const text = getLetterCaseText(layer.text, layer.letterCase);
571
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
572
+ const characterTimings = [];
573
+ if (layer.wordTimings && layer.wordTimings.length > 0) {
574
+ let charIndex = 0;
575
+ for (let wordIndex = 0; wordIndex < layer.wordTimings.length; wordIndex++) {
576
+ const word = layer.wordTimings[wordIndex];
577
+ const wordChars = word.text.split("");
578
+ const wordStartFrame = usToFrame$1(word.startUs, fps);
579
+ const wordEndFrame = usToFrame$1(word.endUs, fps);
580
+ const framesPerChar = wordChars.length > 0 ? (wordEndFrame - wordStartFrame) / wordChars.length : 0;
581
+ for (let i = 0; i < wordChars.length; i++) {
582
+ const charStartFrame = Math.floor(wordStartFrame + i * framesPerChar);
583
+ characterTimings.push({
584
+ char: wordChars[i],
585
+ index: charIndex,
586
+ startFrame: charStartFrame
587
+ });
588
+ charIndex++;
589
+ }
590
+ if (needsSpace && wordIndex < layer.wordTimings.length - 1) {
591
+ const nextWord = layer.wordTimings[wordIndex + 1];
592
+ const nextWordFirstChar = nextWord?.text?.[0] || "";
593
+ const isPunctuation = /[.,!?;:)]/.test(nextWordFirstChar);
594
+ if (!isPunctuation) {
595
+ const spaceStartFrame = characterTimings[characterTimings.length - 1]?.startFrame || 0;
596
+ characterTimings.push({
597
+ char: " ",
598
+ index: charIndex,
599
+ startFrame: spaceStartFrame
600
+ });
601
+ charIndex++;
602
+ }
603
+ }
604
+ }
605
+ } else {
606
+ const totalFrames = 100;
607
+ const chars = text.split("");
608
+ const framesPerChar = chars.length > 0 ? totalFrames / chars.length : 0;
609
+ for (let i = 0; i < chars.length; i++) {
610
+ characterTimings.push({
611
+ char: chars[i],
612
+ index: i,
613
+ startFrame: Math.floor(i * framesPerChar)
614
+ });
615
+ }
616
+ }
617
+ const fullText = characterTimings.map((ct) => ct.char).join("");
618
+ const fullTextLines = wrapText(ctx, fullText, maxWidth, fontSize, fontFamily, fontWeight);
619
+ ctx.save();
620
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
621
+ ctx.textAlign = "left";
622
+ ctx.textBaseline = "middle";
623
+ ctx.lineJoin = "round";
624
+ ctx.lineCap = "round";
625
+ const totalHeight = fullTextLines.length * fontSize * lineHeight;
626
+ const startY = calculateYPosition$1(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
627
+ let charIndexInText = 0;
628
+ for (let lineIndex = 0; lineIndex < fullTextLines.length; lineIndex++) {
629
+ const line = fullTextLines[lineIndex];
630
+ const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
631
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
632
+ let currentX = canvasWidth / 2 - lineWidth / 2;
633
+ for (let i = 0; i < line.length; i++) {
634
+ const char = line[i];
635
+ const timing = characterTimings[charIndexInText];
636
+ if (timing) {
637
+ const hasScanned = relativeFrame >= timing.startFrame;
638
+ const isCurrentlySinging = hasScanned && relativeFrame < timing.startFrame + transitionFrames;
639
+ ctx.fillStyle = hasScanned ? highlightFill : baseFill;
640
+ if (isCurrentlySinging) {
641
+ ctx.shadowColor = glowColor;
642
+ ctx.shadowBlur = glowIntensity * 10;
643
+ } else {
644
+ ctx.shadowBlur = 0;
645
+ }
646
+ if (stroke && strokeWidth > 0) {
647
+ ctx.strokeStyle = stroke;
648
+ ctx.lineWidth = strokeWidth;
649
+ ctx.strokeText(char, currentX, y);
650
+ }
651
+ ctx.fillText(char, currentX, y);
652
+ ctx.shadowBlur = 0;
653
+ }
654
+ currentX += measureTextWidth(ctx, char, fontSize, fontFamily, fontWeight);
655
+ charIndexInText++;
656
+ }
657
+ }
658
+ ctx.restore();
659
+ }
660
+ function usToFrame(us, fps) {
661
+ return Math.floor(us / (1e6 / fps));
662
+ }
663
+ function calculateYPosition(canvasHeight, totalHeight, globalPosition) {
664
+ if (!globalPosition) {
665
+ return canvasHeight / 2 - totalHeight / 2;
666
+ }
667
+ if (globalPosition.top) {
668
+ const topPercent = parseFloat(globalPosition.top) / 100;
669
+ return canvasHeight * topPercent;
670
+ }
671
+ if (globalPosition.bottom) {
672
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
673
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
674
+ }
675
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
676
+ return canvasHeight / 2 - totalHeight / 2;
677
+ }
678
+ return canvasHeight / 2 - totalHeight / 2;
679
+ }
680
+ function renderWordByWordFancy(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
681
+ const fontConfig = layer.fontConfig?.textStyle;
682
+ if (!fontConfig) return;
683
+ const fontSize = fontConfig.fontSize;
684
+ const fontFamily = fontConfig.fontFamily;
685
+ const fontWeight = fontConfig.fontWeight;
686
+ const fill = fontConfig.fill;
687
+ const stroke = fontConfig.stroke;
688
+ const strokeWidth = fontConfig.strokeWidth || 0;
689
+ const lineHeight = fontConfig.lineHeight || 1.2;
690
+ const highlightBackgroundColor = layer.animation?.highlightColor || "rgb(255, 215, 0)";
691
+ const maxWidth = canvasWidth * 0.64;
692
+ const text = getLetterCaseText(layer.text, layer.letterCase);
693
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
694
+ const words = text.split(needsSpace ? /\s+/ : "");
695
+ const lines = formEvenLinesWithWords(
696
+ ctx,
697
+ words,
698
+ maxWidth,
699
+ fontSize,
700
+ needsSpace,
701
+ fontFamily,
702
+ fontWeight
703
+ );
704
+ const wordPositions = [];
705
+ let wordIndex = 0;
706
+ const totalHeight = lines.length * fontSize * lineHeight;
707
+ const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
708
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
709
+ const line = lines[lineIndex];
710
+ const lineWords = line.split(needsSpace ? /\s+/ : "");
711
+ const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
712
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
713
+ let currentX = canvasWidth / 2 - lineWidth / 2;
714
+ for (const word of lineWords) {
715
+ const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
716
+ const wordTimingUs = layer.wordTimings?.[wordIndex];
717
+ const wordTiming = wordTimingUs ? {
718
+ startFrame: usToFrame(wordTimingUs.startUs, fps),
719
+ endFrame: usToFrame(wordTimingUs.endUs, fps)
720
+ } : void 0;
721
+ wordPositions.push({
722
+ text: word,
723
+ x: currentX + wordWidth / 2,
724
+ y,
725
+ width: wordWidth,
726
+ lineIndex,
727
+ wordIndex,
728
+ timing: wordTiming
729
+ });
730
+ currentX += wordWidth + (needsSpace ? measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight) : 0);
731
+ wordIndex++;
732
+ }
733
+ }
734
+ ctx.save();
735
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
736
+ ctx.textAlign = "center";
737
+ ctx.textBaseline = "middle";
738
+ ctx.lineJoin = "round";
739
+ ctx.lineCap = "round";
740
+ for (const wordPos of wordPositions) {
741
+ let backgroundOpacity = 0;
742
+ let backgroundScale = 0.8;
743
+ if (wordPos.timing) {
744
+ const { startFrame, endFrame } = wordPos.timing;
745
+ const preStartFrames = 3;
746
+ const isActive = relativeFrame >= startFrame && relativeFrame <= endFrame;
747
+ if (isActive) {
748
+ const scaleSpringIn = springEasing(relativeFrame - (startFrame - preStartFrames), {
749
+ damping: 6,
750
+ mass: 0.35,
751
+ stiffness: 200,
752
+ overshootClamping: false
753
+ });
754
+ const inProgress = interpolate(relativeFrame, [startFrame, startFrame + 1], [0, 1], {
755
+ extrapolateLeft: "clamp",
756
+ extrapolateRight: "clamp"
757
+ });
758
+ backgroundOpacity = 0.9 * inProgress;
759
+ backgroundScale = 0.8 + scaleSpringIn * 0.45;
760
+ } else if (relativeFrame > endFrame) {
761
+ backgroundOpacity = 0;
762
+ backgroundScale = 0.8;
763
+ }
764
+ if (backgroundOpacity > 0) {
765
+ const padding = 8;
766
+ const bgWidth = (wordPos.width + padding * 2) * backgroundScale;
767
+ const bgHeight = (fontSize + padding) * backgroundScale;
768
+ ctx.save();
769
+ ctx.globalAlpha = backgroundOpacity;
770
+ ctx.fillStyle = highlightBackgroundColor;
771
+ ctx.beginPath();
772
+ ctx.roundRect(wordPos.x - bgWidth / 2, wordPos.y - bgHeight / 2, bgWidth, bgHeight, 8);
773
+ ctx.fill();
774
+ ctx.restore();
775
+ }
776
+ }
777
+ if (stroke && strokeWidth > 0) {
778
+ ctx.strokeStyle = stroke;
779
+ ctx.lineWidth = strokeWidth;
780
+ ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);
781
+ }
782
+ ctx.fillStyle = fill;
783
+ ctx.fillText(wordPos.text, wordPos.x, wordPos.y);
784
+ }
785
+ ctx.restore();
786
+ }
15
787
  class LayerRenderer {
16
788
  ctx;
17
789
  width;
18
790
  height;
19
- constructor(ctx, width, height) {
791
+ currentFrame = 0;
792
+ fps = 30;
793
+ constructor(ctx, width, height, fps = 30) {
20
794
  this.ctx = ctx;
21
795
  this.width = width;
22
796
  this.height = height;
797
+ this.fps = fps;
23
798
  this.ensureHighQualityRendering();
24
799
  }
800
+ setCurrentFrame(frame) {
801
+ this.currentFrame = frame;
802
+ }
25
803
  ensureHighQualityRendering() {
26
804
  this.ctx.imageSmoothingEnabled = true;
27
805
  this.ctx.imageSmoothingQuality = "high";
@@ -211,73 +989,38 @@ class LayerRenderer {
211
989
  }
212
990
  }
213
991
  async renderTextLayer(layer) {
214
- const fontSize = layer.fontSize ?? 16;
215
- const fontFamily = layer.fontFamily ?? "sans-serif";
216
- const fontWeight = layer.fontWeight ?? "normal";
217
- const fontStyle = layer.fontStyle ?? "normal";
218
- this.ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
219
- this.ctx.fillStyle = layer.color ?? "#000000";
220
- this.ctx.textAlign = layer.textAlign ?? "left";
221
- this.ctx.textBaseline = layer.verticalAlign ?? "top";
222
- if (layer.letterSpacing && typeof this.ctx.letterSpacing !== "undefined") {
223
- this.ctx.letterSpacing = `${layer.letterSpacing}px`;
224
- }
225
- this.ensureHighQualityRendering();
226
- const baseX = this.calculateTextX(layer.textAlign);
227
- const baseY = this.calculateTextY(layer.verticalAlign, fontSize);
228
- const x = Math.round(baseX) + 0.5;
229
- const y = Math.round(baseY) + 0.5;
230
- if (layer.shadow) {
231
- this.ctx.shadowColor = layer.shadow.color;
232
- this.ctx.shadowOffsetX = layer.shadow.offsetX;
233
- this.ctx.shadowOffsetY = layer.shadow.offsetY;
234
- this.ctx.shadowBlur = layer.shadow.blur;
235
- }
236
- if (layer.strokeColor && layer.strokeWidth && layer.strokeWidth > 0) {
237
- this.drawEnhancedStroke(layer.text, x, y, layer.strokeColor, layer.strokeWidth);
238
- }
239
- this.ctx.fillText(layer.text, x, y);
240
- if (layer.shadow) {
241
- this.ctx.shadowColor = "transparent";
242
- this.ctx.shadowOffsetX = 0;
243
- this.ctx.shadowOffsetY = 0;
244
- this.ctx.shadowBlur = 0;
245
- }
246
- }
247
- /**
248
- * Draw enhanced multi-layer stroke for better text visibility
249
- */
250
- drawEnhancedStroke(text, x, y, strokeColor, strokeWidth) {
251
- this.ctx.save();
252
- this.ctx.strokeStyle = strokeColor;
253
- this.ctx.lineJoin = "round";
254
- this.ctx.lineCap = "round";
255
- this.ctx.miterLimit = 2;
256
- const layers = [1.1, 1];
257
- layers.forEach((multiplier) => {
258
- this.ctx.lineWidth = strokeWidth * multiplier;
259
- this.ctx.strokeText(text, x, y);
260
- });
261
- this.ctx.restore();
262
- }
263
- calculateTextX(align) {
264
- switch (align) {
265
- case "center":
266
- return this.width / 2;
267
- case "right":
268
- return this.width;
269
- default:
270
- return 0;
992
+ const animationType = layer.animation?.type;
993
+ const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;
994
+ const needsWordTimings = ["wordByWord", "characterKTV", "wordByWordFancy"].includes(
995
+ animationType || ""
996
+ );
997
+ if (needsWordTimings && !hasWordTimings) {
998
+ renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);
999
+ return;
271
1000
  }
272
- }
273
- calculateTextY(align, fontSize = 16) {
274
- switch (align) {
275
- case "middle":
276
- return this.height / 2;
277
- case "bottom":
278
- return this.height * 0.85;
1001
+ switch (animationType) {
1002
+ case "wordByWord":
1003
+ renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
1004
+ break;
1005
+ case "characterKTV":
1006
+ renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
1007
+ break;
1008
+ case "wordByWordFancy":
1009
+ renderWordByWordFancy(
1010
+ this.ctx,
1011
+ layer,
1012
+ this.width,
1013
+ this.height,
1014
+ this.currentFrame,
1015
+ this.fps
1016
+ );
1017
+ break;
1018
+ case "fade":
1019
+ renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);
1020
+ break;
279
1021
  default:
280
- return fontSize;
1022
+ renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);
1023
+ break;
281
1024
  }
282
1025
  }
283
1026
  applyMask(mask) {
@@ -731,8 +1474,14 @@ class VideoComposer {
731
1474
  }
732
1475
  this.ctx = ctx;
733
1476
  this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
1477
+ this.loadFonts();
734
1478
  this.ctx.imageSmoothingQuality = "high";
735
- this.layerRenderer = new LayerRenderer(ctx, this.config.width, this.config.height);
1479
+ this.layerRenderer = new LayerRenderer(
1480
+ ctx,
1481
+ this.config.width,
1482
+ this.config.height,
1483
+ this.config.fps
1484
+ );
736
1485
  this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);
737
1486
  this.filterProcessor = new FilterProcessor();
738
1487
  this.timelineContext = this.config.timeline;
@@ -756,7 +1505,8 @@ class VideoComposer {
756
1505
  clipStartUs: 0,
757
1506
  clipDurationUs: Infinity,
758
1507
  compositionFps: 30
759
- }
1508
+ },
1509
+ fonts: config.fonts ?? []
760
1510
  };
761
1511
  }
762
1512
  createStreams(_instruction) {
@@ -795,6 +1545,9 @@ class VideoComposer {
795
1545
  throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);
796
1546
  }
797
1547
  this.clearCanvas();
1548
+ const frameDurationUs = 1e6 / this.config.fps;
1549
+ const relativeFrame = Math.floor(request.timeUs / frameDurationUs);
1550
+ this.layerRenderer.setCurrentFrame(relativeFrame);
798
1551
  if (request.transition) {
799
1552
  this.ctx.save();
800
1553
  this.transitionProcessor.applyTransition(this.ctx, request.transition);
@@ -870,6 +1623,22 @@ class VideoComposer {
870
1623
  });
871
1624
  return frame;
872
1625
  }
1626
+ async loadFonts() {
1627
+ if (!this.config.fonts || this.config.fonts.length === 0) {
1628
+ return;
1629
+ }
1630
+ for (const font of this.config.fonts) {
1631
+ try {
1632
+ const fontFace = new FontFace(font.family, `url(${font.url})`);
1633
+ await fontFace.load();
1634
+ if ("fonts" in self) {
1635
+ self.fonts.add(fontFace);
1636
+ }
1637
+ } catch (error) {
1638
+ console.warn(`[VideoComposer] Failed to load font ${font.family}:`, error);
1639
+ }
1640
+ }
1641
+ }
873
1642
  updateConfig(config) {
874
1643
  Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
875
1644
  if (config.width || config.height) {
@@ -884,6 +1653,9 @@ class VideoComposer {
884
1653
  if (config.timeline) {
885
1654
  this.timelineContext = config.timeline;
886
1655
  }
1656
+ if (config.fonts) {
1657
+ this.loadFonts();
1658
+ }
887
1659
  }
888
1660
  dispose() {
889
1661
  this.filterProcessor.clearCache();
@@ -1001,15 +1773,11 @@ function materializeLayer(layer, frame, imageMap, globalTimeUs) {
1001
1773
  ...baseLayer,
1002
1774
  type: "text",
1003
1775
  text: payload.text,
1004
- fontFamily: payload.fontFamily,
1005
- fontSize: payload.fontSize,
1006
- fontWeight: payload.fontWeight,
1007
- color: payload.color,
1008
- strokeColor: payload.strokeColor,
1009
- strokeWidth: payload.strokeWidth,
1010
- lineHeight: payload.lineHeight,
1011
- textAlign: payload.align,
1012
- verticalAlign: "bottom"
1776
+ localeCode: payload.localeCode,
1777
+ fontConfig: payload.fontConfig,
1778
+ animation: payload.animation,
1779
+ wordTimings: payload.wordTimings,
1780
+ letterCase: payload.letterCase
1013
1781
  };
1014
1782
  }
1015
1783
  if (layer.type === "image") {