@meframe/core 0.0.18 → 0.0.20

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 (87) hide show
  1. package/dist/Meframe.d.ts.map +1 -1
  2. package/dist/Meframe.js +2 -2
  3. package/dist/Meframe.js.map +1 -1
  4. package/dist/config/types.d.ts +4 -0
  5. package/dist/config/types.d.ts.map +1 -1
  6. package/dist/controllers/PreRenderService.d.ts +1 -4
  7. package/dist/controllers/PreRenderService.d.ts.map +1 -1
  8. package/dist/controllers/PreRenderService.js +7 -40
  9. package/dist/controllers/PreRenderService.js.map +1 -1
  10. package/dist/controllers/types.d.ts +0 -4
  11. package/dist/controllers/types.d.ts.map +1 -1
  12. package/dist/model/CompositionModel.d.ts.map +1 -1
  13. package/dist/model/CompositionModel.js +2 -0
  14. package/dist/model/CompositionModel.js.map +1 -1
  15. package/dist/model/patch.js +1 -25
  16. package/dist/model/patch.js.map +1 -1
  17. package/dist/model/types.d.ts +18 -0
  18. package/dist/model/types.d.ts.map +1 -1
  19. package/dist/model/types.js.map +1 -1
  20. package/dist/orchestrator/CompositionPlanner.d.ts +1 -0
  21. package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
  22. package/dist/orchestrator/CompositionPlanner.js +36 -10
  23. package/dist/orchestrator/CompositionPlanner.js.map +1 -1
  24. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  25. package/dist/orchestrator/GlobalAudioSession.js +0 -1
  26. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  27. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  28. package/dist/orchestrator/Orchestrator.js +2 -1
  29. package/dist/orchestrator/Orchestrator.js.map +1 -1
  30. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  31. package/dist/orchestrator/VideoClipSession.js +0 -2
  32. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  33. package/dist/stages/compose/LayerRenderer.d.ts +4 -7
  34. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  35. package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
  36. package/dist/stages/compose/OfflineAudioMixer.js +1 -9
  37. package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
  38. package/dist/stages/compose/VideoComposer.d.ts +1 -0
  39. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  40. package/dist/stages/compose/font-system/FontManager.d.ts +11 -0
  41. package/dist/stages/compose/font-system/FontManager.d.ts.map +1 -0
  42. package/dist/stages/compose/font-system/FontManager.js +69 -0
  43. package/dist/stages/compose/font-system/FontManager.js.map +1 -0
  44. package/dist/stages/compose/font-system/font-templates.d.ts +12 -0
  45. package/dist/stages/compose/font-system/font-templates.d.ts.map +1 -0
  46. package/dist/stages/compose/font-system/font-templates.js +384 -0
  47. package/dist/stages/compose/font-system/font-templates.js.map +1 -0
  48. package/dist/stages/compose/font-system/index.d.ts +5 -0
  49. package/dist/stages/compose/font-system/index.d.ts.map +1 -0
  50. package/dist/stages/compose/font-system/types.d.ts +60 -0
  51. package/dist/stages/compose/font-system/types.d.ts.map +1 -0
  52. package/dist/stages/compose/instructions.d.ts +50 -0
  53. package/dist/stages/compose/instructions.d.ts.map +1 -1
  54. package/dist/stages/compose/text-renderers/animation-utils.d.ts +16 -0
  55. package/dist/stages/compose/text-renderers/animation-utils.d.ts.map +1 -0
  56. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +5 -0
  57. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -0
  58. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +4 -0
  59. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -0
  60. package/dist/stages/compose/text-renderers/index.d.ts +6 -0
  61. package/dist/stages/compose/text-renderers/index.d.ts.map +1 -0
  62. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +4 -0
  63. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -0
  64. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +4 -0
  65. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -0
  66. package/dist/stages/compose/text-utils/index.d.ts +4 -0
  67. package/dist/stages/compose/text-utils/index.d.ts.map +1 -0
  68. package/dist/stages/compose/text-utils/locale-detector.d.ts +5 -0
  69. package/dist/stages/compose/text-utils/locale-detector.d.ts.map +1 -0
  70. package/dist/stages/compose/text-utils/text-metrics.d.ts +3 -0
  71. package/dist/stages/compose/text-utils/text-metrics.d.ts.map +1 -0
  72. package/dist/stages/compose/text-utils/text-wrapper.d.ts +4 -0
  73. package/dist/stages/compose/text-utils/text-wrapper.d.ts.map +1 -0
  74. package/dist/stages/compose/types.d.ts +51 -1
  75. package/dist/stages/compose/types.d.ts.map +1 -1
  76. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  77. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  78. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  79. package/dist/stages/mux/MuxManager.js +10 -12
  80. package/dist/stages/mux/MuxManager.js.map +1 -1
  81. package/dist/workers/MP4Demuxer.js +8 -1
  82. package/dist/workers/MP4Demuxer.js.map +1 -1
  83. package/dist/workers/stages/compose/video-compose.worker.js +838 -78
  84. package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
  85. package/package.json +1 -1
  86. package/dist/stages/demux/aac-esds-extractor.d.ts +0 -7
  87. package/dist/stages/demux/aac-esds-extractor.d.ts.map +0 -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,30 @@ 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;
