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/README.md +257 -0
- package/dist/index.d.mts +1200 -0
- package/dist/index.d.ts +1200 -0
- package/dist/index.js +1487 -0
- package/dist/index.mjs +1425 -0
- package/package.json +66 -0
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
|
+
};
|