ugcinc-render 1.8.0 → 1.8.2

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.
@@ -0,0 +1,1136 @@
1
+ // src/utils/fonts.ts
2
+ var APPLE_EMOJI_FONT = '"Apple Color Emoji"';
3
+ var FONT_FAMILIES = {
4
+ tiktok: `"TikTok Sans", ${APPLE_EMOJI_FONT}, sans-serif`,
5
+ apple: `"SF Pro", "SF Pro Display", -apple-system, BlinkMacSystemFont, ${APPLE_EMOJI_FONT}, sans-serif`,
6
+ arial: `Arial, ${APPLE_EMOJI_FONT}, sans-serif`
7
+ };
8
+ function getFontFamily(fontType) {
9
+ return FONT_FAMILIES[fontType] ?? FONT_FAMILIES.arial;
10
+ }
11
+ function buildFontString({
12
+ fontType,
13
+ fontSize,
14
+ fontWeight
15
+ }) {
16
+ const fontFamily = getFontFamily(fontType);
17
+ return `${fontWeight} ${fontSize}px ${fontFamily}`;
18
+ }
19
+ var FONT_URLS = {
20
+ tiktok: {
21
+ regular: "/TikTokSans-Regular.ttf",
22
+ bold: "/TikTokSans-Bold.ttf"
23
+ },
24
+ apple: {
25
+ regular: "/SF-Pro.ttf"
26
+ },
27
+ emoji: {
28
+ apple: "/AppleColorEmoji.ttf"
29
+ }
30
+ };
31
+ async function preloadFonts() {
32
+ console.log("[ugcinc-render/fonts] preloadFonts() called");
33
+ if (typeof document !== "undefined") {
34
+ console.log("[ugcinc-render/fonts] Browser environment detected, loading fonts...");
35
+ const fontResults = [];
36
+ try {
37
+ const tiktokRegular = new FontFace(
38
+ "TikTok Sans",
39
+ `url(${FONT_URLS.tiktok.regular})`,
40
+ { weight: "normal" }
41
+ );
42
+ console.log(`[ugcinc-render/fonts] Loading TikTok Sans Regular from: ${FONT_URLS.tiktok.regular}`);
43
+ await tiktokRegular.load();
44
+ document.fonts.add(tiktokRegular);
45
+ fontResults.push({ name: "TikTok Sans Regular", status: "success" });
46
+ } catch (err) {
47
+ const errorMsg = err instanceof Error ? err.message : String(err);
48
+ console.error(`[ugcinc-render/fonts] Failed to load TikTok Sans Regular:`, errorMsg);
49
+ fontResults.push({ name: "TikTok Sans Regular", status: "error", error: errorMsg });
50
+ }
51
+ try {
52
+ const tiktokBold = new FontFace(
53
+ "TikTok Sans",
54
+ `url(${FONT_URLS.tiktok.bold})`,
55
+ { weight: "bold" }
56
+ );
57
+ console.log(`[ugcinc-render/fonts] Loading TikTok Sans Bold from: ${FONT_URLS.tiktok.bold}`);
58
+ await tiktokBold.load();
59
+ document.fonts.add(tiktokBold);
60
+ fontResults.push({ name: "TikTok Sans Bold", status: "success" });
61
+ } catch (err) {
62
+ const errorMsg = err instanceof Error ? err.message : String(err);
63
+ console.error(`[ugcinc-render/fonts] Failed to load TikTok Sans Bold:`, errorMsg);
64
+ fontResults.push({ name: "TikTok Sans Bold", status: "error", error: errorMsg });
65
+ }
66
+ try {
67
+ const sfPro = new FontFace(
68
+ "SF Pro",
69
+ `url(${FONT_URLS.apple.regular})`,
70
+ { weight: "normal" }
71
+ );
72
+ console.log(`[ugcinc-render/fonts] Loading SF Pro from: ${FONT_URLS.apple.regular}`);
73
+ await sfPro.load();
74
+ document.fonts.add(sfPro);
75
+ fontResults.push({ name: "SF Pro", status: "success" });
76
+ } catch (err) {
77
+ const errorMsg = err instanceof Error ? err.message : String(err);
78
+ console.error(`[ugcinc-render/fonts] Failed to load SF Pro:`, errorMsg);
79
+ fontResults.push({ name: "SF Pro", status: "error", error: errorMsg });
80
+ }
81
+ try {
82
+ const appleEmoji = new FontFace(
83
+ "Apple Color Emoji",
84
+ `url(${FONT_URLS.emoji.apple})`,
85
+ { weight: "normal" }
86
+ );
87
+ console.log(`[ugcinc-render/fonts] Loading Apple Color Emoji from: ${FONT_URLS.emoji.apple}`);
88
+ await appleEmoji.load();
89
+ document.fonts.add(appleEmoji);
90
+ fontResults.push({ name: "Apple Color Emoji", status: "success" });
91
+ } catch (err) {
92
+ const errorMsg = err instanceof Error ? err.message : String(err);
93
+ console.error(`[ugcinc-render/fonts] Failed to load Apple Color Emoji:`, errorMsg);
94
+ fontResults.push({ name: "Apple Color Emoji", status: "error", error: errorMsg });
95
+ }
96
+ console.log("[ugcinc-render/fonts] Font loading complete. Results:", fontResults);
97
+ const availableFonts = [];
98
+ document.fonts.forEach((font) => {
99
+ availableFonts.push(`${font.family} (${font.weight}, ${font.status})`);
100
+ });
101
+ console.log("[ugcinc-render/fonts] Available fonts in document:", availableFonts);
102
+ } else {
103
+ console.log("[ugcinc-render/fonts] Not in browser environment, skipping font loading");
104
+ }
105
+ }
106
+ function areFontsLoaded() {
107
+ if (typeof document === "undefined") {
108
+ console.log("[ugcinc-render/fonts] areFontsLoaded: Not in browser environment");
109
+ return false;
110
+ }
111
+ const tiktokRegular = document.fonts.check('normal 16px "TikTok Sans"');
112
+ const tiktokBold = document.fonts.check('bold 16px "TikTok Sans"');
113
+ const sfPro = document.fonts.check('normal 16px "SF Pro"');
114
+ const appleEmoji = document.fonts.check('normal 16px "Apple Color Emoji"');
115
+ console.log("[ugcinc-render/fonts] areFontsLoaded check:", {
116
+ tiktokRegular,
117
+ tiktokBold,
118
+ sfPro,
119
+ appleEmoji
120
+ });
121
+ return tiktokRegular && tiktokBold && sfPro && appleEmoji;
122
+ }
123
+ function debugFontStatus() {
124
+ console.log("[ugcinc-render/fonts] === FONT DEBUG STATUS ===");
125
+ if (typeof document === "undefined") {
126
+ console.log("[ugcinc-render/fonts] Not in browser environment");
127
+ return;
128
+ }
129
+ console.log("[ugcinc-render/fonts] Font checks:");
130
+ console.log(" - TikTok Sans (normal):", document.fonts.check('normal 16px "TikTok Sans"'));
131
+ console.log(" - TikTok Sans (bold):", document.fonts.check('bold 16px "TikTok Sans"'));
132
+ console.log(" - SF Pro:", document.fonts.check('normal 16px "SF Pro"'));
133
+ console.log(" - Apple Color Emoji:", document.fonts.check('normal 16px "Apple Color Emoji"'));
134
+ console.log("[ugcinc-render/fonts] All fonts in document.fonts:");
135
+ document.fonts.forEach((font) => {
136
+ console.log(` - ${font.family}: weight=${font.weight}, style=${font.style}, status=${font.status}`);
137
+ });
138
+ console.log("[ugcinc-render/fonts] Font family stacks:");
139
+ console.log(" - tiktok:", FONT_FAMILIES.tiktok);
140
+ console.log(" - apple:", FONT_FAMILIES.apple);
141
+ console.log(" - arial:", FONT_FAMILIES.arial);
142
+ console.log("[ugcinc-render/fonts] === END FONT DEBUG ===");
143
+ }
144
+
145
+ // src/utils/fit.ts
146
+ function calculateFitDimensions({
147
+ sourceWidth,
148
+ sourceHeight,
149
+ targetWidth,
150
+ targetHeight,
151
+ fit
152
+ }) {
153
+ if (fit === "fill") {
154
+ return {
155
+ x: 0,
156
+ y: 0,
157
+ width: targetWidth,
158
+ height: targetHeight,
159
+ sourceX: 0,
160
+ sourceY: 0,
161
+ sourceWidth,
162
+ sourceHeight
163
+ };
164
+ }
165
+ const sourceRatio = sourceWidth / sourceHeight;
166
+ const targetRatio = targetWidth / targetHeight;
167
+ let width = targetWidth;
168
+ let height = targetHeight;
169
+ let x = 0;
170
+ let y = 0;
171
+ let sourceX = 0;
172
+ let sourceY = 0;
173
+ let croppedSourceWidth = sourceWidth;
174
+ let croppedSourceHeight = sourceHeight;
175
+ if (fit === "contain") {
176
+ if (sourceRatio > targetRatio) {
177
+ height = targetWidth / sourceRatio;
178
+ y = (targetHeight - height) / 2;
179
+ } else {
180
+ width = targetHeight * sourceRatio;
181
+ x = (targetWidth - width) / 2;
182
+ }
183
+ } else if (fit === "cover") {
184
+ if (sourceRatio > targetRatio) {
185
+ croppedSourceWidth = sourceHeight * targetRatio;
186
+ sourceX = (sourceWidth - croppedSourceWidth) / 2;
187
+ } else {
188
+ croppedSourceHeight = sourceWidth / targetRatio;
189
+ sourceY = (sourceHeight - croppedSourceHeight) / 2;
190
+ }
191
+ }
192
+ return {
193
+ x,
194
+ y,
195
+ width,
196
+ height,
197
+ sourceX,
198
+ sourceY,
199
+ sourceWidth: croppedSourceWidth,
200
+ sourceHeight: croppedSourceHeight
201
+ };
202
+ }
203
+
204
+ // src/utils/defaults.ts
205
+ var TEXT_DEFAULTS = {
206
+ fontType: "arial",
207
+ fontSize: 40,
208
+ fontWeight: "normal",
209
+ color: "#000000",
210
+ alignment: "left",
211
+ verticalAlign: "top",
212
+ direction: "ltr",
213
+ lineHeight: 1.2,
214
+ letterSpacing: 0,
215
+ padding: 0,
216
+ textWrap: "word",
217
+ wordBreak: "normal",
218
+ hyphenation: "none",
219
+ maxLines: 0,
220
+ textOverflow: "clip",
221
+ ellipsis: "...",
222
+ strokeWidth: 0,
223
+ backgroundOpacity: 100,
224
+ autoWidth: false,
225
+ boxAlign: "left"
226
+ };
227
+ var IMAGE_DEFAULTS = {
228
+ fit: "cover",
229
+ opacity: 100,
230
+ loop: false,
231
+ speed: 1
232
+ };
233
+ var VIDEO_DEFAULTS = {
234
+ ...IMAGE_DEFAULTS,
235
+ volume: 100
236
+ };
237
+ var VISUAL_DEFAULTS = {
238
+ xOffset: 0,
239
+ yOffset: 0,
240
+ zIndex: 0,
241
+ scale: 1,
242
+ rotation: 0,
243
+ opacity: 100
244
+ };
245
+ function applyTextDefaults(segment) {
246
+ return {
247
+ ...TEXT_DEFAULTS,
248
+ ...segment
249
+ };
250
+ }
251
+ function applyImageDefaults(segment) {
252
+ return {
253
+ ...IMAGE_DEFAULTS,
254
+ ...segment
255
+ };
256
+ }
257
+ function applyVideoDefaults(segment) {
258
+ return {
259
+ ...VIDEO_DEFAULTS,
260
+ ...segment
261
+ };
262
+ }
263
+
264
+ // src/utils/text.ts
265
+ function wrapText({
266
+ text,
267
+ maxWidth,
268
+ letterSpacing = 0,
269
+ textWrap = "word",
270
+ maxLines = 0,
271
+ measureText
272
+ }) {
273
+ const lines = [];
274
+ if (textWrap === "none") {
275
+ lines.push(text);
276
+ return lines;
277
+ }
278
+ const words = textWrap === "word" ? text.split(" ") : text.split("");
279
+ let currentLine = "";
280
+ for (let i = 0; i < words.length; i++) {
281
+ const word = words[i];
282
+ if (!word) continue;
283
+ const testLine = currentLine + (currentLine ? textWrap === "word" ? " " : "" : "") + word;
284
+ const charCount = [...testLine].length;
285
+ const extraSpacing = charCount > 1 ? (charCount - 1) * letterSpacing : 0;
286
+ const totalWidth = measureText(testLine) + extraSpacing;
287
+ if (totalWidth > maxWidth && currentLine) {
288
+ lines.push(currentLine);
289
+ currentLine = word;
290
+ if (maxLines && lines.length >= maxLines) {
291
+ break;
292
+ }
293
+ } else {
294
+ currentLine = testLine;
295
+ }
296
+ }
297
+ if (currentLine && (!maxLines || lines.length < maxLines)) {
298
+ lines.push(currentLine);
299
+ }
300
+ if (maxLines && lines.length > maxLines) {
301
+ lines.splice(maxLines);
302
+ }
303
+ return lines;
304
+ }
305
+ function calculateLineWidth({
306
+ line,
307
+ letterSpacing,
308
+ measureText
309
+ }) {
310
+ const chars = [...line];
311
+ let width = 0;
312
+ for (const char of chars) {
313
+ width += measureText(char) + letterSpacing;
314
+ }
315
+ if (chars.length > 0) {
316
+ width -= letterSpacing;
317
+ }
318
+ return width;
319
+ }
320
+ function getBorderRadii(borderRadius) {
321
+ if (!borderRadius) return null;
322
+ if (typeof borderRadius === "number") {
323
+ if (borderRadius <= 0) return null;
324
+ return [borderRadius, borderRadius, borderRadius, borderRadius];
325
+ }
326
+ const topLeft = borderRadius.topLeft ?? 0;
327
+ const topRight = borderRadius.topRight ?? 0;
328
+ const bottomRight = borderRadius.bottomRight ?? 0;
329
+ const bottomLeft = borderRadius.bottomLeft ?? 0;
330
+ if (topLeft <= 0 && topRight <= 0 && bottomRight <= 0 && bottomLeft <= 0) {
331
+ return null;
332
+ }
333
+ return [topLeft, topRight, bottomRight, bottomLeft];
334
+ }
335
+ function parseHexColor(hex) {
336
+ const cleanHex = hex.replace("#", "");
337
+ return {
338
+ r: parseInt(cleanHex.substring(0, 2), 16),
339
+ g: parseInt(cleanHex.substring(2, 4), 16),
340
+ b: parseInt(cleanHex.substring(4, 6), 16)
341
+ };
342
+ }
343
+ function hexToRgba(hex, opacity = 100) {
344
+ const { r, g, b } = parseHexColor(hex);
345
+ return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
346
+ }
347
+
348
+ // src/utils/positionResolver.ts
349
+ var FONT_FAMILIES2 = {
350
+ tiktok: '"TikTok Sans", sans-serif',
351
+ apple: '"SF Pro", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif',
352
+ arial: "Arial, sans-serif"
353
+ };
354
+ function calculateAutoWidthDimensions(elem, textContent, ctx) {
355
+ if (elem.type !== "text") {
356
+ return null;
357
+ }
358
+ let measureCtx = ctx;
359
+ if (!measureCtx) {
360
+ if (typeof document === "undefined") {
361
+ return null;
362
+ }
363
+ const canvas = document.createElement("canvas");
364
+ measureCtx = canvas.getContext("2d");
365
+ if (!measureCtx) return null;
366
+ }
367
+ const fontSize = elem.fontSize ?? 40;
368
+ const fontWeight = elem.fontWeight ?? "bold";
369
+ const fontType = elem.font ?? "tiktok";
370
+ const paddingLeft = elem.paddingLeft ?? 0;
371
+ const paddingRight = elem.paddingRight ?? 0;
372
+ const paddingTop = elem.paddingTop ?? 0;
373
+ const paddingBottom = elem.paddingBottom ?? 0;
374
+ const letterSpacing = elem.letterSpacing ?? 0;
375
+ const lineHeight = elem.lineHeight ?? 1.2;
376
+ const maxWidth = elem.width;
377
+ const boxAlign = elem.boxAlign ?? "left";
378
+ const autoWidth = elem.autoWidth ?? false;
379
+ const fontFamily = FONT_FAMILIES2[fontType] ?? FONT_FAMILIES2.tiktok;
380
+ measureCtx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
381
+ const availableWidth = maxWidth - paddingLeft - paddingRight;
382
+ const words = textContent.split(" ");
383
+ const lines = [];
384
+ let currentLine = "";
385
+ for (const word of words) {
386
+ if (!word) continue;
387
+ const testLine = currentLine + (currentLine ? " " : "") + word;
388
+ const charCount = [...testLine].length;
389
+ const extraSpacing = charCount > 1 ? (charCount - 1) * letterSpacing : 0;
390
+ const totalWidth = measureCtx.measureText(testLine).width + extraSpacing;
391
+ if (totalWidth > availableWidth && currentLine) {
392
+ lines.push(currentLine);
393
+ currentLine = word;
394
+ } else {
395
+ currentLine = testLine;
396
+ }
397
+ }
398
+ if (currentLine) lines.push(currentLine);
399
+ if (lines.length === 0) lines.push("");
400
+ let widestLineWidth = 0;
401
+ for (const line of lines) {
402
+ const chars = [...line];
403
+ let lineWidth = 0;
404
+ for (const char of chars) {
405
+ lineWidth += measureCtx.measureText(char).width + letterSpacing;
406
+ }
407
+ if (chars.length > 0) lineWidth -= letterSpacing;
408
+ widestLineWidth = Math.max(widestLineWidth, lineWidth);
409
+ }
410
+ const actualWidth = autoWidth ? Math.min(widestLineWidth + paddingLeft + paddingRight, maxWidth) : maxWidth;
411
+ const textHeight = lines.length * fontSize * lineHeight;
412
+ const actualHeight = textHeight + paddingTop + paddingBottom;
413
+ let actualX;
414
+ if (autoWidth) {
415
+ switch (boxAlign) {
416
+ case "right":
417
+ actualX = elem.x + maxWidth - actualWidth;
418
+ break;
419
+ case "center":
420
+ actualX = elem.x + (maxWidth - actualWidth) / 2;
421
+ break;
422
+ case "left":
423
+ default:
424
+ actualX = elem.x;
425
+ break;
426
+ }
427
+ } else {
428
+ actualX = elem.x;
429
+ }
430
+ return { actualWidth, actualX, actualHeight, lineCount: lines.length };
431
+ }
432
+ function detectCircularDependency(elements, startId, axis, visited = /* @__PURE__ */ new Set(), path = []) {
433
+ if (visited.has(startId)) {
434
+ const cycleStartIndex = path.indexOf(startId);
435
+ return path.slice(cycleStartIndex).concat(startId);
436
+ }
437
+ const element = elements.find((e) => e.id === startId);
438
+ const relConfig = axis === "x" ? element?.relativePositionX : element?.relativePositionY;
439
+ if (!relConfig) {
440
+ return null;
441
+ }
442
+ visited.add(startId);
443
+ path.push(startId);
444
+ const result = detectCircularDependency(
445
+ elements,
446
+ relConfig.elementId,
447
+ axis,
448
+ visited,
449
+ path
450
+ );
451
+ visited.delete(startId);
452
+ path.pop();
453
+ return result;
454
+ }
455
+ function topologicalSortForAxis(elements, axis) {
456
+ const errors = [];
457
+ const elementMap = new Map(elements.map((e) => [e.id, e]));
458
+ const dependents = /* @__PURE__ */ new Map();
459
+ const dependencyCount = /* @__PURE__ */ new Map();
460
+ for (const elem of elements) {
461
+ dependents.set(elem.id, []);
462
+ dependencyCount.set(elem.id, 0);
463
+ }
464
+ for (const elem of elements) {
465
+ const relConfig = axis === "x" ? elem.relativePositionX : elem.relativePositionY;
466
+ if (relConfig) {
467
+ const refId = relConfig.elementId;
468
+ if (!elementMap.has(refId)) {
469
+ errors.push({
470
+ elementId: elem.id,
471
+ type: "missing_reference",
472
+ message: `Element references non-existent element "${refId}" for ${axis.toUpperCase()} position`,
473
+ axis
474
+ });
475
+ continue;
476
+ }
477
+ const cycle = detectCircularDependency(elements, elem.id, axis);
478
+ if (cycle) {
479
+ errors.push({
480
+ elementId: elem.id,
481
+ type: "circular_dependency",
482
+ message: `Circular dependency detected for ${axis.toUpperCase()}: ${cycle.join(" \u2192 ")}`,
483
+ axis,
484
+ cyclePath: cycle
485
+ });
486
+ continue;
487
+ }
488
+ dependents.get(refId)?.push(elem.id);
489
+ dependencyCount.set(elem.id, (dependencyCount.get(elem.id) ?? 0) + 1);
490
+ }
491
+ }
492
+ const sorted = [];
493
+ const queue = [];
494
+ for (const [id, count] of dependencyCount) {
495
+ if (count === 0) {
496
+ queue.push(id);
497
+ }
498
+ }
499
+ while (queue.length > 0) {
500
+ const currentId = queue.shift();
501
+ const current = elementMap.get(currentId);
502
+ if (current) {
503
+ sorted.push(current);
504
+ }
505
+ for (const dependentId of dependents.get(currentId) ?? []) {
506
+ const newCount = (dependencyCount.get(dependentId) ?? 1) - 1;
507
+ dependencyCount.set(dependentId, newCount);
508
+ if (newCount === 0) {
509
+ queue.push(dependentId);
510
+ }
511
+ }
512
+ }
513
+ for (const elem of elements) {
514
+ if (!sorted.find((e) => e.id === elem.id)) {
515
+ sorted.push(elem);
516
+ }
517
+ }
518
+ return { sorted, errors };
519
+ }
520
+ function getSelfAnchorOffsetX(element, selfAnchor, actualWidth) {
521
+ const width = actualWidth ?? element.width;
522
+ switch (selfAnchor) {
523
+ case "left":
524
+ return 0;
525
+ case "center":
526
+ return width / 2;
527
+ case "right":
528
+ return width;
529
+ default:
530
+ return 0;
531
+ }
532
+ }
533
+ function getSelfAnchorOffsetY(element, selfAnchor, actualHeight) {
534
+ const height = actualHeight ?? element.height;
535
+ switch (selfAnchor) {
536
+ case "top":
537
+ return 0;
538
+ case "middle":
539
+ return height / 2;
540
+ case "bottom":
541
+ return height;
542
+ default:
543
+ return 0;
544
+ }
545
+ }
546
+ function calculateAbsoluteX(element, referenceElement, anchor, selfAnchor, offset, actualWidth) {
547
+ const refX = anchor === "left" ? referenceElement.x : referenceElement.x + referenceElement.width;
548
+ const selfOffset = getSelfAnchorOffsetX(element, selfAnchor, actualWidth);
549
+ return refX + offset - selfOffset;
550
+ }
551
+ function calculateAbsoluteY(element, referenceElement, anchor, selfAnchor, offset, actualHeight) {
552
+ const refY = anchor === "top" ? referenceElement.y : referenceElement.y + referenceElement.height;
553
+ const selfOffset = getSelfAnchorOffsetY(element, selfAnchor, actualHeight);
554
+ return refY + offset - selfOffset;
555
+ }
556
+ function resolveElementPositions(elements, textValues) {
557
+ if (elements.length === 0) {
558
+ return { elements: [], errors: [] };
559
+ }
560
+ const allErrors = [];
561
+ const { sorted: sortedX, errors: errorsX } = topologicalSortForAxis(elements, "x");
562
+ allErrors.push(...errorsX);
563
+ const { sorted: sortedY, errors: errorsY } = topologicalSortForAxis(elements, "y");
564
+ allErrors.push(...errorsY);
565
+ const resolvedX = /* @__PURE__ */ new Map();
566
+ const resolvedY = /* @__PURE__ */ new Map();
567
+ let measureCtx = null;
568
+ const hasAutoWidthElements = elements.some((e) => e.type === "text" && e.autoWidth);
569
+ if (hasAutoWidthElements && typeof document !== "undefined") {
570
+ const canvas = document.createElement("canvas");
571
+ measureCtx = canvas.getContext("2d");
572
+ }
573
+ const getTextContent = (elem) => {
574
+ if (elem.type !== "text") return "";
575
+ if (elem.textInputId && textValues?.[elem.textInputId]) {
576
+ return textValues[elem.textInputId];
577
+ }
578
+ return elem.text ?? "Text";
579
+ };
580
+ const referenceX = /* @__PURE__ */ new Map();
581
+ const referenceY = /* @__PURE__ */ new Map();
582
+ for (const elem of elements) {
583
+ resolvedX.set(elem.id, { x: elem.x, width: elem.width });
584
+ resolvedY.set(elem.id, { y: elem.y, height: elem.height });
585
+ if (elem.type === "text") {
586
+ const textContent = getTextContent(elem);
587
+ const autoResult = calculateAutoWidthDimensions(elem, textContent, measureCtx);
588
+ if (autoResult) {
589
+ if (elem.autoWidth) {
590
+ referenceX.set(elem.id, { x: autoResult.actualX, width: autoResult.actualWidth });
591
+ } else {
592
+ referenceX.set(elem.id, { x: elem.x, width: elem.width });
593
+ }
594
+ referenceY.set(elem.id, { y: elem.y, height: autoResult.actualHeight });
595
+ } else {
596
+ referenceX.set(elem.id, { x: elem.x, width: elem.width });
597
+ referenceY.set(elem.id, { y: elem.y, height: elem.height });
598
+ }
599
+ } else {
600
+ referenceX.set(elem.id, { x: elem.x, width: elem.width });
601
+ referenceY.set(elem.id, { y: elem.y, height: elem.height });
602
+ }
603
+ }
604
+ for (const elem of sortedX) {
605
+ if (elem.relativePositionX) {
606
+ const refId = elem.relativePositionX.elementId;
607
+ const refPosition = referenceX.get(refId);
608
+ if (refPosition) {
609
+ const defaultSelfAnchor = elem.relativePositionX.anchor === "right" ? "left" : "right";
610
+ const selfAnchor = elem.relativePositionX.selfAnchor ?? defaultSelfAnchor;
611
+ let actualWidth;
612
+ let autoResult = null;
613
+ if (elem.type === "text") {
614
+ const textContent = getTextContent(elem);
615
+ autoResult = calculateAutoWidthDimensions(elem, textContent, measureCtx);
616
+ if (autoResult && elem.autoWidth) {
617
+ actualWidth = autoResult.actualWidth;
618
+ }
619
+ }
620
+ const newX = calculateAbsoluteX(
621
+ elem,
622
+ refPosition,
623
+ elem.relativePositionX.anchor,
624
+ selfAnchor,
625
+ elem.relativePositionX.offset,
626
+ actualWidth
627
+ // Use actual width for self anchor calculation
628
+ );
629
+ resolvedX.set(elem.id, { x: newX, width: elem.width });
630
+ if (autoResult) {
631
+ if (elem.autoWidth) {
632
+ const updatedResult = calculateAutoWidthDimensions({ ...elem, x: newX }, getTextContent(elem), measureCtx);
633
+ if (updatedResult) {
634
+ referenceX.set(elem.id, { x: updatedResult.actualX, width: updatedResult.actualWidth });
635
+ referenceY.set(elem.id, { y: referenceY.get(elem.id)?.y ?? elem.y, height: updatedResult.actualHeight });
636
+ } else {
637
+ referenceX.set(elem.id, { x: newX, width: elem.width });
638
+ }
639
+ } else {
640
+ referenceX.set(elem.id, { x: newX, width: elem.width });
641
+ referenceY.set(elem.id, { y: referenceY.get(elem.id)?.y ?? elem.y, height: autoResult.actualHeight });
642
+ }
643
+ } else {
644
+ referenceX.set(elem.id, { x: newX, width: elem.width });
645
+ }
646
+ }
647
+ }
648
+ }
649
+ for (const elem of sortedY) {
650
+ if (elem.relativePositionY) {
651
+ const refId = elem.relativePositionY.elementId;
652
+ const refPosition = referenceY.get(refId);
653
+ if (refPosition) {
654
+ const defaultSelfAnchor = elem.relativePositionY.anchor === "bottom" ? "top" : "bottom";
655
+ const selfAnchor = elem.relativePositionY.selfAnchor ?? defaultSelfAnchor;
656
+ let actualHeight;
657
+ if (elem.type === "text") {
658
+ const textContent = getTextContent(elem);
659
+ const autoResult = calculateAutoWidthDimensions(elem, textContent, measureCtx);
660
+ if (autoResult) {
661
+ actualHeight = autoResult.actualHeight;
662
+ }
663
+ }
664
+ const newY = calculateAbsoluteY(
665
+ elem,
666
+ refPosition,
667
+ elem.relativePositionY.anchor,
668
+ selfAnchor,
669
+ elem.relativePositionY.offset,
670
+ actualHeight
671
+ // Use actual height for self anchor calculation
672
+ );
673
+ resolvedY.set(elem.id, { y: newY, height: elem.height });
674
+ if (actualHeight !== void 0) {
675
+ referenceY.set(elem.id, { y: newY, height: actualHeight });
676
+ } else {
677
+ referenceY.set(elem.id, { y: newY, height: elem.height });
678
+ }
679
+ }
680
+ }
681
+ }
682
+ const resolvedElements = elements.map((elem) => ({
683
+ ...elem,
684
+ x: resolvedX.get(elem.id)?.x ?? elem.x,
685
+ y: resolvedY.get(elem.id)?.y ?? elem.y
686
+ }));
687
+ return { elements: resolvedElements, errors: allErrors };
688
+ }
689
+ function canSetAsReference(elements, elementId, proposedReferenceId, axis) {
690
+ if (elementId === proposedReferenceId) {
691
+ return false;
692
+ }
693
+ const testElements = elements.map((e) => {
694
+ if (e.id !== elementId) return e;
695
+ if (axis === "x") {
696
+ return {
697
+ ...e,
698
+ relativePositionX: { elementId: proposedReferenceId, anchor: "right", offset: 0 }
699
+ };
700
+ } else {
701
+ return {
702
+ ...e,
703
+ relativePositionY: { elementId: proposedReferenceId, anchor: "bottom", offset: 0 }
704
+ };
705
+ }
706
+ });
707
+ const cycle = detectCircularDependency(testElements, elementId, axis);
708
+ return cycle === null;
709
+ }
710
+ function getDependentElements(elements, elementId) {
711
+ const dependents = [];
712
+ const visited = /* @__PURE__ */ new Set();
713
+ function findDependents(id) {
714
+ for (const elem of elements) {
715
+ const dependsOnX = elem.relativePositionX?.elementId === id;
716
+ const dependsOnY = elem.relativePositionY?.elementId === id;
717
+ if ((dependsOnX || dependsOnY) && !visited.has(elem.id)) {
718
+ visited.add(elem.id);
719
+ dependents.push(elem.id);
720
+ findDependents(elem.id);
721
+ }
722
+ }
723
+ }
724
+ findDependents(elementId);
725
+ return dependents;
726
+ }
727
+ function getReferenceElementX(elements, elementId) {
728
+ const element = elements.find((e) => e.id === elementId);
729
+ if (!element?.relativePositionX) {
730
+ return null;
731
+ }
732
+ return elements.find((e) => e.id === element.relativePositionX.elementId) ?? null;
733
+ }
734
+ function getReferenceElementY(elements, elementId) {
735
+ const element = elements.find((e) => e.id === elementId);
736
+ if (!element?.relativePositionY) {
737
+ return null;
738
+ }
739
+ return elements.find((e) => e.id === element.relativePositionY.elementId) ?? null;
740
+ }
741
+
742
+ // src/utils/cropBounds.ts
743
+ function getActualElementHeight(elem, textValues, measureCtx) {
744
+ if (elem.type !== "text") return elem.height;
745
+ let textContent = elem.text ?? "Text";
746
+ if (elem.textInputId && textValues?.[elem.textInputId]) {
747
+ textContent = textValues[elem.textInputId];
748
+ }
749
+ const result = calculateAutoWidthDimensions(elem, textContent, measureCtx);
750
+ return result?.actualHeight ?? elem.height;
751
+ }
752
+ function calculateCropBounds(elements, dynamicCrop, canvasWidth, canvasHeight, textValues) {
753
+ if (!dynamicCrop) {
754
+ return { x: 0, y: 0, width: canvasWidth, height: canvasHeight };
755
+ }
756
+ const elementMap = /* @__PURE__ */ new Map();
757
+ for (const elem of elements) {
758
+ elementMap.set(elem.id, elem);
759
+ }
760
+ const resolveBoundary = (boundary) => {
761
+ if (!boundary) return void 0;
762
+ if (boundary.elementId) return boundary.elementId;
763
+ return void 0;
764
+ };
765
+ let measureCtx = null;
766
+ const hasTextElements = elements.some((e) => e.type === "text");
767
+ if (hasTextElements && typeof document !== "undefined") {
768
+ const canvas = document.createElement("canvas");
769
+ measureCtx = canvas.getContext("2d");
770
+ }
771
+ let cropY = 0;
772
+ let cropHeight = canvasHeight;
773
+ if (dynamicCrop.vertical?.enabled) {
774
+ const vCrop = dynamicCrop.vertical;
775
+ const paddingStart = vCrop.paddingStart ?? 0;
776
+ const paddingEnd = vCrop.paddingEnd ?? 0;
777
+ if (vCrop.mode === "all-elements") {
778
+ let minY = canvasHeight;
779
+ let maxY = 0;
780
+ for (const elem of elements) {
781
+ const actualHeight = getActualElementHeight(elem, textValues, measureCtx);
782
+ minY = Math.min(minY, elem.y);
783
+ maxY = Math.max(maxY, elem.y + actualHeight);
784
+ }
785
+ if (elements.length > 0) {
786
+ cropY = Math.max(0, minY - paddingStart);
787
+ const bottomY = Math.min(canvasHeight, maxY + paddingEnd);
788
+ cropHeight = bottomY - cropY;
789
+ }
790
+ } else if (vCrop.mode === "between-elements") {
791
+ const startElementId = resolveBoundary(vCrop.startBoundary);
792
+ const endElementId = resolveBoundary(vCrop.endBoundary);
793
+ let topY = 0;
794
+ let bottomY = canvasHeight;
795
+ if (startElementId) {
796
+ const startElem = elementMap.get(startElementId);
797
+ if (startElem) {
798
+ topY = startElem.y;
799
+ }
800
+ }
801
+ if (endElementId) {
802
+ const endElem = elementMap.get(endElementId);
803
+ if (endElem) {
804
+ const actualHeight = getActualElementHeight(endElem, textValues, measureCtx);
805
+ bottomY = endElem.y + actualHeight;
806
+ }
807
+ }
808
+ cropY = Math.max(0, topY - paddingStart);
809
+ const adjustedBottom = Math.min(canvasHeight, bottomY + paddingEnd);
810
+ cropHeight = adjustedBottom - cropY;
811
+ }
812
+ if (vCrop.minSize && cropHeight < vCrop.minSize) {
813
+ cropHeight = vCrop.minSize;
814
+ }
815
+ }
816
+ let cropX = 0;
817
+ let cropWidth = canvasWidth;
818
+ if (dynamicCrop.horizontal?.enabled) {
819
+ const hCrop = dynamicCrop.horizontal;
820
+ const paddingStart = hCrop.paddingStart ?? 0;
821
+ const paddingEnd = hCrop.paddingEnd ?? 0;
822
+ if (hCrop.mode === "all-elements") {
823
+ let minX = canvasWidth;
824
+ let maxX = 0;
825
+ for (const elem of elements) {
826
+ minX = Math.min(minX, elem.x);
827
+ maxX = Math.max(maxX, elem.x + elem.width);
828
+ }
829
+ if (elements.length > 0) {
830
+ cropX = Math.max(0, minX - paddingStart);
831
+ const rightX = Math.min(canvasWidth, maxX + paddingEnd);
832
+ cropWidth = rightX - cropX;
833
+ }
834
+ } else if (hCrop.mode === "between-elements") {
835
+ const startElementId = resolveBoundary(hCrop.startBoundary);
836
+ const endElementId = resolveBoundary(hCrop.endBoundary);
837
+ let leftX = 0;
838
+ let rightX = canvasWidth;
839
+ if (startElementId) {
840
+ const startElem = elementMap.get(startElementId);
841
+ if (startElem) {
842
+ leftX = startElem.x;
843
+ }
844
+ }
845
+ if (endElementId) {
846
+ const endElem = elementMap.get(endElementId);
847
+ if (endElem) {
848
+ rightX = endElem.x + endElem.width;
849
+ }
850
+ }
851
+ cropX = Math.max(0, leftX - paddingStart);
852
+ const adjustedRight = Math.min(canvasWidth, rightX + paddingEnd);
853
+ cropWidth = adjustedRight - cropX;
854
+ }
855
+ if (hCrop.minSize && cropWidth < hCrop.minSize) {
856
+ cropWidth = hCrop.minSize;
857
+ }
858
+ }
859
+ return { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
860
+ }
861
+ function isDynamicCropEnabled(dynamicCrop) {
862
+ if (!dynamicCrop) return false;
863
+ return !!(dynamicCrop.vertical?.enabled || dynamicCrop.horizontal?.enabled);
864
+ }
865
+
866
+ // src/utils/timeline.ts
867
+ function defaultOffset(mode = "flexible") {
868
+ return mode === "flexible" ? { type: "relative", value: 0 } : { type: "absolute", value: 0 };
869
+ }
870
+ function getBaseSegments(channel) {
871
+ return channel.segments.filter((s) => s.parentId === void 0);
872
+ }
873
+ function getOverlays(channel, parentId) {
874
+ return channel.segments.filter((s) => s.parentId === (parentId ?? void 0));
875
+ }
876
+ function getSegmentTimelinePosition(segment, baseSegments, channel) {
877
+ if (segment.parentId) {
878
+ const parent = channel.segments.find((s) => s.id === segment.parentId);
879
+ if (parent) {
880
+ const parentPos = getSegmentTimelinePosition(parent, baseSegments, channel);
881
+ const relStart = segment.relativeStart ?? 0;
882
+ const relEnd = segment.relativeEnd ?? 1;
883
+ return {
884
+ startMs: parentPos.startMs + parentPos.durationMs * relStart,
885
+ durationMs: parentPos.durationMs * (relEnd - relStart)
886
+ };
887
+ }
888
+ }
889
+ const baseIndex = baseSegments.findIndex((s) => s.id === segment.id);
890
+ let accumulatedTime = 0;
891
+ for (let i = 0; i < baseIndex; i++) {
892
+ const prev = baseSegments[i];
893
+ if (prev) {
894
+ accumulatedTime += prev.duration?.type === "absolute" ? prev.duration.value : 5e3;
895
+ }
896
+ }
897
+ const startMs = segment.offset.type === "absolute" ? segment.offset.value : accumulatedTime;
898
+ const durationMs = segment.duration?.type === "absolute" ? segment.duration.value : 5e3;
899
+ return { startMs, durationMs };
900
+ }
901
+ function isSegmentVisibleAtTime(segment, time, channel) {
902
+ const baseSegments = getBaseSegments(channel);
903
+ const { startMs, durationMs } = getSegmentTimelinePosition(segment, baseSegments, channel);
904
+ const endMs = startMs + durationMs;
905
+ return time >= startMs && time < endMs;
906
+ }
907
+ function calculateEstimatedDuration(channels) {
908
+ let maxDuration = 5e3;
909
+ for (const channel of channels) {
910
+ let channelTime = 0;
911
+ for (const segment of channel.segments) {
912
+ if (segment.parentId) continue;
913
+ if (segment.offset.type === "absolute") {
914
+ channelTime = segment.offset.value;
915
+ } else {
916
+ channelTime += 5e3;
917
+ }
918
+ if (segment.duration?.type === "absolute") {
919
+ channelTime += segment.duration.value;
920
+ } else {
921
+ channelTime += 5e3;
922
+ }
923
+ }
924
+ maxDuration = Math.max(maxDuration, channelTime);
925
+ }
926
+ return maxDuration;
927
+ }
928
+ function calculateTimelineContentEnd(channel) {
929
+ const baseSegments = getBaseSegments(channel);
930
+ let lastEnd = 0;
931
+ for (const segment of baseSegments) {
932
+ const { startMs, durationMs } = getSegmentTimelinePosition(segment, baseSegments, channel);
933
+ lastEnd = Math.max(lastEnd, startMs + durationMs);
934
+ }
935
+ return Math.ceil((lastEnd + 2e3) / 1e3) * 1e3;
936
+ }
937
+ function formatTime(ms) {
938
+ const totalSeconds = Math.floor(ms / 1e3);
939
+ const minutes = Math.floor(totalSeconds / 60);
940
+ const seconds = totalSeconds % 60;
941
+ const milliseconds = Math.floor(ms % 1e3 / 10);
942
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${milliseconds.toString().padStart(2, "0")}`;
943
+ }
944
+ function parseTime(timeStr) {
945
+ const parts = timeStr.split(":");
946
+ if (parts.length !== 2) return 0;
947
+ const [minStr, secPart] = parts;
948
+ const minutes = parseInt(minStr ?? "0", 10) || 0;
949
+ const secParts = (secPart ?? "0").split(".");
950
+ const seconds = parseInt(secParts[0] ?? "0", 10) || 0;
951
+ const ms = parseInt((secParts[1] ?? "0").padEnd(2, "0").slice(0, 2), 10) * 10 || 0;
952
+ return (minutes * 60 + seconds) * 1e3 + ms;
953
+ }
954
+ function generateSegmentId() {
955
+ return `segment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
956
+ }
957
+ function generateOverlayId() {
958
+ return `overlay-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
959
+ }
960
+
961
+ // src/utils/captionPresets.ts
962
+ var CAPTION_PRESETS = {
963
+ /**
964
+ * Hormozi style - bold, attention-grabbing captions
965
+ * Yellow highlight on white text with thick black stroke
966
+ */
967
+ hormozi: {
968
+ fontName: "Montserrat",
969
+ fontSize: 80,
970
+ fontWeight: "black",
971
+ fontColor: "#FFFFFF",
972
+ highlightColor: "#FFFF00",
973
+ strokeColor: "#000000",
974
+ strokeWidth: 3,
975
+ position: "bottom",
976
+ yOffset: 100,
977
+ maxWidth: 90,
978
+ enableAnimation: true,
979
+ wordsPerPage: 3
980
+ },
981
+ /**
982
+ * Minimal style - clean and understated
983
+ * White on white (no highlight distinction), smaller text
984
+ */
985
+ minimal: {
986
+ fontName: "Inter",
987
+ fontSize: 60,
988
+ fontWeight: "bold",
989
+ fontColor: "#FFFFFF",
990
+ highlightColor: "#FFFFFF",
991
+ strokeColor: "#000000",
992
+ strokeWidth: 2,
993
+ position: "bottom",
994
+ yOffset: 80,
995
+ maxWidth: 85,
996
+ enableAnimation: false,
997
+ wordsPerPage: 5
998
+ },
999
+ /**
1000
+ * Bold Pop style - maximum impact, one word at a time
1001
+ * Yellow text with red highlight, centered position
1002
+ */
1003
+ "bold-pop": {
1004
+ fontName: "Bangers",
1005
+ fontSize: 90,
1006
+ fontWeight: "bold",
1007
+ fontColor: "#FFFF00",
1008
+ highlightColor: "#FF0000",
1009
+ strokeColor: "#000000",
1010
+ strokeWidth: 4,
1011
+ position: "center",
1012
+ yOffset: 0,
1013
+ maxWidth: 80,
1014
+ enableAnimation: true,
1015
+ wordsPerPage: 2
1016
+ },
1017
+ /**
1018
+ * Clean style - professional look with background
1019
+ * White text with blue highlight, semi-transparent background
1020
+ */
1021
+ clean: {
1022
+ fontName: "Roboto",
1023
+ fontSize: 55,
1024
+ fontWeight: "normal",
1025
+ fontColor: "#FFFFFF",
1026
+ highlightColor: "#3B82F6",
1027
+ strokeColor: "#000000",
1028
+ strokeWidth: 0,
1029
+ backgroundColor: "#000000",
1030
+ backgroundOpacity: 0.7,
1031
+ backgroundBorderRadius: 8,
1032
+ position: "bottom",
1033
+ yOffset: 80,
1034
+ maxWidth: 90,
1035
+ enableAnimation: false,
1036
+ wordsPerPage: 6
1037
+ },
1038
+ /**
1039
+ * Neon style - vibrant, eye-catching colors
1040
+ * Cyan text with magenta highlight
1041
+ */
1042
+ neon: {
1043
+ fontName: "Poppins",
1044
+ fontSize: 70,
1045
+ fontWeight: "bold",
1046
+ fontColor: "#00FFFF",
1047
+ highlightColor: "#FF00FF",
1048
+ strokeColor: "#000000",
1049
+ strokeWidth: 3,
1050
+ position: "bottom",
1051
+ yOffset: 100,
1052
+ maxWidth: 85,
1053
+ enableAnimation: true,
1054
+ wordsPerPage: 3
1055
+ }
1056
+ };
1057
+ var DEFAULT_CAPTION_STYLE = CAPTION_PRESETS.hormozi;
1058
+ function resolveCaptionStyle(style) {
1059
+ if (!style) {
1060
+ return { ...DEFAULT_CAPTION_STYLE };
1061
+ }
1062
+ const presetValues = style.preset ? CAPTION_PRESETS[style.preset] : {};
1063
+ const defaults = DEFAULT_CAPTION_STYLE;
1064
+ return {
1065
+ preset: style.preset,
1066
+ fontName: style.fontName ?? presetValues.fontName ?? defaults.fontName,
1067
+ fontSize: style.fontSize ?? presetValues.fontSize ?? defaults.fontSize,
1068
+ fontWeight: style.fontWeight ?? presetValues.fontWeight ?? defaults.fontWeight,
1069
+ fontColor: style.fontColor ?? presetValues.fontColor ?? defaults.fontColor,
1070
+ highlightColor: style.highlightColor ?? presetValues.highlightColor ?? defaults.highlightColor,
1071
+ strokeColor: style.strokeColor ?? presetValues.strokeColor ?? defaults.strokeColor,
1072
+ strokeWidth: style.strokeWidth ?? presetValues.strokeWidth ?? defaults.strokeWidth,
1073
+ backgroundColor: style.backgroundColor ?? presetValues.backgroundColor,
1074
+ backgroundOpacity: style.backgroundOpacity ?? presetValues.backgroundOpacity,
1075
+ backgroundBorderRadius: style.backgroundBorderRadius ?? presetValues.backgroundBorderRadius,
1076
+ position: style.position ?? presetValues.position ?? defaults.position,
1077
+ yOffset: style.yOffset ?? presetValues.yOffset ?? defaults.yOffset,
1078
+ maxWidth: style.maxWidth ?? presetValues.maxWidth ?? defaults.maxWidth,
1079
+ enableAnimation: style.enableAnimation ?? presetValues.enableAnimation ?? defaults.enableAnimation,
1080
+ wordsPerPage: style.wordsPerPage ?? presetValues.wordsPerPage ?? defaults.wordsPerPage
1081
+ };
1082
+ }
1083
+ function getCaptionPresetNames() {
1084
+ return Object.keys(CAPTION_PRESETS);
1085
+ }
1086
+ function isValidCaptionPreset(name) {
1087
+ return name in CAPTION_PRESETS;
1088
+ }
1089
+
1090
+ export {
1091
+ TEXT_DEFAULTS,
1092
+ IMAGE_DEFAULTS,
1093
+ VIDEO_DEFAULTS,
1094
+ VISUAL_DEFAULTS,
1095
+ applyTextDefaults,
1096
+ applyImageDefaults,
1097
+ applyVideoDefaults,
1098
+ APPLE_EMOJI_FONT,
1099
+ FONT_FAMILIES,
1100
+ getFontFamily,
1101
+ buildFontString,
1102
+ FONT_URLS,
1103
+ preloadFonts,
1104
+ areFontsLoaded,
1105
+ debugFontStatus,
1106
+ wrapText,
1107
+ calculateLineWidth,
1108
+ getBorderRadii,
1109
+ parseHexColor,
1110
+ hexToRgba,
1111
+ calculateAutoWidthDimensions,
1112
+ resolveElementPositions,
1113
+ canSetAsReference,
1114
+ getDependentElements,
1115
+ getReferenceElementX,
1116
+ getReferenceElementY,
1117
+ calculateCropBounds,
1118
+ isDynamicCropEnabled,
1119
+ calculateFitDimensions,
1120
+ defaultOffset,
1121
+ getBaseSegments,
1122
+ getOverlays,
1123
+ getSegmentTimelinePosition,
1124
+ isSegmentVisibleAtTime,
1125
+ calculateEstimatedDuration,
1126
+ calculateTimelineContentEnd,
1127
+ formatTime,
1128
+ parseTime,
1129
+ generateSegmentId,
1130
+ generateOverlayId,
1131
+ CAPTION_PRESETS,
1132
+ DEFAULT_CAPTION_STYLE,
1133
+ resolveCaptionStyle,
1134
+ getCaptionPresetNames,
1135
+ isValidCaptionPreset
1136
+ };