ugcinc-render 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,51 @@
1
+ import {
2
+ APPLE_EMOJI_FONT,
3
+ CAPTION_PRESETS,
4
+ DEFAULT_CAPTION_STYLE,
5
+ FONT_FAMILIES,
6
+ FONT_URLS,
7
+ IMAGE_DEFAULTS,
8
+ TEXT_DEFAULTS,
9
+ VIDEO_DEFAULTS,
10
+ VISUAL_DEFAULTS,
11
+ applyImageDefaults,
12
+ applyTextDefaults,
13
+ applyVideoDefaults,
14
+ areFontsLoaded,
15
+ buildFontString,
16
+ calculateAutoWidthDimensions,
17
+ calculateCropBounds,
18
+ calculateEstimatedDuration,
19
+ calculateFitDimensions,
20
+ calculateLineWidth,
21
+ calculateTimelineContentEnd,
22
+ canSetAsReference,
23
+ debugFontStatus,
24
+ defaultOffset,
25
+ formatTime,
26
+ generateOverlayId,
27
+ generateSegmentId,
28
+ getBaseSegments,
29
+ getBorderRadii,
30
+ getCaptionPresetNames,
31
+ getDependentElements,
32
+ getFontFamily,
33
+ getOverlays,
34
+ getReferenceElementX,
35
+ getReferenceElementY,
36
+ getSegmentTimelinePosition,
37
+ hexToRgba,
38
+ isDynamicCropEnabled,
39
+ isSegmentVisibleAtTime,
40
+ isValidCaptionPreset,
41
+ parseHexColor,
42
+ parseTime,
43
+ preloadFonts,
44
+ resolveCaptionStyle,
45
+ resolveElementPositions,
46
+ wrapText
47
+ } from "./chunk-6DSZRTLA.mjs";
48
+
1
49
  // src/types/element.ts