271
- }
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;
992
+ const animationType = layer.animation?.type;
993
+ switch (animationType) {
994
+ case "wordByWord":
995
+ renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
996
+ break;
997
+ case "characterKTV":
998
+ renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);
999
+ break;
1000
+ case "wordByWordFancy":
1001
+ renderWordByWordFancy(
1002
+ this.ctx,
1003
+ layer,
1004
+ this.width,
1005
+ this.height,
1006
+ this.currentFrame,
1007
+ this.fps
1008
+ );
1009
+ break;
1010
+ case "fade":
1011
+ renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);
1012
+ break;
279
1013
  default:
280
- return fontSize;
1014
+ renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);
1015
+ break;
281
1016
  }
282
1017
  }
283
1018
  applyMask(mask) {
@@ -731,8 +1466,14 @@ class VideoComposer {
731
1466
  }
732
1467
  this.ctx = ctx;
733
1468
  this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
1469
+ this.loadFonts();
734
1470
  this.ctx.imageSmoothingQuality = "high";
735
- this.layerRenderer = new LayerRenderer(ctx, this.config.width, this.config.height);
1471
+ this.layerRenderer = new LayerRenderer(
1472
+ ctx,
1473
+ this.config.width,
1474
+ this.config.height,
1475
+ this.config.fps
1476
+ );
736
1477
  this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);
737
1478
  this.filterProcessor = new FilterProcessor();
738
1479
  this.timelineContext = this.config.timeline;
@@ -756,7 +1497,8 @@ class VideoComposer {
756
1497
  clipStartUs: 0,
757
1498
  clipDurationUs: Infinity,
758
1499
  compositionFps: 30
759
- }
1500
+ },
1501
+ fonts: config.fonts ?? []
760
1502
  };
761
1503
  }
762
1504
  createStreams(_instruction) {
@@ -795,6 +1537,9 @@ class VideoComposer {
795
1537
  throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);
796
1538
  }
797
1539
  this.clearCanvas();
1540
+ const frameDurationUs = 1e6 / this.config.fps;
1541
+ const relativeFrame = Math.floor(request.timeUs / frameDurationUs);
1542
+ this.layerRenderer.setCurrentFrame(relativeFrame);
798
1543
  if (request.transition) {
799
1544
  this.ctx.save();
800
1545
  this.transitionProcessor.applyTransition(this.ctx, request.transition);
@@ -870,6 +1615,22 @@ class VideoComposer {
870
1615
  });
871
1616
  return frame;
872
1617
  }
1618
+ async loadFonts() {
1619
+ if (!this.config.fonts || this.config.fonts.length === 0) {
1620
+ return;
1621
+ }
1622
+ for (const font of this.config.fonts) {
1623
+ try {
1624
+ const fontFace = new FontFace(font.family, `url(${font.url})`);
1625
+ await fontFace.load();
1626
+ if ("fonts" in self) {
1627
+ self.fonts.add(fontFace);
1628
+ }
1629
+ } catch (error) {
1630
+ console.warn(`[VideoComposer] Failed to load font ${font.family}:`, error);
1631
+ }
1632
+ }
1633
+ }
873
1634
  updateConfig(config) {
874
1635
  Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
875
1636
  if (config.width || config.height) {
@@ -884,6 +1645,9 @@ class VideoComposer {
884
1645
  if (config.timeline) {
885
1646
  this.timelineContext = config.timeline;
886
1647
  }
1648
+ if (config.fonts) {
1649
+ this.loadFonts();
1650
+ }
887
1651
  }
888
1652
  dispose() {
889
1653
  this.filterProcessor.clearCache();
@@ -1001,15 +1765,11 @@ function materializeLayer(layer, frame, imageMap, globalTimeUs) {
1001
1765
  ...baseLayer,
1002
1766
  type: "text",
1003
1767
  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"
1768
+ localeCode: payload.localeCode,
1769
+ fontConfig: payload.fontConfig,
1770
+ animation: payload.animation,
1771
+ wordTimings: payload.wordTimings,
1772
+ letterCase: payload.letterCase
1013
1773
  };
1014
1774
  }
1015
1775
  if (layer.type === "image") {