ugcinc-render 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1425 @@
1
+ // src/types/element.ts
2
+ var DIMENSION_PRESETS = {
3
+ "tiktok": { width: 1080, height: 1920, label: "TikTok/Reels (1080\xD71920)", ratio: "9:16" },
4
+ "instagram-square": { width: 1080, height: 1080, label: "Instagram Square (1080\xD71080)", ratio: "1:1" },
5
+ "instagram-post": { width: 1080, height: 1350, label: "Instagram Post (1080\xD71350)", ratio: "4:5" },
6
+ "youtube": { width: 1280, height: 720, label: "YouTube Thumbnail (1280\xD7720)", ratio: "16:9" },
7
+ "twitter": { width: 1200, height: 675, label: "Twitter/X (1200\xD7675)", ratio: "16:9" }
8
+ };
9
+
10
+ // src/compositions/ImageEditorComposition.tsx
11
+ import { useMemo as useMemo3 } from "react";
12
+ import { AbsoluteFill, Img as Img2 } from "remotion";
13
+
14
+ // src/components/TextElement.tsx
15
+ import { useMemo } from "react";
16
+
17
+ // src/utils/defaults.ts
18
+ var TEXT_DEFAULTS = {
19
+ fontType: "arial",
20
+ fontSize: 40,
21
+ fontWeight: "normal",
22
+ color: "#000000",
23
+ alignment: "left",
24
+ verticalAlign: "top",
25
+ direction: "ltr",
26
+ lineHeight: 1.2,
27
+ letterSpacing: 0,
28
+ padding: 0,
29
+ textWrap: "word",
30
+ wordBreak: "normal",
31
+ hyphenation: "none",
32
+ maxLines: 0,
33
+ textOverflow: "clip",
34
+ ellipsis: "...",
35
+ strokeWidth: 0,
36
+ backgroundOpacity: 100,
37
+ autoWidth: false,
38
+ boxAlign: "left"
39
+ };
40
+ var IMAGE_DEFAULTS = {
41
+ fit: "cover",
42
+ opacity: 100,
43
+ loop: false,
44
+ speed: 1
45
+ };
46
+ var VIDEO_DEFAULTS = {
47
+ ...IMAGE_DEFAULTS,
48
+ volume: 100
49
+ };
50
+ var VISUAL_DEFAULTS = {
51
+ xOffset: 0,
52
+ yOffset: 0,
53
+ zIndex: 0,
54
+ scale: 1,
55
+ rotation: 0,
56
+ opacity: 100
57
+ };
58
+ function applyTextDefaults(segment) {
59
+ return {
60
+ ...TEXT_DEFAULTS,
61
+ ...segment
62
+ };
63
+ }
64
+ function applyImageDefaults(segment) {
65
+ return {
66
+ ...IMAGE_DEFAULTS,
67
+ ...segment
68
+ };
69
+ }
70
+ function applyVideoDefaults(segment) {
71
+ return {
72
+ ...VIDEO_DEFAULTS,
73
+ ...segment
74
+ };
75
+ }
76
+
77
+ // src/utils/fonts.ts
78
+ var FONT_FAMILIES = {
79
+ tiktok: '"TikTok Sans", sans-serif',
80
+ apple: '"SF Pro", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif',
81
+ arial: "Arial, sans-serif"
82
+ };
83
+ function getFontFamily(fontType) {
84
+ return FONT_FAMILIES[fontType] ?? FONT_FAMILIES.arial;
85
+ }
86
+ function buildFontString({
87
+ fontType,
88
+ fontSize,
89
+ fontWeight
90
+ }) {
91
+ const fontFamily = getFontFamily(fontType);
92
+ return `${fontWeight} ${fontSize}px ${fontFamily}`;
93
+ }
94
+ var FONT_URLS = {
95
+ tiktok: {
96
+ regular: "/TikTokSans-Regular.ttf",
97
+ bold: "/TikTokSans-Bold.ttf"
98
+ },
99
+ apple: {
100
+ regular: "/SF-Pro.ttf"
101
+ }
102
+ };
103
+ async function preloadFonts() {
104
+ if (typeof document !== "undefined") {
105
+ const fontPromises = [];
106
+ const tiktokRegular = new FontFace(
107
+ "TikTok Sans",
108
+ `url(${FONT_URLS.tiktok.regular})`,
109
+ { weight: "normal" }
110
+ );
111
+ fontPromises.push(tiktokRegular.load());
112
+ const tiktokBold = new FontFace(
113
+ "TikTok Sans",
114
+ `url(${FONT_URLS.tiktok.bold})`,
115
+ { weight: "bold" }
116
+ );
117
+ fontPromises.push(tiktokBold.load());
118
+ const sfPro = new FontFace(
119
+ "SF Pro",
120
+ `url(${FONT_URLS.apple.regular})`,
121
+ { weight: "normal" }
122
+ );
123
+ fontPromises.push(sfPro.load());
124
+ const loadedFonts = await Promise.all(fontPromises);
125
+ loadedFonts.forEach((font) => {
126
+ document.fonts.add(font);
127
+ });
128
+ }
129
+ }
130
+ function areFontsLoaded() {
131
+ if (typeof document === "undefined") {
132
+ return false;
133
+ }
134
+ return document.fonts.check('normal 16px "TikTok Sans"') && document.fonts.check('bold 16px "TikTok Sans"') && document.fonts.check('normal 16px "SF Pro"');
135
+ }
136
+
137
+ // src/utils/text.ts
138
+ function wrapText({
139
+ text,
140
+ maxWidth,
141
+ letterSpacing = 0,
142
+ textWrap = "word",
143
+ maxLines = 0,
144
+ measureText
145
+ }) {
146
+ const lines = [];
147
+ if (textWrap === "none") {
148
+ lines.push(text);
149
+ return lines;
150
+ }
151
+ const words = textWrap === "word" ? text.split(" ") : text.split("");
152
+ let currentLine = "";
153
+ for (let i = 0; i < words.length; i++) {
154
+ const word = words[i];
155
+ if (!word) continue;
156
+ const testLine = currentLine + (currentLine ? textWrap === "word" ? " " : "" : "") + word;
157
+ const charCount = [...testLine].length;
158
+ const extraSpacing = charCount > 1 ? (charCount - 1) * letterSpacing : 0;
159
+ const totalWidth = measureText(testLine) + extraSpacing;
160
+ if (totalWidth > maxWidth && currentLine) {
161
+ lines.push(currentLine);
162
+ currentLine = word;
163
+ if (maxLines && lines.length >= maxLines) {
164
+ break;
165
+ }
166
+ } else {
167
+ currentLine = testLine;
168
+ }
169
+ }
170
+ if (currentLine && (!maxLines || lines.length < maxLines)) {
171
+ lines.push(currentLine);
172
+ }
173
+ if (maxLines && lines.length > maxLines) {
174
+ lines.splice(maxLines);
175
+ }
176
+ return lines;
177
+ }
178
+ function calculateLineWidth({
179
+ line,
180
+ letterSpacing,
181
+ measureText
182
+ }) {
183
+ const chars = [...line];
184
+ let width = 0;
185
+ for (const char of chars) {
186
+ width += measureText(char) + letterSpacing;
187
+ }
188
+ if (chars.length > 0) {
189
+ width -= letterSpacing;
190
+ }
191
+ return width;
192
+ }
193
+ function getBorderRadii(borderRadius) {
194
+ if (!borderRadius) return null;
195
+ if (typeof borderRadius === "number") {
196
+ if (borderRadius <= 0) return null;
197
+ return [borderRadius, borderRadius, borderRadius, borderRadius];
198
+ }
199
+ const topLeft = borderRadius.topLeft ?? 0;
200
+ const topRight = borderRadius.topRight ?? 0;
201
+ const bottomRight = borderRadius.bottomRight ?? 0;
202
+ const bottomLeft = borderRadius.bottomLeft ?? 0;
203
+ if (topLeft <= 0 && topRight <= 0 && bottomRight <= 0 && bottomLeft <= 0) {
204
+ return null;
205
+ }
206
+ return [topLeft, topRight, bottomRight, bottomLeft];
207
+ }
208
+ function parseHexColor(hex) {
209
+ const cleanHex = hex.replace("#", "");
210
+ return {
211
+ r: parseInt(cleanHex.substring(0, 2), 16),
212
+ g: parseInt(cleanHex.substring(2, 4), 16),
213
+ b: parseInt(cleanHex.substring(4, 6), 16)
214
+ };
215
+ }
216
+ function hexToRgba(hex, opacity = 100) {
217
+ const { r, g, b } = parseHexColor(hex);
218
+ return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
219
+ }
220
+
221
+ // src/components/TextElement.tsx
222
+ import { jsx } from "react/jsx-runtime";
223
+ function TextElement({ segment, scale = 1 }) {
224
+ const fontType = segment.fontType ?? TEXT_DEFAULTS.fontType;
225
+ const fontSize = (segment.fontSize ?? TEXT_DEFAULTS.fontSize) * scale;
226
+ const fontWeight = segment.fontWeight ?? TEXT_DEFAULTS.fontWeight;
227
+ const color = segment.color ?? TEXT_DEFAULTS.color;
228
+ const alignment = segment.alignment ?? TEXT_DEFAULTS.alignment;
229
+ const verticalAlign = segment.verticalAlign ?? TEXT_DEFAULTS.verticalAlign;
230
+ const lineHeight = segment.lineHeight ?? TEXT_DEFAULTS.lineHeight;
231
+ const letterSpacing = (segment.letterSpacing ?? TEXT_DEFAULTS.letterSpacing) * scale;
232
+ const strokeColor = segment.strokeColor;
233
+ const strokeWidth = (segment.strokeWidth ?? TEXT_DEFAULTS.strokeWidth) * scale;
234
+ const uniformPadding = (segment.padding ?? TEXT_DEFAULTS.padding) * scale;
235
+ const paddingTop = segment.paddingTop ?? uniformPadding;
236
+ const paddingRight = segment.paddingRight ?? uniformPadding;
237
+ const paddingBottom = segment.paddingBottom ?? uniformPadding;
238
+ const paddingLeft = segment.paddingLeft ?? uniformPadding;
239
+ const x = segment.xOffset * scale;
240
+ const y = segment.yOffset * scale;
241
+ const width = segment.width * scale;
242
+ const height = segment.height * scale;
243
+ const rotation = segment.rotation ?? 0;
244
+ const autoWidth = segment.autoWidth ?? TEXT_DEFAULTS.autoWidth;
245
+ const boxAlign = segment.boxAlign ?? TEXT_DEFAULTS.boxAlign;
246
+ const backgroundColor = segment.backgroundColor;
247
+ const backgroundOpacity = segment.backgroundOpacity ?? TEXT_DEFAULTS.backgroundOpacity;
248
+ const backgroundBorderRadius = segment.backgroundBorderRadius;
249
+ const fontFamily = getFontFamily(fontType);
250
+ const borderRadiusStyle = useMemo(() => {
251
+ if (!backgroundBorderRadius) return void 0;
252
+ const radii = getBorderRadii(backgroundBorderRadius);
253
+ if (!radii) return void 0;
254
+ return `${radii[0] * scale}px ${radii[1] * scale}px ${radii[2] * scale}px ${radii[3] * scale}px`;
255
+ }, [backgroundBorderRadius, scale]);
256
+ const containerStyle = useMemo(() => ({
257
+ position: "absolute",
258
+ left: x,
259
+ top: y,
260
+ width: autoWidth ? "auto" : width,
261
+ maxWidth: width,
262
+ height: autoWidth ? "auto" : height,
263
+ transform: rotation !== 0 ? `rotate(${rotation}deg)` : void 0,
264
+ transformOrigin: "center center",
265
+ display: "flex",
266
+ flexDirection: "column",
267
+ justifyContent: verticalAlign === "top" ? "flex-start" : verticalAlign === "bottom" ? "flex-end" : "center",
268
+ // Auto-width alignment
269
+ ...autoWidth && {
270
+ left: boxAlign === "center" ? x + width / 2 : boxAlign === "right" ? x + width : x,
271
+ transform: rotation !== 0 ? `translateX(${boxAlign === "center" ? "-50%" : boxAlign === "right" ? "-100%" : "0"}) rotate(${rotation}deg)` : boxAlign === "center" ? "translateX(-50%)" : boxAlign === "right" ? "translateX(-100%)" : void 0
272
+ }
273
+ }), [x, y, width, height, rotation, verticalAlign, autoWidth, boxAlign]);
274
+ const backgroundStyle = useMemo(() => {
275
+ if (!backgroundColor) return {};
276
+ return {
277
+ backgroundColor: hexToRgba(backgroundColor, backgroundOpacity),
278
+ borderRadius: borderRadiusStyle
279
+ };
280
+ }, [backgroundColor, backgroundOpacity, borderRadiusStyle]);
281
+ const textStyle = useMemo(() => ({
282
+ fontFamily,
283
+ fontSize,
284
+ fontWeight,
285
+ color,
286
+ lineHeight,
287
+ letterSpacing,
288
+ textAlign: alignment === "justify" ? "justify" : alignment,
289
+ padding: `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px`,
290
+ whiteSpace: "pre-wrap",
291
+ wordBreak: "break-word",
292
+ // Text stroke using text-shadow for cross-browser support
293
+ ...strokeWidth > 0 && strokeColor && {
294
+ textShadow: `
295
+ -${strokeWidth}px -${strokeWidth}px 0 ${strokeColor},
296
+ ${strokeWidth}px -${strokeWidth}px 0 ${strokeColor},
297
+ -${strokeWidth}px ${strokeWidth}px 0 ${strokeColor},
298
+ ${strokeWidth}px ${strokeWidth}px 0 ${strokeColor},
299
+ 0 -${strokeWidth}px 0 ${strokeColor},
300
+ 0 ${strokeWidth}px 0 ${strokeColor},
301
+ -${strokeWidth}px 0 0 ${strokeColor},
302
+ ${strokeWidth}px 0 0 ${strokeColor}
303
+ `,
304
+ // Also use WebKit text stroke for better quality where supported
305
+ WebkitTextStroke: `${strokeWidth}px ${strokeColor}`,
306
+ paintOrder: "stroke fill"
307
+ }
308
+ }), [
309
+ fontFamily,
310
+ fontSize,
311
+ fontWeight,
312
+ color,
313
+ lineHeight,
314
+ letterSpacing,
315
+ alignment,
316
+ paddingTop,
317
+ paddingRight,
318
+ paddingBottom,
319
+ paddingLeft,
320
+ strokeWidth,
321
+ strokeColor
322
+ ]);
323
+ return /* @__PURE__ */ jsx("div", { style: { ...containerStyle, ...backgroundStyle }, children: /* @__PURE__ */ jsx("div", { style: textStyle, children: segment.text }) });
324
+ }
325
+
326
+ // src/components/ImageElement.tsx
327
+ import { useMemo as useMemo2 } from "react";
328
+ import { Img } from "remotion";
329
+ import { jsx as jsx2 } from "react/jsx-runtime";
330
+ function fitModeToCss(fit) {
331
+ return fit;
332
+ }
333
+ function ImageElement({ segment, src, scale = 1 }) {
334
+ const fit = segment.fit ?? IMAGE_DEFAULTS.fit;
335
+ const opacity = (segment.opacity ?? VISUAL_DEFAULTS.opacity) / 100;
336
+ const rotation = segment.rotation ?? 0;
337
+ const borderRadius = segment.borderRadius;
338
+ const x = segment.xOffset * scale;
339
+ const y = segment.yOffset * scale;
340
+ const width = segment.width * scale;
341
+ const height = segment.height * scale;
342
+ const borderRadiusStyle = useMemo2(() => {
343
+ if (!borderRadius) return void 0;
344
+ if (typeof borderRadius === "number") {
345
+ return `${borderRadius * scale}px`;
346
+ }
347
+ const radii = getBorderRadii(borderRadius);
348
+ if (!radii) return void 0;
349
+ return `${radii[0] * scale}px ${radii[1] * scale}px ${radii[2] * scale}px ${radii[3] * scale}px`;
350
+ }, [borderRadius, scale]);
351
+ const containerStyle = useMemo2(() => ({
352
+ position: "absolute",
353
+ left: x,
354
+ top: y,
355
+ width,
356
+ height,
357
+ transform: rotation !== 0 ? `rotate(${rotation}deg)` : void 0,
358
+ transformOrigin: "center center",
359
+ overflow: "hidden",
360
+ borderRadius: borderRadiusStyle
361
+ }), [x, y, width, height, rotation, borderRadiusStyle]);
362
+ const imageStyle = useMemo2(() => ({
363
+ width: "100%",
364
+ height: "100%",
365
+ objectFit: fitModeToCss(fit),
366
+ opacity
367
+ }), [fit, opacity]);
368
+ return /* @__PURE__ */ jsx2("div", { style: containerStyle, children: /* @__PURE__ */ jsx2(
369
+ Img,
370
+ {
371
+ src,
372
+ style: imageStyle
373
+ }
374
+ ) });
375
+ }
376
+
377
+ // src/utils/positionResolver.ts
378
+ var FONT_FAMILIES2 = {
379
+ tiktok: '"TikTok Sans", sans-serif',
380
+ apple: '"SF Pro", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif',
381
+ arial: "Arial, sans-serif"
382
+ };
383
+ function calculateAutoWidthDimensions(elem, textContent, ctx) {
384
+ if (elem.type !== "text" || !elem.autoWidth) {
385
+ return null;
386
+ }
387
+ let measureCtx = ctx;
388
+ if (!measureCtx) {
389
+ if (typeof document === "undefined") {
390
+ return null;
391
+ }
392
+ const canvas = document.createElement("canvas");
393
+ measureCtx = canvas.getContext("2d");
394
+ if (!measureCtx) return null;
395
+ }
396
+ const fontSize = elem.fontSize ?? 40;
397
+ const fontWeight = elem.fontWeight ?? "bold";
398
+ const fontType = elem.font ?? "tiktok";
399
+ const paddingLeft = elem.paddingLeft ?? 0;
400
+ const paddingRight = elem.paddingRight ?? 0;
401
+ const letterSpacing = elem.letterSpacing ?? 0;
402
+ const maxWidth = elem.width;
403
+ const boxAlign = elem.boxAlign ?? "left";
404
+ const fontFamily = FONT_FAMILIES2[fontType] ?? FONT_FAMILIES2.tiktok;
405
+ measureCtx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
406
+ const availableWidth = maxWidth - paddingLeft - paddingRight;
407
+ const words = textContent.split(" ");
408
+ const lines = [];
409
+ let currentLine = "";
410
+ for (const word of words) {
411
+ if (!word) continue;
412
+ const testLine = currentLine + (currentLine ? " " : "") + word;
413
+ const charCount = [...testLine].length;
414
+ const extraSpacing = charCount > 1 ? (charCount - 1) * letterSpacing : 0;
415
+ const totalWidth = measureCtx.measureText(testLine).width + extraSpacing;
416
+ if (totalWidth > availableWidth && currentLine) {
417
+ lines.push(currentLine);
418
+ currentLine = word;
419
+ } else {
420
+ currentLine = testLine;
421
+ }
422
+ }
423
+ if (currentLine) lines.push(currentLine);
424
+ let widestLineWidth = 0;
425
+ for (const line of lines) {
426
+ const chars = [...line];
427
+ let lineWidth = 0;
428
+ for (const char of chars) {
429
+ lineWidth += measureCtx.measureText(char).width + letterSpacing;
430
+ }
431
+ if (chars.length > 0) lineWidth -= letterSpacing;
432
+ widestLineWidth = Math.max(widestLineWidth, lineWidth);
433
+ }
434
+ const actualWidth = Math.min(widestLineWidth + paddingLeft + paddingRight, maxWidth);
435
+ let actualX;
436
+ switch (boxAlign) {
437
+ case "right":
438
+ actualX = elem.x + maxWidth - actualWidth;
439
+ break;
440
+ case "center":
441
+ actualX = elem.x + (maxWidth - actualWidth) / 2;
442
+ break;
443
+ case "left":
444
+ default:
445
+ actualX = elem.x;
446
+ break;
447
+ }
448
+ return { actualWidth, actualX };
449
+ }
450
+ function detectCircularDependency(elements, startId, axis, visited = /* @__PURE__ */ new Set(), path = []) {
451
+ if (visited.has(startId)) {
452
+ const cycleStartIndex = path.indexOf(startId);
453
+ return path.slice(cycleStartIndex).concat(startId);
454
+ }
455
+ const element = elements.find((e) => e.id === startId);
456
+ const relConfig = axis === "x" ? element?.relativePositionX : element?.relativePositionY;
457
+ if (!relConfig) {
458
+ return null;
459
+ }
460
+ visited.add(startId);
461
+ path.push(startId);
462
+ const result = detectCircularDependency(
463
+ elements,
464
+ relConfig.elementId,
465
+ axis,
466
+ visited,
467
+ path
468
+ );
469
+ visited.delete(startId);
470
+ path.pop();
471
+ return result;
472
+ }
473
+ function topologicalSortForAxis(elements, axis) {
474
+ const errors = [];
475
+ const elementMap = new Map(elements.map((e) => [e.id, e]));
476
+ const dependents = /* @__PURE__ */ new Map();
477
+ const dependencyCount = /* @__PURE__ */ new Map();
478
+ for (const elem of elements) {
479
+ dependents.set(elem.id, []);
480
+ dependencyCount.set(elem.id, 0);
481
+ }
482
+ for (const elem of elements) {
483
+ const relConfig = axis === "x" ? elem.relativePositionX : elem.relativePositionY;
484
+ if (relConfig) {
485
+ const refId = relConfig.elementId;
486
+ if (!elementMap.has(refId)) {
487
+ errors.push({
488
+ elementId: elem.id,
489
+ type: "missing_reference",
490
+ message: `Element references non-existent element "${refId}" for ${axis.toUpperCase()} position`,
491
+ axis
492
+ });
493
+ continue;
494
+ }
495
+ const cycle = detectCircularDependency(elements, elem.id, axis);
496
+ if (cycle) {
497
+ errors.push({
498
+ elementId: elem.id,
499
+ type: "circular_dependency",
500
+ message: `Circular dependency detected for ${axis.toUpperCase()}: ${cycle.join(" \u2192 ")}`,
501
+ axis,
502
+ cyclePath: cycle
503
+ });
504
+ continue;
505
+ }
506
+ dependents.get(refId)?.push(elem.id);
507
+ dependencyCount.set(elem.id, (dependencyCount.get(elem.id) ?? 0) + 1);
508
+ }
509
+ }
510
+ const sorted = [];
511
+ const queue = [];
512
+ for (const [id, count] of dependencyCount) {
513
+ if (count === 0) {
514
+ queue.push(id);
515
+ }
516
+ }
517
+ while (queue.length > 0) {
518
+ const currentId = queue.shift();
519
+ const current = elementMap.get(currentId);
520
+ if (current) {
521
+ sorted.push(current);
522
+ }
523
+ for (const dependentId of dependents.get(currentId) ?? []) {
524
+ const newCount = (dependencyCount.get(dependentId) ?? 1) - 1;
525
+ dependencyCount.set(dependentId, newCount);
526
+ if (newCount === 0) {
527
+ queue.push(dependentId);
528
+ }
529
+ }
530
+ }
531
+ for (const elem of elements) {
532
+ if (!sorted.find((e) => e.id === elem.id)) {
533
+ sorted.push(elem);
534
+ }
535
+ }
536
+ return { sorted, errors };
537
+ }
538
+ function getSelfAnchorOffsetX(element, selfAnchor) {
539
+ switch (selfAnchor) {
540
+ case "left":
541
+ return 0;
542
+ case "center":
543
+ return element.width / 2;
544
+ case "right":
545
+ return element.width;
546
+ default:
547
+ return 0;
548
+ }
549
+ }
550
+ function getSelfAnchorOffsetY(element, selfAnchor) {
551
+ switch (selfAnchor) {
552
+ case "top":
553
+ return 0;
554
+ case "middle":
555
+ return element.height / 2;
556
+ case "bottom":
557
+ return element.height;
558
+ default:
559
+ return 0;
560
+ }
561
+ }
562
+ function calculateAbsoluteX(element, referenceElement, anchor, selfAnchor, offset) {
563
+ const refX = anchor === "left" ? referenceElement.x : referenceElement.x + referenceElement.width;
564
+ const selfOffset = getSelfAnchorOffsetX(element, selfAnchor);
565
+ return refX + offset - selfOffset;
566
+ }
567
+ function calculateAbsoluteY(element, referenceElement, anchor, selfAnchor, offset) {
568
+ const refY = anchor === "top" ? referenceElement.y : referenceElement.y + referenceElement.height;
569
+ const selfOffset = getSelfAnchorOffsetY(element, selfAnchor);
570
+ return refY + offset - selfOffset;
571
+ }
572
+ function resolveElementPositions(elements, textValues) {
573
+ if (elements.length === 0) {
574
+ return { elements: [], errors: [] };
575
+ }
576
+ const allErrors = [];
577
+ const { sorted: sortedX, errors: errorsX } = topologicalSortForAxis(elements, "x");
578
+ allErrors.push(...errorsX);
579
+ const { sorted: sortedY, errors: errorsY } = topologicalSortForAxis(elements, "y");
580
+ allErrors.push(...errorsY);
581
+ const resolvedX = /* @__PURE__ */ new Map();
582
+ const resolvedY = /* @__PURE__ */ new Map();
583
+ let measureCtx = null;
584
+ const hasAutoWidthElements = elements.some((e) => e.type === "text" && e.autoWidth);
585
+ if (hasAutoWidthElements && typeof document !== "undefined") {
586
+ const canvas = document.createElement("canvas");
587
+ measureCtx = canvas.getContext("2d");
588
+ }
589
+ const getTextContent = (elem) => {
590
+ if (elem.type !== "text") return "";
591
+ if (elem.textInputId && textValues?.[elem.textInputId]) {
592
+ return textValues[elem.textInputId];
593
+ }
594
+ return elem.text ?? "Text";
595
+ };
596
+ const referenceX = /* @__PURE__ */ new Map();
597
+ const referenceY = /* @__PURE__ */ new Map();
598
+ for (const elem of elements) {
599
+ resolvedX.set(elem.id, { x: elem.x, width: elem.width });
600
+ resolvedY.set(elem.id, { y: elem.y, height: elem.height });
601
+ if (elem.type === "text" && elem.autoWidth) {
602
+ const textContent = getTextContent(elem);
603
+ const autoWidthResult = calculateAutoWidthDimensions(elem, textContent, measureCtx);
604
+ if (autoWidthResult) {
605
+ referenceX.set(elem.id, { x: autoWidthResult.actualX, width: autoWidthResult.actualWidth });
606
+ } else {
607
+ referenceX.set(elem.id, { x: elem.x, width: elem.width });
608
+ }
609
+ } else {
610
+ referenceX.set(elem.id, { x: elem.x, width: elem.width });
611
+ }
612
+ referenceY.set(elem.id, { y: elem.y, height: elem.height });
613
+ }
614
+ for (const elem of sortedX) {
615
+ if (elem.relativePositionX) {
616
+ const refId = elem.relativePositionX.elementId;
617
+ const refPosition = referenceX.get(refId);
618
+ if (refPosition) {
619
+ const defaultSelfAnchor = elem.relativePositionX.anchor === "right" ? "left" : "right";
620
+ const selfAnchor = elem.relativePositionX.selfAnchor ?? defaultSelfAnchor;
621
+ const newX = calculateAbsoluteX(
622
+ elem,
623
+ refPosition,
624
+ elem.relativePositionX.anchor,
625
+ selfAnchor,
626
+ elem.relativePositionX.offset
627
+ );
628
+ resolvedX.set(elem.id, { x: newX, width: elem.width });
629
+ if (elem.type === "text" && elem.autoWidth) {
630
+ const textContent = getTextContent(elem);
631
+ const autoWidthResult = calculateAutoWidthDimensions({ ...elem, x: newX }, textContent, measureCtx);
632
+ if (autoWidthResult) {
633
+ referenceX.set(elem.id, { x: autoWidthResult.actualX, width: autoWidthResult.actualWidth });
634
+ } else {
635
+ referenceX.set(elem.id, { x: newX, width: elem.width });
636
+ }
637
+ } else {
638
+ referenceX.set(elem.id, { x: newX, width: elem.width });
639
+ }
640
+ }
641
+ }
642
+ }
643
+ for (const elem of sortedY) {
644
+ if (elem.relativePositionY) {
645
+ const refId = elem.relativePositionY.elementId;
646
+ const refPosition = referenceY.get(refId);
647
+ if (refPosition) {
648
+ const defaultSelfAnchor = elem.relativePositionY.anchor === "bottom" ? "top" : "bottom";
649
+ const selfAnchor = elem.relativePositionY.selfAnchor ?? defaultSelfAnchor;
650
+ const newY = calculateAbsoluteY(
651
+ elem,
652
+ refPosition,
653
+ elem.relativePositionY.anchor,
654
+ selfAnchor,
655
+ elem.relativePositionY.offset
656
+ );
657
+ resolvedY.set(elem.id, { y: newY, height: elem.height });
658
+ referenceY.set(elem.id, { y: newY, height: elem.height });
659
+ }
660
+ }
661
+ }
662
+ const resolvedElements = elements.map((elem) => ({
663
+ ...elem,
664
+ x: resolvedX.get(elem.id)?.x ?? elem.x,
665
+ y: resolvedY.get(elem.id)?.y ?? elem.y
666
+ }));
667
+ return { elements: resolvedElements, errors: allErrors };
668
+ }
669
+ function canSetAsReference(elements, elementId, proposedReferenceId, axis) {
670
+ if (elementId === proposedReferenceId) {
671
+ return false;
672
+ }
673
+ const testElements = elements.map((e) => {
674
+ if (e.id !== elementId) return e;
675
+ if (axis === "x") {
676
+ return {
677
+ ...e,
678
+ relativePositionX: { elementId: proposedReferenceId, anchor: "right", offset: 0 }
679
+ };
680
+ } else {
681
+ return {
682
+ ...e,
683
+ relativePositionY: { elementId: proposedReferenceId, anchor: "bottom", offset: 0 }
684
+ };
685
+ }
686
+ });
687
+ const cycle = detectCircularDependency(testElements, elementId, axis);
688
+ return cycle === null;
689
+ }
690
+ function getDependentElements(elements, elementId) {
691
+ const dependents = [];
692
+ const visited = /* @__PURE__ */ new Set();
693
+ function findDependents(id) {
694
+ for (const elem of elements) {
695
+ const dependsOnX = elem.relativePositionX?.elementId === id;
696
+ const dependsOnY = elem.relativePositionY?.elementId === id;
697
+ if ((dependsOnX || dependsOnY) && !visited.has(elem.id)) {
698
+ visited.add(elem.id);
699
+ dependents.push(elem.id);
700
+ findDependents(elem.id);
701
+ }
702
+ }
703
+ }
704
+ findDependents(elementId);
705
+ return dependents;
706
+ }
707
+ function getReferenceElementX(elements, elementId) {
708
+ const element = elements.find((e) => e.id === elementId);
709
+ if (!element?.relativePositionX) {
710
+ return null;
711
+ }
712
+ return elements.find((e) => e.id === element.relativePositionX.elementId) ?? null;
713
+ }
714
+ function getReferenceElementY(elements, elementId) {
715
+ const element = elements.find((e) => e.id === elementId);
716
+ if (!element?.relativePositionY) {
717
+ return null;
718
+ }
719
+ return elements.find((e) => e.id === element.relativePositionY.elementId) ?? null;
720
+ }
721
+
722
+ // src/compositions/ImageEditorComposition.tsx
723
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
724
+ function getSortedSegments(config) {
725
+ const allSegments = [];
726
+ for (const channel of config.channels) {
727
+ allSegments.push(...channel.segments);
728
+ }
729
+ return allSegments.sort((a, b) => {
730
+ const aZ = a.zIndex ?? 0;
731
+ const bZ = b.zIndex ?? 0;
732
+ return aZ - bZ;
733
+ });
734
+ }
735
+ function elementToTextSegment(elem) {
736
+ return {
737
+ id: elem.id,
738
+ type: "text",
739
+ source: "",
740
+ order: elem.zIndex,
741
+ offset: { type: "absolute", value: 0 },
742
+ xOffset: elem.x,
743
+ yOffset: elem.y,
744
+ width: elem.width,
745
+ height: elem.height,
746
+ zIndex: elem.zIndex,
747
+ rotation: elem.rotation,
748
+ opacity: elem.opacity,
749
+ text: elem.text ?? "",
750
+ fontType: elem.font ?? "tiktok",
751
+ fontSize: elem.fontSize ?? 40,
752
+ fontWeight: elem.fontWeight ?? "normal",
753
+ color: elem.color ?? "#FFFFFF",
754
+ strokeColor: elem.strokeColor ?? "#000000",
755
+ strokeWidth: elem.outlineWidth ?? 0,
756
+ lineHeight: elem.lineHeight ?? 1.2,
757
+ letterSpacing: elem.letterSpacing ?? 0,
758
+ alignment: elem.textAlign ?? "center",
759
+ verticalAlign: elem.verticalAlign ?? "middle",
760
+ backgroundColor: elem.backgroundColor,
761
+ backgroundOpacity: elem.backgroundOpacity,
762
+ backgroundBorderRadius: typeof elem.backgroundBorderRadius === "object" ? elem.backgroundBorderRadius : void 0,
763
+ paddingTop: elem.paddingTop,
764
+ paddingRight: elem.paddingRight,
765
+ paddingBottom: elem.paddingBottom,
766
+ paddingLeft: elem.paddingLeft,
767
+ autoWidth: elem.autoWidth,
768
+ boxAlign: elem.boxAlign
769
+ };
770
+ }
771
+ function elementToImageSegment(elem, source) {
772
+ return {
773
+ id: elem.id,
774
+ type: "image",
775
+ source,
776
+ order: elem.zIndex,
777
+ offset: { type: "absolute", value: 0 },
778
+ xOffset: elem.x,
779
+ yOffset: elem.y,
780
+ width: elem.width,
781
+ height: elem.height,
782
+ zIndex: elem.zIndex,
783
+ rotation: elem.rotation,
784
+ opacity: elem.opacity,
785
+ fit: elem.fit ?? "cover",
786
+ borderRadius: elem.borderRadius
787
+ };
788
+ }
789
+ function ImageEditorComposition({
790
+ config,
791
+ sources = {},
792
+ scale = 1,
793
+ // New direct element props
794
+ elements,
795
+ width,
796
+ height,
797
+ backgroundFit = "cover",
798
+ backgroundUrl,
799
+ imageUrls = {},
800
+ textValues = {}
801
+ }) {
802
+ const resolvedElements = useMemo3(() => {
803
+ if (!elements) return null;
804
+ const result = resolveElementPositions(elements, textValues);
805
+ if (result.errors.length > 0) {
806
+ console.warn("Position resolution errors:", result.errors);
807
+ }
808
+ return result.elements;
809
+ }, [elements, textValues]);
810
+ const segmentsFromElements = useMemo3(() => {
811
+ if (!resolvedElements) return null;
812
+ const segments = [];
813
+ for (const elem of resolvedElements) {
814
+ if (elem.type === "text") {
815
+ const text = elem.textInputId && textValues[elem.textInputId] ? textValues[elem.textInputId] : elem.text || `{{${elem.textInputId ?? "text"}}}`;
816
+ segments.push(elementToTextSegment({ ...elem, text }));
817
+ } else if (elem.type === "image") {
818
+ const url = elem.inputId ? imageUrls[elem.inputId] ?? "" : "";
819
+ if (url) {
820
+ segments.push(elementToImageSegment(elem, url));
821
+ }
822
+ }
823
+ }
824
+ return segments.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
825
+ }, [resolvedElements, imageUrls, textValues]);
826
+ const canvasWidth = width ?? config?.width ?? 1080;
827
+ const canvasHeight = height ?? config?.height ?? 1920;
828
+ const bgFit = backgroundFit ?? "cover";
829
+ const bgUrl = backgroundUrl ?? sources.background;
830
+ const contentSegments = segmentsFromElements ?? (() => {
831
+ if (!config) return [];
832
+ const sorted = getSortedSegments(config);
833
+ return sorted.filter((s) => s.id !== "background");
834
+ })();
835
+ const legacyBackgroundSegment = useMemo3(() => {
836
+ if (!config || segmentsFromElements) return null;
837
+ const sorted = getSortedSegments(config);
838
+ return sorted.find((s) => s.id === "background" && s.type === "image");
839
+ }, [config, segmentsFromElements]);
840
+ const getSource = (segment) => {
841
+ if (segment.source) return segment.source;
842
+ if (sources[segment.id]) return sources[segment.id];
843
+ const segmentAny = segment;
844
+ if (segmentAny.inputRef && sources[segmentAny.inputRef]) {
845
+ return sources[segmentAny.inputRef];
846
+ }
847
+ return void 0;
848
+ };
849
+ return /* @__PURE__ */ jsxs(AbsoluteFill, { style: { backgroundColor: "#000000" }, children: [
850
+ bgUrl && segmentsFromElements && /* @__PURE__ */ jsx3(
851
+ Img2,
852
+ {
853
+ src: bgUrl,
854
+ style: {
855
+ position: "absolute",
856
+ left: 0,
857
+ top: 0,
858
+ width: canvasWidth * scale,
859
+ height: canvasHeight * scale,
860
+ objectFit: bgFit
861
+ }
862
+ }
863
+ ),
864
+ legacyBackgroundSegment && !segmentsFromElements && /* @__PURE__ */ jsx3(
865
+ BackgroundImage,
866
+ {
867
+ segment: legacyBackgroundSegment,
868
+ src: getSource(legacyBackgroundSegment),
869
+ width: canvasWidth,
870
+ height: canvasHeight,
871
+ scale
872
+ }
873
+ ),
874
+ contentSegments.map((segment) => {
875
+ if (segment.type === "text") {
876
+ return /* @__PURE__ */ jsx3(
877
+ TextElement,
878
+ {
879
+ segment,
880
+ scale
881
+ },
882
+ segment.id
883
+ );
884
+ }
885
+ if (segment.type === "image") {
886
+ const src = segment.source || getSource(segment);
887
+ if (!src) {
888
+ console.warn(`No source found for image segment: ${segment.id}`);
889
+ return null;
890
+ }
891
+ return /* @__PURE__ */ jsx3(
892
+ ImageElement,
893
+ {
894
+ segment,
895
+ src,
896
+ scale
897
+ },
898
+ segment.id
899
+ );
900
+ }
901
+ return null;
902
+ })
903
+ ] });
904
+ }
905
+ function BackgroundImage({
906
+ segment,
907
+ src,
908
+ width,
909
+ height,
910
+ scale
911
+ }) {
912
+ if (!src) {
913
+ return null;
914
+ }
915
+ const fit = segment.fit ?? "cover";
916
+ return /* @__PURE__ */ jsx3(
917
+ Img2,
918
+ {
919
+ src,
920
+ style: {
921
+ position: "absolute",
922
+ left: 0,
923
+ top: 0,
924
+ width: width * scale,
925
+ height: height * scale,
926
+ objectFit: fit
927
+ }
928
+ }
929
+ );
930
+ }
931
+
932
+ // src/compositions/VideoEditorComposition.tsx
933
+ import { useMemo as useMemo5 } from "react";
934
+ import { AbsoluteFill as AbsoluteFill2, useCurrentFrame as useCurrentFrame2, useVideoConfig as useVideoConfig2, Sequence, Audio } from "remotion";
935
+
936
+ // src/components/VideoElement.tsx
937
+ import { useMemo as useMemo4 } from "react";
938
+ import { Video, useCurrentFrame, useVideoConfig } from "remotion";
939
+ import { jsx as jsx4 } from "react/jsx-runtime";
940
+ function fitModeToCss2(fit) {
941
+ return fit;
942
+ }
943
+ function VideoElement({
944
+ segment,
945
+ src,
946
+ startFrame,
947
+ durationInFrames,
948
+ scale = 1
949
+ }) {
950
+ const frame = useCurrentFrame();
951
+ const { fps } = useVideoConfig();
952
+ const fit = segment.fit ?? VIDEO_DEFAULTS.fit;
953
+ const speed = segment.speed ?? VIDEO_DEFAULTS.speed;
954
+ const volume = (segment.volume ?? VIDEO_DEFAULTS.volume) / 100;
955
+ const opacity = (segment.opacity ?? VISUAL_DEFAULTS.opacity) / 100;
956
+ const rotation = segment.rotation ?? 0;
957
+ const borderRadius = segment.borderRadius;
958
+ const fadeIn = segment.fadeIn ?? 0;
959
+ const x = segment.xOffset * scale;
960
+ const y = segment.yOffset * scale;
961
+ const width = segment.width * scale;
962
+ const height = segment.height * scale;
963
+ const startFrom = (segment.startTrim ?? 0) / 1e3;
964
+ const fadeOpacity = useMemo4(() => {
965
+ if (fadeIn <= 0) return 1;
966
+ const framesFromStart = frame - startFrame;
967
+ const fadeInFrames = fadeIn / 1e3 * fps;
968
+ if (framesFromStart >= fadeInFrames) return 1;
969
+ if (framesFromStart <= 0) return 0;
970
+ return framesFromStart / fadeInFrames;
971
+ }, [frame, startFrame, fadeIn, fps]);
972
+ const borderRadiusStyle = useMemo4(() => {
973
+ if (!borderRadius) return void 0;
974
+ if (typeof borderRadius === "number") {
975
+ return `${borderRadius * scale}px`;
976
+ }
977
+ const radii = getBorderRadii(borderRadius);
978
+ if (!radii) return void 0;
979
+ return `${radii[0] * scale}px ${radii[1] * scale}px ${radii[2] * scale}px ${radii[3] * scale}px`;
980
+ }, [borderRadius, scale]);
981
+ const containerStyle = useMemo4(() => ({
982
+ position: "absolute",
983
+ left: x,
984
+ top: y,
985
+ width,
986
+ height,
987
+ transform: rotation !== 0 ? `rotate(${rotation}deg)` : void 0,
988
+ transformOrigin: "center center",
989
+ overflow: "hidden",
990
+ borderRadius: borderRadiusStyle,
991
+ opacity: opacity * fadeOpacity
992
+ }), [x, y, width, height, rotation, borderRadiusStyle, opacity, fadeOpacity]);
993
+ const videoStyle = useMemo4(() => ({
994
+ width: "100%",
995
+ height: "100%",
996
+ objectFit: fitModeToCss2(fit)
997
+ }), [fit]);
998
+ return /* @__PURE__ */ jsx4("div", { style: containerStyle, children: /* @__PURE__ */ jsx4(
999
+ Video,
1000
+ {
1001
+ src,
1002
+ style: videoStyle,
1003
+ startFrom,
1004
+ playbackRate: speed,
1005
+ volume
1006
+ }
1007
+ ) });
1008
+ }
1009
+
1010
+ // src/compositions/VideoEditorComposition.tsx
1011
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1012
+ function calculateSegmentTimings(config, fps) {
1013
+ const timings = [];
1014
+ const timingsMap = /* @__PURE__ */ new Map();
1015
+ for (const channel of config.channels) {
1016
+ const sortedSegments = [...channel.segments].sort((a, b) => a.order - b.order);
1017
+ const baseSegments = sortedSegments.filter((s) => !s.parentId);
1018
+ const overlaySegments = sortedSegments.filter((s) => s.parentId);
1019
+ let currentFrame = 0;
1020
+ for (const segment of baseSegments) {
1021
+ const offsetMs = segment.offset.type === "absolute" ? segment.offset.value : segment.offset.value * (currentFrame / fps * 1e3);
1022
+ const offsetFrames = Math.round(offsetMs / 1e3 * fps);
1023
+ const startFrame = currentFrame + offsetFrames;
1024
+ let durationMs;
1025
+ if (segment.duration) {
1026
+ durationMs = segment.duration.type === "absolute" ? segment.duration.value : segment.duration.value * (config.duration ?? 1e4);
1027
+ } else {
1028
+ durationMs = (config.duration ?? 1e4) - startFrame / fps * 1e3;
1029
+ }
1030
+ const durationInFrames = Math.max(1, Math.round(durationMs / 1e3 * fps));
1031
+ const endFrame = startFrame + durationInFrames;
1032
+ const timing = {
1033
+ segment,
1034
+ startFrame,
1035
+ endFrame,
1036
+ durationInFrames
1037
+ };
1038
+ timings.push(timing);
1039
+ timingsMap.set(segment.id, timing);
1040
+ currentFrame = endFrame;
1041
+ }
1042
+ for (const segment of overlaySegments) {
1043
+ const parentTiming = timingsMap.get(segment.parentId);
1044
+ if (!parentTiming) {
1045
+ console.warn(`Parent segment not found for overlay: ${segment.id}`);
1046
+ continue;
1047
+ }
1048
+ const relativeStart = segment.relativeStart ?? 0;
1049
+ const relativeEnd = segment.relativeEnd ?? 1;
1050
+ const parentDurationFrames = parentTiming.durationInFrames;
1051
+ const startFrame = parentTiming.startFrame + Math.round(parentDurationFrames * relativeStart);
1052
+ const endFrame = parentTiming.startFrame + Math.round(parentDurationFrames * relativeEnd);
1053
+ const durationInFrames = endFrame - startFrame;
1054
+ const timing = {
1055
+ segment,
1056
+ startFrame,
1057
+ endFrame,
1058
+ durationInFrames
1059
+ };
1060
+ timings.push(timing);
1061
+ timingsMap.set(segment.id, timing);
1062
+ }
1063
+ }
1064
+ return timings;
1065
+ }
1066
+ function VideoEditorComposition({
1067
+ config,
1068
+ sources = {},
1069
+ textContent = {}
1070
+ }) {
1071
+ const frame = useCurrentFrame2();
1072
+ const { fps, durationInFrames } = useVideoConfig2();
1073
+ const segmentTimings = useMemo5(
1074
+ () => calculateSegmentTimings(config, fps),
1075
+ [config, fps]
1076
+ );
1077
+ const activeVisualSegments = useMemo5(() => {
1078
+ return segmentTimings.filter(({ segment, startFrame, endFrame }) => {
1079
+ if (segment.type !== "video" && segment.type !== "image" && segment.type !== "text") {
1080
+ return false;
1081
+ }
1082
+ return frame >= startFrame && frame < endFrame;
1083
+ }).sort((a, b) => {
1084
+ const aZ = a.segment.zIndex ?? 0;
1085
+ const bZ = b.segment.zIndex ?? 0;
1086
+ return aZ - bZ;
1087
+ });
1088
+ }, [segmentTimings, frame]);
1089
+ const getSource = (segment) => {
1090
+ if (segment.source) {
1091
+ return segment.source;
1092
+ }
1093
+ if (sources[segment.id]) {
1094
+ return sources[segment.id];
1095
+ }
1096
+ const segmentAny = segment;
1097
+ if (segmentAny.inputRef && sources[segmentAny.inputRef]) {
1098
+ return sources[segmentAny.inputRef];
1099
+ }
1100
+ return void 0;
1101
+ };
1102
+ const getTextContent = (segment) => {
1103
+ if (textContent[segment.id]) {
1104
+ return textContent[segment.id];
1105
+ }
1106
+ const segmentAny = segment;
1107
+ if (segmentAny.textInputRef && textContent[segmentAny.textInputRef]) {
1108
+ return textContent[segmentAny.textInputRef];
1109
+ }
1110
+ return segment.text ?? "";
1111
+ };
1112
+ const audioTimings = useMemo5(() => {
1113
+ return segmentTimings.filter(({ segment }) => segment.type === "audio");
1114
+ }, [segmentTimings]);
1115
+ return /* @__PURE__ */ jsxs2(AbsoluteFill2, { style: { backgroundColor: "#000000" }, children: [
1116
+ activeVisualSegments.map(({ segment, startFrame, durationInFrames: durationInFrames2 }) => {
1117
+ if (segment.type === "text") {
1118
+ const textSegment = segment;
1119
+ const text = getTextContent(textSegment);
1120
+ return /* @__PURE__ */ jsx5(
1121
+ TextElement,
1122
+ {
1123
+ segment: { ...textSegment, text },
1124
+ scale: 1
1125
+ },
1126
+ segment.id
1127
+ );
1128
+ }
1129
+ if (segment.type === "image") {
1130
+ const src = getSource(segment);
1131
+ if (!src) {
1132
+ console.warn(`No source found for image segment: ${segment.id}`);
1133
+ return null;
1134
+ }
1135
+ return /* @__PURE__ */ jsx5(
1136
+ ImageElement,
1137
+ {
1138
+ segment,
1139
+ src,
1140
+ scale: 1
1141
+ },
1142
+ segment.id
1143
+ );
1144
+ }
1145
+ if (segment.type === "video") {
1146
+ const src = getSource(segment);
1147
+ if (!src) {
1148
+ console.warn(`No source found for video segment: ${segment.id}`);
1149
+ return null;
1150
+ }
1151
+ return /* @__PURE__ */ jsx5(
1152
+ VideoElement,
1153
+ {
1154
+ segment,
1155
+ src,
1156
+ startFrame,
1157
+ durationInFrames: durationInFrames2,
1158
+ scale: 1
1159
+ },
1160
+ segment.id
1161
+ );
1162
+ }
1163
+ return null;
1164
+ }),
1165
+ audioTimings.map(({ segment, startFrame, durationInFrames: durationInFrames2 }) => {
1166
+ const audioSegment = segment;
1167
+ const src = getSource(segment);
1168
+ if (!src) {
1169
+ console.warn(`No source found for audio segment: ${segment.id}`);
1170
+ return null;
1171
+ }
1172
+ const volume = (audioSegment.volume ?? 100) / 100;
1173
+ const startFromSeconds = (audioSegment.startTrim ?? 0) / 1e3;
1174
+ return /* @__PURE__ */ jsx5(
1175
+ Sequence,
1176
+ {
1177
+ from: startFrame,
1178
+ durationInFrames: durationInFrames2,
1179
+ children: /* @__PURE__ */ jsx5(
1180
+ Audio,
1181
+ {
1182
+ src,
1183
+ volume,
1184
+ startFrom: startFromSeconds
1185
+ }
1186
+ )
1187
+ },
1188
+ segment.id
1189
+ );
1190
+ })
1191
+ ] });
1192
+ }
1193
+
1194
+ // src/utils/fit.ts
1195
+ function calculateFitDimensions({
1196
+ sourceWidth,
1197
+ sourceHeight,
1198
+ targetWidth,
1199
+ targetHeight,
1200
+ fit
1201
+ }) {
1202
+ if (fit === "fill") {
1203
+ return {
1204
+ x: 0,
1205
+ y: 0,
1206
+ width: targetWidth,
1207
+ height: targetHeight,
1208
+ sourceX: 0,
1209
+ sourceY: 0,
1210
+ sourceWidth,
1211
+ sourceHeight
1212
+ };
1213
+ }
1214
+ const sourceRatio = sourceWidth / sourceHeight;
1215
+ const targetRatio = targetWidth / targetHeight;
1216
+ let width = targetWidth;
1217
+ let height = targetHeight;
1218
+ let x = 0;
1219
+ let y = 0;
1220
+ let sourceX = 0;
1221
+ let sourceY = 0;
1222
+ let croppedSourceWidth = sourceWidth;
1223
+ let croppedSourceHeight = sourceHeight;
1224
+ if (fit === "contain") {
1225
+ if (sourceRatio > targetRatio) {
1226
+ height = targetWidth / sourceRatio;
1227
+ y = (targetHeight - height) / 2;
1228
+ } else {
1229
+ width = targetHeight * sourceRatio;
1230
+ x = (targetWidth - width) / 2;
1231
+ }
1232
+ } else if (fit === "cover") {
1233
+ if (sourceRatio > targetRatio) {
1234
+ croppedSourceWidth = sourceHeight * targetRatio;
1235
+ sourceX = (sourceWidth - croppedSourceWidth) / 2;
1236
+ } else {
1237
+ croppedSourceHeight = sourceWidth / targetRatio;
1238
+ sourceY = (sourceHeight - croppedSourceHeight) / 2;
1239
+ }
1240
+ }
1241
+ return {
1242
+ x,
1243
+ y,
1244
+ width,
1245
+ height,
1246
+ sourceX,
1247
+ sourceY,
1248
+ sourceWidth: croppedSourceWidth,
1249
+ sourceHeight: croppedSourceHeight
1250
+ };
1251
+ }
1252
+
1253
+ // src/hooks/index.ts
1254
+ import { useEffect, useState, useMemo as useMemo6 } from "react";
1255
+ function useFontsLoaded() {
1256
+ const [loaded, setLoaded] = useState(areFontsLoaded());
1257
+ useEffect(() => {
1258
+ if (!loaded) {
1259
+ preloadFonts().then(() => setLoaded(true)).catch(console.error);
1260
+ }
1261
+ }, [loaded]);
1262
+ return loaded;
1263
+ }
1264
+ function useImageLoader(src) {
1265
+ const [image, setImage] = useState(null);
1266
+ useEffect(() => {
1267
+ if (!src) {
1268
+ setImage(null);
1269
+ return;
1270
+ }
1271
+ const img = new Image();
1272
+ img.onload = () => setImage(img);
1273
+ img.onerror = () => {
1274
+ console.error(`Failed to load image: ${src}`);
1275
+ setImage(null);
1276
+ };
1277
+ img.src = src;
1278
+ return () => {
1279
+ img.onload = null;
1280
+ img.onerror = null;
1281
+ };
1282
+ }, [src]);
1283
+ return image;
1284
+ }
1285
+ function useImagePreloader(sources) {
1286
+ const [images, setImages] = useState({});
1287
+ const [loaded, setLoaded] = useState(false);
1288
+ useEffect(() => {
1289
+ const entries = Object.entries(sources);
1290
+ if (entries.length === 0) {
1291
+ setLoaded(true);
1292
+ return;
1293
+ }
1294
+ let cancelled = false;
1295
+ const loadedImages = {};
1296
+ let loadedCount = 0;
1297
+ entries.forEach(([key, src]) => {
1298
+ const img = new Image();
1299
+ img.onload = () => {
1300
+ if (cancelled) return;
1301
+ loadedImages[key] = img;
1302
+ loadedCount++;
1303
+ if (loadedCount === entries.length) {
1304
+ setImages(loadedImages);
1305
+ setLoaded(true);
1306
+ }
1307
+ };
1308
+ img.onerror = () => {
1309
+ if (cancelled) return;
1310
+ console.error(`Failed to load image: ${src}`);
1311
+ loadedCount++;
1312
+ if (loadedCount === entries.length) {
1313
+ setImages(loadedImages);
1314
+ setLoaded(true);
1315
+ }
1316
+ };
1317
+ img.src = src;
1318
+ });
1319
+ return () => {
1320
+ cancelled = true;
1321
+ };
1322
+ }, [sources]);
1323
+ return { loaded, images };
1324
+ }
1325
+ function useResolvedPositions(elements, textValues) {
1326
+ return useMemo6(() => {
1327
+ if (elements.length === 0) {
1328
+ return { elements: [], errors: [] };
1329
+ }
1330
+ return resolveElementPositions(elements, textValues);
1331
+ }, [elements, textValues]);
1332
+ }
1333
+
1334
+ // src/Root.tsx
1335
+ import { Composition } from "remotion";
1336
+ import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1337
+ var defaultImageProps = {
1338
+ config: {
1339
+ width: 1080,
1340
+ height: 1920,
1341
+ fps: 1,
1342
+ channels: []
1343
+ },
1344
+ sources: {},
1345
+ scale: 1
1346
+ };
1347
+ var defaultVideoProps = {
1348
+ config: {
1349
+ width: 1080,
1350
+ height: 1920,
1351
+ fps: 30,
1352
+ duration: 5e3,
1353
+ channels: []
1354
+ },
1355
+ sources: {},
1356
+ scale: 1
1357
+ };
1358
+ var ImageComp = ImageEditorComposition;
1359
+ var VideoComp = VideoEditorComposition;
1360
+ var RemotionRoot = () => {
1361
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
1362
+ /* @__PURE__ */ jsx6(
1363
+ Composition,
1364
+ {
1365
+ id: "ImageEditorComposition",
1366
+ component: ImageComp,
1367
+ durationInFrames: 1,
1368
+ fps: 1,
1369
+ width: 1080,
1370
+ height: 1920,
1371
+ defaultProps: defaultImageProps
1372
+ }
1373
+ ),
1374
+ /* @__PURE__ */ jsx6(
1375
+ Composition,
1376
+ {
1377
+ id: "VideoEditorComposition",
1378
+ component: VideoComp,
1379
+ durationInFrames: 150,
1380
+ fps: 30,
1381
+ width: 1080,
1382
+ height: 1920,
1383
+ defaultProps: defaultVideoProps
1384
+ }
1385
+ )
1386
+ ] });
1387
+ };
1388
+ export {
1389
+ DIMENSION_PRESETS,
1390
+ FONT_FAMILIES,
1391
+ FONT_URLS,
1392
+ IMAGE_DEFAULTS,
1393
+ ImageEditorComposition,
1394
+ ImageElement,
1395
+ RemotionRoot,
1396
+ TEXT_DEFAULTS,
1397
+ TextElement,
1398
+ VIDEO_DEFAULTS,
1399
+ VISUAL_DEFAULTS,
1400
+ VideoEditorComposition,
1401
+ VideoElement,
1402
+ applyImageDefaults,
1403
+ applyTextDefaults,
1404
+ applyVideoDefaults,
1405
+ areFontsLoaded,
1406
+ buildFontString,
1407
+ calculateAutoWidthDimensions,
1408
+ calculateFitDimensions,
1409
+ calculateLineWidth,
1410
+ canSetAsReference,
1411
+ getBorderRadii,
1412
+ getDependentElements,
1413
+ getFontFamily,
1414
+ getReferenceElementX,
1415
+ getReferenceElementY,
1416
+ hexToRgba,
1417
+ parseHexColor,
1418
+ preloadFonts,
1419
+ resolveElementPositions,
1420
+ useFontsLoaded,
1421
+ useImageLoader,
1422
+ useImagePreloader,
1423
+ useResolvedPositions,
1424
+ wrapText
1425
+ };