2
50
  var DIMENSION_PRESETS = {
3
51
  "tiktok": { width: 1080, height: 1920, label: "TikTok/Reels (1080\xD71920)", ratio: "9:16" },
@@ -191,296 +239,6 @@ import { AbsoluteFill, Img as Img2, delayRender, continueRender } from "remotion
191
239
 
192
240
  // src/components/TextElement.tsx
193
241
  import React, { useMemo } from "react";
194
-
195
- // src/utils/defaults.ts
196
- var TEXT_DEFAULTS = {
197
- fontType: "arial",
198
- fontSize: 40,
199
- fontWeight: "normal",
200
- color: "#000000",
201
- alignment: "left",
202
- verticalAlign: "top",
203
- direction: "ltr",
204
- lineHeight: 1.2,
205
- letterSpacing: 0,
206
- padding: 0,
207
- textWrap: "word",
208
- wordBreak: "normal",
209
- hyphenation: "none",
210
- maxLines: 0,
211
- textOverflow: "clip",
212
- ellipsis: "...",
213
- strokeWidth: 0,
214
- backgroundOpacity: 100,
215
- autoWidth: false,
216
- boxAlign: "left"
217
- };
218
- var IMAGE_DEFAULTS = {
219
- fit: "cover",
220
- opacity: 100,
221
- loop: false,
222
- speed: 1
223
- };
224
- var VIDEO_DEFAULTS = {
225
- ...IMAGE_DEFAULTS,
226
- volume: 100
227
- };
228
- var VISUAL_DEFAULTS = {
229
- xOffset: 0,
230
- yOffset: 0,
231
- zIndex: 0,
232
- scale: 1,
233
- rotation: 0,
234
- opacity: 100
235
- };
236
- function applyTextDefaults(segment) {
237
- return {
238
- ...TEXT_DEFAULTS,
239
- ...segment
240
- };
241
- }
242
- function applyImageDefaults(segment) {
243
- return {
244
- ...IMAGE_DEFAULTS,
245
- ...segment
246
- };
247
- }
248
- function applyVideoDefaults(segment) {
249
- return {
250
- ...VIDEO_DEFAULTS,
251
- ...segment
252
- };
253
- }
254
-
255
- // src/utils/fonts.ts
256
- var APPLE_EMOJI_FONT = '"Apple Color Emoji"';
257
- var FONT_FAMILIES = {
258
- tiktok: `"TikTok Sans", ${APPLE_EMOJI_FONT}, sans-serif`,
259
- apple: `"SF Pro", "SF Pro Display", -apple-system, BlinkMacSystemFont, ${APPLE_EMOJI_FONT}, sans-serif`,
260
- arial: `Arial, ${APPLE_EMOJI_FONT}, sans-serif`
261
- };
262
- function getFontFamily(fontType) {
263
- return FONT_FAMILIES[fontType] ?? FONT_FAMILIES.arial;
264
- }
265
- function buildFontString({
266
- fontType,
267
- fontSize,
268
- fontWeight
269
- }) {
270
- const fontFamily = getFontFamily(fontType);
271
- return `${fontWeight} ${fontSize}px ${fontFamily}`;
272
- }
273
- var FONT_URLS = {
274
- tiktok: {
275
- regular: "/TikTokSans-Regular.ttf",
276
- bold: "/TikTokSans-Bold.ttf"
277
- },
278
- apple: {
279
- regular: "/SF-Pro.ttf"
280
- },
281
- emoji: {
282
- apple: "/AppleColorEmoji.ttf"
283
- }
284
- };
285
- async function preloadFonts() {
286
- console.log("[ugcinc-render/fonts] preloadFonts() called");
287
- if (typeof document !== "undefined") {
288
- console.log("[ugcinc-render/fonts] Browser environment detected, loading fonts...");
289
- const fontResults = [];
290
- try {
291
- const tiktokRegular = new FontFace(
292
- "TikTok Sans",
293
- `url(${FONT_URLS.tiktok.regular})`,
294
- { weight: "normal" }
295
- );
296
- console.log(`[ugcinc-render/fonts] Loading TikTok Sans Regular from: ${FONT_URLS.tiktok.regular}`);
297
- await tiktokRegular.load();
298
- document.fonts.add(tiktokRegular);
299
- fontResults.push({ name: "TikTok Sans Regular", status: "success" });
300
- } catch (err) {
301
- const errorMsg = err instanceof Error ? err.message : String(err);
302
- console.error(`[ugcinc-render/fonts] Failed to load TikTok Sans Regular:`, errorMsg);
303
- fontResults.push({ name: "TikTok Sans Regular", status: "error", error: errorMsg });
304
- }
305
- try {
306
- const tiktokBold = new FontFace(
307
- "TikTok Sans",
308
- `url(${FONT_URLS.tiktok.bold})`,
309
- { weight: "bold" }
310
- );
311
- console.log(`[ugcinc-render/fonts] Loading TikTok Sans Bold from: ${FONT_URLS.tiktok.bold}`);
312
- await tiktokBold.load();
313
- document.fonts.add(tiktokBold);
314
- fontResults.push({ name: "TikTok Sans Bold", status: "success" });
315
- } catch (err) {
316
- const errorMsg = err instanceof Error ? err.message : String(err);
317
- console.error(`[ugcinc-render/fonts] Failed to load TikTok Sans Bold:`, errorMsg);
318
- fontResults.push({ name: "TikTok Sans Bold", status: "error", error: errorMsg });
319
- }
320
- try {
321
- const sfPro = new FontFace(
322
- "SF Pro",
323
- `url(${FONT_URLS.apple.regular})`,
324
- { weight: "normal" }
325
- );
326
- console.log(`[ugcinc-render/fonts] Loading SF Pro from: ${FONT_URLS.apple.regular}`);
327
- await sfPro.load();
328
- document.fonts.add(sfPro);
329
- fontResults.push({ name: "SF Pro", status: "success" });
330
- } catch (err) {
331
- const errorMsg = err instanceof Error ? err.message : String(err);
332
- console.error(`[ugcinc-render/fonts] Failed to load SF Pro:`, errorMsg);
333
- fontResults.push({ name: "SF Pro", status: "error", error: errorMsg });
334
- }
335
- try {
336
- const appleEmoji = new FontFace(
337
- "Apple Color Emoji",
338
- `url(${FONT_URLS.emoji.apple})`,
339
- { weight: "normal" }
340
- );
341
- console.log(`[ugcinc-render/fonts] Loading Apple Color Emoji from: ${FONT_URLS.emoji.apple}`);
342
- await appleEmoji.load();
343
- document.fonts.add(appleEmoji);
344
- fontResults.push({ name: "Apple Color Emoji", status: "success" });
345
- } catch (err) {
346
- const errorMsg = err instanceof Error ? err.message : String(err);
347
- console.error(`[ugcinc-render/fonts] Failed to load Apple Color Emoji:`, errorMsg);
348
- fontResults.push({ name: "Apple Color Emoji", status: "error", error: errorMsg });
349
- }
350
- console.log("[ugcinc-render/fonts] Font loading complete. Results:", fontResults);
351
- const availableFonts = [];
352
- document.fonts.forEach((font) => {
353
- availableFonts.push(`${font.family} (${font.weight}, ${font.status})`);
354
- });
355
- console.log("[ugcinc-render/fonts] Available fonts in document:", availableFonts);
356
- } else {
357
- console.log("[ugcinc-render/fonts] Not in browser environment, skipping font loading");
358
- }
359
- }
360
- function areFontsLoaded() {
361
- if (typeof document === "undefined") {
362
- console.log("[ugcinc-render/fonts] areFontsLoaded: Not in browser environment");
363
- return false;
364
- }
365
- const tiktokRegular = document.fonts.check('normal 16px "TikTok Sans"');
366
- const tiktokBold = document.fonts.check('bold 16px "TikTok Sans"');
367
- const sfPro = document.fonts.check('normal 16px "SF Pro"');
368
- const appleEmoji = document.fonts.check('normal 16px "Apple Color Emoji"');
369
- console.log("[ugcinc-render/fonts] areFontsLoaded check:", {
370
- tiktokRegular,
371
- tiktokBold,
372
- sfPro,
373
- appleEmoji
374
- });
375
- return tiktokRegular && tiktokBold && sfPro && appleEmoji;
376
- }
377
- function debugFontStatus() {
378
- console.log("[ugcinc-render/fonts] === FONT DEBUG STATUS ===");
379
- if (typeof document === "undefined") {
380
- console.log("[ugcinc-render/fonts] Not in browser environment");
381
- return;
382
- }
383
- console.log("[ugcinc-render/fonts] Font checks:");
384
- console.log(" - TikTok Sans (normal):", document.fonts.check('normal 16px "TikTok Sans"'));
385
- console.log(" - TikTok Sans (bold):", document.fonts.check('bold 16px "TikTok Sans"'));
386
- console.log(" - SF Pro:", document.fonts.check('normal 16px "SF Pro"'));
387
- console.log(" - Apple Color Emoji:", document.fonts.check('normal 16px "Apple Color Emoji"'));
388
- console.log("[ugcinc-render/fonts] All fonts in document.fonts:");
389
- document.fonts.forEach((font) => {
390
- console.log(` - ${font.family}: weight=${font.weight}, style=${font.style}, status=${font.status}`);
391
- });
392
- console.log("[ugcinc-render/fonts] Font family stacks:");
393
- console.log(" - tiktok:", FONT_FAMILIES.tiktok);
394
- console.log(" - apple:", FONT_FAMILIES.apple);
395
- console.log(" - arial:", FONT_FAMILIES.arial);
396
- console.log("[ugcinc-render/fonts] === END FONT DEBUG ===");
397
- }
398
-
399
- // src/utils/text.ts
400
- function wrapText({
401
- text,
402
- maxWidth,
403
- letterSpacing = 0,
404
- textWrap = "word",
405
- maxLines = 0,
406
- measureText
407
- }) {
408
- const lines = [];
409
- if (textWrap === "none") {
410
- lines.push(text);
411
- return lines;
412
- }
413
- const words = textWrap === "word" ? text.split(" ") : text.split("");
414
- let currentLine = "";
415
- for (let i = 0; i < words.length; i++) {
416
- const word = words[i];
417
- if (!word) continue;
418
- const testLine = currentLine + (currentLine ? textWrap === "word" ? " " : "" : "") + word;
419
- const charCount = [...testLine].length;
420
- const extraSpacing = charCount > 1 ? (charCount - 1) * letterSpacing : 0;
421
- const totalWidth = measureText(testLine) + extraSpacing;
422
- if (totalWidth > maxWidth && currentLine) {
423
- lines.push(currentLine);
424
- currentLine = word;
425
- if (maxLines && lines.length >= maxLines) {
426
- break;
427
- }
428
- } else {
429
- currentLine = testLine;
430
- }
431
- }
432
- if (currentLine && (!maxLines || lines.length < maxLines)) {
433
- lines.push(currentLine);
434
- }
435
- if (maxLines && lines.length > maxLines) {
436
- lines.splice(maxLines);
437
- }
438
- return lines;
439
- }
440
- function calculateLineWidth({
441
- line,
442
- letterSpacing,
443
- measureText
444
- }) {
445
- const chars = [...line];
446
- let width = 0;
447
- for (const char of chars) {
448
- width += measureText(char) + letterSpacing;
449
- }
450
- if (chars.length > 0) {
451
- width -= letterSpacing;
452
- }
453
- return width;
454
- }
455
- function getBorderRadii(borderRadius) {
456
- if (!borderRadius) return null;
457
- if (typeof borderRadius === "number") {
458
- if (borderRadius <= 0) return null;
459
- return [borderRadius, borderRadius, borderRadius, borderRadius];
460
- }
461
- const topLeft = borderRadius.topLeft ?? 0;
462
- const topRight = borderRadius.topRight ?? 0;
463
- const bottomRight = borderRadius.bottomRight ?? 0;
464
- const bottomLeft = borderRadius.bottomLeft ?? 0;
465
- if (topLeft <= 0 && topRight <= 0 && bottomRight <= 0 && bottomLeft <= 0) {
466
- return null;
467
- }
468
- return [topLeft, topRight, bottomRight, bottomLeft];
469
- }
470
- function parseHexColor(hex) {
471
- const cleanHex = hex.replace("#", "");
472
- return {
473
- r: parseInt(cleanHex.substring(0, 2), 16),
474
- g: parseInt(cleanHex.substring(2, 4), 16),
475
- b: parseInt(cleanHex.substring(4, 6), 16)
476
- };
477
- }
478
- function hexToRgba(hex, opacity = 100) {
479
- const { r, g, b } = parseHexColor(hex);
480
- return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
481
- }
482
-
483
- // src/components/TextElement.tsx
484
242
  import { jsx, jsxs } from "react/jsx-runtime";
485
243
  function calculateAutoWidthAndLines({
486
244
  text,
@@ -864,524 +622,6 @@ function ImageElement({ segment, src, startFrame = 0, scale = 1 }) {
864
622
  ) });
865
623
  }
