@pixerate/editor 0.115.0 → 0.117.0

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.
Files changed (127) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/dist/animation-editor/index.cjs +54 -54
  3. package/dist/animation-editor/index.d.cts +2 -2
  4. package/dist/animation-editor/index.d.ts +2 -2
  5. package/dist/animation-editor/index.js +2 -2
  6. package/dist/{chunk-I7FQBWMQ.cjs → chunk-2J3LS7JZ.cjs} +333 -333
  7. package/dist/{chunk-I7FQBWMQ.cjs.map → chunk-2J3LS7JZ.cjs.map} +1 -1
  8. package/dist/{chunk-TFOPHZ3U.cjs → chunk-7ISPWEJA.cjs} +302 -5
  9. package/dist/chunk-7ISPWEJA.cjs.map +1 -0
  10. package/dist/{chunk-VXR7XWMF.js → chunk-7LNZAWSJ.js} +437 -41
  11. package/dist/chunk-7LNZAWSJ.js.map +1 -0
  12. package/dist/{chunk-DFQEX3AM.cjs → chunk-BYAWOAST.cjs} +223 -30
  13. package/dist/chunk-BYAWOAST.cjs.map +1 -0
  14. package/dist/chunk-COAMRKVI.cjs +159 -0
  15. package/dist/chunk-COAMRKVI.cjs.map +1 -0
  16. package/dist/{chunk-ST7E3NEE.cjs → chunk-EBSYZRSW.cjs} +230 -732
  17. package/dist/chunk-EBSYZRSW.cjs.map +1 -0
  18. package/dist/chunk-EJVDBITD.cjs +501 -0
  19. package/dist/chunk-EJVDBITD.cjs.map +1 -0
  20. package/dist/{chunk-LPLCWZTM.cjs → chunk-FODKPD44.cjs} +555 -159
  21. package/dist/chunk-FODKPD44.cjs.map +1 -0
  22. package/dist/{chunk-XK5LZKBD.js → chunk-HKDTM6VC.js} +298 -1
  23. package/dist/chunk-HKDTM6VC.js.map +1 -0
  24. package/dist/chunk-IPFRWILU.js +1908 -0
  25. package/dist/chunk-IPFRWILU.js.map +1 -0
  26. package/dist/chunk-IVSNUXW7.js +3339 -0
  27. package/dist/chunk-IVSNUXW7.js.map +1 -0
  28. package/dist/{chunk-LKCYANMW.js → chunk-JHXMNOMF.js} +198 -5
  29. package/dist/chunk-JHXMNOMF.js.map +1 -0
  30. package/dist/{chunk-FV5AXTSF.cjs → chunk-JSSBHMMU.cjs} +36 -188
  31. package/dist/chunk-JSSBHMMU.cjs.map +1 -0
  32. package/dist/chunk-L2T6EJ5S.cjs +3339 -0
  33. package/dist/chunk-L2T6EJ5S.cjs.map +1 -0
  34. package/dist/chunk-LF4WOZXZ.js +159 -0
  35. package/dist/chunk-LF4WOZXZ.js.map +1 -0
  36. package/dist/{chunk-26PGXHY7.cjs → chunk-M7O4BJRH.cjs} +36 -36
  37. package/dist/{chunk-26PGXHY7.cjs.map → chunk-M7O4BJRH.cjs.map} +1 -1
  38. package/dist/chunk-N3467VD6.js +501 -0
  39. package/dist/chunk-N3467VD6.js.map +1 -0
  40. package/dist/{chunk-XICA4PVO.js → chunk-R2JE6SA7.js} +17 -17
  41. package/dist/chunk-SMJGE7HM.cjs +1908 -0
  42. package/dist/chunk-SMJGE7HM.cjs.map +1 -0
  43. package/dist/{chunk-MMVHBKMM.js → chunk-T2P5TIZ6.js} +2 -2
  44. package/dist/{chunk-C6C5PDKU.js → chunk-VD2UDVF7.js} +102 -604
  45. package/dist/chunk-VD2UDVF7.js.map +1 -0
  46. package/dist/{chunk-FGW5OWJ4.js → chunk-VJUML33I.js} +4 -156
  47. package/dist/chunk-VJUML33I.js.map +1 -0
  48. package/dist/export/index.cjs +4 -2
  49. package/dist/export/index.cjs.map +1 -1
  50. package/dist/export/index.js +5 -3
  51. package/dist/{export-BJE8T0YA.d.cts → export-DMOuzgZZ.d.cts} +9 -4
  52. package/dist/{export-Con3UKkm.d.ts → export-Dul1SbPv.d.ts} +9 -4
  53. package/dist/export-Dv0-ZVrV.d.cts +501 -0
  54. package/dist/export-Dv0-ZVrV.d.ts +501 -0
  55. package/dist/{retouch-Bxkuc0KH.d.cts → flow-4-OC-UkQ.d.cts} +582 -2
  56. package/dist/{retouch-Do6tBs2H.d.ts → flow-Cb7lkPm8.d.ts} +582 -2
  57. package/dist/image-editor/index.cjs +1345 -1095
  58. package/dist/image-editor/index.cjs.map +1 -1
  59. package/dist/image-editor/index.d.cts +8 -8
  60. package/dist/image-editor/index.d.ts +8 -8
  61. package/dist/image-editor/index.js +300 -50
  62. package/dist/image-editor/index.js.map +1 -1
  63. package/dist/index.cjs +4 -2
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.js +5 -3
  66. package/dist/index.js.map +1 -1
  67. package/dist/kernel-wasm-WYCGNZR4.js +10 -0
  68. package/dist/kernel-wasm-WYCGNZR4.js.map +1 -0
  69. package/dist/kernel-wasm-YPVAEMVN.cjs +10 -0
  70. package/dist/kernel-wasm-YPVAEMVN.cjs.map +1 -0
  71. package/dist/paint/index.cjs +170 -4
  72. package/dist/paint/index.cjs.map +1 -1
  73. package/dist/paint/index.d.cts +225 -3
  74. package/dist/paint/index.d.ts +225 -3
  75. package/dist/paint/index.js +171 -5
  76. package/dist/paint-editor/index.cjs +4735 -890
  77. package/dist/paint-editor/index.cjs.map +1 -1
  78. package/dist/paint-editor/index.d.cts +1526 -278
  79. package/dist/paint-editor/index.d.ts +1526 -278
  80. package/dist/paint-editor/index.js +4561 -716
  81. package/dist/paint-editor/index.js.map +1 -1
  82. package/dist/raster/index.cjs +7 -4
  83. package/dist/raster/index.cjs.map +1 -1
  84. package/dist/raster/index.d.cts +2 -2
  85. package/dist/raster/index.d.ts +2 -2
  86. package/dist/raster/index.js +36 -33
  87. package/dist/{render-CrmHOgpo.d.ts → render-B05K68_h.d.ts} +9 -1
  88. package/dist/{render-B1K_OoLy.d.cts → render-DnAbIKcs.d.cts} +9 -1
  89. package/dist/{svg-BnRj0q8i.d.cts → svg-CVdFc80-.d.cts} +2 -2
  90. package/dist/{svg-CmmBFfGT.d.ts → svg-DlEuhwoX.d.ts} +2 -2
  91. package/dist/{types-1SUQKDAw.d.ts → types-CDLGhAZp.d.ts} +40 -2
  92. package/dist/{types-yINF17Ta.d.cts → types-Tfly4Aya.d.cts} +40 -2
  93. package/dist/vector/index.cjs +6536 -4525
  94. package/dist/vector/index.cjs.map +1 -1
  95. package/dist/vector/index.d.cts +426 -32
  96. package/dist/vector/index.d.ts +426 -32
  97. package/dist/vector/index.js +11397 -9386
  98. package/dist/vector/index.js.map +1 -1
  99. package/dist/video-editor/index.cjs +502 -2165
  100. package/dist/video-editor/index.cjs.map +1 -1
  101. package/dist/video-editor/index.d.cts +3 -501
  102. package/dist/video-editor/index.d.ts +3 -501
  103. package/dist/video-editor/index.js +151 -1814
  104. package/dist/video-editor/index.js.map +1 -1
  105. package/package.json +1 -1
  106. package/skills/pixerate-image-editor/references/photoshop-files.md +7 -3
  107. package/skills/pixerate-vector/SKILL.md +3 -0
  108. package/dist/chunk-C6C5PDKU.js.map +0 -1
  109. package/dist/chunk-DFQEX3AM.cjs.map +0 -1
  110. package/dist/chunk-FGW5OWJ4.js.map +0 -1
  111. package/dist/chunk-FV5AXTSF.cjs.map +0 -1
  112. package/dist/chunk-GX332W2C.js +0 -181
  113. package/dist/chunk-GX332W2C.js.map +0 -1
  114. package/dist/chunk-LKCYANMW.js.map +0 -1
  115. package/dist/chunk-LPLCWZTM.cjs.map +0 -1
  116. package/dist/chunk-MUUXNRU4.cjs +0 -181
  117. package/dist/chunk-MUUXNRU4.cjs.map +0 -1
  118. package/dist/chunk-ST7E3NEE.cjs.map +0 -1
  119. package/dist/chunk-TCKIXI46.cjs +0 -1136
  120. package/dist/chunk-TCKIXI46.cjs.map +0 -1
  121. package/dist/chunk-TFOPHZ3U.cjs.map +0 -1
  122. package/dist/chunk-UBIXVXPR.js +0 -1136
  123. package/dist/chunk-UBIXVXPR.js.map +0 -1
  124. package/dist/chunk-VXR7XWMF.js.map +0 -1
  125. package/dist/chunk-XK5LZKBD.js.map +0 -1
  126. /package/dist/{chunk-XICA4PVO.js.map → chunk-R2JE6SA7.js.map} +0 -0
  127. /package/dist/{chunk-MMVHBKMM.js.map → chunk-T2P5TIZ6.js.map} +0 -0
