@pixerate/editor 0.115.0 → 0.116.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 (117) 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-DFQEX3AM.cjs → chunk-2UT4GXYA.cjs} +133 -8
  7. package/dist/chunk-2UT4GXYA.cjs.map +1 -0
  8. package/dist/{chunk-VXR7XWMF.js → chunk-7LNZAWSJ.js} +437 -41
  9. package/dist/chunk-7LNZAWSJ.js.map +1 -0
  10. package/dist/{chunk-LKCYANMW.js → chunk-ARQ7PQ6Q.js} +129 -4
  11. package/dist/chunk-ARQ7PQ6Q.js.map +1 -0
  12. package/dist/chunk-COAMRKVI.cjs +159 -0
  13. package/dist/chunk-COAMRKVI.cjs.map +1 -0
  14. package/dist/{chunk-C6C5PDKU.js → chunk-D53KSJUV.js} +3 -322
  15. package/dist/chunk-D53KSJUV.js.map +1 -0
  16. package/dist/chunk-D53VQGEJ.js +3262 -0
  17. package/dist/chunk-D53VQGEJ.js.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-HOCPSFGA.cjs +3262 -0
  23. package/dist/chunk-HOCPSFGA.cjs.map +1 -0
  24. package/dist/chunk-IPFRWILU.js +1908 -0
  25. package/dist/chunk-IPFRWILU.js.map +1 -0
  26. package/dist/{chunk-FV5AXTSF.cjs → chunk-JSSBHMMU.cjs} +36 -188
  27. package/dist/chunk-JSSBHMMU.cjs.map +1 -0
  28. package/dist/chunk-LF4WOZXZ.js +159 -0
  29. package/dist/chunk-LF4WOZXZ.js.map +1 -0
  30. package/dist/{chunk-26PGXHY7.cjs → chunk-M7O4BJRH.cjs} +36 -36
  31. package/dist/{chunk-26PGXHY7.cjs.map → chunk-M7O4BJRH.cjs.map} +1 -1
  32. package/dist/chunk-N3467VD6.js +501 -0
  33. package/dist/chunk-N3467VD6.js.map +1 -0
  34. package/dist/{chunk-ST7E3NEE.cjs → chunk-PAOWHJ5O.cjs} +18 -337
  35. package/dist/chunk-PAOWHJ5O.cjs.map +1 -0
  36. package/dist/{chunk-XICA4PVO.js → chunk-SEBYNBHW.js} +10 -10
  37. package/dist/chunk-SMJGE7HM.cjs +1908 -0
  38. package/dist/chunk-SMJGE7HM.cjs.map +1 -0
  39. package/dist/{chunk-MMVHBKMM.js → chunk-T2P5TIZ6.js} +2 -2
  40. package/dist/{chunk-FGW5OWJ4.js → chunk-VJUML33I.js} +4 -156
  41. package/dist/chunk-VJUML33I.js.map +1 -0
  42. package/dist/{chunk-I7FQBWMQ.cjs → chunk-ZYZGEILO.cjs} +239 -239
  43. package/dist/{chunk-I7FQBWMQ.cjs.map → chunk-ZYZGEILO.cjs.map} +1 -1
  44. package/dist/export/index.cjs +4 -2
  45. package/dist/export/index.cjs.map +1 -1
  46. package/dist/export/index.js +5 -3
  47. package/dist/{export-BJE8T0YA.d.cts → export-BXxSGBCq.d.cts} +2 -2
  48. package/dist/{export-Con3UKkm.d.ts → export-Dm44YUvS.d.ts} +2 -2
  49. package/dist/export-Dv0-ZVrV.d.cts +501 -0
  50. package/dist/export-Dv0-ZVrV.d.ts +501 -0
  51. package/dist/{retouch-Do6tBs2H.d.ts → flow-BMtHAmg4.d.ts} +561 -2
  52. package/dist/{retouch-Bxkuc0KH.d.cts → flow-COCB42TP.d.cts} +561 -2
  53. package/dist/image-editor/index.cjs +957 -795
  54. package/dist/image-editor/index.cjs.map +1 -1
  55. package/dist/image-editor/index.d.cts +6 -6
  56. package/dist/image-editor/index.d.ts +6 -6
  57. package/dist/image-editor/index.js +180 -18
  58. package/dist/image-editor/index.js.map +1 -1
  59. package/dist/index.cjs +4 -2
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.js +5 -3
  62. package/dist/index.js.map +1 -1
  63. package/dist/kernel-wasm-WYCGNZR4.js +10 -0
  64. package/dist/kernel-wasm-WYCGNZR4.js.map +1 -0
  65. package/dist/kernel-wasm-YPVAEMVN.cjs +10 -0
  66. package/dist/kernel-wasm-YPVAEMVN.cjs.map +1 -0
  67. package/dist/paint/index.cjs +165 -3
  68. package/dist/paint/index.cjs.map +1 -1
  69. package/dist/paint/index.d.cts +225 -3
  70. package/dist/paint/index.d.ts +225 -3
  71. package/dist/paint/index.js +166 -4
  72. package/dist/paint-editor/index.cjs +4690 -845
  73. package/dist/paint-editor/index.cjs.map +1 -1
  74. package/dist/paint-editor/index.d.cts +1525 -277
  75. package/dist/paint-editor/index.d.ts +1525 -277
  76. package/dist/paint-editor/index.js +4560 -715
  77. package/dist/paint-editor/index.js.map +1 -1
  78. package/dist/raster/index.cjs +4 -3
  79. package/dist/raster/index.cjs.map +1 -1
  80. package/dist/raster/index.js +10 -9
  81. package/dist/{svg-BnRj0q8i.d.cts → svg-CVdFc80-.d.cts} +2 -2
  82. package/dist/{svg-CmmBFfGT.d.ts → svg-DlEuhwoX.d.ts} +2 -2
  83. package/dist/{types-1SUQKDAw.d.ts → types-CDLGhAZp.d.ts} +40 -2
  84. package/dist/{types-yINF17Ta.d.cts → types-Tfly4Aya.d.cts} +40 -2
  85. package/dist/vector/index.cjs +1897 -975
  86. package/dist/vector/index.cjs.map +1 -1
  87. package/dist/vector/index.d.cts +292 -32
  88. package/dist/vector/index.d.ts +292 -32
  89. package/dist/vector/index.js +928 -6
  90. package/dist/vector/index.js.map +1 -1
  91. package/dist/video-editor/index.cjs +502 -2165
  92. package/dist/video-editor/index.cjs.map +1 -1
  93. package/dist/video-editor/index.d.cts +3 -501
  94. package/dist/video-editor/index.d.ts +3 -501
  95. package/dist/video-editor/index.js +151 -1814
  96. package/dist/video-editor/index.js.map +1 -1
  97. package/package.json +1 -1
  98. package/skills/pixerate-image-editor/references/photoshop-files.md +7 -3
  99. package/skills/pixerate-vector/SKILL.md +3 -0
  100. package/dist/chunk-C6C5PDKU.js.map +0 -1
  101. package/dist/chunk-DFQEX3AM.cjs.map +0 -1
  102. package/dist/chunk-FGW5OWJ4.js.map +0 -1
  103. package/dist/chunk-FV5AXTSF.cjs.map +0 -1
  104. package/dist/chunk-GX332W2C.js +0 -181
  105. package/dist/chunk-GX332W2C.js.map +0 -1
  106. package/dist/chunk-LKCYANMW.js.map +0 -1
  107. package/dist/chunk-LPLCWZTM.cjs.map +0 -1
  108. package/dist/chunk-MUUXNRU4.cjs +0 -181
  109. package/dist/chunk-MUUXNRU4.cjs.map +0 -1
  110. package/dist/chunk-ST7E3NEE.cjs.map +0 -1
  111. package/dist/chunk-TCKIXI46.cjs +0 -1136
  112. package/dist/chunk-TCKIXI46.cjs.map +0 -1
  113. package/dist/chunk-UBIXVXPR.js +0 -1136
  114. package/dist/chunk-UBIXVXPR.js.map +0 -1
  115. package/dist/chunk-VXR7XWMF.js.map +0 -1
  116. /package/dist/{chunk-XICA4PVO.js.map → chunk-SEBYNBHW.js.map} +0 -0
  117. /package/dist/{chunk-MMVHBKMM.js.map → chunk-T2P5TIZ6.js.map} +0 -0
@@ -1,3 +1,125 @@
1
+ import {
2
+ VIDEO_BLEND_MODES,
3
+ VIDEO_CLIP_ANIMATION_DURATION,
4
+ VIDEO_CLIP_ANIMATION_EFFECTS,
5
+ VIDEO_CLIP_TYPES,
6
+ VIDEO_EASE_NAMES,
7
+ VIDEO_FADE_CURVES,
8
+ VIDEO_FITS,
9
+ VIDEO_GRADE_CONTROLS,
10
+ VIDEO_GRADE_PRESETS,
11
+ VIDEO_GRADE_PRESET_NAMES,
12
+ VIDEO_KEYFRAME_LIMITS,
13
+ VIDEO_KEYFRAME_PROPERTIES,
14
+ VIDEO_LUT_MAX_SIZE,
15
+ VIDEO_PLACEMENT_MODES,
16
+ VIDEO_TIME_EPSILON,
17
+ VIDEO_TRANSFORM_PROPERTIES,
18
+ VIDEO_TRANSITION_TYPES,
19
+ VIDEO_WIPE_DIRECTIONS,
20
+ VideoLutCache,
21
+ allVideoClipIds,
22
+ allVideoTracks,
23
+ applyVideoGrade,
24
+ applyVideoLut,
25
+ applyVideoLutToPixels,
26
+ checkVideoTimeRemap,
27
+ drawVideoFrame,
28
+ ffmpegFilterValue,
29
+ findVideoClip,
30
+ findVideoGaps,
31
+ findVideoSequenceClip,
32
+ findVideoSequenceCycle,
33
+ formatVideoSeconds,
34
+ formatVideoTimecode,
35
+ formatVideoTimecodeFrames,
36
+ isVideoDropFrameRate,
37
+ isVideoKeyframes,
38
+ nextFreeVideoSlot,
39
+ normalizeVideoGrade,
40
+ normalizeVideoGradeLut,
41
+ parseVideoCubeLut,
42
+ parseVideoLoudnessMeasurement,
43
+ placeVideoRect,
44
+ removeVideoKey,
45
+ retimeVideoAnimatable,
46
+ setVideoKey,
47
+ snapToVideoFrame,
48
+ snapVideoTime,
49
+ sortVideoKeys,
50
+ sortedVideoClips,
51
+ splitVideoAnimatable,
52
+ toVideoEdl,
53
+ toVideoFfmpeg,
54
+ toVideoLoudnessMeasure,
55
+ videoAssetHasSound,
56
+ videoAssetMap,
57
+ videoClipAfter,
58
+ videoClipAnimated,
59
+ videoClipAnimationProperties,
60
+ videoClipAnimationSpans,
61
+ videoClipBefore,
62
+ videoClipEnd,
63
+ videoClipHandles,
64
+ videoClipOverhang,
65
+ videoClipSoundGain,
66
+ videoClipSourceRange,
67
+ videoClipSourceTime,
68
+ videoClipSpeed,
69
+ videoClipSpeedAt,
70
+ videoClipsAt,
71
+ videoContactSheetSvg,
72
+ videoEaseBezier,
73
+ videoEaseProgress,
74
+ videoFadeCurveGain,
75
+ videoFrameAt,
76
+ videoFrameDuration,
77
+ videoFrameLeafLayers,
78
+ videoFrameToSvg,
79
+ videoFrames,
80
+ videoFreezeFrameEdit,
81
+ videoGradeCssFilter,
82
+ videoGradeFfmpegFilters,
83
+ videoGradeGainsFilterId,
84
+ videoGradeLook,
85
+ videoGradePreset,
86
+ videoGradeStepMatrix,
87
+ videoGradeStepSvgValues,
88
+ videoGradeSteps,
89
+ videoGradeSvgDefs,
90
+ videoGradeSvgFilter,
91
+ videoHourFrames,
92
+ videoIrisRadius,
93
+ videoKeyframeBreakpoints,
94
+ videoKeyframeFfmpegExpr,
95
+ videoKeysOf,
96
+ videoLutFromFunction,
97
+ videoLutSvgApproximation,
98
+ videoLutSvgPrimitives,
99
+ videoLutToCube,
100
+ videoOverlap,
101
+ videoProjectDuration,
102
+ videoProjectSvg,
103
+ videoSequenceChildren,
104
+ videoSequenceDuration,
105
+ videoSequenceMap,
106
+ videoShellQuote,
107
+ videoSlowMotionRamp,
108
+ videoSnapPoints,
109
+ videoSpeedRampEdit,
110
+ videoSpeedRampKeys,
111
+ videoTextStyle,
112
+ videoTimeRemapBreakpoints,
113
+ videoTimeRemapSpan,
114
+ videoTimecodeFrames,
115
+ videoTitleFrame,
116
+ videoTrackSyncLocked,
117
+ videoTransformAt,
118
+ videoTransitionAudioCurve,
119
+ videoTransitionEase,
120
+ videoValueAt,
121
+ videoWipeRect
122
+ } from "../chunk-IPFRWILU.js";
1
123
  import {
2
124
  namedWithSuggestion
3
125
  } from "../chunk-INZJALPY.js";
@@ -11,708 +133,6 @@ import "../chunk-AGR4P4UZ.js";
11
133
  import "../chunk-ZRJMJIQU.js";
12
134
  import "../chunk-TGD52FEZ.js";
13
135
 