866
624
 
867
- // src/utils/positionResolver.ts
868
- var FONT_FAMILIES2 = {
869
- tiktok: '"TikTok Sans", sans-serif',
870
- apple: '"SF Pro", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif',
871
- arial: "Arial, sans-serif"
872
- };
873
- function calculateAutoWidthDimensions(elem, textContent, ctx) {
874
- if (elem.type !== "text") {
875
- return null;
876
- }
877
- let measureCtx = ctx;
878
- if (!measureCtx) {
879
- if (typeof document === "undefined") {
880
- return null;
881
- }
882
- const canvas = document.createElement("canvas");
883
- measureCtx = canvas.getContext("2d");
884
- if (!measureCtx) return null;
885
- }
886
- const fontSize = elem.fontSize ?? 40;
887
- const fontWeight = elem.fontWeight ?? "bold";
888
- const fontType = elem.font ?? "tiktok";
889
- const paddingLeft = elem.paddingLeft ?? 0;
890
- const paddingRight = elem.paddingRight ?? 0;
891
- const paddingTop = elem.paddingTop ?? 0;
892
- const paddingBottom = elem.paddingBottom ?? 0;
893
- const letterSpacing = elem.letterSpacing ?? 0;
894
- const lineHeight = elem.lineHeight ?? 1.2;
895
- const maxWidth = elem.width;
896
- const boxAlign = elem.boxAlign ?? "left";
897
- const autoWidth = elem.autoWidth ?? false;
898
- const fontFamily = FONT_FAMILIES2[fontType] ?? FONT_FAMILIES2.tiktok;
899
- measureCtx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
900
- const availableWidth = maxWidth - paddingLeft - paddingRight;
901
- const words = textContent.split(" ");
902
- const lines = [];
903
- let currentLine = "";
904
- for (const word of words) {
905
- if (!word) continue;
906
- const testLine = currentLine + (currentLine ? " " : "") + word;
907
- const charCount = [...testLine].length;
908
- const extraSpacing = charCount > 1 ? (charCount - 1) * letterSpacing : 0;
909
- const totalWidth = measureCtx.measureText(testLine).width + extraSpacing;
910
- if (totalWidth > availableWidth && currentLine) {
911
- lines.push(currentLine);
912
- currentLine = word;
913
- } else {
914
- currentLine = testLine;
915
- }
916
- }
917
- if (currentLine) lines.push(currentLine);
918
- if (lines.length === 0) lines.push("");
919
- let widestLineWidth = 0;
920
- for (const line of lines) {
921
- const chars = [...line];
922
- let lineWidth = 0;
923
- for (const char of chars) {
924
- lineWidth += measureCtx.measureText(char).width + letterSpacing;
925
- }
926
- if (chars.length > 0) lineWidth -= letterSpacing;
927
- widestLineWidth = Math.max(widestLineWidth, lineWidth);
928
- }
929
- const actualWidth = autoWidth ? Math.min(widestLineWidth + paddingLeft + paddingRight, maxWidth) : maxWidth;
930
- const textHeight = lines.length * fontSize * lineHeight;
931
- const actualHeight = textHeight + paddingTop + paddingBottom;
932
- let actualX;
933
- if (autoWidth) {
934
- switch (boxAlign) {
935
- case "right":
936
- actualX = elem.x + maxWidth - actualWidth;
937
- break;
938
- case "center":
939
- actualX = elem.x + (maxWidth - actualWidth) / 2;
940
- break;
941
- case "left":
942
- default:
943
- actualX = elem.x;
944
- break;
945
- }
946
- } else {
947
- actualX = elem.x;
948
- }
949
- return { actualWidth, actualX, actualHeight, lineCount: lines.length };
950
- }
951
- function detectCircularDependency(elements, startId, axis, visited = /* @__PURE__ */ new Set(), path = []) {
952
- if (visited.has(startId)) {
953
- const cycleStartIndex = path.indexOf(startId);
954
- return path.slice(cycleStartIndex).concat(startId);
955
- }
956
- const element = elements.find((e) => e.id === startId);
957
- const relConfig = axis === "x" ? element?.relativePositionX : element?.relativePositionY;
958
- if (!relConfig) {
959
- return null;
960
- }
961
- visited.add(startId);
962
- path.push(startId);
963
- const result = detectCircularDependency(
964
- elements,
965
- relConfig.elementId,
966
- axis,
967
- visited,
968
- path
969
- );
970
- visited.delete(startId);
971
- path.pop();
972
- return result;
973
- }
974
- function topologicalSortForAxis(elements, axis) {
975
- const errors = [];
976
- const elementMap = new Map(elements.map((e) => [e.id, e]));
977
- const dependents = /* @__PURE__ */ new Map();
978
- const dependencyCount = /* @__PURE__ */ new Map();
979
- for (const elem of elements) {
980
- dependents.set(elem.id, []);
981
- dependencyCount.set(elem.id, 0);
982
- }
983
- for (const elem of elements) {
984
- const relConfig = axis === "x" ? elem.relativePositionX : elem.relativePositionY;
985
- if (relConfig) {
986
- const refId = relConfig.elementId;
987
- if (!elementMap.has(refId)) {
988
- errors.push({
989
- elementId: elem.id,
990
- type: "missing_reference",
991
- message: `Element references non-existent element "${refId}" for ${axis.toUpperCase()} position`,
992
- axis
993
- });
994
- continue;
995
- }
996
- const cycle = detectCircularDependency(elements, elem.id, axis);
997
- if (cycle) {
998
- errors.push({
999
- elementId: elem.id,
1000
- type: "circular_dependency",
1001
- message: `Circular dependency detected for ${axis.toUpperCase()}: ${cycle.join(" \u2192 ")}`,
1002
- axis,
1003
- cyclePath: cycle
1004
- });
1005
- continue;
1006
- }
1007
- dependents.get(refId)?.push(elem.id);
1008
- dependencyCount.set(elem.id, (dependencyCount.get(elem.id) ?? 0) + 1);
1009
- }
1010
- }
1011
- const sorted = [];
1012
- const queue = [];
1013
- for (const [id, count] of dependencyCount) {
1014
- if (count === 0) {
1015
- queue.push(id);
1016
- }
1017
- }
1018
- while (queue.length > 0) {
1019
- const currentId = queue.shift();
1020
- const current = elementMap.get(currentId);
1021
- if (current) {
1022
- sorted.push(current);
1023
- }
1024
- for (const dependentId of dependents.get(currentId) ?? []) {
1025
- const newCount = (dependencyCount.get(dependentId) ?? 1) - 1;
1026
- dependencyCount.set(dependentId, newCount);
1027
- if (newCount === 0) {
1028
- queue.push(dependentId);
1029
- }
1030
- }
1031
- }
1032
- for (const elem of elements) {
1033
- if (!sorted.find((e) => e.id === elem.id)) {
1034
- sorted.push(elem);
1035
- }
1036
- }
1037
- return { sorted, errors };
1038
- }
1039
- function getSelfAnchorOffsetX(element, selfAnchor, actualWidth) {
1040
- const width = actualWidth ?? element.width;
1041
- switch (selfAnchor) {
1042
- case "left":
1043
- return 0;
1044
- case "center":
1045
- return width / 2;
1046
- case "right":
1047
- return width;
1048
- default:
1049
- return 0;
1050
- }
1051
- }
1052
- function getSelfAnchorOffsetY(element, selfAnchor, actualHeight) {
1053
- const height = actualHeight ?? element.height;
1054
- switch (selfAnchor) {
1055
- case "top":
1056
- return 0;
1057
- case "middle":
1058
- return height / 2;
1059
- case "bottom":
1060
- return height;
1061
- default:
1062
- return 0;
1063
- }
1064
- }
1065
- function calculateAbsoluteX(element, referenceElement, anchor, selfAnchor, offset, actualWidth) {
1066
- const refX = anchor === "left" ? referenceElement.x : referenceElement.x + referenceElement.width;
1067
- const selfOffset = getSelfAnchorOffsetX(element, selfAnchor, actualWidth);
1068
- return refX + offset - selfOffset;
1069
- }
1070
- function calculateAbsoluteY(element, referenceElement, anchor, selfAnchor, offset, actualHeight) {
1071
- const refY = anchor === "top" ? referenceElement.y : referenceElement.y + referenceElement.height;
1072
- const selfOffset = getSelfAnchorOffsetY(element, selfAnchor, actualHeight);
1073
- return refY + offset - selfOffset;
1074
- }
1075
- function resolveElementPositions(elements, textValues) {
1076
- if (elements.length === 0) {
1077
- return { elements: [], errors: [] };
1078
- }
1079
- const allErrors = [];
1080
- const { sorted: sortedX, errors: errorsX } = topologicalSortForAxis(elements, "x");
1081
- allErrors.push(...errorsX);
1082
- const { sorted: sortedY, errors: errorsY } = topologicalSortForAxis(elements, "y");
1083
- allErrors.push(...errorsY);
1084
- const resolvedX = /* @__PURE__ */ new Map();
1085
- const resolvedY = /* @__PURE__ */ new Map();
1086
- let measureCtx = null;
1087
- const hasAutoWidthElements = elements.some((e) => e.type === "text" && e.autoWidth);
1088
- if (hasAutoWidthElements && typeof document !== "undefined") {
1089
- const canvas = document.createElement("canvas");
1090
- measureCtx = canvas.getContext("2d");
1091
- }
1092
- const getTextContent = (elem) => {
1093
- if (elem.type !== "text") return "";
1094
- if (elem.textInputId && textValues?.[elem.textInputId]) {
1095
- return textValues[elem.textInputId];
1096
- }
1097
- return elem.text ?? "Text";
1098
- };
1099
- const referenceX = /* @__PURE__ */ new Map();
1100
- const referenceY = /* @__PURE__ */ new Map();
1101
- for (const elem of elements) {
1102
- resolvedX.set(elem.id, { x: elem.x, width: elem.width });
1103
- resolvedY.set(elem.id, { y: elem.y, height: elem.height });
1104
- if (elem.type === "text") {
1105
- const textContent = getTextContent(elem);
1106
- const autoResult = calculateAutoWidthDimensions(elem, textContent, measureCtx);
1107
- if (autoResult) {
1108
- if (elem.autoWidth) {
1109
- referenceX.set(elem.id, { x: autoResult.actualX, width: autoResult.actualWidth });
1110
- } else {
1111
- referenceX.set(elem.id, { x: elem.x, width: elem.width });
1112
- }
1113
- referenceY.set(elem.id, { y: elem.y, height: autoResult.actualHeight });
1114
- } else {
1115
- referenceX.set(elem.id, { x: elem.x, width: elem.width });
1116
- referenceY.set(elem.id, { y: elem.y, height: elem.height });
1117
- }
1118
- } else {
1119
- referenceX.set(elem.id, { x: elem.x, width: elem.width });
1120
- referenceY.set(elem.id, { y: elem.y, height: elem.height });
1121
- }
1122
- }
1123
- for (const elem of sortedX) {
1124
- if (elem.relativePositionX) {
1125
- const refId = elem.relativePositionX.elementId;
1126
- const refPosition = referenceX.get(refId);
1127
- if (refPosition) {
1128
- const defaultSelfAnchor = elem.relativePositionX.anchor === "right" ? "left" : "right";
1129
- const selfAnchor = elem.relativePositionX.selfAnchor ?? defaultSelfAnchor;
1130
- let actualWidth;
1131
- let autoResult = null;
1132
- if (elem.type === "text") {
1133
- const textContent = getTextContent(elem);
1134
- autoResult = calculateAutoWidthDimensions(elem, textContent, measureCtx);
1135
- if (autoResult && elem.autoWidth) {
1136
- actualWidth = autoResult.actualWidth;
1137
- }
1138
- }
1139
- const newX = calculateAbsoluteX(
1140
- elem,
1141
- refPosition,
1142
- elem.relativePositionX.anchor,
1143
- selfAnchor,
1144
- elem.relativePositionX.offset,
1145
- actualWidth
1146
- // Use actual width for self anchor calculation
1147
- );
1148
- resolvedX.set(elem.id, { x: newX, width: elem.width });
1149
- if (autoResult) {
1150
- if (elem.autoWidth) {
1151
- const updatedResult = calculateAutoWidthDimensions({ ...elem, x: newX }, getTextContent(elem), measureCtx);
1152
- if (updatedResult) {
1153
- referenceX.set(elem.id, { x: updatedResult.actualX, width: updatedResult.actualWidth });
1154
- referenceY.set(elem.id, { y: referenceY.get(elem.id)?.y ?? elem.y, height: updatedResult.actualHeight });
1155
- } else {
1156
- referenceX.set(elem.id, { x: newX, width: elem.width });
1157
- }
1158
- } else {
1159
- referenceX.set(elem.id, { x: newX, width: elem.width });
1160
- referenceY.set(elem.id, { y: referenceY.get(elem.id)?.y ?? elem.y, height: autoResult.actualHeight });
1161
- }
1162
- } else {
1163
- referenceX.set(elem.id, { x: newX, width: elem.width });
1164
- }
1165
- }
1166
- }
1167
- }
1168
- for (const elem of sortedY) {
1169
- if (elem.relativePositionY) {
1170
- const refId = elem.relativePositionY.elementId;
1171
- const refPosition = referenceY.get(refId);
1172
- if (refPosition) {
1173
- const defaultSelfAnchor = elem.relativePositionY.anchor === "bottom" ? "top" : "bottom";
1174
- const selfAnchor = elem.relativePositionY.selfAnchor ?? defaultSelfAnchor;
1175
- let actualHeight;
1176
- if (elem.type === "text") {
1177
- const textContent = getTextContent(elem);
1178
- const autoResult = calculateAutoWidthDimensions(elem, textContent, measureCtx);
1179
- if (autoResult) {
1180
- actualHeight = autoResult.actualHeight;
1181
- }
1182
- }
1183
- const newY = calculateAbsoluteY(
1184
- elem,
1185
- refPosition,
1186
- elem.relativePositionY.anchor,
1187
- selfAnchor,
1188
- elem.relativePositionY.offset,
1189
- actualHeight
1190
- // Use actual height for self anchor calculation
1191
- );
1192
- resolvedY.set(elem.id, { y: newY, height: elem.height });
1193
- if (actualHeight !== void 0) {
1194
- referenceY.set(elem.id, { y: newY, height: actualHeight });
1195
- } else {
1196
- referenceY.set(elem.id, { y: newY, height: elem.height });
1197
- }
1198
- }
1199
- }
1200
- }
1201
- const resolvedElements = elements.map((elem) => ({
1202
- ...elem,
1203
- x: resolvedX.get(elem.id)?.x ?? elem.x,
1204
- y: resolvedY.get(elem.id)?.y ?? elem.y
1205
- }));
1206
- return { elements: resolvedElements, errors: allErrors };
1207
- }
1208
- function canSetAsReference(elements, elementId, proposedReferenceId, axis) {
1209
- if (elementId === proposedReferenceId) {
1210
- return false;
1211
- }
1212
- const testElements = elements.map((e) => {
1213
- if (e.id !== elementId) return e;
1214
- if (axis === "x") {
1215
- return {
1216
- ...e,
1217
- relativePositionX: { elementId: proposedReferenceId, anchor: "right", offset: 0 }
1218
- };
1219
- } else {
1220
- return {
1221
- ...e,
1222
- relativePositionY: { elementId: proposedReferenceId, anchor: "bottom", offset: 0 }
1223
- };
1224
- }
1225
- });
1226
- const cycle = detectCircularDependency(testElements, elementId, axis);
1227
- return cycle === null;
1228
- }
1229
- function getDependentElements(elements, elementId) {
1230
- const dependents = [];
1231
- const visited = /* @__PURE__ */ new Set();
1232
- function findDependents(id) {
1233
- for (const elem of elements) {
1234
- const dependsOnX = elem.relativePositionX?.elementId === id;
1235
- const dependsOnY = elem.relativePositionY?.elementId === id;
1236
- if ((dependsOnX || dependsOnY) && !visited.has(elem.id)) {
1237
- visited.add(elem.id);
1238
- dependents.push(elem.id);
1239
- findDependents(elem.id);
1240
- }
1241
- }
1242
- }
1243
- findDependents(elementId);
1244
- return dependents;
1245
- }
1246
- function getReferenceElementX(elements, elementId) {
1247
- const element = elements.find((e) => e.id === elementId);
1248
- if (!element?.relativePositionX) {
1249
- return null;
1250
- }
1251
- return elements.find((e) => e.id === element.relativePositionX.elementId) ?? null;
1252
- }
1253
- function getReferenceElementY(elements, elementId) {
1254
- const element = elements.find((e) => e.id === elementId);
1255
- if (!element?.relativePositionY) {
1256
- return null;
1257
- }
1258
- return elements.find((e) => e.id === element.relativePositionY.elementId) ?? null;
1259
- }
1260
-
1261
- // src/utils/cropBounds.ts
1262
- function getActualElementHeight(elem, textValues, measureCtx) {
1263
- if (elem.type !== "text") return elem.height;
1264
- let textContent = elem.text ?? "Text";
1265
- if (elem.textInputId && textValues?.[elem.textInputId]) {
1266
- textContent = textValues[elem.textInputId];
1267
- }
1268
- const result = calculateAutoWidthDimensions(elem, textContent, measureCtx);
1269
- return result?.actualHeight ?? elem.height;
1270
- }
1271
- function calculateCropBounds(elements, dynamicCrop, canvasWidth, canvasHeight, textValues) {
1272
- if (!dynamicCrop) {
1273
- return { x: 0, y: 0, width: canvasWidth, height: canvasHeight };
1274
- }
1275
- const elementMap = /* @__PURE__ */ new Map();
1276
- for (const elem of elements) {
1277
- elementMap.set(elem.id, elem);
1278
- }
1279
- const resolveBoundary = (boundary) => {
1280
- if (!boundary) return void 0;
1281
- if (boundary.elementId) return boundary.elementId;
1282
- return void 0;
1283
- };
1284
- let measureCtx = null;
1285
- const hasTextElements = elements.some((e) => e.type === "text");
1286
- if (hasTextElements && typeof document !== "undefined") {
1287
- const canvas = document.createElement("canvas");
1288
- measureCtx = canvas.getContext("2d");
1289
- }
1290
- let cropY = 0;
1291
- let cropHeight = canvasHeight;
1292
- if (dynamicCrop.vertical?.enabled) {
1293
- const vCrop = dynamicCrop.vertical;
1294
- const paddingStart = vCrop.paddingStart ?? 0;
1295
- const paddingEnd = vCrop.paddingEnd ?? 0;
1296
- if (vCrop.mode === "all-elements") {
1297
- let minY = canvasHeight;
1298
- let maxY = 0;
1299
- for (const elem of elements) {
1300
- const actualHeight = getActualElementHeight(elem, textValues, measureCtx);
1301
- minY = Math.min(minY, elem.y);
1302
- maxY = Math.max(maxY, elem.y + actualHeight);
1303
- }
1304
- if (elements.length > 0) {
1305
- cropY = Math.max(0, minY - paddingStart);
1306
- const bottomY = Math.min(canvasHeight, maxY + paddingEnd);
1307
- cropHeight = bottomY - cropY;
1308
- }
1309
- } else if (vCrop.mode === "between-elements") {
1310
- const startElementId = resolveBoundary(vCrop.startBoundary);
1311
- const endElementId = resolveBoundary(vCrop.endBoundary);
1312
- let topY = 0;
1313
- let bottomY = canvasHeight;
1314
- if (startElementId) {
1315
- const startElem = elementMap.get(startElementId);
1316
- if (startElem) {
1317
- topY = startElem.y;
1318
- }
1319
- }
1320
- if (endElementId) {
1321
- const endElem = elementMap.get(endElementId);
1322
- if (endElem) {
1323
- const actualHeight = getActualElementHeight(endElem, textValues, measureCtx);
1324
- bottomY = endElem.y + actualHeight;
1325
- }
1326
- }
1327
- cropY = Math.max(0, topY - paddingStart);
1328
- const adjustedBottom = Math.min(canvasHeight, bottomY + paddingEnd);
1329
- cropHeight = adjustedBottom - cropY;
1330
- }
1331
- if (vCrop.minSize && cropHeight < vCrop.minSize) {
1332
- cropHeight = vCrop.minSize;
1333
- }
1334
- }
1335
- let cropX = 0;
1336
- let cropWidth = canvasWidth;
1337
- if (dynamicCrop.horizontal?.enabled) {
1338
- const hCrop = dynamicCrop.horizontal;
1339
- const paddingStart = hCrop.paddingStart ?? 0;
1340
- const paddingEnd = hCrop.paddingEnd ?? 0;
1341
- if (hCrop.mode === "all-elements") {
1342
- let minX = canvasWidth;
1343
- let maxX = 0;
1344
- for (const elem of elements) {
1345
- minX = Math.min(minX, elem.x);
1346
- maxX = Math.max(maxX, elem.x + elem.width);
1347
- }
1348
- if (elements.length > 0) {
1349
- cropX = Math.max(0, minX - paddingStart);
1350
- const rightX = Math.min(canvasWidth, maxX + paddingEnd);
1351
- cropWidth = rightX - cropX;
1352
- }
1353
- } else if (hCrop.mode === "between-elements") {
1354
- const startElementId = resolveBoundary(hCrop.startBoundary);
1355
- const endElementId = resolveBoundary(hCrop.endBoundary);
1356
- let leftX = 0;
1357
- let rightX = canvasWidth;
1358
- if (startElementId) {
1359
- const startElem = elementMap.get(startElementId);
1360
- if (startElem) {
1361
- leftX = startElem.x;
1362
- }
1363
- }
1364
- if (endElementId) {
1365
- const endElem = elementMap.get(endElementId);
1366
- if (endElem) {
1367
- rightX = endElem.x + endElem.width;
1368
- }
1369
- }
1370
- cropX = Math.max(0, leftX - paddingStart);
1371
- const adjustedRight = Math.min(canvasWidth, rightX + paddingEnd);
1372
- cropWidth = adjustedRight - cropX;
1373
- }
1374
- if (hCrop.minSize && cropWidth < hCrop.minSize) {
1375
- cropWidth = hCrop.minSize;
1376
- }
1377
- }
1378
- return { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
1379
- }
1380
- function isDynamicCropEnabled(dynamicCrop) {
1381
- if (!dynamicCrop) return false;
1382
- return !!(dynamicCrop.vertical?.enabled || dynamicCrop.horizontal?.enabled);
1383
- }
1384
-
1385
625
  // src/compositions/ImageEditorComposition.tsx
1386
626
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1387
627
  function getSortedSegments(config) {
@@ -2145,289 +1385,6 @@ function AutoCaptionCompositionWithVideo({
2145
1385
  ] });
2146
1386
  }