@@ -0,0 +1,1908 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;// src/video-editor/types.ts
2
+ var VIDEO_FITS = ["contain", "cover", "fill", "none"];
3
+ var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe", "push", "slide", "iris"];
4
+ var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
5
+ var videoTransitionAudioCurve = (t) => _nullishCoalesce(t.audioCurve, () => ( (t.type === "dip" ? "linear" : "equal-power")));
6
+ var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
7
+ var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
8
+ var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
9
+ var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
10
+ var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence"];
11
+
12
+ // src/video-editor/keyframes.ts
13
+ var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
14
+ var VIDEO_KEYFRAME_PROPERTIES = ["opacity", "volume", "x", "y", "scale", "rotation"];
15
+ var VIDEO_TRANSFORM_PROPERTIES = ["x", "y", "scale", "rotation"];
16
+ var VIDEO_KEYFRAME_LIMITS = {
17
+ opacity: { default: 1, min: 0, max: 1 },
18
+ volume: { default: 1, min: 0, max: 4 },
19
+ x: { default: 0 },
20
+ y: { default: 0 },
21
+ scale: { default: 1, min: 0 },
22
+ rotation: { default: 0 }
23
+ };
24
+ var NAMED = {
25
+ "ease-in": [0.42, 0, 1, 1],
26
+ "ease-out": [0, 0, 0.58, 1],
27
+ "ease-in-out": [0.42, 0, 0.58, 1]
28
+ };
29
+ var isVideoKeyframes = (v) => typeof v === "object" && v !== null && Array.isArray(v.keys);
30
+ function videoEaseBezier(ease) {
31
+ if (ease === void 0 || ease === "linear" || ease === "hold") return null;
32
+ return Array.isArray(ease) ? ease : NAMED[ease];
33
+ }
34
+ var bez = (a, b, s) => 3 * a * (1 - s) * (1 - s) * s + 3 * b * (1 - s) * s * s + s * s * s;
35
+ var bezSlope = (a, b, s) => 3 * a * (1 - s) * (1 - s) + 6 * (b - a) * (1 - s) * s + 3 * (1 - b) * s * s;
36
+ function bezierParam(x1, x2, x) {
37
+ let s = x;
38
+ for (let i = 0; i < 8; i++) {
39
+ const err = bez(x1, x2, s) - x;
40
+ if (Math.abs(err) < 1e-7) return s;
41
+ const d = bezSlope(x1, x2, s);
42
+ if (Math.abs(d) < 1e-6) break;
43
+ s -= err / d;
44
+ if (s < 0 || s > 1) break;
45
+ }
46
+ let lo = 0;
47
+ let hi = 1;
48
+ s = x;
49
+ for (let i = 0; i < 60; i++) {
50
+ const v = bez(x1, x2, s);
51
+ if (Math.abs(v - x) < 1e-9) break;
52
+ if (v < x) lo = s;
53
+ else hi = s;
54
+ s = (lo + hi) / 2;
55
+ }
56
+ return s;
57
+ }
58
+ function videoEaseProgress(ease, p) {
59
+ if (p <= 0) return 0;
60
+ if (p >= 1) return ease === "hold" ? 0 : 1;
61
+ if (ease === "hold") return 0;
62
+ const b = videoEaseBezier(ease);
63
+ if (!b) return p;
64
+ return bez(b[1], b[3], bezierParam(b[0], b[2], p));
65
+ }
66
+ function keyBefore(keys, t) {
67
+ let lo = 0;
68
+ let hi = keys.length - 1;
69
+ if (t < keys[0].t) return -1;
70
+ while (lo < hi) {
71
+ const mid = lo + hi + 1 >> 1;
72
+ if (keys[mid].t <= t) lo = mid;
73
+ else hi = mid - 1;
74
+ }
75
+ return lo;
76
+ }
77
+ function videoValueAt(prop, clipTime, fallback = 0) {
78
+ if (prop === void 0) return fallback;
79
+ if (typeof prop === "number") return prop;
80
+ const { keys } = prop;
81
+ if (!keys.length) return fallback;
82
+ const i = keyBefore(keys, clipTime);
83
+ if (i < 0) return keys[0].v;
84
+ if (i >= keys.length - 1) return keys[keys.length - 1].v;
85
+ const a = keys[i];
86
+ const b = keys[i + 1];
87
+ if (a.ease === "hold") return a.v;
88
+ return a.v + (b.v - a.v) * videoEaseProgress(a.ease, (clipTime - a.t) / (b.t - a.t));
89
+ }
90
+ var videoKeysOf = (prop) => isVideoKeyframes(prop) ? prop.keys : [];
91
+ function retimeVideoAnimatable(prop, dt, k = 1) {
92
+ if (!isVideoKeyframes(prop)) return prop;
93
+ return { keys: prop.keys.map((key) => ({ ...key, t: key.t * k + dt })) };
94
+ }
95
+ function splitEase(ease, p) {
96
+ if (ease === "hold") return ["hold", "hold"];
97
+ const b = videoEaseBezier(ease);
98
+ if (!b) return [ease, ease];
99
+ const s = bezierParam(b[0], b[2], p);
100
+ const lerp = (u, v, k) => [u[0] + (v[0] - u[0]) * k, u[1] + (v[1] - u[1]) * k];
101
+ const P0 = [0, 0];
102
+ const P1 = [b[0], b[1]];
103
+ const P2 = [b[2], b[3]];
104
+ const P3 = [1, 1];
105
+ const A = lerp(P0, P1, s);
106
+ const B = lerp(P1, P2, s);
107
+ const C = lerp(P2, P3, s);
108
+ const AB = lerp(A, B, s);
109
+ const BC = lerp(B, C, s);
110
+ const M = lerp(AB, BC, s);
111
+ const r = (v) => Math.round(v * 1e6) / 1e6;
112
+ const left = Math.abs(M[1]) < 1e-9 ? "linear" : [r(A[0] / M[0]), r(A[1] / M[1]), r(AB[0] / M[0]), r(AB[1] / M[1])];
113
+ const dx = 1 - M[0];
114
+ const dy = 1 - M[1];
115
+ const right = Math.abs(dy) < 1e-9 ? "linear" : [r((BC[0] - M[0]) / dx), r((BC[1] - M[1]) / dy), r((C[0] - M[0]) / dx), r((C[1] - M[1]) / dy)];
116
+ return [left, right];
117
+ }
118
+ function splitVideoAnimatable(prop, at) {
119
+ if (!isVideoKeyframes(prop)) return [prop, prop];
120
+ const keys = prop.keys;
121
+ const eps = 1e-6;
122
+ const v = videoValueAt(prop, at);
123
+ const i = keyBefore(keys, at);
124
+ const exact = i >= 0 && Math.abs(keys[i].t - at) < eps;
125
+ let leftEase;
126
+ let rightEase;
127
+ if (exact) rightEase = keys[i].ease;
128
+ else if (i >= 0 && i < keys.length - 1) {
129
+ const a = keys[i];
130
+ [leftEase, rightEase] = splitEase(a.ease, (at - a.t) / (keys[i + 1].t - a.t));
131
+ }
132
+ const before = keys.slice(0, exact ? i : i + 1).map((k) => ({ ...k }));
133
+ if (before.length && leftEase !== void 0) before[before.length - 1] = withEase(before[before.length - 1], leftEase);
134
+ const left = [...before, { t: at, v }];
135
+ const right = [withEase({ t: 0, v }, rightEase), ...keys.slice(i + 1).map((k) => ({ ...k, t: k.t - at }))];
136
+ return [{ keys: left }, { keys: right }];
137
+ }
138
+ var withEase = (key, ease) => {
139
+ const { ease: _old, ...rest } = key;
140
+ return ease === void 0 || ease === "linear" ? rest : { ...rest, ease };
141
+ };
142
+ function sortVideoKeys(keys) {
143
+ const sorted = [...keys].sort((a, b) => a.t - b.t);
144
+ for (let i = 1; i < sorted.length; i++) if (Math.abs(sorted[i].t - sorted[i - 1].t) < 1e-6) throw new Error(`Two keys are at ${sorted[i].t} ms; give each key its own time.`);
145
+ return sorted;
146
+ }
147
+ function setVideoKey(prop, key, fallback) {
148
+ const keys = isVideoKeyframes(prop) ? prop.keys.filter((k) => Math.abs(k.t - key.t) >= 1e-6) : [];
149
+ if (!isVideoKeyframes(prop)) {
150
+ const v = _nullishCoalesce(prop, () => ( fallback));
151
+ if (key.t !== 0) keys.push({ t: 0, v });
152
+ }
153
+ return { keys: sortVideoKeys([...keys, key]) };
154
+ }
155
+ function removeVideoKey(prop, t) {
156
+ const keys = prop.keys.filter((k) => Math.abs(k.t - t) >= 1e-6);
157
+ return keys.length ? { keys } : prop.keys[0].v;
158
+ }
159
+ var fmt = (v) => {
160
+ const r = Math.round(v * 1e4) / 1e4;
161
+ return Object.is(r, -0) ? "0" : String(r);
162
+ };
163
+ var signed = (v) => fmt(v).startsWith("-") ? fmt(v) : `+${fmt(v)}`;
164
+ function videoKeyframeBreakpoints(prop, step) {
165
+ const { keys } = prop;
166
+ const points = [];
167
+ const holds = /* @__PURE__ */ new Set();
168
+ for (let i = 0; i < keys.length; i++) {
169
+ const a = keys[i];
170
+ points.push([a.t, a.v]);
171
+ const b = keys[i + 1];
172
+ if (!b) break;
173
+ if (a.ease === "hold") {
174
+ holds.add(points.length - 1);
175
+ continue;
176
+ }
177
+ if (!videoEaseBezier(a.ease)) continue;
178
+ const n2 = Math.max(1, Math.ceil((b.t - a.t) / step));
179
+ for (let k = 1; k < n2; k++) {
180
+ const t = a.t + (b.t - a.t) * k / n2;
181
+ points.push([t, videoValueAt(prop, t)]);
182
+ }
183
+ }
184
+ return { points, holds };
185
+ }
186
+ function videoKeyframeFfmpegExpr(prop, time, fallback, step) {
187
+ if (!isVideoKeyframes(prop)) return fmt(_nullishCoalesce(prop, () => ( fallback)));
188
+ const { points, holds } = videoKeyframeBreakpoints(prop, step);
189
+ if (points.length === 1 || points.every(([, v]) => v === points[0][1])) return fmt(points[0][1]);
190
+ let expr = fmt(points[points.length - 1][1]);
191
+ for (let i = points.length - 2; i >= 0; i--) {
192
+ const [t0, v0] = points[i];
193
+ const [t1, v1] = points[i + 1];
194
+ const seg = holds.has(i) || v0 === v1 ? fmt(v0) : `${fmt(v0)}${signed((v1 - v0) / (t1 - t0))}*(${time}${signed(-t0)})`;
195
+ expr = `if(lt(${time},${fmt(t1)}),${seg},${expr})`;
196
+ }
197
+ return `if(lt(${time},${fmt(points[0][0])}),${fmt(points[0][1])},${expr})`;
198
+ }
199
+
200
+ // src/video-editor/lut.ts
201
+ var VIDEO_LUT_MAX_SIZE = 129;
202
+ function parseVideoCubeLut(text) {
203
+ let size = 0;
204
+ let size1 = 0;
205
+ let title;
206
+ const domainMin = [0, 0, 0];
207
+ const domainMax = [1, 1, 1];
208
+ let data = null;
209
+ let rows = 0;
210
+ const lines = text.split(/\r\n|\r|\n/);
211
+ const fail = (line, message) => {
212
+ throw new Error(`Line ${line + 1} of the .cube file: ${message}.`);
213
+ };
214
+ const numbers = (parts, line) => parts.map((p) => {
215
+ const v = Number(p);
216
+ if (!Number.isFinite(v)) fail(line, `"${p}" is not a number`);
217
+ return v;
218
+ });
219
+ for (let i = 0; i < lines.length; i++) {
220
+ const line = lines[i].replace(/#.*/, "").trim();
221
+ if (!line) continue;
222
+ const parts = line.split(/\s+/);
223
+ const key = parts[0];
224
+ if (/^[A-Za-z]/.test(key)) {
225
+ if (rows) fail(i, `"${key}" comes after the table; keywords go before it`);
226
+ if (key === "TITLE") title = line.slice(5).trim().replace(/^"|"$/g, "");
227
+ else if (key === "LUT_3D_SIZE") {
228
+ size = Number(parts[1]);
229
+ if (!Number.isInteger(size) || size < 2 || size > VIDEO_LUT_MAX_SIZE) fail(i, `LUT_3D_SIZE must be a whole number from 2 to ${VIDEO_LUT_MAX_SIZE}, not "${_nullishCoalesce(parts[1], () => ( ""))}"`);
230
+ } else if (key === "LUT_1D_SIZE") size1 = Number(parts[1]) || 1;
231
+ else if (key === "DOMAIN_MIN" || key === "DOMAIN_MAX") {
232
+ const v2 = numbers(parts.slice(1), i);
233
+ if (v2.length !== 3) fail(i, `${key} needs 3 numbers`);
234
+ (key === "DOMAIN_MIN" ? domainMin : domainMax).splice(0, 3, ...v2);
235
+ } else if (key === "LUT_3D_INPUT_RANGE") {
236
+ const v2 = numbers(parts.slice(1), i);
237
+ if (v2.length !== 2 || !(v2[1] > v2[0])) fail(i, "LUT_3D_INPUT_RANGE needs a minimum and a larger maximum");
238
+ domainMin.fill(v2[0]);
239
+ domainMax.fill(v2[1]);
240
+ }
241
+ continue;
242
+ }
243
+ if (!size) fail(i, size1 ? "this is a 1D LUT (LUT_1D_SIZE); only 3D LUTs (LUT_3D_SIZE) are supported" : "the table starts before LUT_3D_SIZE");
244
+ data ??= new Float32Array(size * size * size * 3);
245
+ const v = numbers(parts, i);
246
+ if (v.length !== 3) fail(i, `a table row needs 3 numbers, not ${v.length}`);
247
+ if (rows >= size ** 3) fail(i, `the table has more than the ${size ** 3} rows LUT_3D_SIZE ${size} needs`);
248
+ data.set(v, rows * 3);
249
+ rows++;
250
+ }
251
+ if (!size) throw new Error(size1 ? "The .cube file is a 1D LUT (LUT_1D_SIZE); only 3D LUTs (LUT_3D_SIZE) are supported." : "The .cube file has no LUT_3D_SIZE line, so it is not a 3D LUT.");
252
+ if (rows !== size ** 3) throw new Error(`The .cube file has ${rows} table rows; LUT_3D_SIZE ${size} needs ${size ** 3}.`);
253
+ for (let c = 0; c < 3; c++) if (!(domainMax[c] > domainMin[c])) throw new Error("The .cube file's DOMAIN_MAX must be larger than its DOMAIN_MIN on every channel.");
254
+ return { ...title ? { title } : {}, size, domainMin, domainMax, data };
255
+ }
256
+ function videoLutFromFunction(size, fn, title) {
257
+ const n2 = size - 1;
258
+ const data = new Float32Array(size ** 3 * 3);
259
+ let i = 0;
260
+ for (let b = 0; b < size; b++)
261
+ for (let g = 0; g < size; g++)
262
+ for (let r = 0; r < size; r++) {
263
+ data.set(fn([r / n2, g / n2, b / n2]), i);
264
+ i += 3;
265
+ }
266
+ return { ...title ? { title } : {}, size, domainMin: [0, 0, 0], domainMax: [1, 1, 1], data };
267
+ }
268
+ function videoLutToCube(lut) {
269
+ const f = (v) => (Math.round(v * 1e6) / 1e6).toString();
270
+ const head = [...lut.title ? [`TITLE "${lut.title.replace(/"/g, "'")}"`] : [], `LUT_3D_SIZE ${lut.size}`];
271
+ if (lut.domainMin.some((v) => v !== 0) || lut.domainMax.some((v) => v !== 1)) head.push(`DOMAIN_MIN ${lut.domainMin.map(f).join(" ")}`, `DOMAIN_MAX ${lut.domainMax.map(f).join(" ")}`);
272
+ const rows = [];
273
+ for (let i = 0; i < lut.data.length; i += 3) rows.push(`${f(lut.data[i])} ${f(lut.data[i + 1])} ${f(lut.data[i + 2])}`);
274
+ return `${head.join("\n")}
275
+ ${rows.join("\n")}
276
+ `;
277
+ }
278
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
279
+ function applyVideoLut(lut, rgb, out = [0, 0, 0]) {
280
+ const { size, data, domainMin, domainMax } = lut;
281
+ const n2 = size - 1;
282
+ const x = clamp01((rgb[0] - domainMin[0]) / (domainMax[0] - domainMin[0])) * n2;
283
+ const y = clamp01((rgb[1] - domainMin[1]) / (domainMax[1] - domainMin[1])) * n2;
284
+ const z = clamp01((rgb[2] - domainMin[2]) / (domainMax[2] - domainMin[2])) * n2;
285
+ const r0 = Math.min(n2 - 1, Math.floor(x));
286
+ const g0 = Math.min(n2 - 1, Math.floor(y));
287
+ const b0 = Math.min(n2 - 1, Math.floor(z));
288
+ const fr = x - r0;
289
+ const fg = y - g0;
290
+ const fb = z - b0;
291
+ const sr = 3;
292
+ const sg = 3 * size;
293
+ const sb = 3 * size * size;
294
+ const o = r0 * sr + g0 * sg + b0 * sb;
295
+ let a, c, w0, w1, w2, w3;
296
+ if (fr > fg) {
297
+ if (fg > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sg, 1 - fr, fr - fg, fg - fb, fb];
298
+ else if (fr > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sb, 1 - fr, fr - fb, fb - fg, fg];
299
+ else [a, c, w0, w1, w2, w3] = [sb, sb + sr, 1 - fb, fb - fr, fr - fg, fg];
300
+ } else if (fb > fg) [a, c, w0, w1, w2, w3] = [sb, sb + sg, 1 - fb, fb - fg, fg - fr, fr];
301
+ else if (fb > fr) [a, c, w0, w1, w2, w3] = [sg, sg + sb, 1 - fg, fg - fb, fb - fr, fr];
302
+ else [a, c, w0, w1, w2, w3] = [sg, sg + sr, 1 - fg, fg - fr, fr - fb, fb];
303
+ const d = sr + sg + sb;
304
+ for (let k = 0; k < 3; k++) out[k] = clamp01(w0 * data[o + k] + w1 * data[o + a + k] + w2 * data[o + c + k] + w3 * data[o + d + k]);
305
+ return out;
306
+ }
307
+ function applyVideoLutToPixels(lut, data, strength = 1) {
308
+ const s = clamp01(strength);
309
+ if (!s) return;
310
+ const rgb = [0, 0, 0];
311
+ const out = [0, 0, 0];
312
+ for (let i = 0; i < data.length; i += 4) {
313
+ if (!data[i + 3]) continue;
314
+ rgb[0] = data[i] / 255;
315
+ rgb[1] = data[i + 1] / 255;
316
+ rgb[2] = data[i + 2] / 255;
317
+ applyVideoLut(lut, rgb, out);
318
+ for (let k = 0; k < 3; k++) data[i + k] = Math.round((rgb[k] + (out[k] - rgb[k]) * s) * 255);
319
+ }
320
+ }
321
+ var approximations = /* @__PURE__ */ new WeakMap();
322
+ function videoLutSvgApproximation(lut) {
323
+ const cached = approximations.get(lut);
324
+ if (cached) return cached;
325
+ const points = 17;
326
+ const tables = [[], [], []];
327
+ for (let i = 0; i < points; i++) {
328
+ const v = i / (points - 1);
329
+ const out = applyVideoLut(lut, [v, v, v]);
330
+ for (let k = 0; k < 3; k++) tables[k].push(out[k]);
331
+ }
332
+ const curve = (k, v) => {
333
+ const x = clamp01(v) * (points - 1);
334
+ const i = Math.min(points - 2, Math.floor(x));
335
+ return tables[k][i] + (tables[k][i + 1] - tables[k][i]) * (x - i);
336
+ };
337
+ const ata = Array.from({ length: 4 }, () => [0, 0, 0, 0]);
338
+ const atb = Array.from({ length: 3 }, () => [0, 0, 0, 0]);
339
+ const grid = 9;
340
+ for (let b = 0; b < grid; b++)
341
+ for (let g = 0; g < grid; g++)
342
+ for (let r = 0; r < grid; r++) {
343
+ const x = [r / (grid - 1), g / (grid - 1), b / (grid - 1)];
344
+ const z = [curve(0, x[0]), curve(1, x[1]), curve(2, x[2]), 1];
345
+ const y = applyVideoLut(lut, x);
346
+ for (let i = 0; i < 4; i++) {
347
+ for (let j = 0; j < 4; j++) ata[i][j] += z[i] * z[j];
348
+ for (let k = 0; k < 3; k++) atb[k][i] += z[i] * y[k];
349
+ }
350
+ }
351
+ const matrix = atb.map((rhs) => solve4(ata, rhs));
352
+ const result = { tables, matrix };
353
+ approximations.set(lut, result);
354
+ return result;
355
+ }
356
+ function solve4(a, b) {
357
+ const m = a.map((row, i) => [...row, b[i]]);
358
+ for (let col = 0; col < 4; col++) {
359
+ let pivot = col;
360
+ for (let r = col + 1; r < 4; r++) if (Math.abs(m[r][col]) > Math.abs(m[pivot][col])) pivot = r;
361
+ if (Math.abs(m[pivot][col]) < 1e-12) continue;
362
+ [m[col], m[pivot]] = [m[pivot], m[col]];
363
+ for (let r = 0; r < 4; r++) {
364
+ if (r === col) continue;
365
+ const f = m[r][col] / m[col][col];
366
+ for (let c = col; c < 5; c++) m[r][c] -= f * m[col][c];
367
+ }
368
+ }
369
+ return m.map((row, i) => Math.abs(row[i]) < 1e-12 ? 0 : row[4] / row[i]);
370
+ }
371
+ var VideoLutCache = (_class = class {
372
+ constructor(loader = async (src) => {
373
+ const res = await fetch(src);
374
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
375
+ return res.text();
376
+ }) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);
377
+ this.loader = loader;
378
+ }
379
+
380
+ __init() {this.ready = /* @__PURE__ */ new Map()}
381
+ __init2() {this.pending = /* @__PURE__ */ new Map()}
382
+ __init3() {this.failed = /* @__PURE__ */ new Map()}
383
+ __init4() {this.listeners = /* @__PURE__ */ new Set()}
384
+ /** The LUT if loaded, without starting to load it. */
385
+ peek(src) {
386
+ return this.ready.get(src);
387
+ }
388
+ /** The LUT if loaded; otherwise `undefined`, and loading starts (once) unless it failed. */
389
+ get(src) {
390
+ const lut = this.ready.get(src);
391
+ if (!lut && !this.failed.has(src)) void this.load(src).catch(() => {
392
+ });
393
+ return lut;
394
+ }
395
+ /** Why loading a LUT failed, if it did. */
396
+ error(src) {
397
+ return this.failed.get(src);
398
+ }
399
+ /** The LUT, loading and reading it if needed. Rejects with a readable error. */
400
+ load(src) {
401
+ const ready = this.ready.get(src);
402
+ if (ready) return Promise.resolve(ready);
403
+ const failed = this.failed.get(src);
404
+ if (failed) return Promise.reject(new Error(failed));
405
+ let pending = this.pending.get(src);
406
+ if (!pending) {
407
+ pending = Promise.resolve().then(() => this.loader(src)).then((v) => typeof v === "string" ? parseVideoCubeLut(v) : v).then(
408
+ (lut) => {
409
+ this.pending.delete(src);
410
+ this.ready.set(src, lut);
411
+ this.emit();
412
+ return lut;
413
+ },
414
+ (e) => {
415
+ this.pending.delete(src);
416
+ const message = `The LUT "${src}" could not be read: ${e instanceof Error ? e.message : String(e)}`;
417
+ this.failed.set(src, message);
418
+ this.emit();
419
+ throw new Error(message);
420
+ }
421
+ );
422
+ this.pending.set(src, pending);
423
+ }
424
+ return pending;
425
+ }
426
+ /** Uses a LUT the host already has (`.cube` text or a parsed LUT), under `src`. */
427
+ set(src, lut) {
428
+ const parsed = typeof lut === "string" ? parseVideoCubeLut(lut) : lut;
429
+ this.ready.set(src, parsed);
430
+ this.failed.delete(src);
431
+ this.emit();
432
+ return parsed;
433
+ }
434
+ /** Forgets a failure so the next `get` tries again. */
435
+ retry(src) {
436
+ if (this.failed.delete(src)) this.emit();
437
+ }
438
+ subscribe(fn) {
439
+ this.listeners.add(fn);
440
+ return () => {
441
+ this.listeners.delete(fn);
442
+ };
443
+ }
444
+ emit() {
445
+ for (const l of [...this.listeners]) l();
446
+ }
447
+ }, _class);
448
+
449
+ // src/video-editor/grade.ts
450
+ var VIDEO_GRADE_CONTROLS = [
451
+ { key: "exposure", label: "Exposure", min: -3, max: 3, step: 0.1, description: "Exposure in stops: +1 doubles every channel, -1 halves it (default 0)." },
452
+ { key: "contrast", label: "Contrast", min: -1, max: 1, step: 0.05, description: "Contrast around mid grey, -1 (flat grey) to 1 (twice as steep); default 0." },
453
+ { key: "saturation", label: "Saturation", min: -1, max: 1, step: 0.05, description: "Saturation, -1 (black and white) to 1 (twice as saturated); default 0." },
454
+ { key: "hue", label: "Hue", min: -180, max: 180, step: 1, description: "Hue rotation in degrees (default 0)." },
455
+ { key: "temperature", label: "Temperature", min: -1, max: 1, step: 0.05, description: "White balance, -1 (cool, bluer) to 1 (warm, more orange); default 0." },
456
+ { key: "tint", label: "Tint", min: -1, max: 1, step: 0.05, description: "White balance, -1 (greener) to 1 (more magenta); default 0." }
457
+ ];
458
+ var VIDEO_GRADE_PRESETS = {
459
+ warm: { temperature: 0.4, saturation: 0.1 },
460
+ cool: { temperature: -0.4, tint: -0.05 },
461
+ punchy: { contrast: 0.25, saturation: 0.3 },
462
+ faded: { exposure: 0.15, contrast: -0.3, saturation: -0.3 },
463
+ "black-and-white": { saturation: -1, contrast: 0.15 },
464
+ "bleach-bypass": { saturation: -0.6, contrast: 0.4 },
465
+ "day-for-night": { exposure: -1.5, saturation: -0.4, temperature: -0.6 }
466
+ };
467
+ var VIDEO_GRADE_PRESET_NAMES = Object.keys(VIDEO_GRADE_PRESETS);
468
+ var clampTo = (c, v) => Math.min(c.max, Math.max(c.min, v));
469
+ function normalizeVideoGradeLut(lut) {
470
+ if (!lut || typeof lut.src !== "string" || !lut.src) return void 0;
471
+ const strength = typeof lut.strength === "number" && Number.isFinite(lut.strength) ? Math.round(Math.min(1, Math.max(0, lut.strength)) * 1e3) / 1e3 : 1;
472
+ if (!strength) return void 0;
473
+ return { src: lut.src, ...strength !== 1 ? { strength } : {}, ...lut.name ? { name: lut.name } : {} };
474
+ }
475
+ function normalizeVideoGrade(grade) {
476
+ if (!grade) return void 0;
477
+ const out = {};
478
+ for (const c of VIDEO_GRADE_CONTROLS) {
479
+ const v = grade[c.key];
480
+ if (typeof v === "number" && Number.isFinite(v) && v !== 0) out[c.key] = Math.round(clampTo(c, v) * 1e3) / 1e3;
481
+ }
482
+ const lut = normalizeVideoGradeLut(grade.lut);
483
+ if (lut) out.lut = lut;
484
+ return Object.keys(out).length ? out : void 0;
485
+ }
486
+ function videoGradePreset(name, amount = 1) {
487
+ const preset = VIDEO_GRADE_PRESETS[name];
488
+ if (!preset) throw new Error(`There is no grade preset "${name}"; the presets are ${VIDEO_GRADE_PRESET_NAMES.join(", ")}.`);
489
+ const k = Math.min(1, Math.max(0, amount));
490
+ return normalizeVideoGrade(Object.fromEntries(Object.entries(preset).map(([key, v]) => [key, v * k])));
491
+ }
492
+ function videoGradeLook(current, name, amount = 1) {
493
+ return normalizeVideoGrade({ ...videoGradePreset(name, amount), ..._optionalChain([current, 'optionalAccess', _2 => _2.lut]) ? { lut: current.lut } : {} });
494
+ }
495
+ function videoGradeSteps(grade) {
496
+ const g = normalizeVideoGrade(grade);
497
+ if (!g) return [];
498
+ const steps = [];
499
+ if (g.exposure) steps.push({ kind: "brightness", amount: 2 ** g.exposure });
500
+ if (g.contrast) steps.push({ kind: "contrast", amount: 1 + g.contrast });
501
+ if (g.saturation) steps.push({ kind: "saturate", amount: 1 + g.saturation });
502
+ if (g.hue) steps.push({ kind: "hue-rotate", degrees: g.hue });
503
+ if (g.temperature || g.tint) {
504
+ const t = _nullishCoalesce(g.temperature, () => ( 0));
505
+ steps.push({ kind: "gains", r: 1 + 0.25 * t, g: 1 - 0.25 * (_nullishCoalesce(g.tint, () => ( 0))), b: 1 - 0.25 * t });
506
+ }
507
+ return steps;
508
+ }
509
+ function videoGradeStepMatrix(step) {
510
+ switch (step.kind) {
511
+ case "brightness":
512
+ return [
513
+ [step.amount, 0, 0, 0],
514
+ [0, step.amount, 0, 0],
515
+ [0, 0, step.amount, 0]
516
+ ];
517
+ case "contrast": {
518
+ const c = step.amount;
519
+ const o = 0.5 - 0.5 * c;
520
+ return [
521
+ [c, 0, 0, o],
522
+ [0, c, 0, o],
523
+ [0, 0, c, o]
524
+ ];
525
+ }
526
+ case "gains":
527
+ return [
528
+ [step.r, 0, 0, 0],
529
+ [0, step.g, 0, 0],
530
+ [0, 0, step.b, 0]
531
+ ];
532
+ case "saturate": {
533
+ const s = step.amount;
534
+ return [
535
+ [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0],
536
+ [0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0],
537
+ [0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0]
538
+ ];
539
+ }
540
+ case "hue-rotate": {
541
+ const a = step.degrees * Math.PI / 180;
542
+ const cos = Math.cos(a);
543
+ const sin = Math.sin(a);
544
+ return [
545
+ [0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928, 0],
546
+ [0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.14, 0.072 - cos * 0.072 - sin * 0.283, 0],
547
+ [0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072, 0]
548
+ ];
549
+ }
550
+ }
551
+ }
552
+ function applyVideoGrade(grade, rgb, lut) {
553
+ let v = rgb.map((x) => x / 255);
554
+ for (const step of videoGradeSteps(grade)) {
555
+ const m = videoGradeStepMatrix(step);
556
+ v = m.map((row) => Math.min(1, Math.max(0, row[0] * v[0] + row[1] * v[1] + row[2] * v[2] + row[3])));
557
+ }
558
+ const setting = normalizeVideoGradeLut(_optionalChain([grade, 'optionalAccess', _3 => _3.lut]));
559
+ if (setting && lut) {
560
+ const out = applyVideoLut(lut, v);
561
+ const s = _nullishCoalesce(setting.strength, () => ( 1));
562
+ v = v.map((x, k) => x + (out[k] - x) * s);
563
+ }
564
+ return v.map((x) => Math.round(x * 255));
565
+ }
566
+ var fmt2 = (v) => String(Math.round(v * 1e5) / 1e5);
567
+ function videoGradeStepSvgValues(step) {
568
+ return videoGradeStepMatrix(step).map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
569
+ }
570
+ function videoLutSvgPrimitives(lut, strength = 1) {
571
+ const { tables, matrix } = videoLutSvgApproximation(lut);
572
+ const table = (k) => tables[k].map(fmt2).join(" ");
573
+ const values = matrix.map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
574
+ const s = Math.min(1, Math.max(0, strength));
575
+ return `<feComponentTransfer result="lutIn"><feFuncA type="identity"/></feComponentTransfer><feComponentTransfer><feFuncR type="table" tableValues="${table(0)}"/><feFuncG type="table" tableValues="${table(1)}"/><feFuncB type="table" tableValues="${table(2)}"/></feComponentTransfer><feColorMatrix type="matrix" values="${values}"/>` + (s < 1 ? `<feComposite in2="lutIn" operator="arithmetic" k1="0" k2="${fmt2(s)}" k3="${fmt2(1 - s)}" k4="0"/>` : "");
576
+ }
577
+ function videoGradeSvgFilter(id, grade, lut) {
578
+ const steps = videoGradeSteps(grade);
579
+ const setting = normalizeVideoGradeLut(_optionalChain([grade, 'optionalAccess', _4 => _4.lut]));
580
+ return `<filter id="${id}" color-interpolation-filters="sRGB" x="0" y="0" width="100%" height="100%">${steps.map((s) => `<feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/>`).join("")}${setting && lut ? videoLutSvgPrimitives(lut, setting.strength) : ""}</filter>`;
581
+ }
582
+ var videoGradeGainsFilterId = (step) => `pixerate-wb-${[step.r, step.g, step.b].map((v) => Math.round(v * 1e3)).join("-")}`;
583
+ function videoGradeCssFilter(grade) {
584
+ return videoGradeSteps(grade).map((s) => s.kind === "brightness" ? `brightness(${fmt2(s.amount)})` : s.kind === "contrast" ? `contrast(${fmt2(s.amount)})` : s.kind === "saturate" ? `saturate(${fmt2(s.amount)})` : s.kind === "hue-rotate" ? `hue-rotate(${fmt2(s.degrees)}deg)` : `url(#${videoGradeGainsFilterId(s)})`).join(" ");
585
+ }
586
+ function videoGradeSvgDefs(grade) {
587
+ return videoGradeSteps(grade).filter((s) => s.kind === "gains").map((s) => {
588
+ const id = videoGradeGainsFilterId(s);
589
+ return { id, markup: `<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/></filter>` };
590
+ });
591
+ }
592
+ function videoGradeFfmpegFilters(grade) {
593
+ return videoGradeSteps(grade).map((step) => {
594
+ const m = videoGradeStepMatrix(step);
595
+ if (step.kind === "brightness" || step.kind === "contrast") {
596
+ const lut = (i) => `clip(val*${fmt2(m[i][i])}+${fmt2(m[i][3] * 255)},0,255)`;
597
+ return `lutrgb=r='${lut(0)}':g='${lut(1)}':b='${lut(2)}'`;
598
+ }
599
+ const k = ["r", "g", "b"];
600
+ return `colorchannelmixer=${m.flatMap((row, i) => row.slice(0, 3).map((v, j) => `${k[i]}${k[j]}=${fmt2(v)}`)).join(":")}`;
601
+ });
602
+ }
603
+
604
+ // src/video-editor/remap.ts
605
+ function videoClipSourceTime(clip, clipTime) {
606
+ const base = _nullishCoalesce(clip.in, () => ( 0));
607
+ return clip.timeRemap ? base + videoValueAt(clip.timeRemap, clipTime, 0) : base + clipTime * (_nullishCoalesce(clip.speed, () => ( 1)));
608
+ }
609
+ function videoClipSpeedAt(clip, clipTime) {
610
+ if (clip.type !== "media") return 1;
611
+ if (!clip.timeRemap) return _nullishCoalesce(clip.speed, () => ( 1));
612
+ const t = Math.min(Math.max(clipTime, 0), Math.max(0, clip.duration - 1));
613
+ return Math.max(0, videoClipSourceTime(clip, t + 1) - videoClipSourceTime(clip, t));
614
+ }
615
+ function videoSpeedRampKeys(segments) {
616
+ if (!segments.length) throw new Error("Give at least one stretch { duration, speed }.");
617
+ const keys = [];
618
+ let t = 0;
619
+ let v = 0;
620
+ segments.forEach((s, i) => {
621
+ if (!(s.duration > 0)) throw new Error(`Stretch ${i + 1} needs a duration above 0 ms.`);
622
+ for (const [name, x] of [["speed", s.speed], ["toSpeed", s.toSpeed]])
623
+ if (x !== void 0 && !(x >= 0 && x <= 20)) throw new Error(`Stretch ${i + 1}'s ${name} must be from 0 (a freeze) to 20; got ${x}.`);
624
+ const a = s.speed;
625
+ const b = _nullishCoalesce(s.toSpeed, () => ( s.speed));
626
+ const avg = (a + b) / 2;
627
+ let ease = "linear";
628
+ if (a !== b && avg > 0) {
629
+ ease = [1 / 3, 2 * a / (3 * (a + b)), 2 / 3, 1 - 2 * b / (3 * (a + b))].map((n2) => Math.round(n2 * 1e6) / 1e6);
630
+ }
631
+ keys.push({ t, v, ...ease === "linear" ? {} : { ease } });
632
+ t += s.duration;
633
+ v += avg * s.duration;
634
+ });
635
+ keys.push({ t, v });
636
+ return { keys: mergeLinear(keys), duration: t, source: v };
637
+ }
638
+ function mergeLinear(keys) {
639
+ const out = [keys[0]];
640
+ for (let i = 1; i < keys.length; i++) {
641
+ const prev = out[out.length - 1];
642
+ const k = keys[i];
643
+ const before = out.length > 1 ? out[out.length - 2] : null;
644
+ if (before && !before.ease && !prev.ease) {
645
+ const s1 = (prev.v - before.v) / (prev.t - before.t);
646
+ const s2 = (k.v - prev.v) / (k.t - prev.t);
647
+ if (Math.abs(s1 - s2) < 1e-9) {
648
+ out[out.length - 1] = k;
649
+ continue;
650
+ }
651
+ }
652
+ out.push(k);
653
+ }
654
+ return out;
655
+ }
656
+ function checkVideoTimeRemap(remap, path = "timeRemap") {
657
+ const { keys } = remap;
658
+ if (!keys.length) throw new Error(`${path} needs at least one key.`);
659
+ keys.forEach((k, i) => {
660
+ if (k.v < 0) throw new Error(`${path}.keys[${i}].v must be at least 0 (ms after the in point).`);
661
+ const next = keys[i + 1];
662
+ if (!next) return;
663
+ if (next.v < k.v - 1e-6) throw new Error(`${path}.keys[${i + 1}] goes back in the source (${next.v} ms after ${k.v} ms); a ramp only plays forwards, so make each value at least the one before.`);
664
+ if (!videoEaseBezier(k.ease)) return;
665
+ let prev = k.v;
666
+ for (let n2 = 1; n2 <= 32; n2++) {
667
+ const v = videoValueAt(remap, k.t + (next.t - k.t) * n2 / 32);
668
+ if (v < prev - 1e-3) throw new Error(`${path}.keys[${i}].ease overshoots, which would play backwards; use a curve that only moves forwards (or linear).`);
669
+ prev = v;
670
+ }
671
+ });
672
+ }
673
+ function videoTimeRemapSpan(remap) {
674
+ return { from: remap.keys[0].v, to: remap.keys[remap.keys.length - 1].v };
675
+ }
676
+ function videoTimeRemapBreakpoints(remap, step) {
677
+ const { points, holds } = videoKeyframeBreakpoints(remap, step);
678
+ const out = [];
679
+ points.forEach(([t, v], i) => {
680
+ out.push({ t, v });
681
+ if (holds.has(i) && points[i + 1]) out.push({ t: points[i + 1][0], v });
682
+ });
683
+ return out;
684
+ }
685
+ function videoSpeedRampEdit(clip, segments, ripple = true) {
686
+ const ramp = videoSpeedRampKeys(segments);
687
+ const offset = videoClipSourceTime(clip, 0) - (_nullishCoalesce(clip.in, () => ( 0)));
688
+ return { op: "set_time_remap", clip: clip.id, keys: ramp.keys.map((k) => ({ ...k, v: k.v + offset })), duration: ramp.duration, ripple };
689
+ }
690
+ function videoFreezeFrameEdit(clip, at, duration, ripple = true) {
691
+ if (at < 0 || at > clip.duration) throw new Error(`${Math.round(at)} ms is outside clip "${clip.id}" (0\u2013${clip.duration} ms of clip time).`);
692
+ const remap = _nullishCoalesce(clip.timeRemap, () => ( { keys: [{ t: 0, v: 0 }, { t: clip.duration, v: clip.duration * (_nullishCoalesce(clip.speed, () => ( 1))) }] }));
693
+ const [left, right] = splitVideoAnimatable(remap, at);
694
+ const keys = [...left.keys.slice(0, -1), { ...left.keys[left.keys.length - 1], ease: "linear" }, ...right.keys.map((k) => ({ ...k, t: k.t + at + duration }))];
695
+ return { op: "set_time_remap", clip: clip.id, keys, duration: clip.duration + duration, ripple };
696
+ }
697
+ var videoSlowMotionRamp = (duration) => [
698
+ { duration: Math.max(1, duration / 2), speed: 1 },
699
+ { duration: 500, speed: 1, toSpeed: 0.3 },
700
+ { duration: 1500, speed: 0.3 }
701
+ ];
702
+
703
+ // src/video-editor/timeline.ts
704
+ var VIDEO_TIME_EPSILON = 0.5;
705
+ var videoClipEnd = (clip) => clip.start + clip.duration;
706
+ var videoTrackSyncLocked = (track) => track.syncLocked !== false;
707
+ var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.speed, () => ( 1)) : 1;
708
+ function videoClipSourceRange(clip) {
709
+ if (clip.type === "sequence") return { in: _nullishCoalesce(clip.in, () => ( 0)), out: (_nullishCoalesce(clip.in, () => ( 0))) + clip.duration };
710
+ if (clip.type !== "media") return null;
711
+ const start = _nullishCoalesce(clip.in, () => ( 0));
712
+ if (_optionalChain([clip, 'access', _5 => _5.timeRemap, 'optionalAccess', _6 => _6.keys, 'access', _7 => _7.length])) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
713
+ return { in: start, out: start + clip.duration * videoClipSpeed(clip) };
714
+ }
715
+ var videoFrameDuration = (fps) => 1e3 / fps;
716
+ function snapToVideoFrame(time, fps) {
717
+ const frame = Math.round(time * fps / 1e3);
718
+ return Math.round(frame * 1e3 / fps * 1e3) / 1e3;
719
+ }
720
+ var videoFrames = (time, fps) => Math.round(time * fps / 1e3);
721
+ function isVideoDropFrameRate(fps) {
722
+ const nominal = Math.round(fps);
723
+ return (nominal === 30 || nominal === 60) && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3;
724
+ }
725
+ var exactVideoRate = (fps) => {
726
+ const nominal = Math.round(fps);
727
+ return nominal !== fps && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3 ? nominal * 1e3 / 1001 : fps;
728
+ };
729
+ var videoTimecodeFrames = (time, fps) => Math.max(0, Math.round(time * exactVideoRate(fps) / 1e3));
730
+ function formatVideoTimecode(time, fps, dropFrame = isVideoDropFrameRate(fps)) {
731
+ return formatVideoTimecodeFrames(videoTimecodeFrames(time, fps), fps, dropFrame);
732
+ }
733
+ function formatVideoTimecodeFrames(frames, fps, dropFrame = isVideoDropFrameRate(fps)) {
734
+ const base = Math.round(fps);
735
+ let total = Math.max(0, Math.round(frames));
736
+ const drop = dropFrame && isVideoDropFrameRate(fps);
737
+ if (drop) {
738
+ const skip = base / 15;
739
+ const perMinute = base * 60 - skip;
740
+ const perTen = perMinute * 10 + skip;
741
+ const tens = Math.floor(total / perTen);
742
+ const rest = total % perTen;
743
+ total += skip * 9 * tens + (rest > skip ? skip * Math.floor((rest - skip) / perMinute) : 0);
744
+ }
745
+ const ff = total % base;
746
+ const secs = Math.floor(total / base);
747
+ const pad = (n2) => String(n2).padStart(2, "0");
748
+ return `${pad(Math.floor(secs / 3600))}:${pad(Math.floor(secs / 60) % 60)}:${pad(secs % 60)}${drop ? ";" : ":"}${pad(ff)}`;
749
+ }
750
+ var videoHourFrames = (fps, dropFrame = isVideoDropFrameRate(fps)) => {
751
+ const base = Math.round(fps);
752
+ return dropFrame && isVideoDropFrameRate(fps) ? base * 3600 - base / 15 * 54 : base * 3600;
753
+ };
754
+ var formatVideoSeconds = (time) => `${(time / 1e3).toFixed(2)} s`;
755
+ var videoAssetHasSound = (asset) => !!asset && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio !== false);
756
+ function videoAssetMap(project) {
757
+ return new Map(project.assets.map((a) => [a.id, a]));
758
+ }
759
+ function videoSequenceMap(project) {
760
+ return new Map((_nullishCoalesce(project.sequences, () => ( []))).map((q) => [q.id, q]));
761
+ }
762
+ function videoSequenceDuration(sequence) {
763
+ let end = 0;
764
+ for (const track of sequence.tracks) for (const clip of track.clips) end = Math.max(end, videoClipEnd(clip));
765
+ return end;
766
+ }
767
+ var videoSequenceChildren = (sequence) => [...new Set(sequence.tracks.flatMap((t) => t.clips.flatMap((c) => c.type === "sequence" ? [c.sequence] : [])))];
768
+ function findVideoSequenceCycle(project, id) {
769
+ const sequences = videoSequenceMap(project);
770
+ const walk = (at, path) => {
771
+ const seq = sequences.get(at);
772
+ if (!seq) return null;
773
+ for (const child of videoSequenceChildren(seq)) {
774
+ if (child === id) return [...path, child];
775
+ if (path.includes(child)) continue;
776
+ const found = walk(child, [...path, child]);
777
+ if (found) return found;
778
+ }
779
+ return null;
780
+ };
781
+ return walk(id, [id]);
782
+ }
783
+ function allVideoTracks(project) {
784
+ return [...project.tracks.map((track) => ({ track })), ...(_nullishCoalesce(project.sequences, () => ( []))).flatMap((q) => q.tracks.map((track) => ({ sequence: q.id, track })))];
785
+ }
786
+ function sortedVideoClips(track) {
787
+ return [...track.clips].sort((a, b) => a.start - b.start || a.duration - b.duration);
788
+ }
789
+ function findVideoClip(project, id) {
790
+ for (let trackIndex = 0; trackIndex < project.tracks.length; trackIndex++) {
791
+ const track = project.tracks[trackIndex];
792
+ const index = track.clips.findIndex((c) => c.id === id);
793
+ if (index >= 0) return { track, trackIndex, clip: track.clips[index], index };
794
+ }
795
+ return null;
796
+ }
797
+ function allVideoClipIds(project) {
798
+ return allVideoTracks(project).flatMap(({ track }) => track.clips.map((c) => c.id));
799
+ }
800
+ function findVideoSequenceClip(project, id) {
801
+ for (const sequence of _nullishCoalesce(project.sequences, () => ( [])))
802
+ for (const track of sequence.tracks) {
803
+ const clip = track.clips.find((c) => c.id === id);
804
+ if (clip) return { sequence, track, clip };
805
+ }
806
+ return null;
807
+ }
808
+ function videoProjectDuration(project) {
809
+ let end = 0;
810
+ for (const track of project.tracks) for (const clip of track.clips) end = Math.max(end, videoClipEnd(clip));
811
+ return end;
812
+ }
813
+ function videoClipBefore(track, clip) {
814
+ let best = null;
815
+ for (const c of track.clips) if (c !== clip && Math.abs(videoClipEnd(c) - clip.start) <= VIDEO_TIME_EPSILON) best = c;
816
+ return best;
817
+ }
818
+ function videoClipAfter(track, clip) {
819
+ for (const c of track.clips) if (c !== clip && Math.abs(c.start - videoClipEnd(clip)) <= VIDEO_TIME_EPSILON) return c;
820
+ return null;
821
+ }
822
+ function videoClipHandles(clip, assets, sequences) {
823
+ if (clip.type === "sequence") {
824
+ const seq = _optionalChain([sequences, 'optionalAccess', _8 => _8.get, 'call', _9 => _9(clip.sequence)]);
825
+ const range2 = videoClipSourceRange(clip);
826
+ return { before: range2.in, after: seq ? Math.max(0, videoSequenceDuration(seq) - range2.out) : Infinity };
827
+ }
828
+ if (clip.type !== "media") return { before: Infinity, after: Infinity };
829
+ const asset = assets.get(clip.asset);
830
+ const speed = videoClipSpeed(clip);
831
+ const range = videoClipSourceRange(clip);
832
+ const before = range.in / speed;
833
+ if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _10 => _10.kind]) === "image" ? Infinity : before, after: Infinity };
834
+ return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
835
+ }
836
+ function videoClipOverhang(track, clip) {
837
+ const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
838
+ const next = videoClipAfter(track, clip);
839
+ const tail = _optionalChain([next, 'optionalAccess', _11 => _11.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
840
+ return { lead, tail };
841
+ }
842
+ function videoSnapPoints(project, options = {}) {
843
+ const exclude = new Set(_nullishCoalesce(options.exclude, () => ( [])));
844
+ const points = /* @__PURE__ */ new Set([0]);
845
+ for (const track of project.tracks)
846
+ for (const clip of track.clips) {
847
+ if (exclude.has(clip.id)) continue;
848
+ points.add(clip.start);
849
+ points.add(videoClipEnd(clip));
850
+ }
851
+ for (const marker of _nullishCoalesce(project.markers, () => ( []))) points.add(marker.time);
852
+ if (options.playhead !== void 0) points.add(options.playhead);
853
+ return [...points].sort((a, b) => a - b);
854
+ }
855
+ function snapVideoTime(time, points, threshold) {
856
+ let best = null;
857
+ for (const p of points) if (Math.abs(p - time) <= threshold && (best === null || Math.abs(p - time) < Math.abs(best - time))) best = p;
858
+ return best === null ? { time, snapped: null } : { time: best, snapped: best };
859
+ }
860
+ function findVideoGaps(project, trackId) {
861
+ const gaps = [];
862
+ for (const track of project.tracks) {
863
+ if (trackId !== void 0 && track.id !== trackId) continue;
864
+ let at = 0;
865
+ for (const clip of sortedVideoClips(track)) {
866
+ if (clip.start - at > VIDEO_TIME_EPSILON) gaps.push({ track: track.id, start: at, end: clip.start });
867
+ at = Math.max(at, videoClipEnd(clip));
868
+ }
869
+ }
870
+ return gaps;
871
+ }
872
+ function videoOverlap(track, start, end, ignore = /* @__PURE__ */ new Set()) {
873
+ for (const c of sortedVideoClips(track)) {
874
+ if (ignore.has(c.id)) continue;
875
+ if (c.start < end - VIDEO_TIME_EPSILON && videoClipEnd(c) > start + VIDEO_TIME_EPSILON) return c;
876
+ }
877
+ return null;
878
+ }
879
+ function nextFreeVideoSlot(track, from, duration, ignore = /* @__PURE__ */ new Set()) {
880
+ let at = from;
881
+ for (const c of sortedVideoClips(track)) {
882
+ if (ignore.has(c.id)) continue;
883
+ if (videoClipEnd(c) <= at + VIDEO_TIME_EPSILON) continue;
884
+ if (c.start >= at + duration - VIDEO_TIME_EPSILON) break;
885
+ at = videoClipEnd(c);
886
+ }
887
+ return at;
888
+ }
889
+ function videoClipsAt(project, time) {
890
+ const out = [];
891
+ for (const track of project.tracks) for (const clip of track.clips) if (time >= clip.start && time < videoClipEnd(clip)) out.push({ track: track.id, clip });
892
+ return out;
893
+ }
894
+
895
+ // src/video-editor/clip-animation.ts
896
+ var VIDEO_CLIP_ANIMATION_DURATION = 500;
897
+ var EFFECTS = {
898
+ fade: { easeIn: "linear", easeOut: "linear" },
899
+ rise: { y: 0.06, easeIn: "ease-out", easeOut: "ease-in" },
900
+ drop: { y: -0.06, easeIn: "ease-out", easeOut: "ease-in" },
901
+ "slide-left": { x: 0.15, easeIn: "ease-out", easeOut: "ease-in" },
902
+ "slide-right": { x: -0.15, easeIn: "ease-out", easeOut: "ease-in" },
903
+ zoom: { scale: 0.6, easeIn: "ease-out", easeOut: "ease-in" },
904
+ // Overshoots past full size, then settles (a "back" curve).
905
+ pop: { scale: 0.3, easeIn: [0.34, 1.56, 0.64, 1], easeOut: [0.36, 0, 0.66, -0.56] }
906
+ };
907
+ var PROPS = ["opacity", "x", "y", "scale"];
908
+ function videoClipAnimationProperties(animation) {
909
+ const spec = EFFECTS[animation.effect];
910
+ return PROPS.filter((p) => p === "opacity" || spec[p] !== void 0);
911
+ }
912
+ function videoClipAnimationSpans(clip) {
913
+ const a = clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0;
914
+ const b = clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0;
915
+ const k = a + b > clip.duration ? clip.duration / (a + b) : 1;
916
+ return { in: a * k, out: b * k };
917
+ }
918
+ function videoClipAnimated(clip, project) {
919
+ if (!clip.animateIn && !clip.animateOut) return clip;
920
+ const spans = videoClipAnimationSpans(clip);
921
+ const out = { ...clip, transform: { ...clip.transform } };
922
+ for (const prop of PROPS) {
923
+ const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _12 => _12.transform, 'optionalAccess', _13 => _13[prop]]);
924
+ if (isVideoKeyframes(current)) continue;
925
+ const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
926
+ const away = (spec) => {
927
+ if (prop === "opacity") return 0;
928
+ const v = spec[prop];
929
+ if (v === void 0) return void 0;
930
+ return prop === "scale" ? base * v : base + v * (prop === "x" ? project.width : project.height);
931
+ };
932
+ const keys = [];
933
+ if (clip.animateIn) {
934
+ const spec = EFFECTS[clip.animateIn.effect];
935
+ const from = away(spec);
936
+ if (from !== void 0) keys.push({ t: 0, v: from, ease: spec.easeIn }, { t: spans.in, v: base });
937
+ }
938
+ if (clip.animateOut) {
939
+ const spec = EFFECTS[clip.animateOut.effect];
940
+ const to = away(spec);
941
+ if (to !== void 0) {
942
+ const start = clip.duration - spans.out;
943
+ if (!keys.length || keys[keys.length - 1].t < start) keys.push({ t: start, v: base, ease: spec.easeOut });
944
+ else keys[keys.length - 1] = { ...keys[keys.length - 1], ease: spec.easeOut };
945
+ keys.push({ t: clip.duration, v: to });
946
+ }
947
+ }
948
+ if (!keys.length) continue;
949
+ if (prop === "opacity") out.opacity = { keys };
950
+ else out.transform[prop] = { keys };
951
+ }
952
+ return out;
953
+ }
954
+
955
+ // src/video-editor/evaluate.ts
956
+ function videoTransformAt(transform, clipTime = 0) {
957
+ return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _14 => _14.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _15 => _15.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _16 => _16.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _17 => _17.rotation]), clipTime, 0) };
958
+ }
959
+ function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
960
+ let w = size.width;
961
+ let h = size.height;
962
+ if (fit === "fill") {
963
+ w = frame.width;
964
+ h = frame.height;
965
+ } else if (fit !== "none") {
966
+ const k = fit === "cover" ? Math.max(frame.width / w, frame.height / h) : Math.min(frame.width / w, frame.height / h);
967
+ w *= k;
968
+ h *= k;
969
+ }
970
+ const t = videoTransformAt(transform, clipTime);
971
+ w *= t.scale;
972
+ h *= t.scale;
973
+ const cx = frame.width / 2 + t.x;
974
+ const cy = frame.height / 2 + t.y;
975
+ return { x: cx - w / 2, y: cy - h / 2, width: w, height: h };
976
+ }
977
+ function videoTextStyle(clip) {
978
+ return { text: clip.text, color: _nullishCoalesce(clip.color, () => ( "#ffffff")), fontSize: _nullishCoalesce(clip.fontSize, () => ( 64)), fontFamily: _nullishCoalesce(clip.fontFamily, () => ( "sans-serif")), fontWeight: _nullishCoalesce(clip.fontWeight, () => ( 700)), ...clip.background ? { background: clip.background } : {} };
979
+ }
980
+ function videoTitleFrame(project, clip, clipTime = 0) {
981
+ const layer = { clip: clip.id, track: "", type: "text", opacity: 1, rect: placeVideoRect(project, project, "fill", clip.transform, clipTime), rotation: videoTransformAt(clip.transform, clipTime).rotation, text: videoTextStyle(clip) };
982
+ return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
983
+ }
984
+ var clamp012 = (v) => Math.min(1, Math.max(0, v));
985
+ var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI * clamp012(progress))) / 2;
986
+ function videoFadeCurveGain(curve, x) {
987
+ const t = clamp012(x);
988
+ switch (curve) {
989
+ case "equal-power":
990
+ return Math.sin(t * Math.PI / 2);
991
+ case "s-curve":
992
+ return (1 - Math.cos(t * Math.PI)) / 2;
993
+ case "exponential":
994
+ return t * t * t;
995
+ case "logarithmic":
996
+ return 1 - (1 - t) * (1 - t);
997
+ default:
998
+ return t;
999
+ }
1000
+ }
1001
+ function fadeGain(clip, time, sound = false) {
1002
+ let g = 1;
1003
+ const shape = sound && (clip.type === "media" || clip.type === "sequence") ? clip : void 0;
1004
+ if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _18 => _18.fadeInCurve]), (time - clip.start) / clip.fadeIn);
1005
+ if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _19 => _19.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
1006
+ return g;
1007
+ }
1008
+ function videoClipSoundGain(clip, clipTime) {
1009
+ const volume = clip.type === "media" || clip.type === "sequence" ? videoValueAt(clip.volume, clipTime, 1) : 1;
1010
+ return volume * fadeGain(clip, clip.start + Math.min(Math.max(clipTime, 0), clip.duration), true);
1011
+ }
1012
+ function activeOnTrack(track, time) {
1013
+ const out = [];
1014
+ for (const clip of sortedVideoClips(track)) {
1015
+ const before = clip.transition ? videoClipBefore(track, clip) : null;
1016
+ const lead = before ? clip.transition.duration / 2 : 0;
1017
+ const next = videoClipAfter(track, clip);
1018
+ const tail = _optionalChain([next, 'optionalAccess', _20 => _20.transition]) ? next.transition.duration / 2 : 0;
1019
+ if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
1020
+ const a = { clip };
1021
+ if (lead && time < clip.start + lead) a.incoming = { progress: clamp012((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
1022
+ if (tail && time >= videoClipEnd(clip) - tail) a.outgoing = { progress: clamp012((time - (videoClipEnd(clip) - tail)) / (2 * tail)), type: next.transition.type, next };
1023
+ out.push(a);
1024
+ }
1025
+ return out;
1026
+ }
1027
+ var sourceTimeAt = (clip, time) => clip.type === "media" ? videoClipSourceTime(clip, time - clip.start) : 0;
1028
+ function videoFrameAt(project, time) {
1029
+ const { layers, audio } = tracksAt(project, project.tracks, time, { assets: videoAssetMap(project), sequences: videoSequenceMap(project), within: [], prefix: "" });
1030
+ return { time, width: project.width, height: project.height, background: _nullishCoalesce(project.background, () => ( "#000000")), layers, audio };
1031
+ }
1032
+ function tracksAt(project, tracks, time, scope) {
1033
+ const { assets, prefix } = scope;
1034
+ const layers = [];
1035
+ const audio = [];
1036
+ for (let ti = tracks.length - 1; ti >= 0; ti--) {
1037
+ const track = tracks[ti];
1038
+ for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
1039
+ if (_optionalChain([incoming, 'optionalAccess', _21 => _21.type]) === "dip" && incoming.progress < 0.5) continue;
1040
+ if (_optionalChain([outgoing, 'optionalAccess', _22 => _22.type]) === "dip" && outgoing.progress >= 0.5) continue;
1041
+ const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
1042
+ const fade = fadeGain(clip, inside);
1043
+ const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
1044
+ const mix = (incoming ? level(incoming, incoming.transition, true) : 1) * (outgoing ? level(outgoing, outgoing.next.transition, false) : 1);
1045
+ const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
1046
+ const sourceTime = clip.type === "sequence" ? (_nullishCoalesce(clip.in, () => ( 0))) + time - clip.start : sourceTimeAt(clip, time);
1047
+ const clipTime = time - clip.start;
1048
+ let nested = null;
1049
+ if (clip.type === "sequence") {
1050
+ const seq = scope.sequences.get(clip.sequence);
1051
+ if (!seq || scope.within.includes(seq.id)) continue;
1052
+ nested = tracksAt(project, seq.tracks, sourceTime, { ...scope, within: [...scope.within, seq.id], prefix: `${prefix}${clip.id}/` });
1053
+ }
1054
+ const audible = videoAssetHasSound(asset);
1055
+ if (audible && clip.type === "media" && !clip.muted && !track.muted) {
1056
+ const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fadeGain(clip, inside, true) * mix;
1057
+ audio.push({ clip: prefix + clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
1058
+ }
1059
+ if (nested && clip.type === "sequence" && !clip.muted && !track.muted) {
1060
+ const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fadeGain(clip, inside, true) * mix;
1061
+ for (const a of nested.audio) audio.push({ ...a, gain: a.gain * gain });
1062
+ }
1063
+ if (track.kind !== "video" || track.hidden) continue;
1064
+ if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
1065
+ if (nested && !nested.layers.length) continue;
1066
+ const shown = videoClipAnimated(clip, project);
1067
+ let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
1068
+ let wipe;
1069
+ let dip = null;
1070
+ let iris;
1071
+ let shift = 0;
1072
+ let shiftDirection = "right";
1073
+ if (incoming) {
1074
+ if (incoming.type === "crossfade") opacity *= incoming.progress;
1075
+ else if (incoming.type === "wipe") wipe = { direction: _nullishCoalesce(incoming.transition.direction, () => ( "right")), progress: incoming.progress };
1076
+ else if (incoming.type === "iris") iris = { progress: incoming.progress };
1077
+ else if (incoming.type === "push" || incoming.type === "slide") {
1078
+ shiftDirection = _nullishCoalesce(incoming.transition.direction, () => ( "right"));
1079
+ shift = videoTransitionEase(incoming.progress) - 1;
1080
+ } else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
1081
+ }
1082
+ if (_optionalChain([outgoing, 'optionalAccess', _23 => _23.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
1083
+ if (_optionalChain([outgoing, 'optionalAccess', _24 => _24.type]) === "push") {
1084
+ shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
1085
+ shift = videoTransitionEase(outgoing.progress);
1086
+ }
1087
+ const size = clip.type === "media" ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
1088
+ const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill")));
1089
+ const placed = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, shown.transform, clipTime);
1090
+ const across = shiftDirection === "left" || shiftDirection === "right";
1091
+ const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
1092
+ const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
1093
+ const layer = { clip: prefix + clip.id, track: track.id, type: clip.type, opacity: clamp012(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _25 => _25.transform, 'optionalAccess', _26 => _26.rotation]), clipTime, 0) };
1094
+ if (wipe) layer.wipe = wipe;
1095
+ if (iris) layer.iris = iris;
1096
+ if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
1097
+ const grade = normalizeVideoGrade(clip.grade);
1098
+ if (grade) layer.grade = grade;
1099
+ if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
1100
+ if (clip.type === "sequence") Object.assign(layer, { sequence: clip.sequence, sourceTime, layers: nested.layers });
1101
+ if (clip.type === "text") layer.text = videoTextStyle(clip);
1102
+ if (clip.type === "color") layer.color = clip.color;
1103
+ if (layer.opacity > 0) layers.push(layer);
1104
+ if (dip && dip.opacity > 0) layers.push({ clip: `transition:${prefix}${incoming ? clip.id : outgoing.next.id}`, track: track.id, type: "color", color: dip.color, opacity: clamp012(dip.opacity), rect: { x: 0, y: 0, width: project.width, height: project.height }, rotation: 0 });
1105
+ }
1106
+ }
1107
+ return { layers, audio };
1108
+ }
1109
+ function videoFrameLeafLayers(layers) {
1110
+ return layers.flatMap((l) => l.layers ? videoFrameLeafLayers(l.layers) : [l]);
1111
+ }
1112
+
1113
+ // src/video-editor/render.ts
1114
+ var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1115
+ var n = (v) => String(Math.round(v * 100) / 100);
1116
+ var n4 = (v) => String(Math.round(v * 1e4) / 1e4);
1117
+ var videoIrisRadius = (frame, progress) => progress * Math.hypot(frame.width / 2, frame.height / 2);
1118
+ function videoWipeRect(rect, wipe) {
1119
+ const { direction, progress } = wipe;
1120
+ const w = rect.width * progress;
1121
+ const h = rect.height * progress;
1122
+ if (direction === "right") return { ...rect, width: w };
1123
+ if (direction === "left") return { ...rect, x: rect.x + rect.width - w, width: w };
1124
+ if (direction === "down") return { ...rect, height: h };
1125
+ return { ...rect, y: rect.y + rect.height - h, height: h };
1126
+ }
1127
+ function videoFrameToSvg(frame, options = {}, assets = []) {
1128
+ const defs = [];
1129
+ const posters = new Map(assets.map((a) => [a.id, a.poster]));
1130
+ const draw = (layers, prefix) => layers.map((layer, index) => {
1131
+ const i = `${prefix}${index}`;
1132
+ const { rect } = layer;
1133
+ const attrs = [];
1134
+ const style = [];
1135
+ if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
1136
+ if (layer.blend) style.push(`mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}`);
1137
+ if (layer.layers) style.push("isolation:isolate");
1138
+ if (style.length) attrs.push(`style="${style.join(";")}"`);
1139
+ if (layer.grade) {
1140
+ defs.push(videoGradeSvgFilter(`g${i}`, layer.grade, layer.grade.lut ? _optionalChain([options, 'access', _27 => _27.lutFor, 'optionalCall', _28 => _28(layer.grade.lut.src)]) : null));
1141
+ attrs.push(`filter="url(#g${i})"`);
1142
+ }
1143
+ if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
1144
+ if (layer.wipe) {
1145
+ const r = videoWipeRect(rect, layer.wipe);
1146
+ defs.push(`<clipPath id="w${i}"><rect x="${n(r.x)}" y="${n(r.y)}" width="${n(r.width)}" height="${n(r.height)}"/></clipPath>`);
1147
+ attrs.push(`clip-path="url(#w${i})"`);
1148
+ }
1149
+ const box = `x="${n(rect.x)}" y="${n(rect.y)}" width="${n(rect.width)}" height="${n(rect.height)}"`;
1150
+ let inner = "";
1151
+ if (layer.layers) {
1152
+ inner = `<g transform="translate(${n(rect.x)} ${n(rect.y)}) scale(${n4(rect.width / frame.width)} ${n4(rect.height / frame.height)})">${draw(layer.layers, `${i}-`)}</g>`;
1153
+ } else if (layer.type === "color") inner = `<rect ${box} fill="${esc(_nullishCoalesce(layer.color, () => ( "#000000")))}"/>`;
1154
+ else if (layer.type === "text" && layer.text) {
1155
+ const t = layer.text;
1156
+ const lines = t.text.split("\n");
1157
+ const lh = t.fontSize * 1.2;
1158
+ const cx = rect.x + rect.width / 2;
1159
+ const cy = rect.y + rect.height / 2;
1160
+ const top = cy - lh * (lines.length - 1) / 2;
1161
+ if (t.background) {
1162
+ const w = Math.max(...lines.map((l) => l.length)) * t.fontSize * 0.6 + t.fontSize;
1163
+ const h = lh * lines.length + t.fontSize * 0.4;
1164
+ inner += `<rect x="${n(cx - w / 2)}" y="${n(cy - h / 2)}" width="${n(w)}" height="${n(h)}" fill="${esc(t.background)}"/>`;
1165
+ }
1166
+ inner += `<text x="${n(cx)}" text-anchor="middle" dominant-baseline="central" font-family="${esc(t.fontFamily)}" font-size="${n(t.fontSize)}" font-weight="${t.fontWeight}" fill="${esc(t.color)}">${lines.map((l, k) => `<tspan x="${n(cx)}" y="${n(top + k * lh)}">${esc(l)}</tspan>`).join("")}</text>`;
1167
+ } else {
1168
+ const href = options.imageFor ? options.imageFor(layer, frame) : layer.assetKind === "image" ? _nullishCoalesce(layer.src, () => ( null)) : _nullishCoalesce(posters.get(_nullishCoalesce(layer.asset, () => ( ""))), () => ( null));
1169
+ if (href) inner = `<image href="${esc(href)}" ${box} preserveAspectRatio="none"/>`;
1170
+ else {
1171
+ const label = `${_nullishCoalesce(layer.asset, () => ( layer.clip))} \xB7 ${formatVideoTimecode(_nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(options.fps, () => ( 30)))}`;
1172
+ const size = Math.max(10, Math.min(rect.height / 8, rect.width / Math.max(8, label.length * 0.6)));
1173
+ inner = `<rect ${box} fill="#334155" stroke="#94a3b8" stroke-width="2"/><text x="${n(rect.x + rect.width / 2)}" y="${n(rect.y + rect.height / 2)}" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="${n(size)}" fill="#e2e8f0">${esc(label)}</text>`;
1174
+ }
1175
+ }
1176
+ let item = attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner;
1177
+ if (layer.iris) {
1178
+ defs.push(`<clipPath id="i${i}"><circle cx="${n(frame.width / 2)}" cy="${n(frame.height / 2)}" r="${n(videoIrisRadius(frame, layer.iris.progress))}"/></clipPath>`);
1179
+ item = `<g clip-path="url(#i${i})">${item}</g>`;
1180
+ }
1181
+ return item;
1182
+ }).join("");
1183
+ const body = draw(frame.layers, "");
1184
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(frame.width)} ${n(frame.height)}" width="${n(frame.width)}" height="${n(frame.height)}">${defs.length ? `<defs>${defs.join("")}</defs>` : ""}<rect width="100%" height="100%" fill="${esc(frame.background)}"/>${body}</svg>`;
1185
+ }
1186
+ function videoProjectSvg(project, time, options = {}) {
1187
+ return videoFrameToSvg(videoFrameAt(project, time), { fps: project.fps, ...options }, project.assets);
1188
+ }
1189
+ function videoContactSheetSvg(project, times, options = {}) {
1190
+ const columns = _nullishCoalesce(options.columns, () => ( Math.ceil(Math.sqrt(times.length))));
1191
+ const w = _nullishCoalesce(options.width, () => ( 320));
1192
+ const h = w * project.height / project.width;
1193
+ const label = 18;
1194
+ const gap = 8;
1195
+ const rows = Math.ceil(times.length / columns);
1196
+ const W = columns * w + (columns + 1) * gap;
1197
+ const H = rows * (h + label) + (rows + 1) * gap;
1198
+ const cells = times.map((t, i) => {
1199
+ const x = gap + i % columns * (w + gap);
1200
+ const y = gap + Math.floor(i / columns) * (h + label + gap);
1201
+ const svg = videoProjectSvg(project, t, options).replace(/^<svg xmlns="http:\/\/www.w3.org\/2000\/svg"/, `<svg x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}"`).replace(/ width="[\d.]+" height="[\d.]+">/, ">");
1202
+ return `${svg}<text x="${n(x)}" y="${n(y + h + 14)}" font-family="sans-serif" font-size="12" fill="#0f172a">${(t / 1e3).toFixed(2)} s \xB7 ${formatVideoTimecode(t, project.fps)}</text>`;
1203
+ });
1204
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(W)} ${n(H)}" width="${n(W)}" height="${n(H)}"><rect width="100%" height="100%" fill="#f8fafc"/>${cells.join("")}</svg>`;
1205
+ }
1206
+ function paintVideoLayer(ctx, frame, layer, sourceFor, options) {
1207
+ const { rect } = layer;
1208
+ if (layer.iris) {
1209
+ ctx.beginPath();
1210
+ ctx.arc(frame.width / 2, frame.height / 2, videoIrisRadius(frame, layer.iris.progress), 0, 2 * Math.PI);
1211
+ ctx.clip();
1212
+ }
1213
+ if (layer.grade) {
1214
+ const defs = videoGradeSvgDefs(layer.grade);
1215
+ if (options.defineSvgFilter) for (const d of defs) options.defineSvgFilter(d.id, d.markup);
1216
+ const css = videoGradeCssFilter(layer.grade);
1217
+ ctx.filter = options.defineSvgFilter ? css || "none" : css.replace(/\s*url\([^)]*\)/g, "").trim() || "none";
1218
+ }
1219
+ if (layer.rotation) {
1220
+ ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
1221
+ ctx.rotate(layer.rotation * Math.PI / 180);
1222
+ ctx.translate(-(rect.x + rect.width / 2), -(rect.y + rect.height / 2));
1223
+ }
1224
+ if (layer.wipe) {
1225
+ const r = videoWipeRect(rect, layer.wipe);
1226
+ ctx.beginPath();
1227
+ ctx.rect(r.x, r.y, r.width, r.height);
1228
+ ctx.clip();
1229
+ }
1230
+ if (layer.type === "color") {
1231
+ ctx.fillStyle = _nullishCoalesce(layer.color, () => ( "#000000"));
1232
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
1233
+ } else if (layer.type === "text" && layer.text) {
1234
+ const t = layer.text;
1235
+ const lines = t.text.split("\n");
1236
+ const lh = t.fontSize * 1.2;
1237
+ const cx = rect.x + rect.width / 2;
1238
+ const top = rect.y + rect.height / 2 - lh * (lines.length - 1) / 2;
1239
+ ctx.font = `${t.fontWeight} ${t.fontSize}px ${t.fontFamily}`;
1240
+ ctx.textAlign = "center";
1241
+ ctx.textBaseline = "middle";
1242
+ if (t.background) {
1243
+ const w = Math.max(...lines.map((l) => ctx.measureText(l).width)) + t.fontSize;
1244
+ const h = lh * lines.length + t.fontSize * 0.4;
1245
+ ctx.fillStyle = t.background;
1246
+ ctx.fillRect(cx - w / 2, rect.y + rect.height / 2 - h / 2, w, h);
1247
+ }
1248
+ ctx.fillStyle = t.color;
1249
+ lines.forEach((l, k) => ctx.fillText(l, cx, top + k * lh));
1250
+ } else {
1251
+ const source = sourceFor(layer);
1252
+ if (source) ctx.drawImage(source, rect.x, rect.y, rect.width, rect.height);
1253
+ else {
1254
+ ctx.fillStyle = "#334155";
1255
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
1256
+ ctx.strokeStyle = "#94a3b8";
1257
+ ctx.lineWidth = 2;
1258
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
1259
+ }
1260
+ }
1261
+ }
1262
+ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
1263
+ ctx.save();
1264
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
1265
+ ctx.globalAlpha = 1;
1266
+ ctx.fillStyle = frame.background;
1267
+ ctx.fillRect(0, 0, frame.width, frame.height);
1268
+ drawVideoLayers(ctx, frame, frame.layers, sourceFor, scale, options, 0);
1269
+ ctx.restore();
1270
+ }
1271
+ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth) {
1272
+ const cw = Math.max(1, Math.round(frame.width * scale));
1273
+ const ch = Math.max(1, Math.round(frame.height * scale));
1274
+ for (const layer of layers) {
1275
+ let source = sourceFor;
1276
+ if (layer.layers) {
1277
+ const group = _optionalChain([options, 'access', _29 => _29.scratch, 'optionalCall', _30 => _30(cw, ch, 2 * depth)]);
1278
+ if (!group) {
1279
+ drawVideoLayers(ctx, frame, layer.layers, sourceFor, scale, options, depth + 1);
1280
+ continue;
1281
+ }
1282
+ const g = group.ctx;
1283
+ g.save();
1284
+ g.setTransform(1, 0, 0, 1, 0, 0);
1285
+ g.clearRect(0, 0, cw, ch);
1286
+ g.setTransform(scale, 0, 0, scale, 0, 0);
1287
+ g.globalAlpha = 1;
1288
+ g.globalCompositeOperation = "source-over";
1289
+ drawVideoLayers(g, frame, layer.layers, sourceFor, scale, options, depth + 1);
1290
+ g.restore();
1291
+ source = (l) => l === layer ? group.canvas : sourceFor(l);
1292
+ }
1293
+ const setting = _optionalChain([layer, 'access', _31 => _31.grade, 'optionalAccess', _32 => _32.lut]);
1294
+ const lut = setting ? _optionalChain([options, 'access', _33 => _33.lutFor, 'optionalCall', _34 => _34(setting.src)]) : null;
1295
+ const scratch = lut ? _optionalChain([options, 'access', _35 => _35.scratch, 'optionalCall', _36 => _36(cw, ch, 2 * depth + 1)]) : null;
1296
+ ctx.save();
1297
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
1298
+ if (lut && scratch) {
1299
+ const s = scratch.ctx;
1300
+ s.save();
1301
+ s.setTransform(1, 0, 0, 1, 0, 0);
1302
+ s.clearRect(0, 0, cw, ch);
1303
+ s.setTransform(scale, 0, 0, scale, 0, 0);
1304
+ s.globalAlpha = 1;
1305
+ s.globalCompositeOperation = "source-over";
1306
+ paintVideoLayer(s, frame, layer, source, options);
1307
+ s.restore();
1308
+ const img = s.getImageData(0, 0, cw, ch);
1309
+ applyVideoLutToPixels(lut, img.data, _nullishCoalesce(setting.strength, () => ( 1)));
1310
+ s.putImageData(img, 0, 0);
1311
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1312
+ ctx.globalAlpha = layer.opacity;
1313
+ if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
1314
+ ctx.drawImage(scratch.canvas, 0, 0, cw, ch);
1315
+ } else {
1316
+ ctx.globalAlpha = layer.opacity;
1317
+ if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
1318
+ paintVideoLayer(ctx, frame, layer, source, options);
1319
+ }
1320
+ ctx.restore();
1321
+ }
1322
+ }
1323
+
1324
+ // src/video-editor/export.ts
1325
+ var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _37 => _37.name]), () => ( _optionalChain([asset, 'optionalAccess', _38 => _38.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
1326
+ function toVideoEdl(project, options = {}) {
1327
+ const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
1328
+ if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
1329
+ const fps = project.fps;
1330
+ const dropFrame = (_nullishCoalesce(options.dropFrame, () => ( true))) && isVideoDropFrameRate(fps);
1331
+ const tc = (ms) => formatVideoTimecode(ms, fps, dropFrame);
1332
+ const rec0 = options.recordStart !== void 0 ? videoTimecodeFrames(options.recordStart, fps) : videoHourFrames(fps, dropFrame);
1333
+ const rtc = (ms) => formatVideoTimecodeFrames(rec0 + videoTimecodeFrames(ms, fps), fps, dropFrame);
1334
+ const assets = videoAssetMap(project);
1335
+ const channel = track.kind === "video" ? "V" : "A";
1336
+ const lines = [`TITLE: ${(_nullishCoalesce(_nullishCoalesce(options.title, () => ( project.name)), () => ( "Untitled"))).toUpperCase()}`, `FCM: ${dropFrame ? "DROP FRAME" : "NON-DROP FRAME"}`, ""];
1337
+ let event = 0;
1338
+ const num = () => String(++event).padStart(3, "0");
1339
+ const sourceAt = (clip, time) => clip.type === "media" ? (_nullishCoalesce(clip.in, () => ( 0))) + (time - clip.start) * videoClipSpeed(clip) : time - clip.start;
1340
+ const row = (n2, reel, kind, srcIn, srcOut, recIn, recOut) => `${n2} ${reel.padEnd(8)} ${channel.padEnd(5)} ${kind.padEnd(8)} ${tc(srcIn)} ${tc(srcOut)} ${rtc(recIn)} ${rtc(recOut)}`;
1341
+ for (const clip of sortedVideoClips(track)) {
1342
+ const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
1343
+ const reel = clip.type === "media" ? reelName(asset) : "BL";
1344
+ const end = videoClipEnd(clip);
1345
+ const before = clip.transition ? videoClipBefore(track, clip) : null;
1346
+ const half = before && clip.transition.type !== "dip" ? clip.transition.duration / 2 : 0;
1347
+ const next = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition && c.transition.type !== "dip");
1348
+ const recOut = next ? end - next.transition.duration / 2 : end;
1349
+ const recIn = clip.start - half;
1350
+ const n2 = num();
1351
+ if (before) {
1352
+ const t = clip.transition;
1353
+ const frames = String(Math.round(t.duration * fps / 1e3)).padStart(3, "0");
1354
+ const outReel = before.type === "media" ? reelName(assets.get(before.asset)) : "BL";
1355
+ const at = sourceAt(before, recIn);
1356
+ lines.push(row(n2, outReel, "C", at, at, recIn, recIn));
1357
+ lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
1358
+ } else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
1359
+ if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
1360
+ if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _39 => _39.name]), () => ( _optionalChain([asset, 'optionalAccess', _40 => _40.src]))), () => ( clip.asset))}`);
1361
+ if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
1362
+ if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
1363
+ if (clip.type === "sequence") lines.push(`* COMPOUND CLIP: ${_nullishCoalesce(clip.name, () => ( clip.sequence))} (sequence ${clip.sequence} from ${tc(_nullishCoalesce(clip.in, () => ( 0)))}, not flattened)`);
1364
+ lines.push("");
1365
+ }
1366
+ return lines.join("\n");
1367
+ }
1368
+ function loudnormFilter(target = {}, measure = false) {
1369
+ const I = _nullishCoalesce(target.integrated, () => ( -14));
1370
+ const TP = _nullishCoalesce(target.truePeak, () => ( -1));
1371
+ const LRA = _nullishCoalesce(target.range, () => ( 11));
1372
+ if (!(I >= -70 && I <= -5)) throw new Error(`The loudness target must be between -70 and -5 LUFS (got ${I}); -14 suits streaming, -23 broadcast.`);
1373
+ if (!(TP >= -9 && TP <= 0)) throw new Error(`The true peak ceiling must be between -9 and 0 dBTP (got ${TP}); -1 is usual.`);
1374
+ if (!(LRA >= 1 && LRA <= 50)) throw new Error(`The loudness range must be between 1 and 50 LU (got ${LRA}).`);
1375
+ const parts = [`loudnorm=I=${I}:TP=${TP}:LRA=${LRA}`];
1376
+ const m = target.measured;
1377
+ if (m && !measure) {
1378
+ for (const [k, v] of Object.entries(m)) if (!Number.isFinite(v)) throw new Error(`The measured ${k} is not a number (${v}); measure again, or leave "measured" out for a one-pass normalisation.`);
1379
+ parts.push(`measured_I=${m.integrated}:measured_TP=${m.truePeak}:measured_LRA=${m.range}:measured_thresh=${m.threshold}:offset=${m.offset}:linear=true`);
1380
+ }
1381
+ if (measure) parts.push("print_format=json");
1382
+ return `${parts.join(":")},aresample=48000`;
1383
+ }
1384
+ function parseVideoLoudnessMeasurement(output) {
1385
+ const blocks = output.match(/\{[^{}]*"input_i"[^{}]*\}/g);
1386
+ if (!blocks) throw new Error("No loudness measurement found: pass the stderr of the command from toVideoLoudnessMeasure.");
1387
+ const json = JSON.parse(blocks[blocks.length - 1]);
1388
+ const n2 = (k) => Number(json[k]);
1389
+ const m = { integrated: n2("input_i"), truePeak: n2("input_tp"), range: n2("input_lra"), threshold: n2("input_thresh"), offset: n2("target_offset") };
1390
+ if (!Number.isFinite(m.integrated)) throw new Error("The mix is silent, so its loudness cannot be normalised.");
1391
+ return m;
1392
+ }
1393
+ var sec = (ms) => String(Math.round(ms) / 1e3);
1394
+ var fmtMs = (v) => String(Math.round(v * 100) / 100);
1395
+ var FFMPEG_BLEND = {
1396
+ multiply: "multiply",
1397
+ screen: "screen",
1398
+ overlay: "overlay",
1399
+ darken: "darken",
1400
+ lighten: "lighten",
1401
+ "color-dodge": "dodge",
1402
+ "color-burn": "burn",
1403
+ "hard-light": "hardlight",
1404
+ "soft-light": "softlight",
1405
+ difference: "difference",
1406
+ exclusion: "exclusion",
1407
+ add: "addition"
1408
+ };
1409
+ var AFADE_CURVES = { linear: "tri", "equal-power": "qsin", "s-curve": "hsin", exponential: "cub", logarithmic: "ipar" };
1410
+ var afadeCurve = (curve) => curve && curve !== "linear" ? `:curve=${AFADE_CURVES[curve]}` : "";
1411
+ var even = (v) => Math.max(2, 2 * Math.round(v / 2));
1412
+ var ffmpegFilterValue = (s) => s.replace(/[\\':%]/g, (c) => `\\${c}`).replace(/[\\'[\],;]/g, (c) => `\\${c}`);
1413
+ var color = (c) => {
1414
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(c);
1415
+ if (!hex) return c.replace(/[^A-Za-z0-9@.]/g, "");
1416
+ const h = hex[1].length === 3 ? [...hex[1]].map((x) => x + x).join("") : hex[1];
1417
+ return `0x${h.slice(0, 6)}${h.length === 8 ? `@${(parseInt(h.slice(6), 16) / 255).toFixed(3)}` : ""}`;
1418
+ };
1419
+ var videoShellQuote = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
1420
+ function atempo(speed) {
1421
+ const out = [];
1422
+ let s = speed;
1423
+ while (s > 2) {
1424
+ out.push("atempo=2");
1425
+ s /= 2;
1426
+ }
1427
+ while (s < 0.5) {
1428
+ out.push("atempo=0.5");
1429
+ s /= 0.5;
1430
+ }
1431
+ if (Math.abs(s - 1) > 1e-6) out.push(`atempo=${Math.round(s * 1e6) / 1e6}`);
1432
+ return out;
1433
+ }
1434
+ function toVideoFfmpeg(project, options = {}) {
1435
+ return buildFfmpeg(project, options, false);
1436
+ }
1437
+ function toVideoLoudnessMeasure(project, options = {}) {
1438
+ return buildFfmpeg(project, { ...options, loudness: { ...options.loudness, measured: void 0 } }, true);
1439
+ }
1440
+ function buildFfmpeg(project, options, measure) {
1441
+ const notes = [];
1442
+ const assets = videoAssetMap(project);
1443
+ const sequences = videoSequenceMap(project);
1444
+ const D = videoProjectDuration(project);
1445
+ if (D <= 0) throw new Error("The project has no clips, so there is nothing to render.");
1446
+ const W = even(project.width);
1447
+ const H = even(project.height);
1448
+ const fps = project.fps;
1449
+ const inputs = [];
1450
+ const graph = measure ? [] : [`color=c=${color(_nullishCoalesce(project.background, () => ( "#000000")))}:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[base]`];
1451
+ let usesDrawtext = false;
1452
+ const animated = /* @__PURE__ */ new Set();
1453
+ const ramped = /* @__PURE__ */ new Set();
1454
+ const rampedPicture = (remap, lead, end) => {
1455
+ const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
1456
+ const first = pts[0];
1457
+ const last = pts[pts.length - 1];
1458
+ const s = `(T*1000+${fmtMs(first.v)})`;
1459
+ let e = fmtMs(last.t - first.t);
1460
+ for (let i = pts.length - 2; i >= 0; i--) {
1461
+ const a = pts[i];
1462
+ const b = pts[i + 1];
1463
+ if (b.v - a.v < 1e-6) continue;
1464
+ e = `if(lt(${s},${fmtMs(b.v)}),${fmtMs(a.t - first.t)}+(${s}-${fmtMs(a.v)})*${fmtMs((b.t - a.t) / (b.v - a.v))},${e})`;
1465
+ }
1466
+ const before = lead + first.t;
1467
+ const after = Math.max(0, end - last.t);
1468
+ return ["setpts=PTS-STARTPTS", `setpts='(${e})/1000/TB'`, `fps=${fps}`, ...before > 0.5 || after > 0.5 ? [`tpad=start_mode=clone:start_duration=${sec(before)}:stop_mode=clone:stop_duration=${sec(after + 1e3 / fps)}`] : [`tpad=stop_mode=clone:stop_duration=${sec(1e3 / fps)}`], `trim=duration=${sec(lead + end)}`];
1469
+ };
1470
+ const rampedSound = (remap, input2, output, lead, end) => {
1471
+ const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
1472
+ const first = pts[0];
1473
+ const last = pts[pts.length - 1];
1474
+ const n2 = ak;
1475
+ const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
1476
+ const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
1477
+ const pieces = [];
1478
+ const out = [];
1479
+ const parts = [];
1480
+ if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
1481
+ const played = [];
1482
+ for (let i = 0; i + 1 < pts.length; i++) {
1483
+ const dt = pts[i + 1].t - pts[i].t;
1484
+ if (dt < 0.5) continue;
1485
+ const speed = (pts[i + 1].v - pts[i].v) / dt;
1486
+ if (speed < 0.1) parts.push(silence(dt));
1487
+ else {
1488
+ played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
1489
+ parts.push(`#${played.length - 1}`);
1490
+ }
1491
+ }
1492
+ if (end - last.t > 0.5) parts.push(silence(end - last.t));
1493
+ if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
1494
+ parts.forEach((p, k2) => {
1495
+ const label = `[r${n2}p${k2}]`;
1496
+ if (p.startsWith("#")) {
1497
+ const piece = played[Number(p.slice(1))];
1498
+ out.push(`[r${n2}s${p.slice(1)}]atrim=start=${sec(piece.from)}:end=${sec(piece.to)},asetpts=PTS-STARTPTS,${atempo(piece.speed).join(",") || "anull"},${fmtA}${label}`);
1499
+ } else out.push(`${p}${label}`);
1500
+ pieces.push(label);
1501
+ });
1502
+ out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
1503
+ return out;
1504
+ };
1505
+ const blended = /* @__PURE__ */ new Set();
1506
+ let k = 0;
1507
+ const step = 2e3 / fps;
1508
+ const expr = (prop, clipTime, fallback, what) => {
1509
+ if (isVideoKeyframes(prop)) animated.add(what);
1510
+ return videoKeyframeFfmpegExpr(prop, clipTime, fallback, step);
1511
+ };
1512
+ const input = (clip, asset, lead, tail) => {
1513
+ const speed = videoClipSpeed(clip);
1514
+ const src = options.resolve ? options.resolve(asset) : asset.src;
1515
+ const span = clip.duration + lead + tail;
1516
+ if (asset.kind === "image") inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", src]);
1517
+ else if (clip.timeRemap) {
1518
+ const { from, to } = videoTimeRemapSpan(clip.timeRemap);
1519
+ inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
1520
+ } else inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) - lead * speed), "-t", sec(span * speed), "-i", src]);
1521
+ return inputs.length - 1;
1522
+ };
1523
+ let ak = 0;
1524
+ let q = 0;
1525
+ const compose = (tracks, base, length, scope) => {
1526
+ let cur2 = base;
1527
+ const sounds = [];
1528
+ for (const track of [...tracks].reverse()) {
1529
+ for (const raw of sortedVideoClips(track)) {
1530
+ const clip = videoClipAnimated(raw, project);
1531
+ const { lead, tail } = videoClipOverhang(track, clip);
1532
+ const t0 = clip.start - lead;
1533
+ const span = clip.duration + lead + tail;
1534
+ const end = videoClipEnd(clip);
1535
+ const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
1536
+ if (clip.type === "media" && !asset) {
1537
+ notes.push(`Clip "${clip.id}" was left out: its asset "${clip.asset}" is missing.`);
1538
+ continue;
1539
+ }
1540
+ const shows = scope.picture && track.kind === "video" && !track.hidden;
1541
+ let compound = null;
1542
+ if (clip.type === "sequence") {
1543
+ const seq = sequences.get(clip.sequence);
1544
+ if (!seq || scope.within.includes(seq.id)) {
1545
+ notes.push(`Compound clip "${clip.id}" was left out: its sequence "${clip.sequence}" ${seq ? "contains itself" : "is missing"}.`);
1546
+ continue;
1547
+ }
1548
+ const from = (_nullishCoalesce(clip.in, () => ( 0))) - lead;
1549
+ if (from < -0.5) notes.push(`Compound clip "${clip.id}"'s transition needs ${Math.round(-from)} ms of sequence "${seq.id}" before its start, which is not there; it shows nothing then.`);
1550
+ const length2 = Math.max(videoSequenceDuration(seq), from + span);
1551
+ const hears = scope.sound && !clip.muted && !track.muted;
1552
+ const n2 = q++;
1553
+ const nestedBase = shows ? `nb${n2}` : null;
1554
+ if (nestedBase) graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length2)},format=yuva420p[${nestedBase}]`);
1555
+ const sub = compose(seq.tracks, nestedBase, length2, { within: [...scope.within, seq.id], picture: shows, sound: hears });
1556
+ compound = { picture: null, sound: null };
1557
+ if (sub.video) {
1558
+ graph.push(`[${sub.video}]trim=start=${sec(Math.max(0, from))}:duration=${sec(span)},setpts=PTS-STARTPTS[sq${n2}]`);
1559
+ compound.picture = `[sq${n2}]`;
1560
+ }
1561
+ if (sub.audio.length) {
1562
+ graph.push(`${sub.audio.join("")}amix=inputs=${sub.audio.length}:normalize=0:duration=longest,apad,atrim=start=${sec(Math.max(0, from))}:end=${sec(from + span)}[sm${n2}]`);
1563
+ compound.sound = `[sm${n2}]`;
1564
+ }
1565
+ }
1566
+ let index;
1567
+ if (shows && asset && asset.kind !== "audio") index = input(clip, asset, lead, tail);
1568
+ const audible = clip.type === "sequence" ? !!_optionalChain([compound, 'optionalAccess', _41 => _41.sound]) : videoAssetHasSound(asset);
1569
+ if (scope.sound && audible && (clip.type === "media" || clip.type === "sequence") && !clip.muted && !track.muted) {
1570
+ const trackGain = _nullishCoalesce(track.volume, () => ( 1));
1571
+ const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
1572
+ const ramp = clip.type === "media" ? clip.timeRemap : void 0;
1573
+ const ai = compound ? -1 : _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
1574
+ let source2 = _nullishCoalesce(_optionalChain([compound, 'optionalAccess', _42 => _42.sound]), () => ( `[${ai}:a]`));
1575
+ if (ramp) {
1576
+ source2 = `[r${ak}]`;
1577
+ graph.push(...rampedSound(ramp, `${ai}:a`, source2, lead, clip.duration + tail));
1578
+ ramped.add("sound");
1579
+ }
1580
+ const chain2 = [...ramp ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
1581
+ const before2 = clip.transition ? videoClipBefore(track, clip) : null;
1582
+ const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
1583
+ const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
1584
+ const outCurve = next2 ? afadeCurve(videoTransitionAudioCurve(next2.transition)) : "";
1585
+ if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
1586
+ if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
1587
+ if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
1588
+ if (_optionalChain([next2, 'optionalAccess', _43 => _43.transition, 'access', _44 => _44.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
1589
+ if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
1590
+ if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
1591
+ chain2.push(`adelay=${Math.round(t0)}:all=1`);
1592
+ graph.push(`${source2}${chain2.join(",")}[a${ak}]`);
1593
+ sounds.push(`[a${ak++}]`);
1594
+ }
1595
+ if (!shows || compound && !compound.picture) continue;
1596
+ const enable = `enable='between(t,${sec(t0)},${sec(t0 + span)})'`;
1597
+ const next = `v${k++}`;
1598
+ const onTimeline = `(t*1000-${Math.round(clip.start)})`;
1599
+ const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
1600
+ const tf = clip.transform;
1601
+ const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _45 => _45[p]])));
1602
+ if (clip.type === "text" && options.titleImage) {
1603
+ inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", options.titleImage(moving ? { ...clip, transform: void 0 } : clip, { width: project.width, height: project.height })]);
1604
+ index = inputs.length - 1;
1605
+ } else if (clip.type === "text") {
1606
+ usesDrawtext = true;
1607
+ if (scope.within.length) notes.push(`Title "${clip.id}" is inside a compound clip and drawn with drawtext, which leaves the sequence's transparent areas transparent, so it shows only over pictures in the sequence; pass titleImage to draw it anywhere.`);
1608
+ const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
1609
+ const alpha = [op, clip.fadeIn ? `min(1,(t-${sec(clip.start)})/${sec(clip.fadeIn)})` : "", clip.fadeOut ? `min(1,(${sec(end)}-t)/${sec(clip.fadeOut)})` : ""].filter(Boolean).join("*");
1610
+ const coord = (axis) => {
1611
+ const v = _optionalChain([tf, 'optionalAccess', _46 => _46[axis]]);
1612
+ const base2 = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
1613
+ return isVideoKeyframes(v) ? `${axis}='${base2}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base2}+${_nullishCoalesce(v, () => ( 0))}`;
1614
+ };
1615
+ const fontSize = _nullishCoalesce(clip.fontSize, () => ( 64));
1616
+ const opts = [
1617
+ `text=${ffmpegFilterValue(clip.text)}`,
1618
+ options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
1619
+ isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _47 => _47.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _48 => _48.scale]), () => ( 1))))}`,
1620
+ `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
1621
+ coord("x"),
1622
+ coord("y"),
1623
+ `alpha='${alpha}'`,
1624
+ ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
1625
+ `enable='between(t,${sec(clip.start)},${sec(end)})'`
1626
+ ];
1627
+ if (_optionalChain([tf, 'optionalAccess', _49 => _49.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
1628
+ if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access', _50 => _50.grade, 'optionalAccess', _51 => _51.lut]))) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
1629
+ if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
1630
+ graph.push(`[${cur2}]drawtext=${opts.join(":")}[${next}]`);
1631
+ cur2 = next;
1632
+ continue;
1633
+ }
1634
+ const size = asset ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
1635
+ const placed = clip.type === "text" && !moving ? void 0 : tf;
1636
+ const still = Object.fromEntries(Object.entries(_nullishCoalesce(placed, () => ( {}))).filter(([, v]) => !isVideoKeyframes(v)));
1637
+ const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : _nullishCoalesce(clip.fit, () => ( (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill"))), still);
1638
+ const chain = [];
1639
+ let source;
1640
+ if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
1641
+ else if (_optionalChain([compound, 'optionalAccess', _52 => _52.picture])) {
1642
+ source = compound.picture;
1643
+ chain.push(`scale=${even(rect.width)}:${even(rect.height)}`);
1644
+ } else {
1645
+ source = `[${index}:v]`;
1646
+ if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
1647
+ else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
1648
+ }
1649
+ const grade = videoGradeFfmpegFilters(clip.grade);
1650
+ if (grade.length) chain.push("format=rgba", ...grade);
1651
+ const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _53 => _53.grade, 'optionalAccess', _54 => _54.lut]));
1652
+ if (lut) {
1653
+ chain.push("format=gbrap");
1654
+ const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
1655
+ if (/^(blob|data|https?):/i.test(path)) notes.push(`ffmpeg's lut3d reads only local files, and clip "${clip.id}"'s LUT is "${path.slice(0, 80)}"; save it and pass resolveLut to give its path.`);
1656
+ const file = ffmpegFilterValue(path);
1657
+ const into2 = `q${next}`;
1658
+ graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
1659
+ const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
1660
+ if ((_nullishCoalesce(lut.strength, () => ( 1))) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
1661
+ else graph.push(`[${into2}]split[o${next}][p${next}]`, `[p${next}]${lut3d}[s${next}]`, `[s${next}][o${next}]blend=all_mode=normal:all_opacity=${lut.strength}[r${next}]`);
1662
+ source = `[r${next}]`;
1663
+ chain.length = 0;
1664
+ }
1665
+ chain.push("format=yuva420p");
1666
+ if (isVideoKeyframes(clip.opacity)) chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='alpha(X,Y)*clip(${expr(clip.opacity, onChain("T"), 1, "opacity")},0,1)'`);
1667
+ else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
1668
+ if (clip.fadeIn) chain.push(`fade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}:alpha=1`);
1669
+ if (clip.fadeOut) chain.push(`fade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}:alpha=1`);
1670
+ if (lead && clip.transition.type === "crossfade") chain.push(`fade=t=in:st=0:d=${sec(2 * lead)}:alpha=1`);
1671
+ if (lead && clip.transition.type === "wipe") {
1672
+ const d = sec(2 * lead);
1673
+ const p = `min(1,T/${d})`;
1674
+ const dir = _nullishCoalesce(clip.transition.direction, () => ( "right"));
1675
+ const test = dir === "right" ? `lt(X,W*${p})` : dir === "left" ? `gt(X,W*(1-${p}))` : dir === "down" ? `lt(Y,H*${p})` : `gt(Y,H*(1-${p}))`;
1676
+ chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
1677
+ }
1678
+ if (lead && clip.transition.type === "iris") {
1679
+ const r = videoIrisRadius({ width: W, height: H }, 1);
1680
+ chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
1681
+ }
1682
+ const rotation = _optionalChain([placed, 'optionalAccess', _55 => _55.rotation]);
1683
+ if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
1684
+ else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
1685
+ if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _56 => _56.scale]))) {
1686
+ const s = expr(placed.scale, onChain("t"), 1, "scale");
1687
+ chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
1688
+ }
1689
+ chain.push(`setpts=PTS+${sec(t0)}/TB`);
1690
+ const cx = rect.x + rect.width / 2;
1691
+ const cy = rect.y + rect.height / 2;
1692
+ const shifts = { x: [], y: [] };
1693
+ const shiftBy = (direction, from, d, entering) => {
1694
+ const dir = _nullishCoalesce(direction, () => ( "right"));
1695
+ const axis = dir === "left" || dir === "right" ? "x" : "y";
1696
+ const size2 = axis === "x" ? W : H;
1697
+ const sign = dir === "left" || dir === "up" ? -1 : 1;
1698
+ const ease = `(1-cos(PI*clip((t-${sec(from)})/${sec(d)},0,1)))/2`;
1699
+ shifts[axis].push(`${sign * size2}*(${ease}${entering ? "-1" : ""})`);
1700
+ };
1701
+ const into = lead ? clip.transition.type : void 0;
1702
+ if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
1703
+ const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
1704
+ if (_optionalChain([after, 'optionalAccess', _57 => _57.transition, 'access', _58 => _58.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
1705
+ const at = (axis, c) => {
1706
+ const v = _optionalChain([placed, 'optionalAccess', _59 => _59[axis]]);
1707
+ const extra = shifts[axis].map((s) => `+${s}`).join("");
1708
+ return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
1709
+ };
1710
+ graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
1711
+ if (clip.blend && clip.blend !== "normal") {
1712
+ graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length)},format=yuva420p[t${next}]`);
1713
+ const clear = scope.within.length > 0;
1714
+ graph.push(`[t${next}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable},format=gbrap,split${clear ? `=3` : ""}[f${next}][g${next}]${clear ? `[h${next}]` : ""}`);
1715
+ graph.push(`[${cur2}]format=gbrap,split${clear ? `=3` : ""}[c${next}][d${next}]${clear ? `[e${next}]` : ""}`);
1716
+ const layerFirst = clip.blend === "color-dodge" || clip.blend === "color-burn";
1717
+ graph.push(`${layerFirst ? `[f${next}][c${next}]` : `[c${next}][f${next}]`}blend=all_mode=${FFMPEG_BLEND[clip.blend]}[b${next}]`);
1718
+ graph.push(`[g${next}]alphaextract[m${next}]`);
1719
+ if (clear) {
1720
+ graph.push(`[e${next}]alphaextract[ea${next}]`);
1721
+ graph.push(`[b${next}][ea${next}]alphamerge[bb${next}]`);
1722
+ graph.push(`[h${next}]format=gbrp[ho${next}]`);
1723
+ graph.push(`[ho${next}][bb${next}]overlay=format=gbrp[bm${next}]`);
1724
+ graph.push(`[bm${next}][m${next}]alphamerge[x${next}]`);
1725
+ } else graph.push(`[b${next}][m${next}]alphamerge[x${next}]`);
1726
+ graph.push(`[d${next}][x${next}]overlay=format=gbrp,format=yuva420p[${next}]`);
1727
+ blended.add(clip.blend);
1728
+ } else graph.push(`[${cur2}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
1729
+ cur2 = next;
1730
+ const before = clip.transition ? videoClipBefore(track, clip) : null;
1731
+ if (before && clip.transition.type === "dip") {
1732
+ const d = clip.transition.duration;
1733
+ const dip = `d${k}`;
1734
+ graph.push(`color=c=${color(_nullishCoalesce(clip.transition.color, () => ( "#000000")))}:s=${W}x${H}:r=${fps}:d=${sec(d)},format=yuva420p,fade=t=in:st=0:d=${sec(d / 2)}:alpha=1,fade=t=out:st=${sec(d / 2)}:d=${sec(d / 2)}:alpha=1,setpts=PTS+${sec(clip.start - d / 2)}/TB[l${dip}]`);
1735
+ graph.push(`[${cur2}][l${dip}]overlay=eof_action=pass:enable='between(t,${sec(clip.start - d / 2)},${sec(clip.start + d / 2)})'[${dip}]`);
1736
+ cur2 = dip;
1737
+ }
1738
+ }
1739
+ }
1740
+ return { video: cur2, audio: sounds };
1741
+ };
1742
+ const top = compose(project.tracks, measure ? null : "base", D, { within: [], picture: !measure, sound: true });
1743
+ const audio = top.audio;
1744
+ const cur = top.video;
1745
+ if (!measure) graph.push(`[${cur}]format=yuv420p[vout]`);
1746
+ if (measure && !audio.length) throw new Error("Nothing on the timeline makes a sound, so there is no loudness to measure.");
1747
+ const loudness = options.loudness || measure ? `,${loudnormFilter(options.loudness, measure)}` : "";
1748
+ if (audio.length) graph.push(`${audio.join("")}amix=inputs=${audio.length}:normalize=0:duration=longest,apad,atrim=0:${sec(D)}${loudness}[aout]`);
1749
+ if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
1750
+ else if (options.loudness && !options.loudness.measured)
1751
+ notes.push("Loudness is normalised in one pass, which adjusts the level as it goes; for a fixed gain that keeps the dynamics, run toVideoLoudnessMeasure first and pass its result as loudness.measured.");
1752
+ if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _60 => _60.get, 'call', _61 => _61(c.asset), 'optionalAccess', _62 => _62.kind]) === "video" && _optionalChain([assets, 'access', _63 => _63.get, 'call', _64 => _64(c.asset), 'optionalAccess', _65 => _65.hasAudio]) === void 0)))
1753
+ notes.push('Video assets are assumed to have sound; set "hasAudio": false on any that do not, or ffmpeg will stop at a missing audio stream.');
1754
+ if (animated.size)
1755
+ notes.push(`Keyframed ${[...animated].sort().join(", ")} ${animated.size > 1 ? "are" : "is"} rendered as straight lines between keys, with eased segments sampled every two frames${animated.has("scale") ? "; animated scale needs ffmpeg 5 or later (scale with eval=frame)" : ""}.`);
1756
+ if (blended.has("soft-light")) notes.push("ffmpeg's soft-light blend uses a slightly different formula from the browser's, so the render can differ a little from the preview.");
1757
+ if (ramped.has("sound")) notes.push("Sound through a speed ramp plays each stretch at its own speed, so a gliding change is heard in small steps (about every 80 ms), and freezes are silent.");
1758
+ if (usesDrawtext) notes.push("Titles use drawtext, which needs an ffmpeg built with libfreetype (Homebrew's is not: check `ffmpeg -filters | grep drawtext`); pass titleImage to render them as images instead.");
1759
+ const filterComplex = graph.join(";");
1760
+ if (measure) {
1761
+ const args2 = ["-hide_banner", "-nostats", ...inputs.flat(), "-filter_complex", filterComplex, "-map", "[aout]", "-f", "null", "-"];
1762
+ return { args: args2, command: ["ffmpeg", ...args2].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
1763
+ }
1764
+ const args = [
1765
+ "-y",
1766
+ ...inputs.flat(),
1767
+ "-filter_complex",
1768
+ filterComplex,
1769
+ "-map",
1770
+ "[vout]",
1771
+ ...audio.length ? ["-map", "[aout]", "-c:a", _nullishCoalesce(options.audioCodec, () => ( "aac"))] : ["-an"],
1772
+ "-c:v",
1773
+ _nullishCoalesce(options.videoCodec, () => ( "libx264")),
1774
+ ...(_nullishCoalesce(options.videoCodec, () => ( "libx264"))) === "libx264" ? ["-crf", String(_nullishCoalesce(options.crf, () => ( 18))), "-preset", "medium"] : [],
1775
+ "-pix_fmt",
1776
+ "yuv420p",
1777
+ "-r",
1778
+ String(fps),
1779
+ "-t",
1780
+ sec(D),
1781
+ _nullishCoalesce(options.output, () => ( "output.mp4"))
1782
+ ];
1783
+ return { args, command: ["ffmpeg", ...args].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
1784
+ }
1785
+
1786
+
1787
+
1788
+
1789
+
1790
+
1791
+
1792
+
1793
+
1794
+
1795
+
1796
+
1797
+
1798
+
1799
+
1800
+
1801
+
1802
+
1803
+
1804
+
1805
+
1806
+
1807
+
1808
+
1809
+
1810
+
1811
+
1812
+
1813
+
1814
+
1815
+
1816
+
1817
+
1818
+
1819
+
1820
+
1821
+
1822
+
1823
+
1824
+
1825
+
1826
+
1827
+
1828
+
1829
+
1830
+
1831
+
1832
+
1833
+
1834
+
1835
+
1836
+
1837
+
1838
+
1839
+
1840
+
1841
+
1842
+
1843
+
1844
+
1845
+
1846
+
1847
+
1848
+
1849
+
1850
+
1851
+
1852
+
1853
+
1854
+
1855
+
1856
+
1857
+
1858
+
1859
+
1860
+
1861
+
1862
+
1863
+
1864
+
1865
+
1866
+
1867
+
1868
+
1869
+
1870
+
1871
+
1872
+
1873
+
1874
+
1875
+
1876
+
1877
+
1878
+
1879
+
1880
+
1881
+
1882
+
1883
+
1884
+
1885
+
1886
+
1887
+
1888
+
1889
+
1890
+
1891
+
1892
+
1893
+
1894
+
1895
+
1896
+
1897
+
1898
+
1899
+
1900
+
1901
+
1902
+
1903
+
1904
+
1905
+
1906
+
1907
+ exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.isVideoKeyframes = isVideoKeyframes; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoValueAt = videoValueAt; exports.videoKeysOf = videoKeysOf; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.splitVideoAnimatable = splitVideoAnimatable; exports.sortVideoKeys = sortVideoKeys; exports.setVideoKey = setVideoKey; exports.removeVideoKey = removeVideoKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.VIDEO_LUT_MAX_SIZE = VIDEO_LUT_MAX_SIZE; exports.parseVideoCubeLut = parseVideoCubeLut; exports.videoLutFromFunction = videoLutFromFunction; exports.videoLutToCube = videoLutToCube; exports.applyVideoLut = applyVideoLut; exports.applyVideoLutToPixels = applyVideoLutToPixels; exports.videoLutSvgApproximation = videoLutSvgApproximation; exports.VideoLutCache = VideoLutCache; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.normalizeVideoGradeLut = normalizeVideoGradeLut; exports.normalizeVideoGrade = normalizeVideoGrade; exports.videoGradePreset = videoGradePreset; exports.videoGradeLook = videoGradeLook; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.applyVideoGrade = applyVideoGrade; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoLutSvgPrimitives = videoLutSvgPrimitives; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.videoClipEnd = videoClipEnd; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSourceRange = videoClipSourceRange; exports.videoFrameDuration = videoFrameDuration; exports.snapToVideoFrame = snapToVideoFrame; exports.videoFrames = videoFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.videoTimecodeFrames = videoTimecodeFrames; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.videoHourFrames = videoHourFrames; exports.formatVideoSeconds = formatVideoSeconds; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetMap = videoAssetMap; exports.videoSequenceMap = videoSequenceMap; exports.videoSequenceDuration = videoSequenceDuration; exports.videoSequenceChildren = videoSequenceChildren; exports.findVideoSequenceCycle = findVideoSequenceCycle; exports.allVideoTracks = allVideoTracks; exports.sortedVideoClips = sortedVideoClips; exports.findVideoClip = findVideoClip; exports.allVideoClipIds = allVideoClipIds; exports.findVideoSequenceClip = findVideoSequenceClip; exports.videoProjectDuration = videoProjectDuration; exports.videoClipBefore = videoClipBefore; exports.videoClipAfter = videoClipAfter; exports.videoClipHandles = videoClipHandles; exports.videoClipOverhang = videoClipOverhang; exports.videoSnapPoints = videoSnapPoints; exports.snapVideoTime = snapVideoTime; exports.findVideoGaps = findVideoGaps; exports.videoOverlap = videoOverlap; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.videoClipsAt = videoClipsAt; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipAnimated = videoClipAnimated; exports.videoTransformAt = videoTransformAt; exports.placeVideoRect = placeVideoRect; exports.videoTextStyle = videoTextStyle; exports.videoTitleFrame = videoTitleFrame; exports.videoTransitionEase = videoTransitionEase; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoClipSoundGain = videoClipSoundGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameLeafLayers = videoFrameLeafLayers; exports.videoIrisRadius = videoIrisRadius; exports.videoWipeRect = videoWipeRect; exports.videoFrameToSvg = videoFrameToSvg; exports.videoProjectSvg = videoProjectSvg; exports.videoContactSheetSvg = videoContactSheetSvg; exports.drawVideoFrame = drawVideoFrame; exports.toVideoEdl = toVideoEdl; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.ffmpegFilterValue = ffmpegFilterValue; exports.videoShellQuote = videoShellQuote; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure;
1908
+ //# sourceMappingURL=chunk-SMJGE7HM.cjs.map