14
- // src/video-editor/types.ts
15
- var VIDEO_FITS = ["contain", "cover", "fill", "none"];
16
- var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe", "push", "slide", "iris"];
17
- var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
18
- var videoTransitionAudioCurve = (t) => t.audioCurve ?? (t.type === "dip" ? "linear" : "equal-power");
19
- var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
20
- var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
21
- var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
22
- var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
23
- var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence"];
24
-
25
- // src/video-editor/keyframes.ts
26
- var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
27
- var VIDEO_KEYFRAME_PROPERTIES = ["opacity", "volume", "x", "y", "scale", "rotation"];
28
- var VIDEO_TRANSFORM_PROPERTIES = ["x", "y", "scale", "rotation"];
29
- var VIDEO_KEYFRAME_LIMITS = {
30
- opacity: { default: 1, min: 0, max: 1 },
31
- volume: { default: 1, min: 0, max: 4 },
32
- x: { default: 0 },
33
- y: { default: 0 },
34
- scale: { default: 1, min: 0 },
35
- rotation: { default: 0 }
36
- };
37
- var NAMED = {
38
- "ease-in": [0.42, 0, 1, 1],
39
- "ease-out": [0, 0, 0.58, 1],
40
- "ease-in-out": [0.42, 0, 0.58, 1]
41
- };
42
- var isVideoKeyframes = (v) => typeof v === "object" && v !== null && Array.isArray(v.keys);
43
- function videoEaseBezier(ease) {
44
- if (ease === void 0 || ease === "linear" || ease === "hold") return null;
45
- return Array.isArray(ease) ? ease : NAMED[ease];
46
- }
47
- var bez = (a, b, s) => 3 * a * (1 - s) * (1 - s) * s + 3 * b * (1 - s) * s * s + s * s * s;
48
- var bezSlope = (a, b, s) => 3 * a * (1 - s) * (1 - s) + 6 * (b - a) * (1 - s) * s + 3 * (1 - b) * s * s;
49
- function bezierParam(x1, x2, x) {
50
- let s = x;
51
- for (let i = 0; i < 8; i++) {
52
- const err = bez(x1, x2, s) - x;
53
- if (Math.abs(err) < 1e-7) return s;
54
- const d = bezSlope(x1, x2, s);
55
- if (Math.abs(d) < 1e-6) break;
56
- s -= err / d;
57
- if (s < 0 || s > 1) break;
58
- }
59
- let lo = 0;
60
- let hi = 1;
61
- s = x;
62
- for (let i = 0; i < 60; i++) {
63
- const v = bez(x1, x2, s);
64
- if (Math.abs(v - x) < 1e-9) break;
65
- if (v < x) lo = s;
66
- else hi = s;
67
- s = (lo + hi) / 2;
68
- }
69
- return s;
70
- }
71
- function videoEaseProgress(ease, p) {
72
- if (p <= 0) return 0;
73
- if (p >= 1) return ease === "hold" ? 0 : 1;
74
- if (ease === "hold") return 0;
75
- const b = videoEaseBezier(ease);
76
- if (!b) return p;
77
- return bez(b[1], b[3], bezierParam(b[0], b[2], p));
78
- }
79
- function keyBefore(keys, t) {
80
- let lo = 0;
81
- let hi = keys.length - 1;
82
- if (t < keys[0].t) return -1;
83
- while (lo < hi) {
84
- const mid = lo + hi + 1 >> 1;
85
- if (keys[mid].t <= t) lo = mid;
86
- else hi = mid - 1;
87
- }
88
- return lo;
89
- }
90
- function videoValueAt(prop, clipTime, fallback = 0) {
91
- if (prop === void 0) return fallback;
92
- if (typeof prop === "number") return prop;
93
- const { keys } = prop;
94
- if (!keys.length) return fallback;
95
- const i = keyBefore(keys, clipTime);
96
- if (i < 0) return keys[0].v;
97
- if (i >= keys.length - 1) return keys[keys.length - 1].v;
98
- const a = keys[i];
99
- const b = keys[i + 1];
100
- if (a.ease === "hold") return a.v;
101
- return a.v + (b.v - a.v) * videoEaseProgress(a.ease, (clipTime - a.t) / (b.t - a.t));
102
- }
103
- var videoKeysOf = (prop) => isVideoKeyframes(prop) ? prop.keys : [];
104
- function retimeVideoAnimatable(prop, dt, k = 1) {
105
- if (!isVideoKeyframes(prop)) return prop;
106
- return { keys: prop.keys.map((key) => ({ ...key, t: key.t * k + dt })) };
107
- }
108
- function splitEase(ease, p) {
109
- if (ease === "hold") return ["hold", "hold"];
110
- const b = videoEaseBezier(ease);
111
- if (!b) return [ease, ease];
112
- const s = bezierParam(b[0], b[2], p);
113
- const lerp = (u, v, k) => [u[0] + (v[0] - u[0]) * k, u[1] + (v[1] - u[1]) * k];
114
- const P0 = [0, 0];
115
- const P1 = [b[0], b[1]];
116
- const P2 = [b[2], b[3]];
117
- const P3 = [1, 1];
118
- const A = lerp(P0, P1, s);
119
- const B = lerp(P1, P2, s);
120
- const C = lerp(P2, P3, s);
121
- const AB = lerp(A, B, s);
122
- const BC = lerp(B, C, s);
123
- const M = lerp(AB, BC, s);
124
- const r = (v) => Math.round(v * 1e6) / 1e6;
125
- 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])];
126
- const dx = 1 - M[0];
127
- const dy = 1 - M[1];
128
- 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)];
129
- return [left, right];
130
- }
131
- function splitVideoAnimatable(prop, at) {
132
- if (!isVideoKeyframes(prop)) return [prop, prop];
133
- const keys = prop.keys;
134
- const eps = 1e-6;
135
- const v = videoValueAt(prop, at);
136
- const i = keyBefore(keys, at);
137
- const exact = i >= 0 && Math.abs(keys[i].t - at) < eps;
138
- let leftEase;
139
- let rightEase;
140
- if (exact) rightEase = keys[i].ease;
141
- else if (i >= 0 && i < keys.length - 1) {
142
- const a = keys[i];
143
- [leftEase, rightEase] = splitEase(a.ease, (at - a.t) / (keys[i + 1].t - a.t));
144
- }
145
- const before = keys.slice(0, exact ? i : i + 1).map((k) => ({ ...k }));
146
- if (before.length && leftEase !== void 0) before[before.length - 1] = withEase(before[before.length - 1], leftEase);
147
- const left = [...before, { t: at, v }];
148
- const right = [withEase({ t: 0, v }, rightEase), ...keys.slice(i + 1).map((k) => ({ ...k, t: k.t - at }))];
149
- return [{ keys: left }, { keys: right }];
150
- }
151
- var withEase = (key, ease) => {
152
- const { ease: _old, ...rest } = key;
153
- return ease === void 0 || ease === "linear" ? rest : { ...rest, ease };
154
- };
155
- function sortVideoKeys(keys) {
156
- const sorted = [...keys].sort((a, b) => a.t - b.t);
157
- 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.`);
158
- return sorted;
159
- }
160
- function setVideoKey(prop, key, fallback) {
161
- const keys = isVideoKeyframes(prop) ? prop.keys.filter((k) => Math.abs(k.t - key.t) >= 1e-6) : [];
162
- if (!isVideoKeyframes(prop)) {
163
- const v = prop ?? fallback;
164
- if (key.t !== 0) keys.push({ t: 0, v });
165
- }
166
- return { keys: sortVideoKeys([...keys, key]) };
167
- }
168
- function removeVideoKey(prop, t) {
169
- const keys = prop.keys.filter((k) => Math.abs(k.t - t) >= 1e-6);
170
- return keys.length ? { keys } : prop.keys[0].v;
171
- }
172
- var fmt = (v) => {
173
- const r = Math.round(v * 1e4) / 1e4;
174
- return Object.is(r, -0) ? "0" : String(r);
175
- };
176
- var signed = (v) => fmt(v).startsWith("-") ? fmt(v) : `+${fmt(v)}`;
177
- function videoKeyframeBreakpoints(prop, step) {
178
- const { keys } = prop;
179
- const points = [];
180
- const holds = /* @__PURE__ */ new Set();
181
- for (let i = 0; i < keys.length; i++) {
182
- const a = keys[i];
183
- points.push([a.t, a.v]);
184
- const b = keys[i + 1];
185
- if (!b) break;
186
- if (a.ease === "hold") {
187
- holds.add(points.length - 1);
188
- continue;
189
- }
190
- if (!videoEaseBezier(a.ease)) continue;
191
- const n2 = Math.max(1, Math.ceil((b.t - a.t) / step));
192
- for (let k = 1; k < n2; k++) {
193
- const t = a.t + (b.t - a.t) * k / n2;
194
- points.push([t, videoValueAt(prop, t)]);
195
- }
196
- }
197
- return { points, holds };
198
- }
199
- function videoKeyframeFfmpegExpr(prop, time, fallback, step) {
200
- if (!isVideoKeyframes(prop)) return fmt(prop ?? fallback);
201
- const { points, holds } = videoKeyframeBreakpoints(prop, step);
202
- if (points.length === 1 || points.every(([, v]) => v === points[0][1])) return fmt(points[0][1]);
203
- let expr = fmt(points[points.length - 1][1]);
204
- for (let i = points.length - 2; i >= 0; i--) {
205
- const [t0, v0] = points[i];
206
- const [t1, v1] = points[i + 1];
207
- const seg = holds.has(i) || v0 === v1 ? fmt(v0) : `${fmt(v0)}${signed((v1 - v0) / (t1 - t0))}*(${time}${signed(-t0)})`;
208
- expr = `if(lt(${time},${fmt(t1)}),${seg},${expr})`;
209
- }
210
- return `if(lt(${time},${fmt(points[0][0])}),${fmt(points[0][1])},${expr})`;
211
- }
212
-
213
- // src/video-editor/lut.ts
214
- var VIDEO_LUT_MAX_SIZE = 129;
215
- function parseVideoCubeLut(text) {
216
- let size = 0;
217
- let size1 = 0;
218
- let title;
219
- const domainMin = [0, 0, 0];
220
- const domainMax = [1, 1, 1];
221
- let data = null;
222
- let rows = 0;
223
- const lines = text.split(/\r\n|\r|\n/);
224
- const fail2 = (line, message) => {
225
- throw new Error(`Line ${line + 1} of the .cube file: ${message}.`);
226
- };
227
- const numbers = (parts, line) => parts.map((p) => {
228
- const v = Number(p);
229
- if (!Number.isFinite(v)) fail2(line, `"${p}" is not a number`);
230
- return v;
231
- });
232
- for (let i = 0; i < lines.length; i++) {
233
- const line = lines[i].replace(/#.*/, "").trim();
234
- if (!line) continue;
235
- const parts = line.split(/\s+/);
236
- const key = parts[0];
237
- if (/^[A-Za-z]/.test(key)) {
238
- if (rows) fail2(i, `"${key}" comes after the table; keywords go before it`);
239
- if (key === "TITLE") title = line.slice(5).trim().replace(/^"|"$/g, "");
240
- else if (key === "LUT_3D_SIZE") {
241
- size = Number(parts[1]);
242
- if (!Number.isInteger(size) || size < 2 || size > VIDEO_LUT_MAX_SIZE) fail2(i, `LUT_3D_SIZE must be a whole number from 2 to ${VIDEO_LUT_MAX_SIZE}, not "${parts[1] ?? ""}"`);
243
- } else if (key === "LUT_1D_SIZE") size1 = Number(parts[1]) || 1;
244
- else if (key === "DOMAIN_MIN" || key === "DOMAIN_MAX") {
245
- const v2 = numbers(parts.slice(1), i);
246
- if (v2.length !== 3) fail2(i, `${key} needs 3 numbers`);
247
- (key === "DOMAIN_MIN" ? domainMin : domainMax).splice(0, 3, ...v2);
248
- } else if (key === "LUT_3D_INPUT_RANGE") {
249
- const v2 = numbers(parts.slice(1), i);
250
- if (v2.length !== 2 || !(v2[1] > v2[0])) fail2(i, "LUT_3D_INPUT_RANGE needs a minimum and a larger maximum");
251
- domainMin.fill(v2[0]);
252
- domainMax.fill(v2[1]);
253
- }
254
- continue;
255
- }
256
- if (!size) fail2(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");
257
- data ??= new Float32Array(size * size * size * 3);
258
- const v = numbers(parts, i);
259
- if (v.length !== 3) fail2(i, `a table row needs 3 numbers, not ${v.length}`);
260
- if (rows >= size ** 3) fail2(i, `the table has more than the ${size ** 3} rows LUT_3D_SIZE ${size} needs`);
261
- data.set(v, rows * 3);
262
- rows++;
263
- }
264
- 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.");
265
- if (rows !== size ** 3) throw new Error(`The .cube file has ${rows} table rows; LUT_3D_SIZE ${size} needs ${size ** 3}.`);
266
- 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.");
267
- return { ...title ? { title } : {}, size, domainMin, domainMax, data };
268
- }
269
- function videoLutFromFunction(size, fn, title) {
270
- const n2 = size - 1;
271
- const data = new Float32Array(size ** 3 * 3);
272
- let i = 0;
273
- for (let b = 0; b < size; b++)
274
- for (let g = 0; g < size; g++)
275
- for (let r = 0; r < size; r++) {
276
- data.set(fn([r / n2, g / n2, b / n2]), i);
277
- i += 3;
278
- }
279
- return { ...title ? { title } : {}, size, domainMin: [0, 0, 0], domainMax: [1, 1, 1], data };
280
- }
281
- function videoLutToCube(lut) {
282
- const f = (v) => (Math.round(v * 1e6) / 1e6).toString();
283
- const head = [...lut.title ? [`TITLE "${lut.title.replace(/"/g, "'")}"`] : [], `LUT_3D_SIZE ${lut.size}`];
284
- 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(" ")}`);
285
- const rows = [];
286
- 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])}`);
287
- return `${head.join("\n")}
288
- ${rows.join("\n")}
289
- `;
290
- }
291
- var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
292
- function applyVideoLut(lut, rgb, out = [0, 0, 0]) {
293
- const { size, data, domainMin, domainMax } = lut;
294
- const n2 = size - 1;
295
- const x = clamp01((rgb[0] - domainMin[0]) / (domainMax[0] - domainMin[0])) * n2;
296
- const y = clamp01((rgb[1] - domainMin[1]) / (domainMax[1] - domainMin[1])) * n2;
297
- const z = clamp01((rgb[2] - domainMin[2]) / (domainMax[2] - domainMin[2])) * n2;
298
- const r0 = Math.min(n2 - 1, Math.floor(x));
299
- const g0 = Math.min(n2 - 1, Math.floor(y));
300
- const b0 = Math.min(n2 - 1, Math.floor(z));
301
- const fr = x - r0;
302
- const fg = y - g0;
303
- const fb = z - b0;
304
- const sr = 3;
305
- const sg = 3 * size;
306
- const sb = 3 * size * size;
307
- const o = r0 * sr + g0 * sg + b0 * sb;
308
- let a, c, w0, w1, w2, w3;
309
- if (fr > fg) {
310
- if (fg > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sg, 1 - fr, fr - fg, fg - fb, fb];
311
- else if (fr > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sb, 1 - fr, fr - fb, fb - fg, fg];
312
- else [a, c, w0, w1, w2, w3] = [sb, sb + sr, 1 - fb, fb - fr, fr - fg, fg];
313
- } else if (fb > fg) [a, c, w0, w1, w2, w3] = [sb, sb + sg, 1 - fb, fb - fg, fg - fr, fr];
314
- else if (fb > fr) [a, c, w0, w1, w2, w3] = [sg, sg + sb, 1 - fg, fg - fb, fb - fr, fr];
315
- else [a, c, w0, w1, w2, w3] = [sg, sg + sr, 1 - fg, fg - fr, fr - fb, fb];
316
- const d = sr + sg + sb;
317
- 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]);
318
- return out;
319
- }
320
- function applyVideoLutToPixels(lut, data, strength = 1) {
321
- const s = clamp01(strength);
322
- if (!s) return;
323
- const rgb = [0, 0, 0];
324
- const out = [0, 0, 0];
325
- for (let i = 0; i < data.length; i += 4) {
326
- if (!data[i + 3]) continue;
327
- rgb[0] = data[i] / 255;
328
- rgb[1] = data[i + 1] / 255;
329
- rgb[2] = data[i + 2] / 255;
330
- applyVideoLut(lut, rgb, out);
331
- for (let k = 0; k < 3; k++) data[i + k] = Math.round((rgb[k] + (out[k] - rgb[k]) * s) * 255);
332
- }
333
- }
334
- var approximations = /* @__PURE__ */ new WeakMap();
335
- function videoLutSvgApproximation(lut) {
336
- const cached = approximations.get(lut);
337
- if (cached) return cached;
338
- const points = 17;
339
- const tables = [[], [], []];
340
- for (let i = 0; i < points; i++) {
341
- const v = i / (points - 1);
342
- const out = applyVideoLut(lut, [v, v, v]);
343
- for (let k = 0; k < 3; k++) tables[k].push(out[k]);
344
- }
345
- const curve = (k, v) => {
346
- const x = clamp01(v) * (points - 1);
347
- const i = Math.min(points - 2, Math.floor(x));
348
- return tables[k][i] + (tables[k][i + 1] - tables[k][i]) * (x - i);
349
- };
350
- const ata = Array.from({ length: 4 }, () => [0, 0, 0, 0]);
351
- const atb = Array.from({ length: 3 }, () => [0, 0, 0, 0]);
352
- const grid = 9;
353
- for (let b = 0; b < grid; b++)
354
- for (let g = 0; g < grid; g++)
355
- for (let r = 0; r < grid; r++) {
356
- const x = [r / (grid - 1), g / (grid - 1), b / (grid - 1)];
357
- const z = [curve(0, x[0]), curve(1, x[1]), curve(2, x[2]), 1];
358
- const y = applyVideoLut(lut, x);
359
- for (let i = 0; i < 4; i++) {
360
- for (let j = 0; j < 4; j++) ata[i][j] += z[i] * z[j];
361
- for (let k = 0; k < 3; k++) atb[k][i] += z[i] * y[k];
362
- }
363
- }
364
- const matrix = atb.map((rhs) => solve4(ata, rhs));
365
- const result = { tables, matrix };
366
- approximations.set(lut, result);
367
- return result;
368
- }
369
- function solve4(a, b) {
370
- const m = a.map((row, i) => [...row, b[i]]);
371
- for (let col = 0; col < 4; col++) {
372
- let pivot = col;
373
- for (let r = col + 1; r < 4; r++) if (Math.abs(m[r][col]) > Math.abs(m[pivot][col])) pivot = r;
374
- if (Math.abs(m[pivot][col]) < 1e-12) continue;
375
- [m[col], m[pivot]] = [m[pivot], m[col]];
376
- for (let r = 0; r < 4; r++) {
377
- if (r === col) continue;
378
- const f = m[r][col] / m[col][col];
379
- for (let c = col; c < 5; c++) m[r][c] -= f * m[col][c];
380
- }
381
- }
382
- return m.map((row, i) => Math.abs(row[i]) < 1e-12 ? 0 : row[4] / row[i]);
383
- }
384
- var VideoLutCache = class {
385
- constructor(loader = async (src) => {
386
- const res = await fetch(src);
387
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
388
- return res.text();
389
- }) {
390
- this.loader = loader;
391
- }
392
- loader;
393
- ready = /* @__PURE__ */ new Map();
394
- pending = /* @__PURE__ */ new Map();
395
- failed = /* @__PURE__ */ new Map();
396
- listeners = /* @__PURE__ */ new Set();
397
- /** The LUT if loaded, without starting to load it. */
398
- peek(src) {
399
- return this.ready.get(src);
400
- }
401
- /** The LUT if loaded; otherwise `undefined`, and loading starts (once) unless it failed. */
402
- get(src) {
403
- const lut = this.ready.get(src);
404
- if (!lut && !this.failed.has(src)) void this.load(src).catch(() => {
405
- });
406
- return lut;
407
- }
408
- /** Why loading a LUT failed, if it did. */
409
- error(src) {
410
- return this.failed.get(src);
411
- }
412
- /** The LUT, loading and reading it if needed. Rejects with a readable error. */
413
- load(src) {
414
- const ready = this.ready.get(src);
415
- if (ready) return Promise.resolve(ready);
416
- const failed = this.failed.get(src);
417
- if (failed) return Promise.reject(new Error(failed));
418
- let pending = this.pending.get(src);
419
- if (!pending) {
420
- pending = Promise.resolve().then(() => this.loader(src)).then((v) => typeof v === "string" ? parseVideoCubeLut(v) : v).then(
421
- (lut) => {
422
- this.pending.delete(src);
423
- this.ready.set(src, lut);
424
- this.emit();
425
- return lut;
426
- },
427
- (e) => {
428
- this.pending.delete(src);
429
- const message = `The LUT "${src}" could not be read: ${e instanceof Error ? e.message : String(e)}`;
430
- this.failed.set(src, message);
431
- this.emit();
432
- throw new Error(message);
433
- }
434
- );
435
- this.pending.set(src, pending);
436
- }
437
- return pending;
438
- }
439
- /** Uses a LUT the host already has (`.cube` text or a parsed LUT), under `src`. */
440
- set(src, lut) {
441
- const parsed = typeof lut === "string" ? parseVideoCubeLut(lut) : lut;
442
- this.ready.set(src, parsed);
443
- this.failed.delete(src);
444
- this.emit();
445
- return parsed;
446
- }
447
- /** Forgets a failure so the next `get` tries again. */
448
- retry(src) {
449
- if (this.failed.delete(src)) this.emit();
450
- }
451
- subscribe(fn) {
452
- this.listeners.add(fn);
453
- return () => {
454
- this.listeners.delete(fn);
455
- };
456
- }
457
- emit() {
458
- for (const l of [...this.listeners]) l();
459
- }
460
- };
461
-
462
- // src/video-editor/grade.ts
463
- var VIDEO_GRADE_CONTROLS = [
464
- { key: "exposure", label: "Exposure", min: -3, max: 3, step: 0.1, description: "Exposure in stops: +1 doubles every channel, -1 halves it (default 0)." },
465
- { 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." },
466
- { key: "saturation", label: "Saturation", min: -1, max: 1, step: 0.05, description: "Saturation, -1 (black and white) to 1 (twice as saturated); default 0." },
467
- { key: "hue", label: "Hue", min: -180, max: 180, step: 1, description: "Hue rotation in degrees (default 0)." },
468
- { key: "temperature", label: "Temperature", min: -1, max: 1, step: 0.05, description: "White balance, -1 (cool, bluer) to 1 (warm, more orange); default 0." },
469
- { key: "tint", label: "Tint", min: -1, max: 1, step: 0.05, description: "White balance, -1 (greener) to 1 (more magenta); default 0." }
470
- ];
471
- var VIDEO_GRADE_PRESETS = {
472
- warm: { temperature: 0.4, saturation: 0.1 },
473
- cool: { temperature: -0.4, tint: -0.05 },
474
- punchy: { contrast: 0.25, saturation: 0.3 },
475
- faded: { exposure: 0.15, contrast: -0.3, saturation: -0.3 },
476
- "black-and-white": { saturation: -1, contrast: 0.15 },
477
- "bleach-bypass": { saturation: -0.6, contrast: 0.4 },
478
- "day-for-night": { exposure: -1.5, saturation: -0.4, temperature: -0.6 }
479
- };
480
- var VIDEO_GRADE_PRESET_NAMES = Object.keys(VIDEO_GRADE_PRESETS);
481
- var clampTo = (c, v) => Math.min(c.max, Math.max(c.min, v));
482
- function normalizeVideoGradeLut(lut) {
483
- if (!lut || typeof lut.src !== "string" || !lut.src) return void 0;
484
- const strength = typeof lut.strength === "number" && Number.isFinite(lut.strength) ? Math.round(Math.min(1, Math.max(0, lut.strength)) * 1e3) / 1e3 : 1;
485
- if (!strength) return void 0;
486
- return { src: lut.src, ...strength !== 1 ? { strength } : {}, ...lut.name ? { name: lut.name } : {} };
487
- }
488
- function normalizeVideoGrade(grade) {
489
- if (!grade) return void 0;
490
- const out = {};
491
- for (const c of VIDEO_GRADE_CONTROLS) {
492
- const v = grade[c.key];
493
- if (typeof v === "number" && Number.isFinite(v) && v !== 0) out[c.key] = Math.round(clampTo(c, v) * 1e3) / 1e3;
494
- }
495
- const lut = normalizeVideoGradeLut(grade.lut);
496
- if (lut) out.lut = lut;
497
- return Object.keys(out).length ? out : void 0;
498
- }
499
- function videoGradePreset(name, amount = 1) {
500
- const preset = VIDEO_GRADE_PRESETS[name];
501
- if (!preset) throw new Error(`There is no grade preset "${name}"; the presets are ${VIDEO_GRADE_PRESET_NAMES.join(", ")}.`);
502
- const k = Math.min(1, Math.max(0, amount));
503
- return normalizeVideoGrade(Object.fromEntries(Object.entries(preset).map(([key, v]) => [key, v * k])));
504
- }
505
- function videoGradeLook(current, name, amount = 1) {
506
- return normalizeVideoGrade({ ...videoGradePreset(name, amount), ...current?.lut ? { lut: current.lut } : {} });
507
- }
508
- function videoGradeSteps(grade) {
509
- const g = normalizeVideoGrade(grade);
510
- if (!g) return [];
511
- const steps = [];
512
- if (g.exposure) steps.push({ kind: "brightness", amount: 2 ** g.exposure });
513
- if (g.contrast) steps.push({ kind: "contrast", amount: 1 + g.contrast });
514
- if (g.saturation) steps.push({ kind: "saturate", amount: 1 + g.saturation });
515
- if (g.hue) steps.push({ kind: "hue-rotate", degrees: g.hue });
516
- if (g.temperature || g.tint) {
517
- const t = g.temperature ?? 0;
518
- steps.push({ kind: "gains", r: 1 + 0.25 * t, g: 1 - 0.25 * (g.tint ?? 0), b: 1 - 0.25 * t });
519
- }
520
- return steps;
521
- }
522
- function videoGradeStepMatrix(step) {
523
- switch (step.kind) {
524
- case "brightness":
525
- return [
526
- [step.amount, 0, 0, 0],
527
- [0, step.amount, 0, 0],
528
- [0, 0, step.amount, 0]
529
- ];
530
- case "contrast": {
531
- const c = step.amount;
532
- const o = 0.5 - 0.5 * c;
533
- return [
534
- [c, 0, 0, o],
535
- [0, c, 0, o],
536
- [0, 0, c, o]
537
- ];
538
- }
539
- case "gains":
540
- return [
541
- [step.r, 0, 0, 0],
542
- [0, step.g, 0, 0],
543
- [0, 0, step.b, 0]
544
- ];
545
- case "saturate": {
546
- const s = step.amount;
547
- return [
548
- [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0],
549
- [0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0],
550
- [0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0]
551
- ];
552
- }
553
- case "hue-rotate": {
554
- const a = step.degrees * Math.PI / 180;
555
- const cos = Math.cos(a);
556
- const sin = Math.sin(a);
557
- return [
558
- [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],
559
- [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],
560
- [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]
561
- ];
562
- }
563
- }
564
- }
565
- function applyVideoGrade(grade, rgb, lut) {
566
- let v = rgb.map((x) => x / 255);
567
- for (const step of videoGradeSteps(grade)) {
568
- const m = videoGradeStepMatrix(step);
569
- v = m.map((row) => Math.min(1, Math.max(0, row[0] * v[0] + row[1] * v[1] + row[2] * v[2] + row[3])));
570
- }
571
- const setting = normalizeVideoGradeLut(grade?.lut);
572
- if (setting && lut) {
573
- const out = applyVideoLut(lut, v);
574
- const s = setting.strength ?? 1;
575
- v = v.map((x, k) => x + (out[k] - x) * s);
576
- }
577
- return v.map((x) => Math.round(x * 255));
578
- }
579
- var fmt2 = (v) => String(Math.round(v * 1e5) / 1e5);
580
- function videoGradeStepSvgValues(step) {
581
- return videoGradeStepMatrix(step).map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
582
- }
583
- function videoLutSvgPrimitives(lut, strength = 1) {
584
- const { tables, matrix } = videoLutSvgApproximation(lut);
585
- const table = (k) => tables[k].map(fmt2).join(" ");
586
- const values = matrix.map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
587
- const s = Math.min(1, Math.max(0, strength));
588
- 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"/>` : "");
589
- }
590
- function videoGradeSvgFilter(id, grade, lut) {
591
- const steps = videoGradeSteps(grade);
592
- const setting = normalizeVideoGradeLut(grade?.lut);
593
- 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>`;
594
- }
595
- var videoGradeGainsFilterId = (step) => `pixerate-wb-${[step.r, step.g, step.b].map((v) => Math.round(v * 1e3)).join("-")}`;
596
- function videoGradeCssFilter(grade) {
597
- 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(" ");
598
- }
599
- function videoGradeSvgDefs(grade) {
600
- return videoGradeSteps(grade).filter((s) => s.kind === "gains").map((s) => {
601
- const id = videoGradeGainsFilterId(s);
602
- return { id, markup: `<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/></filter>` };
603
- });
604
- }
605
- function videoGradeFfmpegFilters(grade) {
606
- return videoGradeSteps(grade).map((step) => {
607
- const m = videoGradeStepMatrix(step);
608
- if (step.kind === "brightness" || step.kind === "contrast") {
609
- const lut = (i) => `clip(val*${fmt2(m[i][i])}+${fmt2(m[i][3] * 255)},0,255)`;
610
- return `lutrgb=r='${lut(0)}':g='${lut(1)}':b='${lut(2)}'`;
611
- }
612
- const k = ["r", "g", "b"];
613
- return `colorchannelmixer=${m.flatMap((row, i) => row.slice(0, 3).map((v, j) => `${k[i]}${k[j]}=${fmt2(v)}`)).join(":")}`;
614
- });
615
- }
616
-
617
- // src/video-editor/remap.ts
618
- function videoClipSourceTime(clip, clipTime) {
619
- const base = clip.in ?? 0;
620
- return clip.timeRemap ? base + videoValueAt(clip.timeRemap, clipTime, 0) : base + clipTime * (clip.speed ?? 1);
621
- }
622
- function videoClipSpeedAt(clip, clipTime) {
623
- if (clip.type !== "media") return 1;
624
- if (!clip.timeRemap) return clip.speed ?? 1;
625
- const t = Math.min(Math.max(clipTime, 0), Math.max(0, clip.duration - 1));
626
- return Math.max(0, videoClipSourceTime(clip, t + 1) - videoClipSourceTime(clip, t));
627
- }
628
- function videoSpeedRampKeys(segments) {
629
- if (!segments.length) throw new Error("Give at least one stretch { duration, speed }.");
630
- const keys = [];
631
- let t = 0;
632
- let v = 0;
633
- segments.forEach((s, i) => {
634
- if (!(s.duration > 0)) throw new Error(`Stretch ${i + 1} needs a duration above 0 ms.`);
635
- for (const [name, x] of [["speed", s.speed], ["toSpeed", s.toSpeed]])
636
- 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}.`);
637
- const a = s.speed;
638
- const b = s.toSpeed ?? s.speed;
639
- const avg = (a + b) / 2;
640
- let ease = "linear";
641
- if (a !== b && avg > 0) {
642
- ease = [1 / 3, 2 * a / (3 * (a + b)), 2 / 3, 1 - 2 * b / (3 * (a + b))].map((n2) => Math.round(n2 * 1e6) / 1e6);
643
- }
644
- keys.push({ t, v, ...ease === "linear" ? {} : { ease } });
645
- t += s.duration;
646
- v += avg * s.duration;
647
- });
648
- keys.push({ t, v });
649
- return { keys: mergeLinear(keys), duration: t, source: v };
650
- }
651
- function mergeLinear(keys) {
652
- const out = [keys[0]];
653
- for (let i = 1; i < keys.length; i++) {
654
- const prev = out[out.length - 1];
655
- const k = keys[i];
656
- const before = out.length > 1 ? out[out.length - 2] : null;
657
- if (before && !before.ease && !prev.ease) {
658
- const s1 = (prev.v - before.v) / (prev.t - before.t);
659
- const s2 = (k.v - prev.v) / (k.t - prev.t);
660
- if (Math.abs(s1 - s2) < 1e-9) {
661
- out[out.length - 1] = k;
662
- continue;
663
- }
664
- }
665
- out.push(k);
666
- }
667
- return out;
668
- }
669
- function checkVideoTimeRemap(remap, path = "timeRemap") {
670
- const { keys } = remap;
671
- if (!keys.length) throw new Error(`${path} needs at least one key.`);
672
- keys.forEach((k, i) => {
673
- if (k.v < 0) throw new Error(`${path}.keys[${i}].v must be at least 0 (ms after the in point).`);
674
- const next = keys[i + 1];
675
- if (!next) return;
676
- 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.`);
677
- if (!videoEaseBezier(k.ease)) return;
678
- let prev = k.v;
679
- for (let n2 = 1; n2 <= 32; n2++) {
680
- const v = videoValueAt(remap, k.t + (next.t - k.t) * n2 / 32);
681
- 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).`);
682
- prev = v;
683
- }
684
- });
685
- }
686
- function videoTimeRemapSpan(remap) {
687
- return { from: remap.keys[0].v, to: remap.keys[remap.keys.length - 1].v };
688
- }
689
- function videoTimeRemapBreakpoints(remap, step) {
690
- const { points, holds } = videoKeyframeBreakpoints(remap, step);
691
- const out = [];
692
- points.forEach(([t, v], i) => {
693
- out.push({ t, v });
694
- if (holds.has(i) && points[i + 1]) out.push({ t: points[i + 1][0], v });
695
- });
696
- return out;
697
- }
698
- function videoSpeedRampEdit(clip, segments, ripple = true) {
699
- const ramp = videoSpeedRampKeys(segments);
700
- const offset = videoClipSourceTime(clip, 0) - (clip.in ?? 0);
701
- return { op: "set_time_remap", clip: clip.id, keys: ramp.keys.map((k) => ({ ...k, v: k.v + offset })), duration: ramp.duration, ripple };
702
- }
703
- function videoFreezeFrameEdit(clip, at, duration, ripple = true) {
704
- 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).`);
705
- const remap = clip.timeRemap ?? { keys: [{ t: 0, v: 0 }, { t: clip.duration, v: clip.duration * (clip.speed ?? 1) }] };
706
- const [left, right] = splitVideoAnimatable(remap, at);
707
- 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 }))];
708
- return { op: "set_time_remap", clip: clip.id, keys, duration: clip.duration + duration, ripple };
709
- }
710
- var videoSlowMotionRamp = (duration) => [
711
- { duration: Math.max(1, duration / 2), speed: 1 },
712
- { duration: 500, speed: 1, toSpeed: 0.3 },
713
- { duration: 1500, speed: 0.3 }
714
- ];
715
-
716
136
  // src/video-editor/parse.ts
717
137
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
718
138
  var fail = (path, what) => {
@@ -745,7 +165,7 @@ var compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !=
745
165
  function parseVideoEase(v, path) {
746
166
  if (v === void 0) return void 0;
747
167
  if (Array.isArray(v)) {
748
- if (v.length !== 4 || !v.every((n2) => typeof n2 === "number" && Number.isFinite(n2))) fail(path, "must be four numbers [x1, y1, x2, y2] (CSS cubic-bezier)");
168
+ if (v.length !== 4 || !v.every((n) => typeof n === "number" && Number.isFinite(n))) fail(path, "must be four numbers [x1, y1, x2, y2] (CSS cubic-bezier)");
749
169
  if (v[0] < 0 || v[0] > 1 || v[2] < 0 || v[2] > 1) fail(path, "must have x1 and x2 between 0 and 1");
750
170
  return [v[0], v[1], v[2], v[3]];
751
171
  }
@@ -1026,258 +446,6 @@ function createVideoProject(options = {}) {
1026
446
  });
1027
447
  }
1028
448
 
1029
- // src/video-editor/timeline.ts
1030
- var VIDEO_TIME_EPSILON = 0.5;
1031
- var videoClipEnd = (clip) => clip.start + clip.duration;
1032
- var videoTrackSyncLocked = (track) => track.syncLocked !== false;
1033
- var videoClipSpeed = (clip) => clip.type === "media" ? clip.speed ?? 1 : 1;
1034
- function videoClipSourceRange(clip) {
1035
- if (clip.type === "sequence") return { in: clip.in ?? 0, out: (clip.in ?? 0) + clip.duration };
1036
- if (clip.type !== "media") return null;
1037
- const start = clip.in ?? 0;
1038
- if (clip.timeRemap?.keys.length) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
1039
- return { in: start, out: start + clip.duration * videoClipSpeed(clip) };
1040
- }
1041
- var videoFrameDuration = (fps) => 1e3 / fps;
1042
- function snapToVideoFrame(time, fps) {
1043
- const frame = Math.round(time * fps / 1e3);
1044
- return Math.round(frame * 1e3 / fps * 1e3) / 1e3;
1045
- }
1046
- var videoFrames = (time, fps) => Math.round(time * fps / 1e3);
1047
- function isVideoDropFrameRate(fps) {
1048
- const nominal = Math.round(fps);
1049
- return (nominal === 30 || nominal === 60) && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3;
1050
- }
1051
- var exactVideoRate = (fps) => {
1052
- const nominal = Math.round(fps);
1053
- return nominal !== fps && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3 ? nominal * 1e3 / 1001 : fps;
1054
- };
1055
- var videoTimecodeFrames = (time, fps) => Math.max(0, Math.round(time * exactVideoRate(fps) / 1e3));
1056
- function formatVideoTimecode(time, fps, dropFrame = isVideoDropFrameRate(fps)) {
1057
- return formatVideoTimecodeFrames(videoTimecodeFrames(time, fps), fps, dropFrame);
1058
- }
1059
- function formatVideoTimecodeFrames(frames, fps, dropFrame = isVideoDropFrameRate(fps)) {
1060
- const base = Math.round(fps);
1061
- let total = Math.max(0, Math.round(frames));
1062
- const drop = dropFrame && isVideoDropFrameRate(fps);
1063
- if (drop) {
1064
- const skip = base / 15;
1065
- const perMinute = base * 60 - skip;
1066
- const perTen = perMinute * 10 + skip;
1067
- const tens = Math.floor(total / perTen);
1068
- const rest = total % perTen;
1069
- total += skip * 9 * tens + (rest > skip ? skip * Math.floor((rest - skip) / perMinute) : 0);
1070
- }
1071
- const ff = total % base;
1072
- const secs = Math.floor(total / base);
1073
- const pad = (n2) => String(n2).padStart(2, "0");
1074
- return `${pad(Math.floor(secs / 3600))}:${pad(Math.floor(secs / 60) % 60)}:${pad(secs % 60)}${drop ? ";" : ":"}${pad(ff)}`;
1075
- }
1076
- var videoHourFrames = (fps, dropFrame = isVideoDropFrameRate(fps)) => {
1077
- const base = Math.round(fps);
1078
- return dropFrame && isVideoDropFrameRate(fps) ? base * 3600 - base / 15 * 54 : base * 3600;
1079
- };
1080
- var formatVideoSeconds = (time) => `${(time / 1e3).toFixed(2)} s`;
1081
- var videoAssetHasSound = (asset) => !!asset && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio !== false);
1082
- function videoAssetMap(project) {
1083
- return new Map(project.assets.map((a) => [a.id, a]));
1084
- }
1085
- function videoSequenceMap(project) {
1086
- return new Map((project.sequences ?? []).map((q) => [q.id, q]));
1087
- }
1088
- function videoSequenceDuration(sequence) {
1089
- let end = 0;
1090
- for (const track of sequence.tracks) for (const clip of track.clips) end = Math.max(end, videoClipEnd(clip));
1091
- return end;
1092
- }
1093
- var videoSequenceChildren = (sequence) => [...new Set(sequence.tracks.flatMap((t) => t.clips.flatMap((c) => c.type === "sequence" ? [c.sequence] : [])))];
1094
- function findVideoSequenceCycle(project, id) {
1095
- const sequences = videoSequenceMap(project);
1096
- const walk = (at, path) => {
1097
- const seq = sequences.get(at);
1098
- if (!seq) return null;
1099
- for (const child of videoSequenceChildren(seq)) {
1100
- if (child === id) return [...path, child];
1101
- if (path.includes(child)) continue;
1102
- const found = walk(child, [...path, child]);
1103
- if (found) return found;
1104
- }
1105
- return null;
1106
- };
1107
- return walk(id, [id]);
1108
- }
1109
- function allVideoTracks(project) {
1110
- return [...project.tracks.map((track) => ({ track })), ...(project.sequences ?? []).flatMap((q) => q.tracks.map((track) => ({ sequence: q.id, track })))];
1111
- }
1112
- function sortedVideoClips(track) {
1113
- return [...track.clips].sort((a, b) => a.start - b.start || a.duration - b.duration);
1114
- }
1115
- function findVideoClip(project, id) {
1116
- for (let trackIndex = 0; trackIndex < project.tracks.length; trackIndex++) {
1117
- const track = project.tracks[trackIndex];
1118
- const index = track.clips.findIndex((c) => c.id === id);
1119
- if (index >= 0) return { track, trackIndex, clip: track.clips[index], index };
1120
- }
1121
- return null;
1122
- }
1123
- function allVideoClipIds(project) {
1124
- return allVideoTracks(project).flatMap(({ track }) => track.clips.map((c) => c.id));
1125
- }
1126
- function findVideoSequenceClip(project, id) {
1127
- for (const sequence of project.sequences ?? [])
1128
- for (const track of sequence.tracks) {
1129
- const clip = track.clips.find((c) => c.id === id);
1130
- if (clip) return { sequence, track, clip };
1131
- }
1132
- return null;
1133
- }
1134
- function videoProjectDuration(project) {
1135
- let end = 0;
1136
- for (const track of project.tracks) for (const clip of track.clips) end = Math.max(end, videoClipEnd(clip));
1137
- return end;
1138
- }
1139
- function videoClipBefore(track, clip) {
1140
- let best = null;
1141
- for (const c of track.clips) if (c !== clip && Math.abs(videoClipEnd(c) - clip.start) <= VIDEO_TIME_EPSILON) best = c;
1142
- return best;
1143
- }
1144
- function videoClipAfter(track, clip) {
1145
- for (const c of track.clips) if (c !== clip && Math.abs(c.start - videoClipEnd(clip)) <= VIDEO_TIME_EPSILON) return c;
1146
- return null;
1147
- }
1148
- function videoClipHandles(clip, assets, sequences) {
1149
- if (clip.type === "sequence") {
1150
- const seq = sequences?.get(clip.sequence);
1151
- const range2 = videoClipSourceRange(clip);
1152
- return { before: range2.in, after: seq ? Math.max(0, videoSequenceDuration(seq) - range2.out) : Infinity };
1153
- }
1154
- if (clip.type !== "media") return { before: Infinity, after: Infinity };
1155
- const asset = assets.get(clip.asset);
1156
- const speed = videoClipSpeed(clip);
1157
- const range = videoClipSourceRange(clip);
1158
- const before = range.in / speed;
1159
- if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: asset?.kind === "image" ? Infinity : before, after: Infinity };
1160
- return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
1161
- }
1162
- function videoClipOverhang(track, clip) {
1163
- const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
1164
- const next = videoClipAfter(track, clip);
1165
- const tail = next?.transition && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
1166
- return { lead, tail };
1167
- }
1168
- function videoSnapPoints(project, options = {}) {
1169
- const exclude = new Set(options.exclude ?? []);
1170
- const points = /* @__PURE__ */ new Set([0]);
1171
- for (const track of project.tracks)
1172
- for (const clip of track.clips) {
1173
- if (exclude.has(clip.id)) continue;
1174
- points.add(clip.start);
1175
- points.add(videoClipEnd(clip));
1176
- }
1177
- for (const marker of project.markers ?? []) points.add(marker.time);
1178
- if (options.playhead !== void 0) points.add(options.playhead);
1179
- return [...points].sort((a, b) => a - b);
1180
- }
1181
- function snapVideoTime(time, points, threshold) {
1182
- let best = null;
1183
- for (const p of points) if (Math.abs(p - time) <= threshold && (best === null || Math.abs(p - time) < Math.abs(best - time))) best = p;
1184
- return best === null ? { time, snapped: null } : { time: best, snapped: best };
1185
- }
1186
- function findVideoGaps(project, trackId) {
1187
- const gaps = [];
1188
- for (const track of project.tracks) {
1189
- if (trackId !== void 0 && track.id !== trackId) continue;
1190
- let at = 0;
1191
- for (const clip of sortedVideoClips(track)) {
1192
- if (clip.start - at > VIDEO_TIME_EPSILON) gaps.push({ track: track.id, start: at, end: clip.start });
1193
- at = Math.max(at, videoClipEnd(clip));
1194
- }
1195
- }
1196
- return gaps;
1197
- }
1198
- function videoOverlap(track, start, end, ignore = /* @__PURE__ */ new Set()) {
1199
- for (const c of sortedVideoClips(track)) {
1200
- if (ignore.has(c.id)) continue;
1201
- if (c.start < end - VIDEO_TIME_EPSILON && videoClipEnd(c) > start + VIDEO_TIME_EPSILON) return c;
1202
- }
1203
- return null;
1204
- }
1205
- function nextFreeVideoSlot(track, from, duration, ignore = /* @__PURE__ */ new Set()) {
1206
- let at = from;
1207
- for (const c of sortedVideoClips(track)) {
1208
- if (ignore.has(c.id)) continue;
1209
- if (videoClipEnd(c) <= at + VIDEO_TIME_EPSILON) continue;
1210
- if (c.start >= at + duration - VIDEO_TIME_EPSILON) break;
1211
- at = videoClipEnd(c);
1212
- }
1213
- return at;
1214
- }
1215
- function videoClipsAt(project, time) {
1216
- const out = [];
1217
- 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 });
1218
- return out;
1219
- }
1220
-
1221
- // src/video-editor/clip-animation.ts
1222
- var VIDEO_CLIP_ANIMATION_DURATION = 500;
1223
- var EFFECTS = {
1224
- fade: { easeIn: "linear", easeOut: "linear" },
1225
- rise: { y: 0.06, easeIn: "ease-out", easeOut: "ease-in" },
1226
- drop: { y: -0.06, easeIn: "ease-out", easeOut: "ease-in" },
1227
- "slide-left": { x: 0.15, easeIn: "ease-out", easeOut: "ease-in" },
1228
- "slide-right": { x: -0.15, easeIn: "ease-out", easeOut: "ease-in" },
1229
- zoom: { scale: 0.6, easeIn: "ease-out", easeOut: "ease-in" },
1230
- // Overshoots past full size, then settles (a "back" curve).
1231
- pop: { scale: 0.3, easeIn: [0.34, 1.56, 0.64, 1], easeOut: [0.36, 0, 0.66, -0.56] }
1232
- };
1233
- var PROPS = ["opacity", "x", "y", "scale"];
1234
- function videoClipAnimationProperties(animation) {
1235
- const spec = EFFECTS[animation.effect];
1236
- return PROPS.filter((p) => p === "opacity" || spec[p] !== void 0);
1237
- }
1238
- function videoClipAnimationSpans(clip) {
1239
- const a = clip.animateIn ? clip.animateIn.duration ?? VIDEO_CLIP_ANIMATION_DURATION : 0;
1240
- const b = clip.animateOut ? clip.animateOut.duration ?? VIDEO_CLIP_ANIMATION_DURATION : 0;
1241
- const k = a + b > clip.duration ? clip.duration / (a + b) : 1;
1242
- return { in: a * k, out: b * k };
1243
- }
1244
- function videoClipAnimated(clip, project) {
1245
- if (!clip.animateIn && !clip.animateOut) return clip;
1246
- const spans = videoClipAnimationSpans(clip);
1247
- const out = { ...clip, transform: { ...clip.transform } };
1248
- for (const prop of PROPS) {
1249
- const current = prop === "opacity" ? clip.opacity : clip.transform?.[prop];
1250
- if (isVideoKeyframes(current)) continue;
1251
- const base = current ?? (prop === "opacity" || prop === "scale" ? 1 : 0);
1252
- const away = (spec) => {
1253
- if (prop === "opacity") return 0;
1254
- const v = spec[prop];
1255
- if (v === void 0) return void 0;
1256
- return prop === "scale" ? base * v : base + v * (prop === "x" ? project.width : project.height);
1257
- };
1258
- const keys = [];
1259
- if (clip.animateIn) {
1260
- const spec = EFFECTS[clip.animateIn.effect];
1261
- const from = away(spec);
1262
- if (from !== void 0) keys.push({ t: 0, v: from, ease: spec.easeIn }, { t: spans.in, v: base });
1263
- }
1264
- if (clip.animateOut) {
1265
- const spec = EFFECTS[clip.animateOut.effect];
1266
- const to = away(spec);
1267
- if (to !== void 0) {
1268
- const start = clip.duration - spans.out;
1269
- if (!keys.length || keys[keys.length - 1].t < start) keys.push({ t: start, v: base, ease: spec.easeOut });
1270
- else keys[keys.length - 1] = { ...keys[keys.length - 1], ease: spec.easeOut };
1271
- keys.push({ t: clip.duration, v: to });
1272
- }
1273
- }
1274
- if (!keys.length) continue;
1275
- if (prop === "opacity") out.opacity = { keys };
1276
- else out.transform[prop] = { keys };
1277
- }
1278
- return out;
1279
- }
1280
-
1281
449
  // src/video-editor/check.ts
1282
450
  function videoClipKeyframes(clip) {
1283
451
  const out = [
@@ -1455,7 +623,7 @@ var refuse = (message) => {
1455
623
  };
1456
624
  function freshId(prefix, taken) {
1457
625
  const used = new Set(taken);
1458
- for (let n2 = 1; ; n2++) if (!used.has(`${prefix}${n2}`)) return `${prefix}${n2}`;
626
+ for (let n = 1; ; n++) if (!used.has(`${prefix}${n}`)) return `${prefix}${n}`;
1459
627
  }
1460
628
  var slug = (s) => s.replace(/^.*[\\/]/, "").replace(/\.[a-z0-9]+(\?.*)?$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
1461
629
  function mergeSpans(spans) {
@@ -1736,8 +904,8 @@ var Editor = class {
1736
904
  const ids = this.p.tracks.map((t) => t.id);
1737
905
  if (e.track.id && ids.includes(e.track.id)) refuse(`There is already a track "${e.track.id}".`);
1738
906
  const id = e.track.id ?? freshId(e.track.kind === "video" ? "v" : "a", ids);
1739
- const n2 = this.p.tracks.filter((t) => t.kind === e.track.kind).length + 1;
1740
- const track = { name: `${e.track.kind === "video" ? "Video" : "Audio"} ${n2}`, ...e.track, id, clips: [] };
907
+ const n = this.p.tracks.filter((t) => t.kind === e.track.kind).length + 1;
908
+ const track = { name: `${e.track.kind === "video" ? "Video" : "Audio"} ${n}`, ...e.track, id, clips: [] };
1741
909
  const at = e.at ?? (e.track.kind === "video" ? 0 : this.p.tracks.length);
1742
910
  if (at > this.p.tracks.length) refuse(`"at" is ${at} but there are only ${this.p.tracks.length} tracks; use 0\u2013${this.p.tracks.length}.`);
1743
911
  this.p.tracks.splice(at, 0, track);
@@ -2188,169 +1356,11 @@ function applyVideoEdits(project, edits) {
2188
1356
  return { ok: true, project: next, issues, outcomes: editor.outcomes };
2189
1357
  }
2190
1358
 
2191
- // src/video-editor/evaluate.ts
2192
- function videoTransformAt(transform, clipTime = 0) {
2193
- return { x: videoValueAt(transform?.x, clipTime, 0), y: videoValueAt(transform?.y, clipTime, 0), scale: videoValueAt(transform?.scale, clipTime, 1), rotation: videoValueAt(transform?.rotation, clipTime, 0) };
2194
- }
2195
- function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
2196
- let w = size.width;
2197
- let h = size.height;
2198
- if (fit === "fill") {
2199
- w = frame.width;
2200
- h = frame.height;
2201
- } else if (fit !== "none") {
2202
- const k = fit === "cover" ? Math.max(frame.width / w, frame.height / h) : Math.min(frame.width / w, frame.height / h);
2203
- w *= k;
2204
- h *= k;
2205
- }
2206
- const t = videoTransformAt(transform, clipTime);
2207
- w *= t.scale;
2208
- h *= t.scale;
2209
- const cx = frame.width / 2 + t.x;
2210
- const cy = frame.height / 2 + t.y;
2211
- return { x: cx - w / 2, y: cy - h / 2, width: w, height: h };
2212
- }
2213
- function videoTextStyle(clip) {
2214
- return { text: clip.text, color: clip.color ?? "#ffffff", fontSize: clip.fontSize ?? 64, fontFamily: clip.fontFamily ?? "sans-serif", fontWeight: clip.fontWeight ?? 700, ...clip.background ? { background: clip.background } : {} };
2215
- }
2216
- function videoTitleFrame(project, clip, clipTime = 0) {
2217
- 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) };
2218
- return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
2219
- }
2220
- var clamp012 = (v) => Math.min(1, Math.max(0, v));
2221
- var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI * clamp012(progress))) / 2;
2222
- function videoFadeCurveGain(curve, x) {
2223
- const t = clamp012(x);
2224
- switch (curve) {
2225
- case "equal-power":
2226
- return Math.sin(t * Math.PI / 2);
2227
- case "s-curve":
2228
- return (1 - Math.cos(t * Math.PI)) / 2;
2229
- case "exponential":
2230
- return t * t * t;
2231
- case "logarithmic":
2232
- return 1 - (1 - t) * (1 - t);
2233
- default:
2234
- return t;
2235
- }
2236
- }
2237
- function fadeGain(clip, time, sound = false) {
2238
- let g = 1;
2239
- const shape = sound && (clip.type === "media" || clip.type === "sequence") ? clip : void 0;
2240
- if (clip.fadeIn) g *= videoFadeCurveGain(shape?.fadeInCurve, (time - clip.start) / clip.fadeIn);
2241
- if (clip.fadeOut) g *= videoFadeCurveGain(shape?.fadeOutCurve, (videoClipEnd(clip) - time) / clip.fadeOut);
2242
- return g;
2243
- }
2244
- function videoClipSoundGain(clip, clipTime) {
2245
- const volume = clip.type === "media" || clip.type === "sequence" ? videoValueAt(clip.volume, clipTime, 1) : 1;
2246
- return volume * fadeGain(clip, clip.start + Math.min(Math.max(clipTime, 0), clip.duration), true);
2247
- }
2248
- function activeOnTrack(track, time) {
2249
- const out = [];
2250
- for (const clip of sortedVideoClips(track)) {
2251
- const before = clip.transition ? videoClipBefore(track, clip) : null;
2252
- const lead = before ? clip.transition.duration / 2 : 0;
2253
- const next = videoClipAfter(track, clip);
2254
- const tail = next?.transition ? next.transition.duration / 2 : 0;
2255
- if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
2256
- const a = { clip };
2257
- if (lead && time < clip.start + lead) a.incoming = { progress: clamp012((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
2258
- if (tail && time >= videoClipEnd(clip) - tail) a.outgoing = { progress: clamp012((time - (videoClipEnd(clip) - tail)) / (2 * tail)), type: next.transition.type, next };
2259
- out.push(a);
2260
- }
2261
- return out;
2262
- }
2263
- var sourceTimeAt = (clip, time) => clip.type === "media" ? videoClipSourceTime(clip, time - clip.start) : 0;
2264
- function videoFrameAt(project, time) {
2265
- const { layers, audio } = tracksAt(project, project.tracks, time, { assets: videoAssetMap(project), sequences: videoSequenceMap(project), within: [], prefix: "" });
2266
- return { time, width: project.width, height: project.height, background: project.background ?? "#000000", layers, audio };
2267
- }
2268
- function tracksAt(project, tracks, time, scope) {
2269
- const { assets, prefix } = scope;
2270
- const layers = [];
2271
- const audio = [];
2272
- for (let ti = tracks.length - 1; ti >= 0; ti--) {
2273
- const track = tracks[ti];
2274
- for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
2275
- if (incoming?.type === "dip" && incoming.progress < 0.5) continue;
2276
- if (outgoing?.type === "dip" && outgoing.progress >= 0.5) continue;
2277
- const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
2278
- const fade = fadeGain(clip, inside);
2279
- const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
2280
- const mix = (incoming ? level(incoming, incoming.transition, true) : 1) * (outgoing ? level(outgoing, outgoing.next.transition, false) : 1);
2281
- const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
2282
- const sourceTime = clip.type === "sequence" ? (clip.in ?? 0) + time - clip.start : sourceTimeAt(clip, time);
2283
- const clipTime = time - clip.start;
2284
- let nested = null;
2285
- if (clip.type === "sequence") {
2286
- const seq = scope.sequences.get(clip.sequence);
2287
- if (!seq || scope.within.includes(seq.id)) continue;
2288
- nested = tracksAt(project, seq.tracks, sourceTime, { ...scope, within: [...scope.within, seq.id], prefix: `${prefix}${clip.id}/` });
2289
- }
2290
- const audible = videoAssetHasSound(asset);
2291
- if (audible && clip.type === "media" && !clip.muted && !track.muted) {
2292
- const gain = videoValueAt(clip.volume, clipTime, 1) * (track.volume ?? 1) * fadeGain(clip, inside, true) * mix;
2293
- audio.push({ clip: prefix + clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
2294
- }
2295
- if (nested && clip.type === "sequence" && !clip.muted && !track.muted) {
2296
- const gain = videoValueAt(clip.volume, clipTime, 1) * (track.volume ?? 1) * fadeGain(clip, inside, true) * mix;
2297
- for (const a of nested.audio) audio.push({ ...a, gain: a.gain * gain });
2298
- }
2299
- if (track.kind !== "video" || track.hidden) continue;
2300
- if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
2301
- if (nested && !nested.layers.length) continue;
2302
- const shown = videoClipAnimated(clip, project);
2303
- let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
2304
- let wipe;
2305
- let dip = null;
2306
- let iris;
2307
- let shift = 0;
2308
- let shiftDirection = "right";
2309
- if (incoming) {
2310
- if (incoming.type === "crossfade") opacity *= incoming.progress;
2311
- else if (incoming.type === "wipe") wipe = { direction: incoming.transition.direction ?? "right", progress: incoming.progress };
2312
- else if (incoming.type === "iris") iris = { progress: incoming.progress };
2313
- else if (incoming.type === "push" || incoming.type === "slide") {
2314
- shiftDirection = incoming.transition.direction ?? "right";
2315
- shift = videoTransitionEase(incoming.progress) - 1;
2316
- } else dip = { color: incoming.transition.color ?? "#000000", opacity: (1 - incoming.progress) * 2 };
2317
- }
2318
- if (outgoing?.type === "dip") dip = { color: outgoing.next.transition.color ?? "#000000", opacity: outgoing.progress * 2 };
2319
- if (outgoing?.type === "push") {
2320
- shiftDirection = outgoing.next.transition.direction ?? "right";
2321
- shift = videoTransitionEase(outgoing.progress);
2322
- }
2323
- const size = clip.type === "media" ? { width: asset.width ?? project.width, height: asset.height ?? project.height } : { width: project.width, height: project.height };
2324
- const fit = clip.fit ?? (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill");
2325
- const placed = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, shown.transform, clipTime);
2326
- const across = shiftDirection === "left" || shiftDirection === "right";
2327
- const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
2328
- const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
2329
- const layer = { clip: prefix + clip.id, track: track.id, type: clip.type, opacity: clamp012(opacity), rect, rotation: videoValueAt(shown.transform?.rotation, clipTime, 0) };
2330
- if (wipe) layer.wipe = wipe;
2331
- if (iris) layer.iris = iris;
2332
- if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
2333
- const grade = normalizeVideoGrade(clip.grade);
2334
- if (grade) layer.grade = grade;
2335
- if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
2336
- if (clip.type === "sequence") Object.assign(layer, { sequence: clip.sequence, sourceTime, layers: nested.layers });
2337
- if (clip.type === "text") layer.text = videoTextStyle(clip);
2338
- if (clip.type === "color") layer.color = clip.color;
2339
- if (layer.opacity > 0) layers.push(layer);
2340
- 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 });
2341
- }
2342
- }
2343
- return { layers, audio };
2344
- }
2345
- function videoFrameLeafLayers(layers) {
2346
- return layers.flatMap((l) => l.layers ? videoFrameLeafLayers(l.layers) : [l]);
2347
- }
2348
-
2349
1359
  // src/video-editor/audio.ts
2350
1360
  var VIDEO_PEAK_BLOCK = 32;
2351
1361
  function buildVideoAudioPeaks(channels, sampleRate, blockSize = VIDEO_PEAK_BLOCK) {
2352
1362
  if (!(sampleRate > 0)) throw new Error("The sample rate must be above 0.");
2353
- const length = channels.reduce((n2, c) => Math.max(n2, c.length), 0);
1363
+ const length = channels.reduce((n, c) => Math.max(n, c.length), 0);
2354
1364
  const blocks = Math.max(1, Math.ceil(length / blockSize));
2355
1365
  const level0 = new Float32Array(blocks * 2);
2356
1366
  for (let b = 0; b < blocks; b++) {
@@ -2371,12 +1381,12 @@ function buildVideoAudioPeaks(channels, sampleRate, blockSize = VIDEO_PEAK_BLOCK
2371
1381
  const levels = [level0];
2372
1382
  while (levels[levels.length - 1].length > 2) {
2373
1383
  const below = levels[levels.length - 1];
2374
- const n2 = below.length / 2;
2375
- const up = new Float32Array(Math.ceil(n2 / 4) * 2);
1384
+ const n = below.length / 2;
1385
+ const up = new Float32Array(Math.ceil(n / 4) * 2);
2376
1386
  for (let j = 0; j < up.length / 2; j++) {
2377
1387
  let lo = Infinity;
2378
1388
  let hi = -Infinity;
2379
- for (let i = 4 * j; i < Math.min(n2, 4 * j + 4); i++) {
1389
+ for (let i = 4 * j; i < Math.min(n, 4 * j + 4); i++) {
2380
1390
  lo = Math.min(lo, below[2 * i]);
2381
1391
  hi = Math.max(hi, below[2 * i + 1]);
2382
1392
  }
@@ -2417,10 +1427,10 @@ function videoAudioPeakRange(peaks, from, to) {
2417
1427
  return min === Infinity ? { min: 0, max: 0 } : { min, max };
2418
1428
  }
2419
1429
  function videoAudioPeakSpans(peaks, from, to, columns) {
2420
- const n2 = Math.max(0, Math.floor(columns));
2421
- const out = new Float32Array(n2 * 2);
2422
- const step = (to - from) / Math.max(1, n2);
2423
- for (let c = 0; c < n2; c++) {
1430
+ const n = Math.max(0, Math.floor(columns));
1431
+ const out = new Float32Array(n * 2);
1432
+ const step = (to - from) / Math.max(1, n);
1433
+ for (let c = 0; c < n; c++) {
2424
1434
  const { min, max } = videoAudioPeakRange(peaks, from + c * step, from + (c + 1) * step);
2425
1435
  out[2 * c] = min;
2426
1436
  out[2 * c + 1] = max;
@@ -2428,12 +1438,12 @@ function videoAudioPeakSpans(peaks, from, to, columns) {
2428
1438
  return out;
2429
1439
  }
2430
1440
  function videoClipWaveform(clip, peaks, columns) {
2431
- const n2 = Math.max(0, Math.floor(columns));
2432
- const out = new Float32Array(n2 * 2);
1441
+ const n = Math.max(0, Math.floor(columns));
1442
+ const out = new Float32Array(n * 2);
2433
1443
  const speed = clip.speed ?? 1;
2434
1444
  const start = clip.in ?? 0;
2435
- const step = clip.duration / Math.max(1, n2);
2436
- for (let c = 0; c < n2; c++) {
1445
+ const step = clip.duration / Math.max(1, n);
1446
+ for (let c = 0; c < n; c++) {
2437
1447
  const { min, max } = videoAudioPeakRange(peaks, start + c * step * speed, start + (c + 1) * step * speed);
2438
1448
  const gain = videoClipSoundGain(clip, (c + 0.5) * step);
2439
1449
  out[2 * c] = min * gain;
@@ -2442,16 +1452,16 @@ function videoClipWaveform(clip, peaks, columns) {
2442
1452
  return out;
2443
1453
  }
2444
1454
  function videoWaveformPath(spans, width, height) {
2445
- const n2 = spans.length / 2;
2446
- if (!n2 || width <= 0 || height <= 0) return "";
1455
+ const n = spans.length / 2;
1456
+ if (!n || width <= 0 || height <= 0) return "";
2447
1457
  const mid = height / 2;
2448
1458
  const y = (v) => (mid - Math.max(-1, Math.min(1, v)) * mid).toFixed(1);
2449
- const x = (i) => ((i + 0.5) * (width / n2)).toFixed(1);
1459
+ const x = (i) => ((i + 0.5) * (width / n)).toFixed(1);
2450
1460
  const floor = 0.5 / mid;
2451
1461
  const top = [];
2452
1462
  const bottom = [];
2453
- for (let i = 0; i < n2; i++) top.push(`${i ? "L" : "M"}${x(i)} ${y(Math.max(spans[2 * i + 1], floor))}`);
2454
- for (let i = n2 - 1; i >= 0; i--) bottom.push(`L${x(i)} ${y(Math.min(spans[2 * i], -floor))}`);
1463
+ for (let i = 0; i < n; i++) top.push(`${i ? "L" : "M"}${x(i)} ${y(Math.max(spans[2 * i + 1], floor))}`);
1464
+ for (let i = n - 1; i >= 0; i--) bottom.push(`L${x(i)} ${y(Math.min(spans[2 * i], -floor))}`);
2455
1465
  return `${top.join("")}${bottom.join("")}Z`;
2456
1466
  }
2457
1467
  function videoClipWaveformPath(clip, peaks, width, height) {
@@ -2708,7 +1718,7 @@ function summarizeVideoScopes(pixels, options = {}) {
2708
1718
  const w = videoLumaWaveform(pixels, { ...options, columns, levels: 256 });
2709
1719
  const waveform = Array.from({ length: w.columns }, (_, c) => {
2710
1720
  const counts = w.data.subarray(c * 256, c * 256 + 256);
2711
- const n2 = counts.reduce((a, b) => a + b, 0);
1721
+ const n = counts.reduce((a, b) => a + b, 0);
2712
1722
  let lo = 0;
2713
1723
  while (lo < 255 && !counts[lo]) lo++;
2714
1724
  let hi = 255;
@@ -2717,7 +1727,7 @@ function summarizeVideoScopes(pixels, options = {}) {
2717
1727
  let median = 0;
2718
1728
  for (let i = 0; i < 256; i++) {
2719
1729
  seen += counts[i];
2720
- if (seen >= n2 / 2 && n2) {
1730
+ if (seen >= n / 2 && n) {
2721
1731
  median = i;
2722
1732
  break;
2723
1733
  }
@@ -3027,7 +2037,7 @@ function videoTimelineTranscript(project, options = {}) {
3027
2037
  return out;
3028
2038
  }
3029
2039
  var normalizeVideoWord = (word) => word.toLowerCase().replace(/^[^\p{L}\p{N}']+|[^\p{L}\p{N}']+$/gu, "");
3030
- var rangeOf = (words, i, n2) => ({ from: words[i].start, to: words[i + n2 - 1].end, text: words.slice(i, i + n2).map((w) => w.word).join(" ") });
2040
+ var rangeOf = (words, i, n) => ({ from: words[i].start, to: words[i + n - 1].end, text: words.slice(i, i + n).map((w) => w.word).join(" ") });
3031
2041
  function findVideoTranscriptText(words, query) {
3032
2042
  const q = query.split(/\s+/).map(normalizeVideoWord).filter(Boolean);
3033
2043
  if (!q.length) throw new Error("Search for at least one word.");
@@ -3101,679 +2111,6 @@ function videoWordAt(words, time) {
3101
2111
  return -1;
3102
2112
  }
3103
2113
 
3104
- // src/video-editor/render.ts
3105
- var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3106
- var n = (v) => String(Math.round(v * 100) / 100);
3107
- var n4 = (v) => String(Math.round(v * 1e4) / 1e4);
3108
- var videoIrisRadius = (frame, progress) => progress * Math.hypot(frame.width / 2, frame.height / 2);
3109
- function videoWipeRect(rect, wipe) {
3110
- const { direction, progress } = wipe;
3111
- const w = rect.width * progress;
3112
- const h = rect.height * progress;
3113
- if (direction === "right") return { ...rect, width: w };
3114
- if (direction === "left") return { ...rect, x: rect.x + rect.width - w, width: w };
3115
- if (direction === "down") return { ...rect, height: h };
3116
- return { ...rect, y: rect.y + rect.height - h, height: h };
3117
- }
3118
- function videoFrameToSvg(frame, options = {}, assets = []) {
3119
- const defs = [];
3120
- const posters = new Map(assets.map((a) => [a.id, a.poster]));
3121
- const draw = (layers, prefix) => layers.map((layer, index) => {
3122
- const i = `${prefix}${index}`;
3123
- const { rect } = layer;
3124
- const attrs = [];
3125
- const style = [];
3126
- if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
3127
- if (layer.blend) style.push(`mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}`);
3128
- if (layer.layers) style.push("isolation:isolate");
3129
- if (style.length) attrs.push(`style="${style.join(";")}"`);
3130
- if (layer.grade) {
3131
- defs.push(videoGradeSvgFilter(`g${i}`, layer.grade, layer.grade.lut ? options.lutFor?.(layer.grade.lut.src) : null));
3132
- attrs.push(`filter="url(#g${i})"`);
3133
- }
3134
- if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
3135
- if (layer.wipe) {
3136
- const r = videoWipeRect(rect, layer.wipe);
3137
- defs.push(`<clipPath id="w${i}"><rect x="${n(r.x)}" y="${n(r.y)}" width="${n(r.width)}" height="${n(r.height)}"/></clipPath>`);
3138
- attrs.push(`clip-path="url(#w${i})"`);
3139
- }
3140
- const box = `x="${n(rect.x)}" y="${n(rect.y)}" width="${n(rect.width)}" height="${n(rect.height)}"`;
3141
- let inner = "";
3142
- if (layer.layers) {
3143
- 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>`;
3144
- } else if (layer.type === "color") inner = `<rect ${box} fill="${esc(layer.color ?? "#000000")}"/>`;
3145
- else if (layer.type === "text" && layer.text) {
3146
- const t = layer.text;
3147
- const lines = t.text.split("\n");
3148
- const lh = t.fontSize * 1.2;
3149
- const cx = rect.x + rect.width / 2;
3150
- const cy = rect.y + rect.height / 2;
3151
- const top = cy - lh * (lines.length - 1) / 2;
3152
- if (t.background) {
3153
- const w = Math.max(...lines.map((l) => l.length)) * t.fontSize * 0.6 + t.fontSize;
3154
- const h = lh * lines.length + t.fontSize * 0.4;
3155
- inner += `<rect x="${n(cx - w / 2)}" y="${n(cy - h / 2)}" width="${n(w)}" height="${n(h)}" fill="${esc(t.background)}"/>`;
3156
- }
3157
- 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>`;
3158
- } else {
3159
- const href = options.imageFor ? options.imageFor(layer, frame) : layer.assetKind === "image" ? layer.src ?? null : posters.get(layer.asset ?? "") ?? null;
3160
- if (href) inner = `<image href="${esc(href)}" ${box} preserveAspectRatio="none"/>`;
3161
- else {
3162
- const label = `${layer.asset ?? layer.clip} \xB7 ${formatVideoTimecode(layer.sourceTime ?? 0, options.fps ?? 30)}`;
3163
- const size = Math.max(10, Math.min(rect.height / 8, rect.width / Math.max(8, label.length * 0.6)));
3164
- 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>`;
3165
- }
3166
- }
3167
- let item = attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner;
3168
- if (layer.iris) {
3169
- 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>`);
3170
- item = `<g clip-path="url(#i${i})">${item}</g>`;
3171
- }
3172
- return item;
3173
- }).join("");
3174
- const body = draw(frame.layers, "");
3175
- 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>`;
3176
- }
3177
- function videoProjectSvg(project, time, options = {}) {
3178
- return videoFrameToSvg(videoFrameAt(project, time), { fps: project.fps, ...options }, project.assets);
3179
- }
3180
- function videoContactSheetSvg(project, times, options = {}) {
3181
- const columns = options.columns ?? Math.ceil(Math.sqrt(times.length));
3182
- const w = options.width ?? 320;
3183
- const h = w * project.height / project.width;
3184
- const label = 18;
3185
- const gap = 8;
3186
- const rows = Math.ceil(times.length / columns);
3187
- const W = columns * w + (columns + 1) * gap;
3188
- const H = rows * (h + label) + (rows + 1) * gap;
3189
- const cells = times.map((t, i) => {
3190
- const x = gap + i % columns * (w + gap);
3191
- const y = gap + Math.floor(i / columns) * (h + label + gap);
3192
- 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.]+">/, ">");
3193
- 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>`;
3194
- });
3195
- 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>`;
3196
- }
3197
- function paintVideoLayer(ctx, frame, layer, sourceFor, options) {
3198
- const { rect } = layer;
3199
- if (layer.iris) {
3200
- ctx.beginPath();
3201
- ctx.arc(frame.width / 2, frame.height / 2, videoIrisRadius(frame, layer.iris.progress), 0, 2 * Math.PI);
3202
- ctx.clip();
3203
- }
3204
- if (layer.grade) {
3205
- const defs = videoGradeSvgDefs(layer.grade);
3206
- if (options.defineSvgFilter) for (const d of defs) options.defineSvgFilter(d.id, d.markup);
3207
- const css = videoGradeCssFilter(layer.grade);
3208
- ctx.filter = options.defineSvgFilter ? css || "none" : css.replace(/\s*url\([^)]*\)/g, "").trim() || "none";
3209
- }
3210
- if (layer.rotation) {
3211
- ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
3212
- ctx.rotate(layer.rotation * Math.PI / 180);
3213
- ctx.translate(-(rect.x + rect.width / 2), -(rect.y + rect.height / 2));
3214
- }
3215
- if (layer.wipe) {
3216
- const r = videoWipeRect(rect, layer.wipe);
3217
- ctx.beginPath();
3218
- ctx.rect(r.x, r.y, r.width, r.height);
3219
- ctx.clip();
3220
- }
3221
- if (layer.type === "color") {
3222
- ctx.fillStyle = layer.color ?? "#000000";
3223
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
3224
- } else if (layer.type === "text" && layer.text) {
3225
- const t = layer.text;
3226
- const lines = t.text.split("\n");
3227
- const lh = t.fontSize * 1.2;
3228
- const cx = rect.x + rect.width / 2;
3229
- const top = rect.y + rect.height / 2 - lh * (lines.length - 1) / 2;
3230
- ctx.font = `${t.fontWeight} ${t.fontSize}px ${t.fontFamily}`;
3231
- ctx.textAlign = "center";
3232
- ctx.textBaseline = "middle";
3233
- if (t.background) {
3234
- const w = Math.max(...lines.map((l) => ctx.measureText(l).width)) + t.fontSize;
3235
- const h = lh * lines.length + t.fontSize * 0.4;
3236
- ctx.fillStyle = t.background;
3237
- ctx.fillRect(cx - w / 2, rect.y + rect.height / 2 - h / 2, w, h);
3238
- }
3239
- ctx.fillStyle = t.color;
3240
- lines.forEach((l, k) => ctx.fillText(l, cx, top + k * lh));
3241
- } else {
3242
- const source = sourceFor(layer);
3243
- if (source) ctx.drawImage(source, rect.x, rect.y, rect.width, rect.height);
3244
- else {
3245
- ctx.fillStyle = "#334155";
3246
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
3247
- ctx.strokeStyle = "#94a3b8";
3248
- ctx.lineWidth = 2;
3249
- ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
3250
- }
3251
- }
3252
- }
3253
- function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
3254
- ctx.save();
3255
- ctx.setTransform(scale, 0, 0, scale, 0, 0);
3256
- ctx.globalAlpha = 1;
3257
- ctx.fillStyle = frame.background;
3258
- ctx.fillRect(0, 0, frame.width, frame.height);
3259
- drawVideoLayers(ctx, frame, frame.layers, sourceFor, scale, options, 0);
3260
- ctx.restore();
3261
- }
3262
- function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth) {
3263
- const cw = Math.max(1, Math.round(frame.width * scale));
3264
- const ch = Math.max(1, Math.round(frame.height * scale));
3265
- for (const layer of layers) {
3266
- let source = sourceFor;
3267
- if (layer.layers) {
3268
- const group = options.scratch?.(cw, ch, 2 * depth);
3269
- if (!group) {
3270
- drawVideoLayers(ctx, frame, layer.layers, sourceFor, scale, options, depth + 1);
3271
- continue;
3272
- }
3273
- const g = group.ctx;
3274
- g.save();
3275
- g.setTransform(1, 0, 0, 1, 0, 0);
3276
- g.clearRect(0, 0, cw, ch);
3277
- g.setTransform(scale, 0, 0, scale, 0, 0);
3278
- g.globalAlpha = 1;
3279
- g.globalCompositeOperation = "source-over";
3280
- drawVideoLayers(g, frame, layer.layers, sourceFor, scale, options, depth + 1);
3281
- g.restore();
3282
- source = (l) => l === layer ? group.canvas : sourceFor(l);
3283
- }
3284
- const setting = layer.grade?.lut;
3285
- const lut = setting ? options.lutFor?.(setting.src) : null;
3286
- const scratch = lut ? options.scratch?.(cw, ch, 2 * depth + 1) : null;
3287
- ctx.save();
3288
- ctx.setTransform(scale, 0, 0, scale, 0, 0);
3289
- if (lut && scratch) {
3290
- const s = scratch.ctx;
3291
- s.save();
3292
- s.setTransform(1, 0, 0, 1, 0, 0);
3293
- s.clearRect(0, 0, cw, ch);
3294
- s.setTransform(scale, 0, 0, scale, 0, 0);
3295
- s.globalAlpha = 1;
3296
- s.globalCompositeOperation = "source-over";
3297
- paintVideoLayer(s, frame, layer, source, options);
3298
- s.restore();
3299
- const img = s.getImageData(0, 0, cw, ch);
3300
- applyVideoLutToPixels(lut, img.data, setting.strength ?? 1);
3301
- s.putImageData(img, 0, 0);
3302
- ctx.setTransform(1, 0, 0, 1, 0, 0);
3303
- ctx.globalAlpha = layer.opacity;
3304
- if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
3305
- ctx.drawImage(scratch.canvas, 0, 0, cw, ch);
3306
- } else {
3307
- ctx.globalAlpha = layer.opacity;
3308
- if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
3309
- paintVideoLayer(ctx, frame, layer, source, options);
3310
- }
3311
- ctx.restore();
3312
- }
3313
- }
3314
-
3315
- // src/video-editor/export.ts
3316
- var reelName = (asset) => (asset?.name ?? asset?.id ?? "AX").replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
3317
- function toVideoEdl(project, options = {}) {
3318
- const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
3319
- if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
3320
- const fps = project.fps;
3321
- const dropFrame = (options.dropFrame ?? true) && isVideoDropFrameRate(fps);
3322
- const tc = (ms) => formatVideoTimecode(ms, fps, dropFrame);
3323
- const rec0 = options.recordStart !== void 0 ? videoTimecodeFrames(options.recordStart, fps) : videoHourFrames(fps, dropFrame);
3324
- const rtc = (ms) => formatVideoTimecodeFrames(rec0 + videoTimecodeFrames(ms, fps), fps, dropFrame);
3325
- const assets = videoAssetMap(project);
3326
- const channel = track.kind === "video" ? "V" : "A";
3327
- const lines = [`TITLE: ${(options.title ?? project.name ?? "Untitled").toUpperCase()}`, `FCM: ${dropFrame ? "DROP FRAME" : "NON-DROP FRAME"}`, ""];
3328
- let event = 0;
3329
- const num2 = () => String(++event).padStart(3, "0");
3330
- const sourceAt = (clip, time) => clip.type === "media" ? (clip.in ?? 0) + (time - clip.start) * videoClipSpeed(clip) : time - clip.start;
3331
- 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)}`;
3332
- for (const clip of sortedVideoClips(track)) {
3333
- const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
3334
- const reel = clip.type === "media" ? reelName(asset) : "BL";
3335
- const end = videoClipEnd(clip);
3336
- const before = clip.transition ? videoClipBefore(track, clip) : null;
3337
- const half = before && clip.transition.type !== "dip" ? clip.transition.duration / 2 : 0;
3338
- const next = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition && c.transition.type !== "dip");
3339
- const recOut = next ? end - next.transition.duration / 2 : end;
3340
- const recIn = clip.start - half;
3341
- const n2 = num2();
3342
- if (before) {
3343
- const t = clip.transition;
3344
- const frames = String(Math.round(t.duration * fps / 1e3)).padStart(3, "0");
3345
- const outReel = before.type === "media" ? reelName(assets.get(before.asset)) : "BL";
3346
- const at = sourceAt(before, recIn);
3347
- lines.push(row(n2, outReel, "C", at, at, recIn, recIn));
3348
- lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
3349
- } else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
3350
- if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
3351
- if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${asset?.name ?? asset?.src ?? clip.asset}`);
3352
- if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
3353
- if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
3354
- if (clip.type === "sequence") lines.push(`* COMPOUND CLIP: ${clip.name ?? clip.sequence} (sequence ${clip.sequence} from ${tc(clip.in ?? 0)}, not flattened)`);
3355
- lines.push("");
3356
- }
3357
- return lines.join("\n");
3358
- }
3359
- function loudnormFilter(target = {}, measure = false) {
3360
- const I = target.integrated ?? -14;
3361
- const TP = target.truePeak ?? -1;
3362
- const LRA = target.range ?? 11;
3363
- 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.`);
3364
- if (!(TP >= -9 && TP <= 0)) throw new Error(`The true peak ceiling must be between -9 and 0 dBTP (got ${TP}); -1 is usual.`);
3365
- if (!(LRA >= 1 && LRA <= 50)) throw new Error(`The loudness range must be between 1 and 50 LU (got ${LRA}).`);
3366
- const parts = [`loudnorm=I=${I}:TP=${TP}:LRA=${LRA}`];
3367
- const m = target.measured;
3368
- if (m && !measure) {
3369
- 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.`);
3370
- parts.push(`measured_I=${m.integrated}:measured_TP=${m.truePeak}:measured_LRA=${m.range}:measured_thresh=${m.threshold}:offset=${m.offset}:linear=true`);
3371
- }
3372
- if (measure) parts.push("print_format=json");
3373
- return `${parts.join(":")},aresample=48000`;
3374
- }
3375
- function parseVideoLoudnessMeasurement(output) {
3376
- const blocks = output.match(/\{[^{}]*"input_i"[^{}]*\}/g);
3377
- if (!blocks) throw new Error("No loudness measurement found: pass the stderr of the command from toVideoLoudnessMeasure.");
3378
- const json = JSON.parse(blocks[blocks.length - 1]);
3379
- const n2 = (k) => Number(json[k]);
3380
- const m = { integrated: n2("input_i"), truePeak: n2("input_tp"), range: n2("input_lra"), threshold: n2("input_thresh"), offset: n2("target_offset") };
3381
- if (!Number.isFinite(m.integrated)) throw new Error("The mix is silent, so its loudness cannot be normalised.");
3382
- return m;
3383
- }
3384
- var sec = (ms) => String(Math.round(ms) / 1e3);
3385
- var fmtMs = (v) => String(Math.round(v * 100) / 100);
3386
- var FFMPEG_BLEND = {
3387
- multiply: "multiply",
3388
- screen: "screen",
3389
- overlay: "overlay",
3390
- darken: "darken",
3391
- lighten: "lighten",
3392
- "color-dodge": "dodge",
3393
- "color-burn": "burn",
3394
- "hard-light": "hardlight",
3395
- "soft-light": "softlight",
3396
- difference: "difference",
3397
- exclusion: "exclusion",
3398
- add: "addition"
3399
- };
3400
- var AFADE_CURVES = { linear: "tri", "equal-power": "qsin", "s-curve": "hsin", exponential: "cub", logarithmic: "ipar" };
3401
- var afadeCurve = (curve) => curve && curve !== "linear" ? `:curve=${AFADE_CURVES[curve]}` : "";
3402
- var even = (v) => Math.max(2, 2 * Math.round(v / 2));
3403
- var ffmpegFilterValue = (s) => s.replace(/[\\':%]/g, (c) => `\\${c}`).replace(/[\\'[\],;]/g, (c) => `\\${c}`);
3404
- var color = (c) => {
3405
- const hex = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(c);
3406
- if (!hex) return c.replace(/[^A-Za-z0-9@.]/g, "");
3407
- const h = hex[1].length === 3 ? [...hex[1]].map((x) => x + x).join("") : hex[1];
3408
- return `0x${h.slice(0, 6)}${h.length === 8 ? `@${(parseInt(h.slice(6), 16) / 255).toFixed(3)}` : ""}`;
3409
- };
3410
- var videoShellQuote = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
3411
- function atempo(speed) {
3412
- const out = [];
3413
- let s = speed;
3414
- while (s > 2) {
3415
- out.push("atempo=2");
3416
- s /= 2;
3417
- }
3418
- while (s < 0.5) {
3419
- out.push("atempo=0.5");
3420
- s /= 0.5;
3421
- }
3422
- if (Math.abs(s - 1) > 1e-6) out.push(`atempo=${Math.round(s * 1e6) / 1e6}`);
3423
- return out;
3424
- }
3425
- function toVideoFfmpeg(project, options = {}) {
3426
- return buildFfmpeg(project, options, false);
3427
- }
3428
- function toVideoLoudnessMeasure(project, options = {}) {
3429
- return buildFfmpeg(project, { ...options, loudness: { ...options.loudness, measured: void 0 } }, true);
3430
- }
3431
- function buildFfmpeg(project, options, measure) {
3432
- const notes = [];
3433
- const assets = videoAssetMap(project);
3434
- const sequences = videoSequenceMap(project);
3435
- const D = videoProjectDuration(project);
3436
- if (D <= 0) throw new Error("The project has no clips, so there is nothing to render.");
3437
- const W = even(project.width);
3438
- const H = even(project.height);
3439
- const fps = project.fps;
3440
- const inputs = [];
3441
- const graph = measure ? [] : [`color=c=${color(project.background ?? "#000000")}:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[base]`];
3442
- let usesDrawtext = false;
3443
- const animated = /* @__PURE__ */ new Set();
3444
- const ramped = /* @__PURE__ */ new Set();
3445
- const rampedPicture = (remap, lead, end) => {
3446
- const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
3447
- const first = pts[0];
3448
- const last = pts[pts.length - 1];
3449
- const s = `(T*1000+${fmtMs(first.v)})`;
3450
- let e = fmtMs(last.t - first.t);
3451
- for (let i = pts.length - 2; i >= 0; i--) {
3452
- const a = pts[i];
3453
- const b = pts[i + 1];
3454
- if (b.v - a.v < 1e-6) continue;
3455
- 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})`;
3456
- }
3457
- const before = lead + first.t;
3458
- const after = Math.max(0, end - last.t);
3459
- 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)}`];
3460
- };
3461
- const rampedSound = (remap, input2, output, lead, end) => {
3462
- const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
3463
- const first = pts[0];
3464
- const last = pts[pts.length - 1];
3465
- const n2 = ak;
3466
- const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
3467
- const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
3468
- const pieces = [];
3469
- const out = [];
3470
- const parts = [];
3471
- if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
3472
- const played = [];
3473
- for (let i = 0; i + 1 < pts.length; i++) {
3474
- const dt = pts[i + 1].t - pts[i].t;
3475
- if (dt < 0.5) continue;
3476
- const speed = (pts[i + 1].v - pts[i].v) / dt;
3477
- if (speed < 0.1) parts.push(silence(dt));
3478
- else {
3479
- played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
3480
- parts.push(`#${played.length - 1}`);
3481
- }
3482
- }
3483
- if (end - last.t > 0.5) parts.push(silence(end - last.t));
3484
- if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
3485
- parts.forEach((p, k2) => {
3486
- const label = `[r${n2}p${k2}]`;
3487
- if (p.startsWith("#")) {
3488
- const piece = played[Number(p.slice(1))];
3489
- 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}`);
3490
- } else out.push(`${p}${label}`);
3491
- pieces.push(label);
3492
- });
3493
- out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
3494
- return out;
3495
- };
3496
- const blended = /* @__PURE__ */ new Set();
3497
- let k = 0;
3498
- const step = 2e3 / fps;
3499
- const expr = (prop, clipTime, fallback, what) => {
3500
- if (isVideoKeyframes(prop)) animated.add(what);
3501
- return videoKeyframeFfmpegExpr(prop, clipTime, fallback, step);
3502
- };
3503
- const input = (clip, asset, lead, tail) => {
3504
- const speed = videoClipSpeed(clip);
3505
- const src = options.resolve ? options.resolve(asset) : asset.src;
3506
- const span = clip.duration + lead + tail;
3507
- if (asset.kind === "image") inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", src]);
3508
- else if (clip.timeRemap) {
3509
- const { from, to } = videoTimeRemapSpan(clip.timeRemap);
3510
- inputs.push(["-ss", sec((clip.in ?? 0) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
3511
- } else inputs.push(["-ss", sec((clip.in ?? 0) - lead * speed), "-t", sec(span * speed), "-i", src]);
3512
- return inputs.length - 1;
3513
- };
3514
- let ak = 0;
3515
- let q = 0;
3516
- const compose = (tracks, base, length, scope) => {
3517
- let cur2 = base;
3518
- const sounds = [];
3519
- for (const track of [...tracks].reverse()) {
3520
- for (const raw of sortedVideoClips(track)) {
3521
- const clip = videoClipAnimated(raw, project);
3522
- const { lead, tail } = videoClipOverhang(track, clip);
3523
- const t0 = clip.start - lead;
3524
- const span = clip.duration + lead + tail;
3525
- const end = videoClipEnd(clip);
3526
- const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
3527
- if (clip.type === "media" && !asset) {
3528
- notes.push(`Clip "${clip.id}" was left out: its asset "${clip.asset}" is missing.`);
3529
- continue;
3530
- }
3531
- const shows = scope.picture && track.kind === "video" && !track.hidden;
3532
- let compound = null;
3533
- if (clip.type === "sequence") {
3534
- const seq = sequences.get(clip.sequence);
3535
- if (!seq || scope.within.includes(seq.id)) {
3536
- notes.push(`Compound clip "${clip.id}" was left out: its sequence "${clip.sequence}" ${seq ? "contains itself" : "is missing"}.`);
3537
- continue;
3538
- }
3539
- const from = (clip.in ?? 0) - lead;
3540
- 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.`);
3541
- const length2 = Math.max(videoSequenceDuration(seq), from + span);
3542
- const hears = scope.sound && !clip.muted && !track.muted;
3543
- const n2 = q++;
3544
- const nestedBase = shows ? `nb${n2}` : null;
3545
- if (nestedBase) graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length2)},format=yuva420p[${nestedBase}]`);
3546
- const sub = compose(seq.tracks, nestedBase, length2, { within: [...scope.within, seq.id], picture: shows, sound: hears });
3547
- compound = { picture: null, sound: null };
3548
- if (sub.video) {
3549
- graph.push(`[${sub.video}]trim=start=${sec(Math.max(0, from))}:duration=${sec(span)},setpts=PTS-STARTPTS[sq${n2}]`);
3550
- compound.picture = `[sq${n2}]`;
3551
- }
3552
- if (sub.audio.length) {
3553
- 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}]`);
3554
- compound.sound = `[sm${n2}]`;
3555
- }
3556
- }
3557
- let index;
3558
- if (shows && asset && asset.kind !== "audio") index = input(clip, asset, lead, tail);
3559
- const audible = clip.type === "sequence" ? !!compound?.sound : videoAssetHasSound(asset);
3560
- if (scope.sound && audible && (clip.type === "media" || clip.type === "sequence") && !clip.muted && !track.muted) {
3561
- const trackGain = track.volume ?? 1;
3562
- const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(clip.volume ?? 1) * trackGain}`;
3563
- const ramp = clip.type === "media" ? clip.timeRemap : void 0;
3564
- const ai = compound ? -1 : index ?? input(clip, asset, lead, tail);
3565
- let source2 = compound?.sound ?? `[${ai}:a]`;
3566
- if (ramp) {
3567
- source2 = `[r${ak}]`;
3568
- graph.push(...rampedSound(ramp, `${ai}:a`, source2, lead, clip.duration + tail));
3569
- ramped.add("sound");
3570
- }
3571
- const chain2 = [...ramp ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
3572
- const before2 = clip.transition ? videoClipBefore(track, clip) : null;
3573
- const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
3574
- const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
3575
- const outCurve = next2 ? afadeCurve(videoTransitionAudioCurve(next2.transition)) : "";
3576
- if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
3577
- if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
3578
- if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
3579
- if (next2?.transition.type === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
3580
- if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
3581
- if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
3582
- chain2.push(`adelay=${Math.round(t0)}:all=1`);
3583
- graph.push(`${source2}${chain2.join(",")}[a${ak}]`);
3584
- sounds.push(`[a${ak++}]`);
3585
- }
3586
- if (!shows || compound && !compound.picture) continue;
3587
- const enable = `enable='between(t,${sec(t0)},${sec(t0 + span)})'`;
3588
- const next = `v${k++}`;
3589
- const onTimeline = `(t*1000-${Math.round(clip.start)})`;
3590
- const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
3591
- const tf = clip.transform;
3592
- const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(tf?.[p]));
3593
- if (clip.type === "text" && options.titleImage) {
3594
- 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 })]);
3595
- index = inputs.length - 1;
3596
- } else if (clip.type === "text") {
3597
- usesDrawtext = true;
3598
- 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.`);
3599
- const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(clip.opacity ?? 1);
3600
- 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("*");
3601
- const coord = (axis) => {
3602
- const v = tf?.[axis];
3603
- const base2 = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
3604
- return isVideoKeyframes(v) ? `${axis}='${base2}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base2}+${v ?? 0}`;
3605
- };
3606
- const fontSize = clip.fontSize ?? 64;
3607
- const opts = [
3608
- `text=${ffmpegFilterValue(clip.text)}`,
3609
- options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(clip.fontFamily ?? "sans-serif")}`,
3610
- isVideoKeyframes(tf?.scale) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (tf?.scale ?? 1))}`,
3611
- `fontcolor=${color(clip.color ?? "#ffffff")}`,
3612
- coord("x"),
3613
- coord("y"),
3614
- `alpha='${alpha}'`,
3615
- ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((clip.fontSize ?? 64) / 2)}`] : [],
3616
- `enable='between(t,${sec(clip.start)},${sec(end)})'`
3617
- ];
3618
- if (tf?.rotation) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
3619
- if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(clip.grade?.lut)) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
3620
- 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.`);
3621
- graph.push(`[${cur2}]drawtext=${opts.join(":")}[${next}]`);
3622
- cur2 = next;
3623
- continue;
3624
- }
3625
- const size = asset ? { width: asset.width ?? project.width, height: asset.height ?? project.height } : { width: project.width, height: project.height };
3626
- const placed = clip.type === "text" && !moving ? void 0 : tf;
3627
- const still = Object.fromEntries(Object.entries(placed ?? {}).filter(([, v]) => !isVideoKeyframes(v)));
3628
- const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : clip.fit ?? (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill"), still);
3629
- const chain = [];
3630
- let source;
3631
- if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
3632
- else if (compound?.picture) {
3633
- source = compound.picture;
3634
- chain.push(`scale=${even(rect.width)}:${even(rect.height)}`);
3635
- } else {
3636
- source = `[${index}:v]`;
3637
- if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
3638
- else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
3639
- }
3640
- const grade = videoGradeFfmpegFilters(clip.grade);
3641
- if (grade.length) chain.push("format=rgba", ...grade);
3642
- const lut = normalizeVideoGradeLut(clip.grade?.lut);
3643
- if (lut) {
3644
- chain.push("format=gbrap");
3645
- const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
3646
- 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.`);
3647
- const file = ffmpegFilterValue(path);
3648
- const into2 = `q${next}`;
3649
- graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
3650
- const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
3651
- if ((lut.strength ?? 1) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
3652
- 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}]`);
3653
- source = `[r${next}]`;
3654
- chain.length = 0;
3655
- }
3656
- chain.push("format=yuva420p");
3657
- 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)'`);
3658
- else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
3659
- if (clip.fadeIn) chain.push(`fade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}:alpha=1`);
3660
- if (clip.fadeOut) chain.push(`fade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}:alpha=1`);
3661
- if (lead && clip.transition.type === "crossfade") chain.push(`fade=t=in:st=0:d=${sec(2 * lead)}:alpha=1`);
3662
- if (lead && clip.transition.type === "wipe") {
3663
- const d = sec(2 * lead);
3664
- const p = `min(1,T/${d})`;
3665
- const dir = clip.transition.direction ?? "right";
3666
- 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}))`;
3667
- chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
3668
- }
3669
- if (lead && clip.transition.type === "iris") {
3670
- const r = videoIrisRadius({ width: W, height: H }, 1);
3671
- 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)'`);
3672
- }
3673
- const rotation = placed?.rotation;
3674
- if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
3675
- else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
3676
- if (isVideoKeyframes(placed?.scale)) {
3677
- const s = expr(placed.scale, onChain("t"), 1, "scale");
3678
- chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
3679
- }
3680
- chain.push(`setpts=PTS+${sec(t0)}/TB`);
3681
- const cx = rect.x + rect.width / 2;
3682
- const cy = rect.y + rect.height / 2;
3683
- const shifts = { x: [], y: [] };
3684
- const shiftBy = (direction, from, d, entering) => {
3685
- const dir = direction ?? "right";
3686
- const axis = dir === "left" || dir === "right" ? "x" : "y";
3687
- const size2 = axis === "x" ? W : H;
3688
- const sign = dir === "left" || dir === "up" ? -1 : 1;
3689
- const ease = `(1-cos(PI*clip((t-${sec(from)})/${sec(d)},0,1)))/2`;
3690
- shifts[axis].push(`${sign * size2}*(${ease}${entering ? "-1" : ""})`);
3691
- };
3692
- const into = lead ? clip.transition.type : void 0;
3693
- if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
3694
- const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
3695
- if (after?.transition.type === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
3696
- const at = (axis, c) => {
3697
- const v = placed?.[axis];
3698
- const extra = shifts[axis].map((s) => `+${s}`).join("");
3699
- 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}'`;
3700
- };
3701
- graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
3702
- if (clip.blend && clip.blend !== "normal") {
3703
- graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length)},format=yuva420p[t${next}]`);
3704
- const clear = scope.within.length > 0;
3705
- 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}]` : ""}`);
3706
- graph.push(`[${cur2}]format=gbrap,split${clear ? `=3` : ""}[c${next}][d${next}]${clear ? `[e${next}]` : ""}`);
3707
- const layerFirst = clip.blend === "color-dodge" || clip.blend === "color-burn";
3708
- graph.push(`${layerFirst ? `[f${next}][c${next}]` : `[c${next}][f${next}]`}blend=all_mode=${FFMPEG_BLEND[clip.blend]}[b${next}]`);
3709
- graph.push(`[g${next}]alphaextract[m${next}]`);
3710
- if (clear) {
3711
- graph.push(`[e${next}]alphaextract[ea${next}]`);
3712
- graph.push(`[b${next}][ea${next}]alphamerge[bb${next}]`);
3713
- graph.push(`[h${next}]format=gbrp[ho${next}]`);
3714
- graph.push(`[ho${next}][bb${next}]overlay=format=gbrp[bm${next}]`);
3715
- graph.push(`[bm${next}][m${next}]alphamerge[x${next}]`);
3716
- } else graph.push(`[b${next}][m${next}]alphamerge[x${next}]`);
3717
- graph.push(`[d${next}][x${next}]overlay=format=gbrp,format=yuva420p[${next}]`);
3718
- blended.add(clip.blend);
3719
- } else graph.push(`[${cur2}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
3720
- cur2 = next;
3721
- const before = clip.transition ? videoClipBefore(track, clip) : null;
3722
- if (before && clip.transition.type === "dip") {
3723
- const d = clip.transition.duration;
3724
- const dip = `d${k}`;
3725
- graph.push(`color=c=${color(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}]`);
3726
- graph.push(`[${cur2}][l${dip}]overlay=eof_action=pass:enable='between(t,${sec(clip.start - d / 2)},${sec(clip.start + d / 2)})'[${dip}]`);
3727
- cur2 = dip;
3728
- }
3729
- }
3730
- }
3731
- return { video: cur2, audio: sounds };
3732
- };
3733
- const top = compose(project.tracks, measure ? null : "base", D, { within: [], picture: !measure, sound: true });
3734
- const audio = top.audio;
3735
- const cur = top.video;
3736
- if (!measure) graph.push(`[${cur}]format=yuv420p[vout]`);
3737
- if (measure && !audio.length) throw new Error("Nothing on the timeline makes a sound, so there is no loudness to measure.");
3738
- const loudness = options.loudness || measure ? `,${loudnormFilter(options.loudness, measure)}` : "";
3739
- if (audio.length) graph.push(`${audio.join("")}amix=inputs=${audio.length}:normalize=0:duration=longest,apad,atrim=0:${sec(D)}${loudness}[aout]`);
3740
- if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
3741
- else if (options.loudness && !options.loudness.measured)
3742
- 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.");
3743
- if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && assets.get(c.asset)?.kind === "video" && assets.get(c.asset)?.hasAudio === void 0)))
3744
- 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.');
3745
- if (animated.size)
3746
- 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)" : ""}.`);
3747
- 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.");
3748
- 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.");
3749
- 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.");
3750
- const filterComplex = graph.join(";");
3751
- if (measure) {
3752
- const args2 = ["-hide_banner", "-nostats", ...inputs.flat(), "-filter_complex", filterComplex, "-map", "[aout]", "-f", "null", "-"];
3753
- return { args: args2, command: ["ffmpeg", ...args2].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
3754
- }
3755
- const args = [
3756
- "-y",
3757
- ...inputs.flat(),
3758
- "-filter_complex",
3759
- filterComplex,
3760
- "-map",
3761
- "[vout]",
3762
- ...audio.length ? ["-map", "[aout]", "-c:a", options.audioCodec ?? "aac"] : ["-an"],
3763
- "-c:v",
3764
- options.videoCodec ?? "libx264",
3765
- ...(options.videoCodec ?? "libx264") === "libx264" ? ["-crf", String(options.crf ?? 18), "-preset", "medium"] : [],
3766
- "-pix_fmt",
3767
- "yuv420p",
3768
- "-r",
3769
- String(fps),
3770
- "-t",
3771
- sec(D),
3772
- options.output ?? "output.mp4"
3773
- ];
3774
- return { args, command: ["ffmpeg", ...args].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
3775
- }
3776
-
3777
2114
  // src/video-editor/proxy.ts
3778
2115
  var VIDEO_PROXY_SCALES = [0.5, 0.25];
3779
2116
  function videoAssetProxy(asset, scale) {
@@ -3884,7 +2221,7 @@ function planVideoPrefetch(project, time, options = {}) {
3884
2221
  return out;
3885
2222
  }
3886
2223
  function videoCacheEvictions(entries, options) {
3887
- let total = entries.reduce((n2, e) => n2 + e.bytes, 0);
2224
+ let total = entries.reduce((n, e) => n + e.bytes, 0);
3888
2225
  if (total <= options.maxBytes) return [];
3889
2226
  const dir = options.direction ?? 1;
3890
2227
  const behind = options.behind ?? 2;
@@ -5038,7 +3375,7 @@ function describeVideoSequence(project, sequence, tracks = true) {
5038
3375
  id: sequence.id,
5039
3376
  ...sequence.name ? { name: sequence.name } : {},
5040
3377
  duration: videoSequenceDuration(sequence),
5041
- clipCount: sequence.tracks.reduce((n2, t) => n2 + t.clips.length, 0),
3378
+ clipCount: sequence.tracks.reduce((n, t) => n + t.clips.length, 0),
5042
3379
  nests: videoSequenceChildren(sequence),
5043
3380
  usedBy: videoSequenceUsers(project, sequence.id),
5044
3381
  ...cycle ? { cycle } : {},
@@ -5627,7 +3964,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
5627
3964
  annotations: { title: "Find filler words", ...READ },
5628
3965
  handler: ({ track, fillers }) => {
5629
3966
  const found = findVideoFillers(timelineWords(track), fillers);
5630
- return { fillers: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
3967
+ return { fillers: found.map(RANGE_OUT), total: round(found.reduce((n, r) => n + r.to - r.from, 0)) };
5631
3968
  }
5632
3969
  };
5633
3970
  const findPauses = {
@@ -5642,7 +3979,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
5642
3979
  annotations: { title: "Find pauses between words", ...READ },
5643
3980
  handler: ({ track, min, keep }) => {
5644
3981
  const found = findVideoTranscriptPauses(timelineWords(track), { min, keep, fps: project().fps });
5645
- return { pauses: found.map(RANGE_OUT), total: round(found.reduce((n2, r) => n2 + r.to - r.from, 0)) };
3982
+ return { pauses: found.map(RANGE_OUT), total: round(found.reduce((n, r) => n + r.to - r.from, 0)) };
5646
3983
  }
5647
3984
  };
5648
3985
  const removeRanges = {
@@ -5958,7 +4295,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
5958
4295
  const r = soundRange(args);
5959
4296
  const peaks = await controller.audioPeaks.load(r.asset);
5960
4297
  const silences = findVideoAudioSilences(peaks, r.toSource(r.from), r.toSource(r.to), { thresholdDb: args.thresholdDb, minDuration: args.minDuration }).map((x) => ({ start: round(r.toOut(x.start)), end: round(r.toOut(x.end)) }));
5961
- return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
4298
+ return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n, x) => n + x.end - x.start, 0)) };
5962
4299
  }
5963
4300
  };
5964
4301
  const pixelsAt = async (time) => {