2147
1387
 
2148
- // src/utils/fit.ts
2149
- function calculateFitDimensions({
2150
- sourceWidth,
2151
- sourceHeight,
2152
- targetWidth,
2153
- targetHeight,
2154
- fit
2155
- }) {
2156
- if (fit === "fill") {
2157
- return {
2158
- x: 0,
2159
- y: 0,
2160
- width: targetWidth,
2161
- height: targetHeight,
2162
- sourceX: 0,
2163
- sourceY: 0,
2164
- sourceWidth,
2165
- sourceHeight
2166
- };
2167
- }
2168
- const sourceRatio = sourceWidth / sourceHeight;
2169
- const targetRatio = targetWidth / targetHeight;
2170
- let width = targetWidth;
2171
- let height = targetHeight;
2172
- let x = 0;
2173
- let y = 0;
2174
- let sourceX = 0;
2175
- let sourceY = 0;
2176
- let croppedSourceWidth = sourceWidth;
2177
- let croppedSourceHeight = sourceHeight;
2178
- if (fit === "contain") {
2179
- if (sourceRatio > targetRatio) {
2180
- height = targetWidth / sourceRatio;
2181
- y = (targetHeight - height) / 2;
2182
- } else {
2183
- width = targetHeight * sourceRatio;
2184
- x = (targetWidth - width) / 2;
2185
- }
2186
- } else if (fit === "cover") {
2187
- if (sourceRatio > targetRatio) {
2188
- croppedSourceWidth = sourceHeight * targetRatio;
2189
- sourceX = (sourceWidth - croppedSourceWidth) / 2;
2190
- } else {
2191
- croppedSourceHeight = sourceWidth / targetRatio;
2192
- sourceY = (sourceHeight - croppedSourceHeight) / 2;
2193
- }
2194
- }
2195
- return {
2196
- x,
2197
- y,
2198
- width,
2199
- height,
2200
- sourceX,
2201
- sourceY,
2202
- sourceWidth: croppedSourceWidth,
2203
- sourceHeight: croppedSourceHeight
2204
- };
2205
- }
2206
-
2207
- // src/utils/timeline.ts
2208
- function defaultOffset(mode = "flexible") {
2209
- return mode === "flexible" ? { type: "relative", value: 0 } : { type: "absolute", value: 0 };
2210
- }
2211
- function getBaseSegments(channel) {
2212
- return channel.segments.filter((s) => s.parentId === void 0);
2213
- }
2214
- function getOverlays(channel, parentId) {
2215
- return channel.segments.filter((s) => s.parentId === (parentId ?? void 0));
2216
- }
2217
- function getSegmentTimelinePosition(segment, baseSegments, channel) {
2218
- if (segment.parentId) {
2219
- const parent = channel.segments.find((s) => s.id === segment.parentId);
2220
- if (parent) {
2221
- const parentPos = getSegmentTimelinePosition(parent, baseSegments, channel);
2222
- const relStart = segment.relativeStart ?? 0;
2223
- const relEnd = segment.relativeEnd ?? 1;
2224
- return {
2225
- startMs: parentPos.startMs + parentPos.durationMs * relStart,
2226
- durationMs: parentPos.durationMs * (relEnd - relStart)
2227
- };
2228
- }
2229
- }
2230
- const baseIndex = baseSegments.findIndex((s) => s.id === segment.id);
2231
- let accumulatedTime = 0;
2232
- for (let i = 0; i < baseIndex; i++) {
2233
- const prev = baseSegments[i];
2234
- if (prev) {
2235
- accumulatedTime += prev.duration?.type === "absolute" ? prev.duration.value : 5e3;
2236
- }
2237
- }
2238
- const startMs = segment.offset.type === "absolute" ? segment.offset.value : accumulatedTime;
2239
- const durationMs = segment.duration?.type === "absolute" ? segment.duration.value : 5e3;
2240
- return { startMs, durationMs };
2241
- }
2242
- function isSegmentVisibleAtTime(segment, time, channel) {
2243
- const baseSegments = getBaseSegments(channel);
2244
- const { startMs, durationMs } = getSegmentTimelinePosition(segment, baseSegments, channel);
2245
- const endMs = startMs + durationMs;
2246
- return time >= startMs && time < endMs;
2247
- }
2248
- function calculateEstimatedDuration(channels) {
2249
- let maxDuration = 5e3;
2250
- for (const channel of channels) {
2251
- let channelTime = 0;
2252
- for (const segment of channel.segments) {
2253
- if (segment.parentId) continue;
2254
- if (segment.offset.type === "absolute") {
2255
- channelTime = segment.offset.value;
2256
- } else {
2257
- channelTime += 5e3;
2258
- }
2259
- if (segment.duration?.type === "absolute") {
2260
- channelTime += segment.duration.value;
2261
- } else {
2262
- channelTime += 5e3;
2263
- }
2264
- }
2265
- maxDuration = Math.max(maxDuration, channelTime);
2266
- }
2267
- return maxDuration;
2268
- }
2269
- function calculateTimelineContentEnd(channel) {
2270
- const baseSegments = getBaseSegments(channel);
2271
- let lastEnd = 0;
2272
- for (const segment of baseSegments) {
2273
- const { startMs, durationMs } = getSegmentTimelinePosition(segment, baseSegments, channel);
2274
- lastEnd = Math.max(lastEnd, startMs + durationMs);
2275
- }
2276
- return Math.ceil((lastEnd + 2e3) / 1e3) * 1e3;
2277
- }
2278
- function formatTime(ms) {
2279
- const totalSeconds = Math.floor(ms / 1e3);
2280
- const minutes = Math.floor(totalSeconds / 60);
2281
- const seconds = totalSeconds % 60;
2282
- const milliseconds = Math.floor(ms % 1e3 / 10);
2283
- return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${milliseconds.toString().padStart(2, "0")}`;
2284
- }
2285
- function parseTime(timeStr) {
2286
- const parts = timeStr.split(":");
2287
- if (parts.length !== 2) return 0;
2288
- const [minStr, secPart] = parts;
2289
- const minutes = parseInt(minStr ?? "0", 10) || 0;
2290
- const secParts = (secPart ?? "0").split(".");
2291
- const seconds = parseInt(secParts[0] ?? "0", 10) || 0;
2292
- const ms = parseInt((secParts[1] ?? "0").padEnd(2, "0").slice(0, 2), 10) * 10 || 0;
2293
- return (minutes * 60 + seconds) * 1e3 + ms;
2294
- }
2295
- function generateSegmentId() {
2296
- return `segment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2297
- }
2298
- function generateOverlayId() {
2299
- return `overlay-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2300
- }
2301
-
2302
- // src/utils/captionPresets.ts
2303
- var CAPTION_PRESETS = {
2304
- /**
2305
- * Hormozi style - bold, attention-grabbing captions
2306
- * Yellow highlight on white text with thick black stroke
2307
- */
2308
- hormozi: {
2309
- fontName: "Montserrat",
2310
- fontSize: 80,
2311
- fontWeight: "black",
2312
- fontColor: "#FFFFFF",
2313
- highlightColor: "#FFFF00",
2314
- strokeColor: "#000000",
2315
- strokeWidth: 3,
2316
- position: "bottom",
2317
- yOffset: 100,
2318
- maxWidth: 90,
2319
- enableAnimation: true,
2320
- wordsPerPage: 3
2321
- },
2322
- /**
2323
- * Minimal style - clean and understated
2324
- * White on white (no highlight distinction), smaller text
2325
- */
2326
- minimal: {
2327
- fontName: "Inter",
2328
- fontSize: 60,
2329
- fontWeight: "bold",
2330
- fontColor: "#FFFFFF",
2331
- highlightColor: "#FFFFFF",
2332
- strokeColor: "#000000",
2333
- strokeWidth: 2,
2334
- position: "bottom",
2335
- yOffset: 80,
2336
- maxWidth: 85,
2337
- enableAnimation: false,
2338
- wordsPerPage: 5
2339
- },
2340
- /**
2341
- * Bold Pop style - maximum impact, one word at a time
2342
- * Yellow text with red highlight, centered position
2343
- */
2344
- "bold-pop": {
2345
- fontName: "Bangers",
2346
- fontSize: 90,
2347
- fontWeight: "bold",
2348
- fontColor: "#FFFF00",
2349
- highlightColor: "#FF0000",
2350
- strokeColor: "#000000",
2351
- strokeWidth: 4,
2352
- position: "center",
2353
- yOffset: 0,
2354
- maxWidth: 80,
2355
- enableAnimation: true,
2356
- wordsPerPage: 2
2357
- },
2358
- /**
2359
- * Clean style - professional look with background
2360
- * White text with blue highlight, semi-transparent background
2361
- */
2362
- clean: {
2363
- fontName: "Roboto",
2364
- fontSize: 55,
2365
- fontWeight: "normal",
2366
- fontColor: "#FFFFFF",
2367
- highlightColor: "#3B82F6",
2368
- strokeColor: "#000000",
2369
- strokeWidth: 0,
2370
- backgroundColor: "#000000",
2371
- backgroundOpacity: 0.7,
2372
- backgroundBorderRadius: 8,
2373
- position: "bottom",
2374
- yOffset: 80,
2375
- maxWidth: 90,
2376
- enableAnimation: false,
2377
- wordsPerPage: 6
2378
- },
2379
- /**
2380
- * Neon style - vibrant, eye-catching colors
2381
- * Cyan text with magenta highlight
2382
- */
2383
- neon: {
2384
- fontName: "Poppins",
2385
- fontSize: 70,
2386
- fontWeight: "bold",
2387
- fontColor: "#00FFFF",
2388
- highlightColor: "#FF00FF",
2389
- strokeColor: "#000000",
2390
- strokeWidth: 3,
2391
- position: "bottom",
2392
- yOffset: 100,
2393
- maxWidth: 85,
2394
- enableAnimation: true,
2395
- wordsPerPage: 3
2396
- }
2397
- };
2398
- var DEFAULT_CAPTION_STYLE = CAPTION_PRESETS.hormozi;
2399
- function resolveCaptionStyle(style) {
2400
- if (!style) {
2401
- return { ...DEFAULT_CAPTION_STYLE };
2402
- }
2403
- const presetValues = style.preset ? CAPTION_PRESETS[style.preset] : {};
2404
- const defaults = DEFAULT_CAPTION_STYLE;
2405
- return {
2406
- preset: style.preset,
2407
- fontName: style.fontName ?? presetValues.fontName ?? defaults.fontName,
2408
- fontSize: style.fontSize ?? presetValues.fontSize ?? defaults.fontSize,
2409
- fontWeight: style.fontWeight ?? presetValues.fontWeight ?? defaults.fontWeight,
2410
- fontColor: style.fontColor ?? presetValues.fontColor ?? defaults.fontColor,
2411
- highlightColor: style.highlightColor ?? presetValues.highlightColor ?? defaults.highlightColor,
2412
- strokeColor: style.strokeColor ?? presetValues.strokeColor ?? defaults.strokeColor,
2413
- strokeWidth: style.strokeWidth ?? presetValues.strokeWidth ?? defaults.strokeWidth,
2414
- backgroundColor: style.backgroundColor ?? presetValues.backgroundColor,
2415
- backgroundOpacity: style.backgroundOpacity ?? presetValues.backgroundOpacity,
2416
- backgroundBorderRadius: style.backgroundBorderRadius ?? presetValues.backgroundBorderRadius,
2417
- position: style.position ?? presetValues.position ?? defaults.position,
2418
- yOffset: style.yOffset ?? presetValues.yOffset ?? defaults.yOffset,
2419
- maxWidth: style.maxWidth ?? presetValues.maxWidth ?? defaults.maxWidth,
2420
- enableAnimation: style.enableAnimation ?? presetValues.enableAnimation ?? defaults.enableAnimation,
2421
- wordsPerPage: style.wordsPerPage ?? presetValues.wordsPerPage ?? defaults.wordsPerPage
2422
- };
2423
- }
2424
- function getCaptionPresetNames() {
2425
- return Object.keys(CAPTION_PRESETS);
2426
- }
2427
- function isValidCaptionPreset(name) {
2428
- return name in CAPTION_PRESETS;
2429
- }
2430
-
2431
1388
  // src/hooks/index.ts
2432
1389
  import { useEffect as useEffect2, useState as useState2, useMemo as useMemo7 } from "react";
2433
1390
  function useFontsLoaded() {