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