@pixerate/editor 0.78.0 → 0.80.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.
- package/dist/video-editor/index.cjs +947 -117
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +231 -20
- package/dist/video-editor/index.d.ts +231 -20
- package/dist/video-editor/index.js +925 -95
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -15,8 +15,197 @@ require('../chunk-2FHLHZAL.cjs');
|
|
|
15
15
|
var VIDEO_FITS = ["contain", "cover", "fill", "none"];
|
|
16
16
|
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe"];
|
|
17
17
|
var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
18
|
+
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
18
19
|
var VIDEO_CLIP_TYPES = ["media", "text", "color"];
|
|
19
20
|
|
|
21
|
+
// src/video-editor/keyframes.ts
|
|
22
|
+
var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
|
|
23
|
+
var VIDEO_KEYFRAME_PROPERTIES = ["opacity", "volume", "x", "y", "scale", "rotation"];
|
|
24
|
+
var VIDEO_TRANSFORM_PROPERTIES = ["x", "y", "scale", "rotation"];
|
|
25
|
+
var VIDEO_KEYFRAME_LIMITS = {
|
|
26
|
+
opacity: { default: 1, min: 0, max: 1 },
|
|
27
|
+
volume: { default: 1, min: 0, max: 4 },
|
|
28
|
+
x: { default: 0 },
|
|
29
|
+
y: { default: 0 },
|
|
30
|
+
scale: { default: 1, min: 0 },
|
|
31
|
+
rotation: { default: 0 }
|
|
32
|
+
};
|
|
33
|
+
var NAMED = {
|
|
34
|
+
"ease-in": [0.42, 0, 1, 1],
|
|
35
|
+
"ease-out": [0, 0, 0.58, 1],
|
|
36
|
+
"ease-in-out": [0.42, 0, 0.58, 1]
|
|
37
|
+
};
|
|
38
|
+
var isVideoKeyframes = (v) => typeof v === "object" && v !== null && Array.isArray(v.keys);
|
|
39
|
+
function videoEaseBezier(ease) {
|
|
40
|
+
if (ease === void 0 || ease === "linear" || ease === "hold") return null;
|
|
41
|
+
return Array.isArray(ease) ? ease : NAMED[ease];
|
|
42
|
+
}
|
|
43
|
+
var bez = (a, b, s) => 3 * a * (1 - s) * (1 - s) * s + 3 * b * (1 - s) * s * s + s * s * s;
|
|
44
|
+
var bezSlope = (a, b, s) => 3 * a * (1 - s) * (1 - s) + 6 * (b - a) * (1 - s) * s + 3 * (1 - b) * s * s;
|
|
45
|
+
function bezierParam(x1, x2, x) {
|
|
46
|
+
let s = x;
|
|
47
|
+
for (let i = 0; i < 8; i++) {
|
|
48
|
+
const err = bez(x1, x2, s) - x;
|
|
49
|
+
if (Math.abs(err) < 1e-7) return s;
|
|
50
|
+
const d = bezSlope(x1, x2, s);
|
|
51
|
+
if (Math.abs(d) < 1e-6) break;
|
|
52
|
+
s -= err / d;
|
|
53
|
+
if (s < 0 || s > 1) break;
|
|
54
|
+
}
|
|
55
|
+
let lo = 0;
|
|
56
|
+
let hi = 1;
|
|
57
|
+
s = x;
|
|
58
|
+
for (let i = 0; i < 60; i++) {
|
|
59
|
+
const v = bez(x1, x2, s);
|
|
60
|
+
if (Math.abs(v - x) < 1e-9) break;
|
|
61
|
+
if (v < x) lo = s;
|
|
62
|
+
else hi = s;
|
|
63
|
+
s = (lo + hi) / 2;
|
|
64
|
+
}
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
function videoEaseProgress(ease, p) {
|
|
68
|
+
if (p <= 0) return 0;
|
|
69
|
+
if (p >= 1) return ease === "hold" ? 0 : 1;
|
|
70
|
+
if (ease === "hold") return 0;
|
|
71
|
+
const b = videoEaseBezier(ease);
|
|
72
|
+
if (!b) return p;
|
|
73
|
+
return bez(b[1], b[3], bezierParam(b[0], b[2], p));
|
|
74
|
+
}
|
|
75
|
+
function keyBefore(keys, t) {
|
|
76
|
+
let lo = 0;
|
|
77
|
+
let hi = keys.length - 1;
|
|
78
|
+
if (t < keys[0].t) return -1;
|
|
79
|
+
while (lo < hi) {
|
|
80
|
+
const mid = lo + hi + 1 >> 1;
|
|
81
|
+
if (keys[mid].t <= t) lo = mid;
|
|
82
|
+
else hi = mid - 1;
|
|
83
|
+
}
|
|
84
|
+
return lo;
|
|
85
|
+
}
|
|
86
|
+
function videoValueAt(prop, clipTime, fallback = 0) {
|
|
87
|
+
if (prop === void 0) return fallback;
|
|
88
|
+
if (typeof prop === "number") return prop;
|
|
89
|
+
const { keys } = prop;
|
|
90
|
+
if (!keys.length) return fallback;
|
|
91
|
+
const i = keyBefore(keys, clipTime);
|
|
92
|
+
if (i < 0) return keys[0].v;
|
|
93
|
+
if (i >= keys.length - 1) return keys[keys.length - 1].v;
|
|
94
|
+
const a = keys[i];
|
|
95
|
+
const b = keys[i + 1];
|
|
96
|
+
if (a.ease === "hold") return a.v;
|
|
97
|
+
return a.v + (b.v - a.v) * videoEaseProgress(a.ease, (clipTime - a.t) / (b.t - a.t));
|
|
98
|
+
}
|
|
99
|
+
var videoKeysOf = (prop) => isVideoKeyframes(prop) ? prop.keys : [];
|
|
100
|
+
function retimeVideoAnimatable(prop, dt, k = 1) {
|
|
101
|
+
if (!isVideoKeyframes(prop)) return prop;
|
|
102
|
+
return { keys: prop.keys.map((key) => ({ ...key, t: key.t * k + dt })) };
|
|
103
|
+
}
|
|
104
|
+
function splitEase(ease, p) {
|
|
105
|
+
if (ease === "hold") return ["hold", "hold"];
|
|
106
|
+
const b = videoEaseBezier(ease);
|
|
107
|
+
if (!b) return [ease, ease];
|
|
108
|
+
const s = bezierParam(b[0], b[2], p);
|
|
109
|
+
const lerp = (u, v, k) => [u[0] + (v[0] - u[0]) * k, u[1] + (v[1] - u[1]) * k];
|
|
110
|
+
const P0 = [0, 0];
|
|
111
|
+
const P1 = [b[0], b[1]];
|
|
112
|
+
const P2 = [b[2], b[3]];
|
|
113
|
+
const P3 = [1, 1];
|
|
114
|
+
const A = lerp(P0, P1, s);
|
|
115
|
+
const B = lerp(P1, P2, s);
|
|
116
|
+
const C = lerp(P2, P3, s);
|
|
117
|
+
const AB = lerp(A, B, s);
|
|
118
|
+
const BC = lerp(B, C, s);
|
|
119
|
+
const M = lerp(AB, BC, s);
|
|
120
|
+
const r = (v) => Math.round(v * 1e6) / 1e6;
|
|
121
|
+
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])];
|
|
122
|
+
const dx = 1 - M[0];
|
|
123
|
+
const dy = 1 - M[1];
|
|
124
|
+
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)];
|
|
125
|
+
return [left, right];
|
|
126
|
+
}
|
|
127
|
+
function splitVideoAnimatable(prop, at) {
|
|
128
|
+
if (!isVideoKeyframes(prop)) return [prop, prop];
|
|
129
|
+
const keys = prop.keys;
|
|
130
|
+
const eps = 1e-6;
|
|
131
|
+
const v = videoValueAt(prop, at);
|
|
132
|
+
const i = keyBefore(keys, at);
|
|
133
|
+
const exact = i >= 0 && Math.abs(keys[i].t - at) < eps;
|
|
134
|
+
let leftEase;
|
|
135
|
+
let rightEase;
|
|
136
|
+
if (exact) rightEase = keys[i].ease;
|
|
137
|
+
else if (i >= 0 && i < keys.length - 1) {
|
|
138
|
+
const a = keys[i];
|
|
139
|
+
[leftEase, rightEase] = splitEase(a.ease, (at - a.t) / (keys[i + 1].t - a.t));
|
|
140
|
+
}
|
|
141
|
+
const before = keys.slice(0, exact ? i : i + 1).map((k) => ({ ...k }));
|
|
142
|
+
if (before.length && leftEase !== void 0) before[before.length - 1] = withEase(before[before.length - 1], leftEase);
|
|
143
|
+
const left = [...before, { t: at, v }];
|
|
144
|
+
const right = [withEase({ t: 0, v }, rightEase), ...keys.slice(i + 1).map((k) => ({ ...k, t: k.t - at }))];
|
|
145
|
+
return [{ keys: left }, { keys: right }];
|
|
146
|
+
}
|
|
147
|
+
var withEase = (key, ease) => {
|
|
148
|
+
const { ease: _old, ...rest } = key;
|
|
149
|
+
return ease === void 0 || ease === "linear" ? rest : { ...rest, ease };
|
|
150
|
+
};
|
|
151
|
+
function sortVideoKeys(keys) {
|
|
152
|
+
const sorted = [...keys].sort((a, b) => a.t - b.t);
|
|
153
|
+
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.`);
|
|
154
|
+
return sorted;
|
|
155
|
+
}
|
|
156
|
+
function setVideoKey(prop, key, fallback) {
|
|
157
|
+
const keys = isVideoKeyframes(prop) ? prop.keys.filter((k) => Math.abs(k.t - key.t) >= 1e-6) : [];
|
|
158
|
+
if (!isVideoKeyframes(prop)) {
|
|
159
|
+
const v = _nullishCoalesce(prop, () => ( fallback));
|
|
160
|
+
if (key.t !== 0) keys.push({ t: 0, v });
|
|
161
|
+
}
|
|
162
|
+
return { keys: sortVideoKeys([...keys, key]) };
|
|
163
|
+
}
|
|
164
|
+
function removeVideoKey(prop, t) {
|
|
165
|
+
const keys = prop.keys.filter((k) => Math.abs(k.t - t) >= 1e-6);
|
|
166
|
+
return keys.length ? { keys } : prop.keys[0].v;
|
|
167
|
+
}
|
|
168
|
+
var fmt = (v) => {
|
|
169
|
+
const r = Math.round(v * 1e4) / 1e4;
|
|
170
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
171
|
+
};
|
|
172
|
+
var signed = (v) => fmt(v).startsWith("-") ? fmt(v) : `+${fmt(v)}`;
|
|
173
|
+
function videoKeyframeBreakpoints(prop, step) {
|
|
174
|
+
const { keys } = prop;
|
|
175
|
+
const points = [];
|
|
176
|
+
const holds = /* @__PURE__ */ new Set();
|
|
177
|
+
for (let i = 0; i < keys.length; i++) {
|
|
178
|
+
const a = keys[i];
|
|
179
|
+
points.push([a.t, a.v]);
|
|
180
|
+
const b = keys[i + 1];
|
|
181
|
+
if (!b) break;
|
|
182
|
+
if (a.ease === "hold") {
|
|
183
|
+
holds.add(points.length - 1);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!videoEaseBezier(a.ease)) continue;
|
|
187
|
+
const n2 = Math.max(1, Math.ceil((b.t - a.t) / step));
|
|
188
|
+
for (let k = 1; k < n2; k++) {
|
|
189
|
+
const t = a.t + (b.t - a.t) * k / n2;
|
|
190
|
+
points.push([t, videoValueAt(prop, t)]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { points, holds };
|
|
194
|
+
}
|
|
195
|
+
function videoKeyframeFfmpegExpr(prop, time, fallback, step) {
|
|
196
|
+
if (!isVideoKeyframes(prop)) return fmt(_nullishCoalesce(prop, () => ( fallback)));
|
|
197
|
+
const { points, holds } = videoKeyframeBreakpoints(prop, step);
|
|
198
|
+
if (points.length === 1 || points.every(([, v]) => v === points[0][1])) return fmt(points[0][1]);
|
|
199
|
+
let expr = fmt(points[points.length - 1][1]);
|
|
200
|
+
for (let i = points.length - 2; i >= 0; i--) {
|
|
201
|
+
const [t0, v0] = points[i];
|
|
202
|
+
const [t1, v1] = points[i + 1];
|
|
203
|
+
const seg = holds.has(i) || v0 === v1 ? fmt(v0) : `${fmt(v0)}${signed((v1 - v0) / (t1 - t0))}*(${time}${signed(-t0)})`;
|
|
204
|
+
expr = `if(lt(${time},${fmt(t1)}),${seg},${expr})`;
|
|
205
|
+
}
|
|
206
|
+
return `if(lt(${time},${fmt(points[0][0])}),${fmt(points[0][1])},${expr})`;
|
|
207
|
+
}
|
|
208
|
+
|
|
20
209
|
// src/video-editor/parse.ts
|
|
21
210
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
22
211
|
var fail = (path, what) => {
|
|
@@ -46,11 +235,44 @@ var oneOf = (v, options, path, optional = true) => {
|
|
|
46
235
|
return v;
|
|
47
236
|
};
|
|
48
237
|
var compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
238
|
+
function parseVideoEase(v, path) {
|
|
239
|
+
if (v === void 0) return void 0;
|
|
240
|
+
if (Array.isArray(v)) {
|
|
241
|
+
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)");
|
|
242
|
+
if (v[0] < 0 || v[0] > 1 || v[2] < 0 || v[2] > 1) fail(path, "must have x1 and x2 between 0 and 1");
|
|
243
|
+
return [v[0], v[1], v[2], v[3]];
|
|
244
|
+
}
|
|
245
|
+
return oneOf(v, VIDEO_EASE_NAMES, path);
|
|
246
|
+
}
|
|
247
|
+
function parseVideoKeyframes(v, path, { min, max } = {}) {
|
|
248
|
+
if (!isObject(v)) return fail(path, "must be a number or keyframes { keys: [{ t, v, ease }] }");
|
|
249
|
+
for (const key of Object.keys(v)) if (key !== "keys") fail(`${path}.${key}`, "is not a keyframes field (keys)");
|
|
250
|
+
if (!Array.isArray(v.keys) || !v.keys.length) fail(`${path}.keys`, "must be an array of at least one key { t, v, ease }");
|
|
251
|
+
const keys = v.keys.map((k, i) => {
|
|
252
|
+
const at = `${path}.keys[${i}]`;
|
|
253
|
+
if (!isObject(k)) return fail(at, "must be an object { t, v, ease }");
|
|
254
|
+
for (const key of Object.keys(k)) if (!["t", "v", "ease"].includes(key)) fail(`${at}.${key}`, "is not a key field (t, v, ease)");
|
|
255
|
+
return compact({ t: num(k.t, `${at}.t`), v: num(k.v, `${at}.v`, { min, max }), ease: parseVideoEase(k.ease, `${at}.ease`) });
|
|
256
|
+
});
|
|
257
|
+
for (let i = 1; i < keys.length; i++) if (!(keys[i].t > keys[i - 1].t)) fail(`${path}.keys[${i}].t`, `must be later than the key before (${keys[i - 1].t} ms); keys go in time order, one per time`);
|
|
258
|
+
return { keys };
|
|
259
|
+
}
|
|
260
|
+
function parseVideoAnimatable(v, path, range = {}) {
|
|
261
|
+
if (v === void 0 && range.optional) return void 0;
|
|
262
|
+
const { min, max } = range;
|
|
263
|
+
if (isObject(v)) return parseVideoKeyframes(v, path, { min, max });
|
|
264
|
+
return num(v, path, { min, max });
|
|
265
|
+
}
|
|
49
266
|
function parseVideoTransform(v, path) {
|
|
50
267
|
if (v === void 0) return void 0;
|
|
51
268
|
if (!isObject(v)) return fail(path, "must be an object { x, y, scale, rotation }");
|
|
52
269
|
for (const key of Object.keys(v)) if (!["x", "y", "scale", "rotation"].includes(key)) fail(`${path}.${key}`, "is not a transform field (x, y, scale, rotation)");
|
|
53
|
-
return compact({
|
|
270
|
+
return compact({
|
|
271
|
+
x: parseVideoAnimatable(v.x, `${path}.x`, { optional: true }),
|
|
272
|
+
y: parseVideoAnimatable(v.y, `${path}.y`, { optional: true }),
|
|
273
|
+
scale: parseVideoAnimatable(v.scale, `${path}.scale`, { optional: true, min: 0 }),
|
|
274
|
+
rotation: parseVideoAnimatable(v.rotation, `${path}.rotation`, { optional: true })
|
|
275
|
+
});
|
|
54
276
|
}
|
|
55
277
|
function parseVideoTransition(v, path) {
|
|
56
278
|
if (v === void 0) return void 0;
|
|
@@ -87,7 +309,7 @@ function parseVideoClip(v, path = "clip") {
|
|
|
87
309
|
name: str(v.name, `${path}.name`, true),
|
|
88
310
|
start: num(v.start, `${path}.start`, { min: 0 }),
|
|
89
311
|
duration: num(v.duration, `${path}.duration`, { positive: true }),
|
|
90
|
-
opacity:
|
|
312
|
+
opacity: parseVideoAnimatable(v.opacity, `${path}.opacity`, { optional: true, min: 0, max: 1 }),
|
|
91
313
|
transform: parseVideoTransform(v.transform, `${path}.transform`),
|
|
92
314
|
fit: oneOf(v.fit, VIDEO_FITS, `${path}.fit`),
|
|
93
315
|
fadeIn: num(v.fadeIn, `${path}.fadeIn`, { optional: true, min: 0 }),
|
|
@@ -101,7 +323,7 @@ function parseVideoClip(v, path = "clip") {
|
|
|
101
323
|
asset: str(v.asset, `${path}.asset`),
|
|
102
324
|
in: num(v.in, `${path}.in`, { optional: true, min: 0 }),
|
|
103
325
|
speed: num(v.speed, `${path}.speed`, { optional: true, min: 0.05, max: 20 }),
|
|
104
|
-
volume:
|
|
326
|
+
volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
|
|
105
327
|
muted: bool(v.muted, `${path}.muted`)
|
|
106
328
|
});
|
|
107
329
|
if (type === "text")
|
|
@@ -127,6 +349,7 @@ function parseVideoTrack(v, path = "track") {
|
|
|
127
349
|
muted: bool(v.muted, `${path}.muted`),
|
|
128
350
|
hidden: bool(v.hidden, `${path}.hidden`),
|
|
129
351
|
locked: bool(v.locked, `${path}.locked`),
|
|
352
|
+
syncLocked: bool(v.syncLocked, `${path}.syncLocked`),
|
|
130
353
|
volume: num(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
|
|
131
354
|
clips: (_nullishCoalesce(v.clips, () => ( []))).map((c, i) => parseVideoClip(c, `${path}.clips[${i}]`))
|
|
132
355
|
});
|
|
@@ -190,6 +413,7 @@ function createVideoProject(options = {}) {
|
|
|
190
413
|
// src/video-editor/timeline.ts
|
|
191
414
|
var VIDEO_TIME_EPSILON = 0.5;
|
|
192
415
|
var videoClipEnd = (clip) => clip.start + clip.duration;
|
|
416
|
+
var videoTrackSyncLocked = (track) => track.syncLocked !== false;
|
|
193
417
|
var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.speed, () => ( 1)) : 1;
|
|
194
418
|
function videoClipSourceRange(clip) {
|
|
195
419
|
if (clip.type !== "media") return null;
|
|
@@ -311,6 +535,14 @@ function videoClipsAt(project, time) {
|
|
|
311
535
|
}
|
|
312
536
|
|
|
313
537
|
// src/video-editor/check.ts
|
|
538
|
+
function videoClipKeyframes(clip) {
|
|
539
|
+
const out = [
|
|
540
|
+
{ property: "opacity", keys: videoKeysOf(clip.opacity) },
|
|
541
|
+
{ property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
|
|
542
|
+
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _3 => _3.transform, 'optionalAccess', _4 => _4[p]])) }))
|
|
543
|
+
];
|
|
544
|
+
return out.filter((o) => o.keys.length);
|
|
545
|
+
}
|
|
314
546
|
var VIDEO_ISSUE_CODES = [
|
|
315
547
|
"missing-asset",
|
|
316
548
|
"wrong-track",
|
|
@@ -320,6 +552,7 @@ var VIDEO_ISSUE_CODES = [
|
|
|
320
552
|
"transition-too-long",
|
|
321
553
|
"transition-handles",
|
|
322
554
|
"fades-too-long",
|
|
555
|
+
"keyframes-outside-clip",
|
|
323
556
|
"unknown-duration",
|
|
324
557
|
"unused-asset",
|
|
325
558
|
"marker-past-end",
|
|
@@ -355,6 +588,16 @@ function checkVideoProject(project) {
|
|
|
355
588
|
issues.push({ code: "overlap", severity: "error", ...at, message: `Clips "${clip.id}" and "${next.id}" overlap on track "${track.id}" from ${formatVideoSeconds(next.start)}; move one of them.` });
|
|
356
589
|
if ((_nullishCoalesce(clip.fadeIn, () => ( 0))) + (_nullishCoalesce(clip.fadeOut, () => ( 0))) > clip.duration + VIDEO_TIME_EPSILON)
|
|
357
590
|
issues.push({ code: "fades-too-long", severity: "warning", ...at, message: `Clip "${clip.id}"'s fades (${_nullishCoalesce(clip.fadeIn, () => ( 0))} + ${_nullishCoalesce(clip.fadeOut, () => ( 0))} ms) are longer than the clip (${clip.duration} ms); shorten them.` });
|
|
591
|
+
for (const { property, keys } of videoClipKeyframes(clip)) {
|
|
592
|
+
const outside = keys.filter((k) => k.t < -VIDEO_TIME_EPSILON || k.t > clip.duration + VIDEO_TIME_EPSILON);
|
|
593
|
+
if (outside.length)
|
|
594
|
+
issues.push({
|
|
595
|
+
code: "keyframes-outside-clip",
|
|
596
|
+
severity: "warning",
|
|
597
|
+
...at,
|
|
598
|
+
message: `Clip "${clip.id}" has ${property} key${outside.length > 1 ? "s" : ""} at ${outside.map((k) => `${Math.round(k.t)} ms`).join(", ")}, outside the clip (0\u2013${Math.round(clip.duration)} ms in clip time), so ${outside.length > 1 ? "they never play" : "it never plays"} directly; move ${outside.length > 1 ? "them" : "it"} inside or remove ${outside.length > 1 ? "them" : "it"} (set_video_keyframes).`
|
|
599
|
+
});
|
|
600
|
+
}
|
|
358
601
|
if (clip.transition) {
|
|
359
602
|
const d = clip.transition.duration;
|
|
360
603
|
const before = videoClipBefore(track, clip);
|
|
@@ -392,6 +635,7 @@ function checkVideoProject(project) {
|
|
|
392
635
|
}
|
|
393
636
|
|
|
394
637
|
// src/video-editor/edit.ts
|
|
638
|
+
var TRACK_FIELD_KEYS = ["name", "muted", "hidden", "locked", "syncLocked", "volume"];
|
|
395
639
|
var VIDEO_EDIT_OPS = [
|
|
396
640
|
"set_settings",
|
|
397
641
|
"add_asset",
|
|
@@ -408,6 +652,13 @@ var VIDEO_EDIT_OPS = [
|
|
|
408
652
|
"delete_clips",
|
|
409
653
|
"update_clip",
|
|
410
654
|
"set_transition",
|
|
655
|
+
"slip_clip",
|
|
656
|
+
"roll_edit",
|
|
657
|
+
"slide_clip",
|
|
658
|
+
"lift_range",
|
|
659
|
+
"extract_range",
|
|
660
|
+
"split_at",
|
|
661
|
+
"set_keyframes",
|
|
411
662
|
"close_gaps",
|
|
412
663
|
"add_marker",
|
|
413
664
|
"update_marker",
|
|
@@ -426,7 +677,32 @@ function freshId(prefix, taken) {
|
|
|
426
677
|
for (let n2 = 1; ; n2++) if (!used.has(`${prefix}${n2}`)) return `${prefix}${n2}`;
|
|
427
678
|
}
|
|
428
679
|
var slug = (s) => s.replace(/^.*[\\/]/, "").replace(/\.[a-z0-9]+(\?.*)?$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
680
|
+
function mergeSpans(spans) {
|
|
681
|
+
const out = [];
|
|
682
|
+
for (const [from, to] of [...spans].sort((a, b) => a[0] - b[0])) {
|
|
683
|
+
const last = out[out.length - 1];
|
|
684
|
+
if (last && from <= last[1] + VIDEO_TIME_EPSILON) last[1] = Math.max(last[1], to);
|
|
685
|
+
else out.push([from, to]);
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
function clipAnimatables(clip) {
|
|
690
|
+
const c = clip;
|
|
691
|
+
const out = [
|
|
692
|
+
{ get: () => c.opacity, set: (v) => c.opacity = v },
|
|
693
|
+
{ get: () => c.volume, set: (v) => c.volume = v }
|
|
694
|
+
];
|
|
695
|
+
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _5 => _5.transform, 'optionalAccess', _6 => _6[p]]), set: (v) => c.transform && (c.transform[p] = v) });
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
function retimeClipKeys(clip, fn) {
|
|
699
|
+
for (const a of clipAnimatables(clip)) {
|
|
700
|
+
const v = a.get();
|
|
701
|
+
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
429
704
|
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
705
|
+
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
430
706
|
var Editor = (_class = class {
|
|
431
707
|
constructor(p) {;_class.prototype.__init.call(this);
|
|
432
708
|
this.p = p;
|
|
@@ -458,20 +734,172 @@ var Editor = (_class = class {
|
|
|
458
734
|
const hit = videoOverlap(track, start, start + duration, ignore);
|
|
459
735
|
if (hit) {
|
|
460
736
|
const free = nextFreeVideoSlot(track, start, duration, ignore);
|
|
461
|
-
refuse(`${what} at ${formatVideoSeconds(start)}\u2013${formatVideoSeconds(start + duration)} would overlap clip "${hit.id}" (${formatVideoSeconds(hit.start)}\u2013${formatVideoSeconds(videoClipEnd(hit))}) on track "${track.id}"; the next free place is ${formatVideoSeconds(free)}, or
|
|
737
|
+
refuse(`${what} at ${formatVideoSeconds(start)}\u2013${formatVideoSeconds(start + duration)} would overlap clip "${hit.id}" (${formatVideoSeconds(hit.start)}\u2013${formatVideoSeconds(videoClipEnd(hit))}) on track "${track.id}"; the next free place is ${formatVideoSeconds(free)}, or use mode "insert" to push later clips along or "overwrite" to replace what is there.`);
|
|
462
738
|
}
|
|
463
739
|
}
|
|
464
740
|
kindFits(track, clip) {
|
|
465
741
|
const asset = clip.type === "media" ? this.asset(clip.asset) : null;
|
|
466
742
|
if (track.kind === "audio" && (clip.type !== "media" || asset.kind === "image")) refuse(`Track "${track.id}" is an audio track; ${clip.type === "media" ? "images" : `${clip.type} clips`} go on a video track.`);
|
|
467
|
-
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess',
|
|
743
|
+
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _7 => _7.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
|
|
468
744
|
}
|
|
469
745
|
/** Moves every clip on `track` starting at or after `from` by `delta`. */
|
|
470
746
|
shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
|
|
471
747
|
if (!delta) return;
|
|
472
748
|
for (const c of track.clips) if (!except.has(c.id) && c.start >= from - VIDEO_TIME_EPSILON) c.start = Math.max(0, c.start + delta);
|
|
749
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
750
|
+
}
|
|
751
|
+
get frame() {
|
|
752
|
+
return 1e3 / this.p.fps;
|
|
753
|
+
}
|
|
754
|
+
ids() {
|
|
755
|
+
return this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The tracks a ripple on `sources` moves, top to bottom: the sources, and every unlocked
|
|
759
|
+
* sync-locked track when one of the sources is sync-locked.
|
|
760
|
+
*/
|
|
761
|
+
rippleTracks(sources) {
|
|
762
|
+
const set = new Set(sources);
|
|
763
|
+
if ([...set].some(videoTrackSyncLocked)) {
|
|
764
|
+
for (const t of this.p.tracks) if (videoTrackSyncLocked(t) && !t.locked) set.add(t);
|
|
765
|
+
}
|
|
766
|
+
return this.p.tracks.filter((t) => set.has(t));
|
|
767
|
+
}
|
|
768
|
+
/** The tracks a range edit works on: the named ones (unlocked), or every unlocked track. */
|
|
769
|
+
rangeTracks(ids) {
|
|
770
|
+
if (ids) {
|
|
771
|
+
if (!ids.length) refuse('"tracks" is empty; name at least one track, or leave it out for every unlocked track.');
|
|
772
|
+
const tracks = [...new Set(ids)].map((id) => this.track(id));
|
|
773
|
+
tracks.forEach((t) => this.unlocked(t));
|
|
774
|
+
return this.p.tracks.filter((t) => tracks.includes(t));
|
|
775
|
+
}
|
|
776
|
+
const open = this.p.tracks.filter((t) => !t.locked);
|
|
777
|
+
if (!open.length) refuse("Every track is locked; unlock one (update_track with locked false) first.");
|
|
778
|
+
return open;
|
|
779
|
+
}
|
|
780
|
+
range(from, to) {
|
|
781
|
+
if (!(to - from > VIDEO_TIME_EPSILON)) refuse(`"to" (${formatVideoSeconds(to)}) must be after "from" (${formatVideoSeconds(from)}).`);
|
|
782
|
+
}
|
|
783
|
+
/** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
|
|
784
|
+
moveIn(clip, delta) {
|
|
785
|
+
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _8 => _8(this.p), 'access', _9 => _9.get, 'call', _10 => _10(clip.asset), 'optionalAccess', _11 => _11.kind]) === "image") return;
|
|
786
|
+
clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
|
|
787
|
+
if (clip.in < 1e-9) delete clip.in;
|
|
788
|
+
}
|
|
789
|
+
/** Moves a clip's head `delta` later in time (start, in point and keys); the caller sets start and duration. */
|
|
790
|
+
trimHead(clip, delta) {
|
|
791
|
+
this.moveIn(clip, delta);
|
|
792
|
+
retimeClipKeys(clip, (v) => retimeVideoAnimatable(v, -delta));
|
|
793
|
+
}
|
|
794
|
+
/** Cuts `clip` at `at` (inside it): the left part keeps the id, the right part is returned. */
|
|
795
|
+
cut(track, clip, at, id) {
|
|
796
|
+
const end = videoClipEnd(clip);
|
|
797
|
+
const right = { ...structuredClone(clip), id: _nullishCoalesce(id, () => ( freshId(`${clip.id}-`, this.ids()))), start: at, duration: end - at };
|
|
798
|
+
delete right.transition;
|
|
799
|
+
delete right.fadeIn;
|
|
800
|
+
const leftValues = clipAnimatables(clip);
|
|
801
|
+
clipAnimatables(right).forEach((r, i) => {
|
|
802
|
+
const [l, rr] = splitVideoAnimatable(leftValues[i].get(), at - clip.start);
|
|
803
|
+
if (isVideoKeyframes(l)) leftValues[i].set(l);
|
|
804
|
+
if (isVideoKeyframes(rr)) r.set(rr);
|
|
805
|
+
});
|
|
806
|
+
this.moveIn(right, at - clip.start);
|
|
807
|
+
clip.duration = at - clip.start;
|
|
808
|
+
delete clip.fadeOut;
|
|
809
|
+
track.clips.push(right);
|
|
810
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
811
|
+
return right;
|
|
812
|
+
}
|
|
813
|
+
/** Cuts every clip on `tracks` that runs across `at`; returns the new right-hand parts. */
|
|
814
|
+
cutAcross(tracks, at) {
|
|
815
|
+
const made = [];
|
|
816
|
+
for (const t of tracks) for (const c of [...t.clips]) if (c.start < at - VIDEO_TIME_EPSILON && videoClipEnd(c) > at + VIDEO_TIME_EPSILON) made.push(this.cut(t, c, at));
|
|
817
|
+
return made;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Opens `amount` of empty time at `at` on `tracks`: clips across `at` are cut there and everything
|
|
821
|
+
* from `at` on moves later. Returns the clips made by cutting.
|
|
822
|
+
*/
|
|
823
|
+
open(tracks, at, amount, except = /* @__PURE__ */ new Set()) {
|
|
824
|
+
const made = this.cutAcross(tracks, at).map((c) => c.id);
|
|
825
|
+
for (const t of tracks) this.shift(t, at, amount, except);
|
|
826
|
+
return made;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Closes `[from, to)` on `tracks`, pulling everything after it earlier. Refuses, before moving
|
|
830
|
+
* anything, when a track still has a clip in that stretch (it would be pulled out of sync).
|
|
831
|
+
*/
|
|
832
|
+
close(tracks, from, to) {
|
|
833
|
+
for (const t of tracks) {
|
|
834
|
+
const hit = videoOverlap(t, from, to);
|
|
835
|
+
if (hit)
|
|
836
|
+
refuse(
|
|
837
|
+
`Rippling out ${formatVideoSeconds(from)}\u2013${formatVideoSeconds(to)} would pull track "${t.id}" out of sync: clip "${hit.id}" (${formatVideoSeconds(hit.start)}\u2013${formatVideoSeconds(videoClipEnd(hit))}) is in that stretch. Extract the range across "${t.id}" too (extract_range with its id in "tracks"), turn its sync lock off (update_track with syncLocked false) or lock it, or lift instead of rippling.`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
for (const t of tracks) this.shift(t, to, from - to);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Empties `[from, to)` on one track, leaving a gap: clips inside go, clips across an edge are
|
|
844
|
+
* trimmed back to it, and a clip across both is cut in two around the gap.
|
|
845
|
+
*/
|
|
846
|
+
clear(track, from, to, report) {
|
|
847
|
+
for (const c of [...track.clips]) {
|
|
848
|
+
const start = c.start;
|
|
849
|
+
const end = videoClipEnd(c);
|
|
850
|
+
if (end <= from + VIDEO_TIME_EPSILON || start >= to - VIDEO_TIME_EPSILON) continue;
|
|
851
|
+
const coversHead = start >= from - VIDEO_TIME_EPSILON;
|
|
852
|
+
const coversTail = end <= to + VIDEO_TIME_EPSILON;
|
|
853
|
+
if (coversHead && coversTail) {
|
|
854
|
+
track.clips = track.clips.filter((x) => x !== c);
|
|
855
|
+
report.removed.push(c.id);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (!coversHead && !coversTail) report.created.push(this.cut(track, c, to).id);
|
|
859
|
+
else if (coversHead) {
|
|
860
|
+
this.trimHead(c, to - start);
|
|
861
|
+
c.start = to;
|
|
862
|
+
c.duration = end - to;
|
|
863
|
+
delete c.transition;
|
|
864
|
+
report.trimmed.push(c.id);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
c.duration = from - start;
|
|
868
|
+
if (!report.trimmed.includes(c.id)) report.trimmed.push(c.id);
|
|
869
|
+
}
|
|
870
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
871
|
+
}
|
|
872
|
+
/** Clamps a slip, roll or slide to `[lo, hi]`, refusing when it cannot move that way at all. */
|
|
873
|
+
clamp(delta, lo, hi, why) {
|
|
874
|
+
const applied = Math.min(Math.max(delta, Math.min(0, lo)), Math.max(0, hi));
|
|
875
|
+
if (Math.abs(delta) > VIDEO_TIME_EPSILON && Math.abs(applied) <= VIDEO_TIME_EPSILON) refuse(why());
|
|
876
|
+
return Math.abs(applied) <= VIDEO_TIME_EPSILON ? 0 : applied;
|
|
877
|
+
}
|
|
878
|
+
/** Places a clip on a track by `mode`, returning what it cut, trimmed, removed and rippled. */
|
|
879
|
+
place(track, clip, mode, outcome) {
|
|
880
|
+
if (mode === "insert") {
|
|
881
|
+
const tracks = this.rippleTracks([track]);
|
|
882
|
+
outcome.created = this.open(tracks, clip.start, clip.duration);
|
|
883
|
+
outcome.rippled = tracks.map((t) => t.id);
|
|
884
|
+
} else if (mode === "overwrite") {
|
|
885
|
+
const report = { removed: [], trimmed: [], created: [] };
|
|
886
|
+
this.clear(track, clip.start, videoClipEnd(clip), report);
|
|
887
|
+
Object.assign(outcome, report);
|
|
888
|
+
} else this.fits(track, clip.start, clip.duration, /* @__PURE__ */ new Set([clip.id]), `Clip "${clip.id}"`);
|
|
889
|
+
track.clips.push(clip);
|
|
890
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
891
|
+
}
|
|
892
|
+
placement(mode, ripple) {
|
|
893
|
+
if (mode !== void 0 && !VIDEO_PLACEMENT_MODES.includes(mode)) refuse(`"mode" must be ${VIDEO_PLACEMENT_MODES.map((m) => `"${m}"`).join(", ")}.`);
|
|
894
|
+
if (ripple && mode !== void 0 && mode !== "insert") refuse(`"ripple" means mode "insert", so it cannot go with mode "${mode}"; pass one or the other.`);
|
|
895
|
+
return _nullishCoalesce(mode, () => ( (ripple ? "insert" : "refuse")));
|
|
473
896
|
}
|
|
474
897
|
run(e) {
|
|
898
|
+
for (const k of _nullishCoalesce(TIME_FIELDS[e.op], () => ( []))) {
|
|
899
|
+
const v = e[k];
|
|
900
|
+
if (typeof v !== "number" || !Number.isFinite(v)) refuse(`"${k}" must be a number of milliseconds.`);
|
|
901
|
+
if (k !== "delta" && v < 0) refuse(`"${k}" must be a time of at least 0.`);
|
|
902
|
+
}
|
|
475
903
|
switch (e.op) {
|
|
476
904
|
case "set_settings": {
|
|
477
905
|
const { op: _op, ...settings } = e;
|
|
@@ -517,7 +945,7 @@ var Editor = (_class = class {
|
|
|
517
945
|
}
|
|
518
946
|
case "update_track": {
|
|
519
947
|
const track = this.track(e.track);
|
|
520
|
-
for (const k of Object.keys(_nullishCoalesce(e.set, () => ( {})))) if (!
|
|
948
|
+
for (const k of Object.keys(_nullishCoalesce(e.set, () => ( {})))) if (!TRACK_FIELD_KEYS.includes(k)) refuse(`"${k}" cannot be set on a track; use ${TRACK_FIELD_KEYS.join(", ")}.`);
|
|
521
949
|
Object.assign(track, e.set);
|
|
522
950
|
for (const k of _nullishCoalesce(e.remove, () => ( []))) delete track[k];
|
|
523
951
|
return this.outcomes.push({ op: e.op, track: track.id });
|
|
@@ -538,6 +966,7 @@ var Editor = (_class = class {
|
|
|
538
966
|
case "add_clip": {
|
|
539
967
|
const track = this.track(e.track);
|
|
540
968
|
this.unlocked(track);
|
|
969
|
+
const mode = this.placement(e.mode, e.ripple);
|
|
541
970
|
const draft = { ...e.clip, type: _nullishCoalesce(e.clip.type, () => ( "media")) };
|
|
542
971
|
this.kindFits(track, draft);
|
|
543
972
|
const ids = this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
|
|
@@ -555,14 +984,9 @@ var Editor = (_class = class {
|
|
|
555
984
|
const end = sortedVideoClips(track).reduce((m, c) => Math.max(m, videoClipEnd(c)), 0);
|
|
556
985
|
draft.start ??= end;
|
|
557
986
|
const clip = parseVideoClip(draft, "clip");
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
this.shift(track, clip.start, clip.duration);
|
|
562
|
-
} else this.fits(track, clip.start, clip.duration, /* @__PURE__ */ new Set(), `Clip "${clip.id}"`);
|
|
563
|
-
track.clips.push(clip);
|
|
564
|
-
track.clips.sort((a, b) => a.start - b.start);
|
|
565
|
-
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id });
|
|
987
|
+
const outcome = { op: e.op, clip: clip.id, track: track.id };
|
|
988
|
+
this.place(track, clip, mode, outcome);
|
|
989
|
+
return this.outcomes.push(outcome);
|
|
566
990
|
}
|
|
567
991
|
case "move_clip": {
|
|
568
992
|
const from = this.clip(e.clip);
|
|
@@ -570,15 +994,14 @@ var Editor = (_class = class {
|
|
|
570
994
|
const to = e.track !== void 0 ? this.track(e.track) : from.track;
|
|
571
995
|
this.unlocked(to);
|
|
572
996
|
if (to !== from.track) this.kindFits(to, from.clip);
|
|
997
|
+
const mode = this.placement(e.mode);
|
|
573
998
|
const start = _nullishCoalesce(e.start, () => ( from.clip.start));
|
|
574
|
-
|
|
999
|
+
if (start < 0) refuse("A clip cannot start before 0.");
|
|
1000
|
+
from.track.clips.splice(from.index, 1);
|
|
575
1001
|
from.clip.start = start;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
to.clips.sort((a, b) => a.start - b.start);
|
|
581
|
-
return this.outcomes.push({ op: e.op, clip: from.clip.id, track: to.id });
|
|
1002
|
+
const outcome = { op: e.op, clip: from.clip.id, track: to.id };
|
|
1003
|
+
this.place(to, from.clip, mode, outcome);
|
|
1004
|
+
return this.outcomes.push(outcome);
|
|
582
1005
|
}
|
|
583
1006
|
case "trim_clip": {
|
|
584
1007
|
const { track, clip } = this.clip(e.clip);
|
|
@@ -596,9 +1019,19 @@ var Editor = (_class = class {
|
|
|
596
1019
|
clip.in = Math.max(0, sourceIn);
|
|
597
1020
|
if (clip.in === 0) delete clip.in;
|
|
598
1021
|
}
|
|
1022
|
+
retimeClipKeys(clip, (v) => retimeVideoAnimatable(v, -delta));
|
|
599
1023
|
if (e.ripple) {
|
|
600
|
-
|
|
601
|
-
|
|
1024
|
+
const tracks = this.rippleTracks([track]);
|
|
1025
|
+
if (delta > 0) {
|
|
1026
|
+
clip.start += delta;
|
|
1027
|
+
clip.duration -= delta;
|
|
1028
|
+
this.close(tracks, clip.start - delta, clip.start);
|
|
1029
|
+
} else if (delta < 0) {
|
|
1030
|
+
const created = this.open(tracks, clip.start, -delta, /* @__PURE__ */ new Set([clip.id]));
|
|
1031
|
+
clip.duration -= delta;
|
|
1032
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, rippled: tracks.map((t) => t.id), ...created.length ? { created } : {} });
|
|
1033
|
+
}
|
|
1034
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, rippled: tracks.map((t) => t.id) });
|
|
602
1035
|
} else {
|
|
603
1036
|
if (e.to < 0) refuse("A clip cannot start before 0.");
|
|
604
1037
|
if (delta < 0) this.fits(track, e.to, -delta, /* @__PURE__ */ new Set([clip.id]), `Extending "${clip.id}"`);
|
|
@@ -611,11 +1044,22 @@ var Editor = (_class = class {
|
|
|
611
1044
|
if (clip.type === "media") {
|
|
612
1045
|
const asset = assets.get(clip.asset);
|
|
613
1046
|
const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
|
|
614
|
-
if (_optionalChain([asset, 'optionalAccess',
|
|
1047
|
+
if (_optionalChain([asset, 'optionalAccess', _12 => _12.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
615
1048
|
refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
|
|
616
1049
|
}
|
|
617
|
-
if (e.ripple)
|
|
618
|
-
|
|
1050
|
+
if (e.ripple) {
|
|
1051
|
+
const tracks = this.rippleTracks([track]);
|
|
1052
|
+
let created = [];
|
|
1053
|
+
if (e.to < end) {
|
|
1054
|
+
clip.duration = duration;
|
|
1055
|
+
this.close(tracks, e.to, end);
|
|
1056
|
+
} else if (e.to > end) {
|
|
1057
|
+
created = this.open(tracks, end, e.to - end, /* @__PURE__ */ new Set([clip.id]));
|
|
1058
|
+
clip.duration = duration;
|
|
1059
|
+
}
|
|
1060
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, rippled: tracks.map((t) => t.id), ...created.length ? { created } : {} });
|
|
1061
|
+
}
|
|
1062
|
+
if (e.to > end) this.fits(track, end, e.to - end, /* @__PURE__ */ new Set([clip.id]), `Extending "${clip.id}"`);
|
|
619
1063
|
clip.duration = duration;
|
|
620
1064
|
}
|
|
621
1065
|
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id });
|
|
@@ -626,31 +1070,25 @@ var Editor = (_class = class {
|
|
|
626
1070
|
const end = videoClipEnd(clip);
|
|
627
1071
|
const minimum = 1e3 / this.p.fps;
|
|
628
1072
|
if (e.at < clip.start + minimum - VIDEO_TIME_EPSILON || e.at > end - minimum + VIDEO_TIME_EPSILON) refuse(`${formatVideoSeconds(e.at)} is not inside "${clip.id}" (${formatVideoSeconds(clip.start)}\u2013${formatVideoSeconds(end)}), at least a frame from either end.`);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const left = e.at - clip.start;
|
|
633
|
-
const right = { ...structuredClone(clip), id, start: e.at, duration: end - e.at };
|
|
634
|
-
delete right.transition;
|
|
635
|
-
delete right.fadeIn;
|
|
636
|
-
if (clip.type === "media") {
|
|
637
|
-
right.in = (_nullishCoalesce(clip.in, () => ( 0))) + left * videoClipSpeed(clip);
|
|
638
|
-
}
|
|
639
|
-
clip.duration = left;
|
|
640
|
-
delete clip.fadeOut;
|
|
641
|
-
track.clips.push(right);
|
|
642
|
-
track.clips.sort((a, b) => a.start - b.start);
|
|
643
|
-
return this.outcomes.push({ op: e.op, clips: [clip.id, id], clip: id, track: track.id });
|
|
1073
|
+
if (e.id !== void 0 && this.ids().includes(e.id)) refuse(`There is already a clip "${e.id}".`);
|
|
1074
|
+
const right = this.cut(track, clip, e.at, e.id);
|
|
1075
|
+
return this.outcomes.push({ op: e.op, clips: [clip.id, right.id], clip: right.id, track: track.id });
|
|
644
1076
|
}
|
|
645
1077
|
case "delete_clips": {
|
|
646
1078
|
const found = [...new Set(e.clips)].map((id) => this.clip(id));
|
|
647
1079
|
for (const f of found) this.unlocked(f.track);
|
|
648
|
-
for (const track of new Set(found.map((f) => f.track)))
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1080
|
+
for (const track of new Set(found.map((f) => f.track))) track.clips = track.clips.filter((c) => !found.some((f) => f.clip === c));
|
|
1081
|
+
if (!e.ripple) return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id) });
|
|
1082
|
+
const rippled = /* @__PURE__ */ new Set();
|
|
1083
|
+
const groups = [...new Set(found.map((f) => f.track))].filter((t) => !videoTrackSyncLocked(t)).map((t) => [t]);
|
|
1084
|
+
if (found.some((f) => videoTrackSyncLocked(f.track))) groups.push(found.filter((f) => videoTrackSyncLocked(f.track)).map((f) => f.track));
|
|
1085
|
+
for (const sources of groups) {
|
|
1086
|
+
const tracks = this.rippleTracks(sources);
|
|
1087
|
+
tracks.forEach((t) => rippled.add(t));
|
|
1088
|
+
const spans = mergeSpans(found.filter((f) => sources.includes(f.track)).map((f) => [f.clip.start, videoClipEnd(f.clip)]));
|
|
1089
|
+
for (const [from, to] of spans.reverse()) this.close(tracks, from, to);
|
|
652
1090
|
}
|
|
653
|
-
return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id) });
|
|
1091
|
+
return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id), rippled: this.p.tracks.filter((t) => rippled.has(t)).map((t) => t.id) });
|
|
654
1092
|
}
|
|
655
1093
|
case "update_clip": {
|
|
656
1094
|
const at = this.clip(e.clip);
|
|
@@ -659,12 +1097,17 @@ var Editor = (_class = class {
|
|
|
659
1097
|
for (const k of ["id", "type"]) if (k in set) refuse(`A clip's ${k} cannot be changed; delete it and add a new one.`);
|
|
660
1098
|
for (const k of _nullishCoalesce(e.remove, () => ( []))) if (!OPTIONAL_CLIP_FIELDS.includes(k)) refuse(`"${k}" cannot be removed; removable fields are ${OPTIONAL_CLIP_FIELDS.join(", ")}.`);
|
|
661
1099
|
if ("speed" in set && (typeof set.speed !== "number" || !(set.speed > 0))) refuse('"speed" must be a number greater than 0 (1 = normal speed).');
|
|
1100
|
+
let stretch = 1;
|
|
662
1101
|
if (at.clip.type === "media" && !("duration" in set)) {
|
|
663
1102
|
const was = videoClipSpeed(at.clip);
|
|
664
|
-
const now = "speed" in set ? set.speed : _optionalChain([e, 'access',
|
|
665
|
-
if (now !== was)
|
|
1103
|
+
const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _13 => _13.remove, 'optionalAccess', _14 => _14.includes, 'call', _15 => _15("speed")]) ? 1 : was;
|
|
1104
|
+
if (now !== was) {
|
|
1105
|
+
set.duration = at.clip.duration * was / now;
|
|
1106
|
+
stretch = was / now;
|
|
1107
|
+
}
|
|
666
1108
|
}
|
|
667
|
-
const next = { ...at.clip, ...set };
|
|
1109
|
+
const next = { ...structuredClone(at.clip), ...set };
|
|
1110
|
+
if (stretch !== 1) retimeClipKeys(next, (v) => retimeVideoAnimatable(v, 0, stretch));
|
|
668
1111
|
for (const k of _nullishCoalesce(e.remove, () => ( []))) delete next[k];
|
|
669
1112
|
const clip = parseVideoClip(next, "set");
|
|
670
1113
|
if (clip.type === "media" && clip.asset !== at.clip.asset) this.kindFits(at.track, clip);
|
|
@@ -680,6 +1123,133 @@ var Editor = (_class = class {
|
|
|
680
1123
|
else clip.transition = parseVideoTransition(e.transition, "transition");
|
|
681
1124
|
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id });
|
|
682
1125
|
}
|
|
1126
|
+
case "slip_clip": {
|
|
1127
|
+
const { track, clip } = this.clip(e.clip);
|
|
1128
|
+
this.unlocked(track);
|
|
1129
|
+
if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to slip; only video and audio clips slip.`);
|
|
1130
|
+
const asset = this.asset(clip.asset);
|
|
1131
|
+
if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no source to slip; only video and audio clips slip.`);
|
|
1132
|
+
const handles = videoClipHandles(clip, videoAssetMap(this.p));
|
|
1133
|
+
const { lead, tail } = videoClipOverhang(track, clip);
|
|
1134
|
+
const delta = this.clamp(
|
|
1135
|
+
e.delta,
|
|
1136
|
+
lead - handles.before,
|
|
1137
|
+
handles.after - tail,
|
|
1138
|
+
() => e.delta > 0 ? `"${clip.id}" already plays to the end of "${asset.id}"${tail ? ` (less the ${tail} ms the transition after it needs)` : ""}, so it cannot slip later; slip it earlier (a negative delta).` : `"${clip.id}" already starts at the beginning of "${asset.id}"${lead ? ` (plus the ${lead} ms its transition needs)` : ""}, so it cannot slip earlier; slip it later (a positive delta).`
|
|
1139
|
+
);
|
|
1140
|
+
this.moveIn(clip, delta);
|
|
1141
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, delta });
|
|
1142
|
+
}
|
|
1143
|
+
case "roll_edit": {
|
|
1144
|
+
const left = this.clip(e.left);
|
|
1145
|
+
const right = this.clip(e.right);
|
|
1146
|
+
if (left.track !== right.track) refuse(`"${e.left}" is on track "${left.track.id}" and "${e.right}" on "${right.track.id}"; a roll moves the cut between two clips on one track.`);
|
|
1147
|
+
const track = left.track;
|
|
1148
|
+
this.unlocked(track);
|
|
1149
|
+
const L = left.clip;
|
|
1150
|
+
const R = right.clip;
|
|
1151
|
+
if (Math.abs(videoClipEnd(L) - R.start) > VIDEO_TIME_EPSILON) {
|
|
1152
|
+
if (Math.abs(videoClipEnd(R) - L.start) <= VIDEO_TIME_EPSILON) refuse(`"${R.id}" comes before "${L.id}"; pass left "${R.id}" and right "${L.id}".`);
|
|
1153
|
+
refuse(`"${L.id}" ends at ${formatVideoSeconds(videoClipEnd(L))} but "${R.id}" starts at ${formatVideoSeconds(R.start)}; a roll needs two clips that meet at a cut (use trim_clip to move one edge).`);
|
|
1154
|
+
}
|
|
1155
|
+
const assets = videoAssetMap(this.p);
|
|
1156
|
+
const lo = Math.max(-(L.duration - this.frame), -(videoClipHandles(R, assets).before - videoClipOverhang(track, R).lead));
|
|
1157
|
+
const hi = Math.min(R.duration - this.frame, videoClipHandles(L, assets).after - videoClipOverhang(track, L).tail);
|
|
1158
|
+
const delta = this.clamp(
|
|
1159
|
+
e.delta,
|
|
1160
|
+
lo,
|
|
1161
|
+
hi,
|
|
1162
|
+
() => e.delta > 0 ? `The cut cannot move later: ${R.duration - this.frame < 1 ? `"${R.id}" is already a frame long` : `"${L.id}" has no media after its out point`}. Roll it earlier (a negative delta).` : `The cut cannot move earlier: ${L.duration - this.frame < 1 ? `"${L.id}" is already a frame long` : `"${R.id}" has no media before its in point`}. Roll it later (a positive delta).`
|
|
1163
|
+
);
|
|
1164
|
+
L.duration += delta;
|
|
1165
|
+
R.start += delta;
|
|
1166
|
+
R.duration -= delta;
|
|
1167
|
+
this.trimHead(R, delta);
|
|
1168
|
+
return this.outcomes.push({ op: e.op, clip: R.id, clips: [L.id, R.id], track: track.id, delta });
|
|
1169
|
+
}
|
|
1170
|
+
case "slide_clip": {
|
|
1171
|
+
const { track, clip } = this.clip(e.clip);
|
|
1172
|
+
this.unlocked(track);
|
|
1173
|
+
const before = videoClipBefore(track, clip);
|
|
1174
|
+
const after = videoClipAfter(track, clip);
|
|
1175
|
+
if (!before && !after) refuse(`"${clip.id}" has no clip right before or after it on track "${track.id}", so there is nothing to slide against; use move_clip.`);
|
|
1176
|
+
const assets = videoAssetMap(this.p);
|
|
1177
|
+
const end = videoClipEnd(clip);
|
|
1178
|
+
const others = track.clips.filter((c) => c !== clip);
|
|
1179
|
+
const room = {
|
|
1180
|
+
earlier: before ? before.duration - this.frame : clip.start - others.filter((c) => videoClipEnd(c) <= clip.start + VIDEO_TIME_EPSILON).reduce((m, c) => Math.max(m, videoClipEnd(c)), 0),
|
|
1181
|
+
later: after ? after.duration - this.frame : others.filter((c) => c.start >= end - VIDEO_TIME_EPSILON).reduce((m, c) => Math.min(m, c.start - end), Infinity)
|
|
1182
|
+
};
|
|
1183
|
+
const lo = -Math.min(room.earlier, after ? videoClipHandles(after, assets).before - videoClipOverhang(track, after).lead : Infinity);
|
|
1184
|
+
const hi = Math.min(room.later, before ? videoClipHandles(before, assets).after - videoClipOverhang(track, before).tail : Infinity);
|
|
1185
|
+
const delta = this.clamp(
|
|
1186
|
+
e.delta,
|
|
1187
|
+
lo,
|
|
1188
|
+
hi,
|
|
1189
|
+
() => e.delta > 0 ? `"${clip.id}" cannot slide later: ${room.later < 1 ? after ? `"${after.id}" is already a frame long` : "the next clip is right after it" : `"${before.id}" has no media after its out point to fill in behind it`}. Slide it earlier (a negative delta).` : `"${clip.id}" cannot slide earlier: ${room.earlier < 1 ? before ? `"${before.id}" is already a frame long` : "it is against the clip before it or the start" : `"${after.id}" has no media before its in point to fill in behind it`}. Slide it later (a positive delta).`
|
|
1190
|
+
);
|
|
1191
|
+
clip.start += delta;
|
|
1192
|
+
if (before) before.duration += delta;
|
|
1193
|
+
if (after) {
|
|
1194
|
+
after.start += delta;
|
|
1195
|
+
after.duration -= delta;
|
|
1196
|
+
this.trimHead(after, delta);
|
|
1197
|
+
}
|
|
1198
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
1199
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _16 => _16.id]), clip.id, _optionalChain([after, 'optionalAccess', _17 => _17.id])].filter((x) => !!x), track: track.id, delta });
|
|
1200
|
+
}
|
|
1201
|
+
case "lift_range":
|
|
1202
|
+
case "extract_range": {
|
|
1203
|
+
this.range(e.from, e.to);
|
|
1204
|
+
const tracks = this.rangeTracks(e.tracks);
|
|
1205
|
+
const report = { removed: [], trimmed: [], created: [] };
|
|
1206
|
+
for (const t of tracks) this.clear(t, e.from, e.to, report);
|
|
1207
|
+
const outcome = { op: e.op, ...report };
|
|
1208
|
+
if (e.op === "extract_range") {
|
|
1209
|
+
const rippled = this.rippleTracks(tracks);
|
|
1210
|
+
this.close(rippled, e.from, e.to);
|
|
1211
|
+
outcome.rippled = rippled.map((t) => t.id);
|
|
1212
|
+
}
|
|
1213
|
+
return this.outcomes.push(outcome);
|
|
1214
|
+
}
|
|
1215
|
+
case "split_at": {
|
|
1216
|
+
const tracks = this.rangeTracks(e.tracks);
|
|
1217
|
+
const lefts = tracks.flatMap((t) => t.clips.filter((c) => c.start < e.at - VIDEO_TIME_EPSILON && videoClipEnd(c) > e.at + VIDEO_TIME_EPSILON).map((c) => c.id));
|
|
1218
|
+
if (!lefts.length) refuse(`No clip runs across ${formatVideoSeconds(e.at)} on ${e.tracks ? `track${tracks.length > 1 ? "s" : ""} ${tracks.map((t) => `"${t.id}"`).join(", ")}` : "the unlocked tracks"}, so there is nothing to split there.`);
|
|
1219
|
+
const created = this.cutAcross(tracks, e.at).map((c) => c.id);
|
|
1220
|
+
return this.outcomes.push({ op: e.op, clips: lefts, created });
|
|
1221
|
+
}
|
|
1222
|
+
case "set_keyframes": {
|
|
1223
|
+
const { track, clip } = this.clip(e.clip);
|
|
1224
|
+
this.unlocked(track);
|
|
1225
|
+
if (!VIDEO_KEYFRAME_PROPERTIES.includes(e.property)) refuse(`"${e.property}" cannot be keyframed; use one of ${VIDEO_KEYFRAME_PROPERTIES.join(", ")}.`);
|
|
1226
|
+
if (e.property === "volume" && clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type} clip and has no volume; volume keys go on media clips.`);
|
|
1227
|
+
const limits = VIDEO_KEYFRAME_LIMITS[e.property];
|
|
1228
|
+
const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
|
|
1229
|
+
const c = clip;
|
|
1230
|
+
const current = transform ? _optionalChain([c, 'access', _18 => _18.transform, 'optionalAccess', _19 => _19[e.property]]) : c[e.property];
|
|
1231
|
+
let value;
|
|
1232
|
+
if (e.keys === null) {
|
|
1233
|
+
const v = videoValueAt(current, 0, limits.default);
|
|
1234
|
+
value = isVideoKeyframes(current) ? v === limits.default ? void 0 : v : current;
|
|
1235
|
+
} else {
|
|
1236
|
+
if (!Array.isArray(e.keys) || !e.keys.length) refuse('"keys" must list at least one key { t, v }; pass null to remove the keyframes.');
|
|
1237
|
+
let sorted = [];
|
|
1238
|
+
try {
|
|
1239
|
+
sorted = sortVideoKeys(e.keys);
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
refuse(error.message);
|
|
1242
|
+
}
|
|
1243
|
+
value = parseVideoAnimatable({ keys: sorted }, e.property, limits);
|
|
1244
|
+
}
|
|
1245
|
+
if (transform) {
|
|
1246
|
+
c.transform = { ...c.transform, [e.property]: value };
|
|
1247
|
+
if (value === void 0) delete c.transform[e.property];
|
|
1248
|
+
if (!Object.keys(c.transform).length) delete c.transform;
|
|
1249
|
+
} else if (value === void 0) delete c[e.property];
|
|
1250
|
+
else c[e.property] = value;
|
|
1251
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id });
|
|
1252
|
+
}
|
|
683
1253
|
case "close_gaps": {
|
|
684
1254
|
const track = this.track(e.track);
|
|
685
1255
|
this.unlocked(track);
|
|
@@ -739,7 +1309,7 @@ function applyVideoEdits(project, edits) {
|
|
|
739
1309
|
editor.run(edit);
|
|
740
1310
|
} catch (error) {
|
|
741
1311
|
const message = error.message;
|
|
742
|
-
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1312
|
+
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _20 => _20.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
|
|
743
1313
|
}
|
|
744
1314
|
}
|
|
745
1315
|
let next;
|
|
@@ -757,7 +1327,10 @@ function applyVideoEdits(project, edits) {
|
|
|
757
1327
|
}
|
|
758
1328
|
|
|
759
1329
|
// src/video-editor/evaluate.ts
|
|
760
|
-
function
|
|
1330
|
+
function videoTransformAt(transform, clipTime = 0) {
|
|
1331
|
+
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _21 => _21.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _22 => _22.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _23 => _23.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _24 => _24.rotation]), clipTime, 0) };
|
|
1332
|
+
}
|
|
1333
|
+
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
761
1334
|
let w = size.width;
|
|
762
1335
|
let h = size.height;
|
|
763
1336
|
if (fit === "fill") {
|
|
@@ -768,18 +1341,18 @@ function placeVideoRect(frame, size, fit = "contain", transform = {}) {
|
|
|
768
1341
|
w *= k;
|
|
769
1342
|
h *= k;
|
|
770
1343
|
}
|
|
771
|
-
const
|
|
772
|
-
w *= scale;
|
|
773
|
-
h *= scale;
|
|
774
|
-
const cx = frame.width / 2 +
|
|
775
|
-
const cy = frame.height / 2 +
|
|
1344
|
+
const t = videoTransformAt(transform, clipTime);
|
|
1345
|
+
w *= t.scale;
|
|
1346
|
+
h *= t.scale;
|
|
1347
|
+
const cx = frame.width / 2 + t.x;
|
|
1348
|
+
const cy = frame.height / 2 + t.y;
|
|
776
1349
|
return { x: cx - w / 2, y: cy - h / 2, width: w, height: h };
|
|
777
1350
|
}
|
|
778
1351
|
function videoTextStyle(clip) {
|
|
779
1352
|
return { text: clip.text, color: _nullishCoalesce(clip.color, () => ( "#ffffff")), fontSize: _nullishCoalesce(clip.fontSize, () => ( 64)), fontFamily: _nullishCoalesce(clip.fontFamily, () => ( "sans-serif")), fontWeight: _nullishCoalesce(clip.fontWeight, () => ( 700)), ...clip.background ? { background: clip.background } : {} };
|
|
780
1353
|
}
|
|
781
|
-
function videoTitleFrame(project, clip) {
|
|
782
|
-
const layer = { clip: clip.id, track: "", type: "text", opacity: 1, rect: placeVideoRect(project, project, "fill", clip.transform), rotation:
|
|
1354
|
+
function videoTitleFrame(project, clip, clipTime = 0) {
|
|
1355
|
+
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) };
|
|
783
1356
|
return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
|
|
784
1357
|
}
|
|
785
1358
|
var clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
@@ -795,7 +1368,7 @@ function activeOnTrack(track, time) {
|
|
|
795
1368
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
796
1369
|
const lead = before ? clip.transition.duration / 2 : 0;
|
|
797
1370
|
const next = videoClipAfter(track, clip);
|
|
798
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
1371
|
+
const tail = _optionalChain([next, 'optionalAccess', _25 => _25.transition]) ? next.transition.duration / 2 : 0;
|
|
799
1372
|
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
800
1373
|
const a = { clip };
|
|
801
1374
|
if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
@@ -812,21 +1385,22 @@ function videoFrameAt(project, time) {
|
|
|
812
1385
|
for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
|
|
813
1386
|
const track = project.tracks[ti];
|
|
814
1387
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
815
|
-
if (_optionalChain([incoming, 'optionalAccess',
|
|
816
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1388
|
+
if (_optionalChain([incoming, 'optionalAccess', _26 => _26.type]) === "dip" && incoming.progress < 0.5) continue;
|
|
1389
|
+
if (_optionalChain([outgoing, 'optionalAccess', _27 => _27.type]) === "dip" && outgoing.progress >= 0.5) continue;
|
|
817
1390
|
const fade = fadeGain(clip, Math.min(Math.max(time, clip.start), videoClipEnd(clip)));
|
|
818
1391
|
const level = (t, into) => t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress;
|
|
819
1392
|
const mix = (incoming ? level(incoming, true) : 1) * (outgoing ? level(outgoing, false) : 1);
|
|
820
1393
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
821
1394
|
const sourceTime = sourceTimeAt(clip, time);
|
|
1395
|
+
const clipTime = time - clip.start;
|
|
822
1396
|
const audible = asset && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio !== false);
|
|
823
1397
|
if (audible && clip.type === "media" && !clip.muted && !track.muted) {
|
|
824
|
-
const gain = (
|
|
1398
|
+
const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fade * mix;
|
|
825
1399
|
audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeed(clip) });
|
|
826
1400
|
}
|
|
827
1401
|
if (track.kind !== "video" || track.hidden) continue;
|
|
828
1402
|
if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
|
|
829
|
-
let opacity = (
|
|
1403
|
+
let opacity = videoValueAt(clip.opacity, clipTime, 1) * fade;
|
|
830
1404
|
let wipe;
|
|
831
1405
|
let dip = null;
|
|
832
1406
|
if (incoming) {
|
|
@@ -834,11 +1408,11 @@ function videoFrameAt(project, time) {
|
|
|
834
1408
|
else if (incoming.type === "wipe") wipe = { direction: _nullishCoalesce(incoming.transition.direction, () => ( "right")), progress: incoming.progress };
|
|
835
1409
|
else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
|
|
836
1410
|
}
|
|
837
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1411
|
+
if (_optionalChain([outgoing, 'optionalAccess', _28 => _28.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
838
1412
|
const size = clip.type === "media" ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
|
|
839
1413
|
const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill")));
|
|
840
|
-
const rect = clip.type === "text" ?
|
|
841
|
-
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation:
|
|
1414
|
+
const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, clip.transform, clipTime);
|
|
1415
|
+
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([clip, 'access', _29 => _29.transform, 'optionalAccess', _30 => _30.rotation]), clipTime, 0) };
|
|
842
1416
|
if (wipe) layer.wipe = wipe;
|
|
843
1417
|
if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
|
|
844
1418
|
if (clip.type === "text") layer.text = videoTextStyle(clip);
|
|
@@ -983,7 +1557,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
|
983
1557
|
}
|
|
984
1558
|
|
|
985
1559
|
// src/video-editor/export.ts
|
|
986
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
1560
|
+
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _31 => _31.name]), () => ( _optionalChain([asset, 'optionalAccess', _32 => _32.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
987
1561
|
function toVideoEdl(project, options = {}) {
|
|
988
1562
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
989
1563
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
@@ -1016,7 +1590,7 @@ function toVideoEdl(project, options = {}) {
|
|
|
1016
1590
|
lines.push(row(n2, reel, `${t.type === "wipe" ? "W001" : "D"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1017
1591
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1018
1592
|
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
1019
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
1593
|
+
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _33 => _33.name]), () => ( _optionalChain([asset, 'optionalAccess', _34 => _34.src]))), () => ( clip.asset))}`);
|
|
1020
1594
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
1021
1595
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
1022
1596
|
lines.push("");
|
|
@@ -1060,7 +1634,13 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1060
1634
|
const audio = [];
|
|
1061
1635
|
let cur = "base";
|
|
1062
1636
|
let usesDrawtext = false;
|
|
1637
|
+
const animated = /* @__PURE__ */ new Set();
|
|
1063
1638
|
let k = 0;
|
|
1639
|
+
const step = 2e3 / fps;
|
|
1640
|
+
const expr = (prop, clipTime, fallback, what) => {
|
|
1641
|
+
if (isVideoKeyframes(prop)) animated.add(what);
|
|
1642
|
+
return videoKeyframeFfmpegExpr(prop, clipTime, fallback, step);
|
|
1643
|
+
};
|
|
1064
1644
|
const input = (clip, asset, lead, tail) => {
|
|
1065
1645
|
const speed = videoClipSpeed(clip);
|
|
1066
1646
|
const src = options.resolve ? options.resolve(asset) : asset.src;
|
|
@@ -1085,13 +1665,14 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1085
1665
|
const audible = asset && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio !== false);
|
|
1086
1666
|
if (audible && clip.type === "media" && !clip.muted && !track.muted) {
|
|
1087
1667
|
const ai = _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
|
|
1088
|
-
const
|
|
1089
|
-
const
|
|
1668
|
+
const trackGain = _nullishCoalesce(track.volume, () => ( 1));
|
|
1669
|
+
const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
|
|
1670
|
+
const chain2 = ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip)), volume];
|
|
1090
1671
|
const before2 = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1091
1672
|
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}`);
|
|
1092
1673
|
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}`);
|
|
1093
1674
|
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}`);
|
|
1094
|
-
const after = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && _optionalChain([c, 'access',
|
|
1675
|
+
const after = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && _optionalChain([c, 'access', _35 => _35.transition, 'optionalAccess', _36 => _36.type]) === "dip");
|
|
1095
1676
|
if (after) chain2.push(`afade=t=out:st=${sec(span - after.transition.duration / 2)}:d=${sec(after.transition.duration / 2)}`);
|
|
1096
1677
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}`);
|
|
1097
1678
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}`);
|
|
@@ -1102,33 +1683,43 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1102
1683
|
if (track.kind !== "video" || track.hidden) continue;
|
|
1103
1684
|
const enable = `enable='between(t,${sec(t0)},${sec(t0 + span)})'`;
|
|
1104
1685
|
const next = `v${k++}`;
|
|
1686
|
+
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
1687
|
+
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
1688
|
+
const tf = clip.transform;
|
|
1689
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _37 => _37[p]])));
|
|
1105
1690
|
if (clip.type === "text" && options.titleImage) {
|
|
1106
|
-
inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", options.titleImage(clip, { width: project.width, height: project.height })]);
|
|
1691
|
+
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 })]);
|
|
1107
1692
|
index = inputs.length - 1;
|
|
1108
1693
|
} else if (clip.type === "text") {
|
|
1109
1694
|
usesDrawtext = true;
|
|
1110
|
-
const op = _nullishCoalesce(clip.opacity, () => ( 1));
|
|
1111
|
-
const alpha = [
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1695
|
+
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
1696
|
+
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("*");
|
|
1697
|
+
const coord = (axis) => {
|
|
1698
|
+
const v = _optionalChain([tf, 'optionalAccess', _38 => _38[axis]]);
|
|
1699
|
+
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
1700
|
+
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
1701
|
+
};
|
|
1702
|
+
const fontSize = _nullishCoalesce(clip.fontSize, () => ( 64));
|
|
1114
1703
|
const opts = [
|
|
1115
1704
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
1116
1705
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
1117
|
-
`fontsize
|
|
1706
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _39 => _39.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _40 => _40.scale]), () => ( 1))))}`,
|
|
1118
1707
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
1119
|
-
|
|
1120
|
-
|
|
1708
|
+
coord("x"),
|
|
1709
|
+
coord("y"),
|
|
1121
1710
|
`alpha='${alpha}'`,
|
|
1122
1711
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
1123
1712
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
1124
1713
|
];
|
|
1125
|
-
if (_optionalChain([
|
|
1714
|
+
if (_optionalChain([tf, 'optionalAccess', _41 => _41.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
1126
1715
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
1127
1716
|
cur = next;
|
|
1128
1717
|
continue;
|
|
1129
1718
|
}
|
|
1130
1719
|
const size = asset ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
|
|
1131
|
-
const
|
|
1720
|
+
const placed = clip.type === "text" && !moving ? void 0 : tf;
|
|
1721
|
+
const still = Object.fromEntries(Object.entries(_nullishCoalesce(placed, () => ( {}))).filter(([, v]) => !isVideoKeyframes(v)));
|
|
1722
|
+
const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill"))), still);
|
|
1132
1723
|
const chain = [];
|
|
1133
1724
|
let source;
|
|
1134
1725
|
if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
|
|
@@ -1136,8 +1727,13 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1136
1727
|
source = `[${index}:v]`;
|
|
1137
1728
|
chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
1138
1729
|
}
|
|
1730
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _42 => _42.scale]))) {
|
|
1731
|
+
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
1732
|
+
chain.push(`scale=w='max(2,trunc(${even(rect.width)}*(${s})/2)*2)':h='max(2,trunc(${even(rect.height)}*(${s})/2)*2)':eval=frame`);
|
|
1733
|
+
}
|
|
1139
1734
|
chain.push("format=yuva420p");
|
|
1140
|
-
if (clip.opacity
|
|
1735
|
+
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)'`);
|
|
1736
|
+
else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
|
|
1141
1737
|
if (clip.fadeIn) chain.push(`fade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}:alpha=1`);
|
|
1142
1738
|
if (clip.fadeOut) chain.push(`fade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}:alpha=1`);
|
|
1143
1739
|
if (lead && clip.transition.type === "crossfade") chain.push(`fade=t=in:st=0:d=${sec(2 * lead)}:alpha=1`);
|
|
@@ -1148,12 +1744,18 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1148
1744
|
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}))`;
|
|
1149
1745
|
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
|
|
1150
1746
|
}
|
|
1151
|
-
|
|
1747
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _43 => _43.rotation]);
|
|
1748
|
+
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=none:ow='hypot(iw,ih)':oh=ow`);
|
|
1749
|
+
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=none:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
1152
1750
|
chain.push(`setpts=PTS+${sec(t0)}/TB`);
|
|
1153
1751
|
const cx = rect.x + rect.width / 2;
|
|
1154
1752
|
const cy = rect.y + rect.height / 2;
|
|
1753
|
+
const at = (axis, c) => {
|
|
1754
|
+
const v = _optionalChain([placed, 'optionalAccess', _44 => _44[axis]]);
|
|
1755
|
+
return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2'`;
|
|
1756
|
+
};
|
|
1155
1757
|
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
|
|
1156
|
-
graph.push(`[${cur}][l${next}]overlay
|
|
1758
|
+
graph.push(`[${cur}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
|
|
1157
1759
|
cur = next;
|
|
1158
1760
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1159
1761
|
if (before && clip.transition.type === "dip") {
|
|
@@ -1167,8 +1769,10 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1167
1769
|
}
|
|
1168
1770
|
graph.push(`[${cur}]format=yuv420p[vout]`);
|
|
1169
1771
|
if (audio.length) graph.push(`${audio.join("")}amix=inputs=${audio.length}:normalize=0:duration=longest,apad,atrim=0:${sec(D)}[aout]`);
|
|
1170
|
-
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
1772
|
+
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _45 => _45.get, 'call', _46 => _46(c.asset), 'optionalAccess', _47 => _47.kind]) === "video" && _optionalChain([assets, 'access', _48 => _48.get, 'call', _49 => _49(c.asset), 'optionalAccess', _50 => _50.hasAudio]) === void 0)))
|
|
1171
1773
|
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.');
|
|
1774
|
+
if (animated.size)
|
|
1775
|
+
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)" : ""}.`);
|
|
1172
1776
|
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.");
|
|
1173
1777
|
const filterComplex = graph.join(";");
|
|
1174
1778
|
const args = [
|
|
@@ -1199,7 +1803,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1199
1803
|
this.source = source;
|
|
1200
1804
|
this.options = options;
|
|
1201
1805
|
this.canvas = canvas;
|
|
1202
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access',
|
|
1806
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _51 => _51.getContext, 'optionalCall', _52 => _52("2d")]), () => ( null));
|
|
1203
1807
|
this.unsubscribe = source.subscribe(() => this.update());
|
|
1204
1808
|
this.update();
|
|
1205
1809
|
}
|
|
@@ -1247,8 +1851,8 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1247
1851
|
entry.media.pause();
|
|
1248
1852
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
1249
1853
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
1250
|
-
_optionalChain([entry, 'access',
|
|
1251
|
-
_optionalChain([entry, 'access',
|
|
1854
|
+
_optionalChain([entry, 'access', _53 => _53.media, 'access', _54 => _54.removeAttribute, 'optionalCall', _55 => _55("src")]);
|
|
1855
|
+
_optionalChain([entry, 'access', _56 => _56.media, 'access', _57 => _57.load, 'optionalCall', _58 => _58()]);
|
|
1252
1856
|
this.media.delete(entry.clip);
|
|
1253
1857
|
}
|
|
1254
1858
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -1281,7 +1885,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1281
1885
|
for (const layer of frame.layers) {
|
|
1282
1886
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
1283
1887
|
const sound = sounding.get(layer.clip);
|
|
1284
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
1888
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _59 => _59.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
1285
1889
|
sounding.delete(layer.clip);
|
|
1286
1890
|
}
|
|
1287
1891
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -1314,7 +1918,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1314
1918
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
1315
1919
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
1316
1920
|
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution);
|
|
1317
|
-
_optionalChain([this, 'access',
|
|
1921
|
+
_optionalChain([this, 'access', _60 => _60.options, 'access', _61 => _61.onFrame, 'optionalCall', _62 => _62(frame)]);
|
|
1318
1922
|
}
|
|
1319
1923
|
/** Stops following the editor and releases every media element. */
|
|
1320
1924
|
destroy() {
|
|
@@ -1421,7 +2025,7 @@ var VideoEditorController = (_class3 = class {
|
|
|
1421
2025
|
this.applying = false;
|
|
1422
2026
|
}
|
|
1423
2027
|
this.notify();
|
|
1424
|
-
_optionalChain([this, 'access',
|
|
2028
|
+
_optionalChain([this, 'access', _63 => _63.onChange, 'optionalCall', _64 => _64(project)]);
|
|
1425
2029
|
}
|
|
1426
2030
|
commit(label, next) {
|
|
1427
2031
|
const before = this.project;
|
|
@@ -1485,7 +2089,7 @@ var VideoEditorController = (_class3 = class {
|
|
|
1485
2089
|
}
|
|
1486
2090
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
1487
2091
|
select(clips, options = {}) {
|
|
1488
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
2092
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _65 => _65.filter, 'call', _66 => _66((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
1489
2093
|
let next;
|
|
1490
2094
|
if (options.add) {
|
|
1491
2095
|
next = [...this.selection];
|
|
@@ -1581,7 +2185,17 @@ var CLIP_ID = { type: "string", minLength: 1, description: 'A clip id from get_v
|
|
|
1581
2185
|
var TRACK_ID = { type: "string", minLength: 1, description: 'A track id from get_video_project (e.g. "v1" or "a1").' };
|
|
1582
2186
|
var ASSET_ID = { type: "string", minLength: 1, description: "An asset id from get_video_project." };
|
|
1583
2187
|
var COLOR = (what) => ({ type: "string", minLength: 1, description: `${what}: a CSS colour such as "#ffcc00".` });
|
|
1584
|
-
var RIPPLE = {
|
|
2188
|
+
var RIPPLE = {
|
|
2189
|
+
type: "boolean",
|
|
2190
|
+
description: "Push (or pull) later material along by the change, keeping the cuts after it in sync (default false: refuse if it would overlap). When the track is sync-locked (the default), every other unlocked sync-locked track moves too: clips across the edit point are cut when time opens, and closing time is refused if another sync-locked track has a clip in that stretch."
|
|
2191
|
+
};
|
|
2192
|
+
var MODE = {
|
|
2193
|
+
type: "string",
|
|
2194
|
+
enum: [...VIDEO_PLACEMENT_MODES],
|
|
2195
|
+
description: 'What to do with clips already where it lands: "refuse" (default) fails with the next free place; "overwrite" cuts, trims or removes what is underneath on that track; "insert" cuts everything across the start and pushes later material along by its length on this track and every sync-locked track.'
|
|
2196
|
+
};
|
|
2197
|
+
var DELTA = (what) => ({ type: "number", description: `${what}, in milliseconds of timeline time: positive is later, negative earlier. It is clamped to what the media and neighbours allow; the result says how far it went.` });
|
|
2198
|
+
var RANGE_TRACKS = { type: "array", items: TRACK_ID, minItems: 1, uniqueItems: true, description: "Tracks to work on (default: every unlocked track). Locked tracks are refused." };
|
|
1585
2199
|
var TRANSFORM = object(
|
|
1586
2200
|
{
|
|
1587
2201
|
x: { type: "number", description: "Offset of the centre to the right of the frame's centre, in pixels (default 0)." },
|
|
@@ -1604,7 +2218,7 @@ var TRANSITION = object(
|
|
|
1604
2218
|
);
|
|
1605
2219
|
var CLIP_COMMON = {
|
|
1606
2220
|
name: { type: "string", description: "A label for the clip." },
|
|
1607
|
-
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1)." },
|
|
2221
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1). A number replaces any keyframes; set_video_keyframes animates it." },
|
|
1608
2222
|
transform: TRANSFORM,
|
|
1609
2223
|
fit: { type: "string", enum: [...VIDEO_FITS], description: "How the picture fits the frame before the transform, as CSS object-fit (default contain for media, fill for colours)." },
|
|
1610
2224
|
fadeIn: MS("Fade up from nothing (picture and sound) over this long"),
|
|
@@ -1613,7 +2227,7 @@ var CLIP_COMMON = {
|
|
|
1613
2227
|
var MEDIA_FIELDS = {
|
|
1614
2228
|
in: MS("Where in the source it starts (default 0)"),
|
|
1615
2229
|
speed: { type: "number", minimum: 0.05, maximum: 20, description: "Playback speed (default 1). Changing it keeps the stretch of source, so the clip gets shorter or longer." },
|
|
1616
|
-
volume: { type: "number", minimum: 0, maximum: 4, description: "Gain (1 = unchanged, default 1)." },
|
|
2230
|
+
volume: { type: "number", minimum: 0, maximum: 4, description: "Gain (1 = unchanged, default 1). A number replaces any keyframes; set_video_keyframes animates it." },
|
|
1617
2231
|
muted: { type: "boolean", description: "Silence this clip (default false)." }
|
|
1618
2232
|
};
|
|
1619
2233
|
var TEXT_FIELDS = {
|
|
@@ -1649,6 +2263,7 @@ var TRACK_FIELDS = {
|
|
|
1649
2263
|
muted: { type: "boolean", description: "Silence every clip on it." },
|
|
1650
2264
|
hidden: { type: "boolean", description: "Leave its pictures out of the frame." },
|
|
1651
2265
|
locked: { type: "boolean", description: "Refuse edits to its clips." },
|
|
2266
|
+
syncLocked: { type: "boolean", description: "Move with ripple edits on other sync-locked tracks so it stays in sync (default true). Turn off for a track that should stay put, e.g. a music bed." },
|
|
1652
2267
|
volume: { type: "number", minimum: 0, maximum: 4, description: "Gain for every clip on it (1 = unchanged)." }
|
|
1653
2268
|
};
|
|
1654
2269
|
var ASSET_FIELDS = {
|
|
@@ -1660,6 +2275,32 @@ var ASSET_FIELDS = {
|
|
|
1660
2275
|
hasAudio: { type: "boolean", description: "For video: whether it has sound (default true)." },
|
|
1661
2276
|
poster: { type: "string", description: "For video: a still (URL) to show in rendered frames." }
|
|
1662
2277
|
};
|
|
2278
|
+
var KEY_PROPERTY = {
|
|
2279
|
+
type: "string",
|
|
2280
|
+
enum: [...VIDEO_KEYFRAME_PROPERTIES],
|
|
2281
|
+
description: "Which property: opacity (0\u20131), volume (media only, 0\u20134 gain), x / y (pixels from the frame's centre, right / down), scale (1 = fitted size) or rotation (degrees clockwise)."
|
|
2282
|
+
};
|
|
2283
|
+
var KEYS = {
|
|
2284
|
+
type: "array",
|
|
2285
|
+
minItems: 1,
|
|
2286
|
+
maxItems: 500,
|
|
2287
|
+
description: "The keys, replacing any the property had (any order, one per time). Before the first key the property holds the first value, after the last the last value.",
|
|
2288
|
+
items: object(
|
|
2289
|
+
{
|
|
2290
|
+
t: { type: "number", description: "Time in ms from the clip's start (clip time, not timeline time: 0 is the clip's first frame, its duration the end). Keys stay with the clip when it moves, is trimmed or split." },
|
|
2291
|
+
v: { type: "number", description: "The value at that time, in the property's units." },
|
|
2292
|
+
ease: {
|
|
2293
|
+
description: "How the value moves from this key to the next (default linear): linear, hold (stays, then jumps at the next key), ease-in, ease-out, ease-in-out, or CSS cubic-bezier points [x1, y1, x2, y2] with x1 and x2 in 0\u20131.",
|
|
2294
|
+
anyOf: [
|
|
2295
|
+
{ type: "string", enum: [...VIDEO_EASE_NAMES] },
|
|
2296
|
+
{ type: "array", items: { type: "number" }, minItems: 4, maxItems: 4 }
|
|
2297
|
+
]
|
|
2298
|
+
}
|
|
2299
|
+
},
|
|
2300
|
+
["t", "v"],
|
|
2301
|
+
"One keyframe."
|
|
2302
|
+
)
|
|
2303
|
+
};
|
|
1663
2304
|
var issueSummary = (issues) => ({
|
|
1664
2305
|
errors: issues.filter((i) => i.severity === "error").length,
|
|
1665
2306
|
warnings: issues.filter((i) => i.severity === "warning").length,
|
|
@@ -1683,6 +2324,7 @@ function describeVideoProject(project) {
|
|
|
1683
2324
|
...t.muted ? { muted: true } : {},
|
|
1684
2325
|
...t.hidden ? { hidden: true } : {},
|
|
1685
2326
|
...t.locked ? { locked: true } : {},
|
|
2327
|
+
...t.syncLocked === false ? { syncLocked: false } : {},
|
|
1686
2328
|
...t.volume !== void 0 ? { volume: t.volume } : {},
|
|
1687
2329
|
clips: sortedVideoClips(t).map((c) => ({ ...c, end: videoClipEnd(c), ...c.type === "media" ? { source: videoClipSourceRange(c) } : {} }))
|
|
1688
2330
|
})),
|
|
@@ -1711,6 +2353,17 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1711
2353
|
}
|
|
1712
2354
|
return null;
|
|
1713
2355
|
};
|
|
2356
|
+
const placed = (o) => ({
|
|
2357
|
+
..._optionalChain([o, 'access', _67 => _67.created, 'optionalAccess', _68 => _68.length]) ? { created: o.created } : {},
|
|
2358
|
+
..._optionalChain([o, 'access', _69 => _69.trimmed, 'optionalAccess', _70 => _70.length]) ? { trimmed: o.trimmed } : {},
|
|
2359
|
+
..._optionalChain([o, 'access', _71 => _71.removed, 'optionalAccess', _72 => _72.length]) ? { removed: o.removed } : {},
|
|
2360
|
+
...o.rippled ? { rippled: o.rippled } : {}
|
|
2361
|
+
});
|
|
2362
|
+
const keysOut = (p, clip, property) => {
|
|
2363
|
+
const c = clipOut(p, clip);
|
|
2364
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _73 => _73.transform, 'optionalAccess', _74 => _74[property]]) : _optionalChain([c, 'optionalAccess', _75 => _75[property]]);
|
|
2365
|
+
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
2366
|
+
};
|
|
1714
2367
|
const getProject = {
|
|
1715
2368
|
name: "get_video_project",
|
|
1716
2369
|
title: "Read the video project",
|
|
@@ -1748,7 +2401,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1748
2401
|
const checkProject = {
|
|
1749
2402
|
name: "check_video_project",
|
|
1750
2403
|
title: "Check the video project",
|
|
1751
|
-
description: "Lists the project's problems, errors first, each with a code, the clip / track / asset it is about and how to fix it: missing assets, overlaps, clips past the end of their source, transitions without a cut or handles, fades longer than the clip, assets with no duration, unused assets.",
|
|
2404
|
+
description: "Lists the project's problems, errors first, each with a code, the clip / track / asset it is about and how to fix it: missing assets, overlaps, clips past the end of their source, transitions without a cut or handles, fades longer than the clip, keyframes outside their clip, assets with no duration, unused assets.",
|
|
1752
2405
|
inputSchema: object({}),
|
|
1753
2406
|
annotations: { title: "Check the video project", ...READ },
|
|
1754
2407
|
handler: () => {
|
|
@@ -1875,8 +2528,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1875
2528
|
const updateTrack = {
|
|
1876
2529
|
name: "update_video_track",
|
|
1877
2530
|
title: "Change a track",
|
|
1878
|
-
description: "Renames, mutes, hides, locks or sets the volume of a track (one undo step). Returns the track's settings.",
|
|
1879
|
-
inputSchema: object({ track: TRACK_ID, set: object(TRACK_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "muted", "hidden", "locked", "volume"] }, description: "Fields to clear." } }, ["track"]),
|
|
2531
|
+
description: "Renames, mutes, hides, locks, sync-locks or sets the volume of a track (one undo step). Returns the track's settings.",
|
|
2532
|
+
inputSchema: object({ track: TRACK_ID, set: object(TRACK_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "muted", "hidden", "locked", "syncLocked", "volume"] }, description: "Fields to clear." } }, ["track"]),
|
|
1880
2533
|
annotations: { title: "Change a track", ...WRITE_IDEMPOTENT },
|
|
1881
2534
|
handler: ({ track, set, remove }) => apply({ op: "update_track", track, set, remove }, "Change track", (r) => {
|
|
1882
2535
|
const { clips, ...t } = r.project.tracks.find((x) => x.id === track);
|
|
@@ -1902,20 +2555,20 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1902
2555
|
const addClip = {
|
|
1903
2556
|
name: "add_video_clip",
|
|
1904
2557
|
title: "Add a clip",
|
|
1905
|
-
description: 'Puts a clip on a track (one undo step): media (a stretch of an asset from its "in" point), a title or a colour. With no start it goes at the end of the track
|
|
1906
|
-
inputSchema: object({ track: TRACK_ID, clip: NEW_CLIP, ripple:
|
|
2558
|
+
description: 'Puts a clip on a track (one undo step): media (a stretch of an asset from its "in" point), a title or a colour. With no start it goes at the end of the track. Where it lands on other clips, mode decides: refuse (default, with the next free place), overwrite (replace what is underneath) or insert (push later material along on every sync-locked track). Returns the clip with its end and source range, plus any clips it cut (created), trimmed or removed and the tracks it rippled.',
|
|
2559
|
+
inputSchema: object({ track: TRACK_ID, clip: NEW_CLIP, mode: MODE, ripple: { type: "boolean", description: 'Same as mode "insert" (kept for older callers; default false).' } }, ["track", "clip"]),
|
|
1907
2560
|
annotations: { title: "Add a clip", ...WRITE },
|
|
1908
|
-
handler: ({ track, clip, ripple }) => apply({ op: "add_clip", track, clip, ripple }, "Add clip", (r) => ({ clip: clipOut(r.project, r.outcomes[0].clip) }))
|
|
2561
|
+
handler: ({ track, clip, mode, ripple }) => apply({ op: "add_clip", track, clip, mode, ripple }, mode === "overwrite" ? "Overwrite clip" : mode === "insert" || ripple ? "Insert clip" : "Add clip", (r) => ({ clip: clipOut(r.project, r.outcomes[0].clip), ...placed(r.outcomes[0]) }))
|
|
1909
2562
|
};
|
|
1910
2563
|
const moveClip = {
|
|
1911
2564
|
name: "move_video_clip",
|
|
1912
2565
|
title: "Move a clip",
|
|
1913
|
-
description: "Moves a clip to a new start and/or another track of the same kind (one undo step)
|
|
1914
|
-
inputSchema: object({ clip: CLIP_ID, start: MS("New start on the timeline (default: unchanged)"), track: { ...TRACK_ID, description: "Track to move it to (default: its own)." } }, ["clip"]),
|
|
1915
|
-
annotations: { title: "Move a clip", ...
|
|
1916
|
-
handler: ({ clip, start, track }) => {
|
|
2566
|
+
description: "Moves a clip to a new start and/or another track of the same kind (one undo step), leaving a gap where it was. Where it lands on other clips, mode decides: refuse (default, naming the clip and the next free place), overwrite or insert. Returns the clip, plus any clips it cut (created), trimmed or removed and the tracks it rippled.",
|
|
2567
|
+
inputSchema: object({ clip: CLIP_ID, start: MS("New start on the timeline (default: unchanged)"), track: { ...TRACK_ID, description: "Track to move it to (default: its own)." }, mode: MODE }, ["clip"]),
|
|
2568
|
+
annotations: { title: "Move a clip", ...WRITE },
|
|
2569
|
+
handler: ({ clip, start, track, mode }) => {
|
|
1917
2570
|
if (start === void 0 && track === void 0) throw new Error('Give "start", "track" or both.');
|
|
1918
|
-
return apply({ op: "move_clip", clip, start, track }, "Move clip", (r) => ({ clip: clipOut(r.project, clip) }));
|
|
2571
|
+
return apply({ op: "move_clip", clip, start, track, mode }, mode === "overwrite" ? "Overwrite clip" : mode === "insert" ? "Insert clip" : "Move clip", (r) => ({ clip: clipOut(r.project, clip), ...placed(r.outcomes[0]) }));
|
|
1919
2572
|
}
|
|
1920
2573
|
};
|
|
1921
2574
|
const trimClip = {
|
|
@@ -1924,7 +2577,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1924
2577
|
description: 'Moves one edge of a clip to a timeline time (one undo step): "start" also moves its source in point, "end" its out point. Refused past the ends of the source (the error says how far it can go) or below one frame. With ripple the clip keeps its start and the later clips on the track move by the change, so no gap or overlap opens. Returns the clip.',
|
|
1925
2578
|
inputSchema: object({ clip: CLIP_ID, edge: { type: "string", enum: ["start", "end"], description: "Which edge to move." }, to: MS("Where that edge goes on the timeline"), ripple: RIPPLE }, ["clip", "edge", "to"]),
|
|
1926
2579
|
annotations: { title: "Trim a clip", ...WRITE_IDEMPOTENT },
|
|
1927
|
-
handler: ({ clip, edge, to, ripple }) => apply({ op: "trim_clip", clip, edge, to, ripple }, "Trim clip", (r) => ({ clip: clipOut(r.project, clip) }))
|
|
2580
|
+
handler: ({ clip, edge, to, ripple }) => apply({ op: "trim_clip", clip, edge, to, ripple }, ripple ? "Ripple trim" : "Trim clip", (r) => ({ clip: clipOut(r.project, clip), ...placed(r.outcomes[0]) }))
|
|
1928
2581
|
};
|
|
1929
2582
|
const splitClip = {
|
|
1930
2583
|
name: "split_video_clip",
|
|
@@ -1937,10 +2590,13 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1937
2590
|
const deleteClips = {
|
|
1938
2591
|
name: "delete_video_clips",
|
|
1939
2592
|
title: "Delete clips",
|
|
1940
|
-
description: "Removes clips (one undo step). With ripple the later
|
|
1941
|
-
inputSchema: object(
|
|
2593
|
+
description: "Removes clips (one undo step). With ripple the later material moves up to close the gaps they leave: on every sync-locked track when they were on one (refused, naming the clip, if another sync-locked track has something in a gap), else on their own track. Returns the ids removed and the tracks rippled.",
|
|
2594
|
+
inputSchema: object(
|
|
2595
|
+
{ clips: { type: "array", items: CLIP_ID, minItems: 1, description: "Clip ids." }, ripple: { type: "boolean", description: "Close the gaps they leave, on every sync-locked track (default false)." } },
|
|
2596
|
+
["clips"]
|
|
2597
|
+
),
|
|
1942
2598
|
annotations: { title: "Delete clips", ...DESTRUCTIVE },
|
|
1943
|
-
handler: ({ clips, ripple }) => apply({ op: "delete_clips", clips, ripple }, ripple ? "Ripple delete" : "Delete clips", () => ({ removed: clips }))
|
|
2599
|
+
handler: ({ clips, ripple }) => apply({ op: "delete_clips", clips, ripple }, ripple ? "Ripple delete" : "Delete clips", (r) => ({ removed: clips, ...r.outcomes[0].rippled ? { rippled: r.outcomes[0].rippled } : {} }))
|
|
1944
2600
|
};
|
|
1945
2601
|
const updateClip = {
|
|
1946
2602
|
name: "update_video_clip",
|
|
@@ -1973,6 +2629,82 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1973
2629
|
annotations: { title: "Set a transition", ...WRITE_IDEMPOTENT },
|
|
1974
2630
|
handler: ({ clip, transition }) => apply({ op: "set_transition", clip, transition }, transition ? "Set transition" : "Remove transition", (r) => ({ clip: clipOut(r.project, clip) }))
|
|
1975
2631
|
};
|
|
2632
|
+
const slipClip = {
|
|
2633
|
+
name: "slip_video_clip",
|
|
2634
|
+
title: "Slip a clip",
|
|
2635
|
+
description: "Shows a different stretch of a video or audio clip's source without moving it on the timeline: its in and out points both move by delta (one undo step). Clamped to the media past either end, keeping what its transitions need. Returns the clip (with its new source range) and the delta applied.",
|
|
2636
|
+
inputSchema: object({ clip: CLIP_ID, delta: DELTA("How far to move the source (positive shows later media)") }, ["clip", "delta"]),
|
|
2637
|
+
annotations: { title: "Slip a clip", ...WRITE },
|
|
2638
|
+
handler: ({ clip, delta }) => apply({ op: "slip_clip", clip, delta }, "Slip clip", (r) => ({ clip: clipOut(r.project, clip), delta: r.outcomes[0].delta }))
|
|
2639
|
+
};
|
|
2640
|
+
const rollEdit = {
|
|
2641
|
+
name: "roll_video_edit",
|
|
2642
|
+
title: "Roll a cut",
|
|
2643
|
+
description: "Moves the cut between two clips that meet on one track (one undo step): the left one's out point and the right one's in point move together, so nothing else moves and no gap opens. Clamped to each clip's media and at least a frame of each. Returns both clips, the new cut time and the delta applied.",
|
|
2644
|
+
inputSchema: object({ left: { ...CLIP_ID, description: "The clip before the cut (from get_video_project)." }, right: { ...CLIP_ID, description: 'The clip after the cut, starting where "left" ends.' }, delta: DELTA("How far to move the cut") }, ["left", "right", "delta"]),
|
|
2645
|
+
annotations: { title: "Roll a cut", ...WRITE },
|
|
2646
|
+
handler: ({ left, right, delta }) => apply({ op: "roll_edit", left, right, delta }, "Roll edit", (r) => {
|
|
2647
|
+
const after = clipOut(r.project, right);
|
|
2648
|
+
return { left: clipOut(r.project, left), right: after, cut: after.start, delta: r.outcomes[0].delta };
|
|
2649
|
+
})
|
|
2650
|
+
};
|
|
2651
|
+
const slideClip = {
|
|
2652
|
+
name: "slide_video_clip",
|
|
2653
|
+
title: "Slide a clip",
|
|
2654
|
+
description: "Moves a clip along its track keeping its content, trimming the clips either side so the stretch stays gapless (one undo step): the one before gets longer as the one after gets shorter, or the other way. Without a neighbour on one side it moves through the gap there. Clamped to the neighbours' media and at least a frame of each. Returns the clip, its neighbours and the delta applied.",
|
|
2655
|
+
inputSchema: object({ clip: CLIP_ID, delta: DELTA("How far to move it") }, ["clip", "delta"]),
|
|
2656
|
+
annotations: { title: "Slide a clip", ...WRITE },
|
|
2657
|
+
handler: ({ clip, delta }) => apply({ op: "slide_clip", clip, delta }, "Slide clip", (r) => {
|
|
2658
|
+
const ids = r.outcomes[0].clips;
|
|
2659
|
+
const i = ids.indexOf(clip);
|
|
2660
|
+
return { clip: clipOut(r.project, clip), ...i > 0 ? { before: clipOut(r.project, ids[0]) } : {}, ...i < ids.length - 1 ? { after: clipOut(r.project, ids[ids.length - 1]) } : {}, delta: r.outcomes[0].delta };
|
|
2661
|
+
})
|
|
2662
|
+
};
|
|
2663
|
+
const rangeArgs = object({ from: MS("Start of the range on the timeline"), to: MS('End of the range on the timeline (after "from")'), tracks: RANGE_TRACKS }, ["from", "to"]);
|
|
2664
|
+
const rangeOut = (o) => ({ removed: _nullishCoalesce(o.removed, () => ( [])), trimmed: _nullishCoalesce(o.trimmed, () => ( [])), created: _nullishCoalesce(o.created, () => ( [])), ...o.rippled ? { rippled: o.rippled } : {} });
|
|
2665
|
+
const liftRange = {
|
|
2666
|
+
name: "lift_video_range",
|
|
2667
|
+
title: "Lift a range",
|
|
2668
|
+
description: "Empties a stretch of time on some or all tracks, leaving a gap (one undo step): clips inside are deleted, clips across an edge are trimmed to it, and a clip across both is cut in two. Returns the clips removed, trimmed and created (right-hand parts of cuts).",
|
|
2669
|
+
inputSchema: rangeArgs,
|
|
2670
|
+
annotations: { title: "Lift a range", ...DESTRUCTIVE },
|
|
2671
|
+
handler: ({ from, to, tracks }) => apply({ op: "lift_range", from, to, tracks }, "Lift", (r) => rangeOut(r.outcomes[0]))
|
|
2672
|
+
};
|
|
2673
|
+
const extractRange = {
|
|
2674
|
+
name: "extract_video_range",
|
|
2675
|
+
title: "Extract a range",
|
|
2676
|
+
description: 'Removes a stretch of time and closes it up (one undo step): like lift, then everything after it moves earlier by its length on those tracks and every sync-locked track. Refused, naming the clip, when a sync-locked track left out of "tracks" has something in the range. Returns the clips removed, trimmed and created and the tracks that moved.',
|
|
2677
|
+
inputSchema: rangeArgs,
|
|
2678
|
+
annotations: { title: "Extract a range", ...DESTRUCTIVE },
|
|
2679
|
+
handler: ({ from, to, tracks }) => apply({ op: "extract_range", from, to, tracks }, "Extract", (r) => rangeOut(r.outcomes[0]))
|
|
2680
|
+
};
|
|
2681
|
+
const splitAt = {
|
|
2682
|
+
name: "split_video_at",
|
|
2683
|
+
title: "Cut across tracks",
|
|
2684
|
+
description: "Cuts every clip that runs across a time on some or all tracks (a razor through the timeline), as one undo step. Each left part keeps its id, fade in and transition; each right part gets a new id and the fade out. Refused when no clip runs across that time. Returns the pairs { left, right }.",
|
|
2685
|
+
inputSchema: object({ at: MS("Where to cut, on the timeline"), tracks: RANGE_TRACKS }, ["at"]),
|
|
2686
|
+
annotations: { title: "Cut across tracks", ...WRITE },
|
|
2687
|
+
handler: ({ at, tracks }) => apply({ op: "split_at", at, tracks }, "Split", (r) => {
|
|
2688
|
+
const o = r.outcomes[0];
|
|
2689
|
+
return { cuts: o.clips.map((left, i) => ({ left: clipOut(r.project, left), right: clipOut(r.project, o.created[i]) })) };
|
|
2690
|
+
})
|
|
2691
|
+
};
|
|
2692
|
+
const setKeyframes = {
|
|
2693
|
+
name: "set_video_keyframes",
|
|
2694
|
+
title: "Animate a clip property",
|
|
2695
|
+
description: "Animates one property of a clip with keyframes (one undo step), replacing any keys it had: opacity, volume, or the transform's x, y, scale and rotation. Times are clip time (ms from the clip's start), so a key at 0 is the clip's first frame; get_video_project gives each clip's duration. Plays in the preview, renders and the ffmpeg export. Returns the property's keys, sorted.",
|
|
2696
|
+
inputSchema: object({ clip: CLIP_ID, property: KEY_PROPERTY, keys: KEYS }, ["clip", "property", "keys"]),
|
|
2697
|
+
annotations: { title: "Animate a clip property", ...WRITE_IDEMPOTENT },
|
|
2698
|
+
handler: ({ clip, property, keys }) => apply({ op: "set_keyframes", clip, property, keys }, `Keyframe ${property}`, (r) => ({ clip, property, ...keysOut(r.project, clip, property) }))
|
|
2699
|
+
};
|
|
2700
|
+
const removeKeyframes = {
|
|
2701
|
+
name: "remove_video_keyframes",
|
|
2702
|
+
title: "Stop animating a clip property",
|
|
2703
|
+
description: "Removes all of a property's keyframes (one undo step), leaving it at the value it had at the clip's start. Returns the remaining keys (none) and that value (null for the default).",
|
|
2704
|
+
inputSchema: object({ clip: CLIP_ID, property: KEY_PROPERTY }, ["clip", "property"]),
|
|
2705
|
+
annotations: { title: "Stop animating a clip property", ...WRITE_IDEMPOTENT },
|
|
2706
|
+
handler: ({ clip, property }) => apply({ op: "set_keyframes", clip, property, keys: null }, `Remove ${property} keyframes`, (r) => ({ clip, property, ...keysOut(r.project, clip, property) }))
|
|
2707
|
+
};
|
|
1976
2708
|
const closeGaps = {
|
|
1977
2709
|
name: "close_video_gaps",
|
|
1978
2710
|
title: "Close gaps",
|
|
@@ -1999,7 +2731,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
1999
2731
|
};
|
|
2000
2732
|
const EDIT_OP = {
|
|
2001
2733
|
type: "object",
|
|
2002
|
-
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, ripple }, move_clip { clip, start, track }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
2734
|
+
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
2003
2735
|
properties: { op: { type: "string", enum: VIDEO_EDIT_OPS.filter((o) => o !== "replace_project"), description: "Which edit." } },
|
|
2004
2736
|
required: ["op"]
|
|
2005
2737
|
};
|
|
@@ -2040,7 +2772,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2040
2772
|
}
|
|
2041
2773
|
};
|
|
2042
2774
|
const reads = [getProject, getFrame, findGaps, checkProject, renderFrame, exportFfmpeg, exportEdl, seek];
|
|
2043
|
-
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
2775
|
+
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
2044
2776
|
return {
|
|
2045
2777
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
2046
2778
|
namespace: options.namespace,
|
|
@@ -2082,7 +2814,7 @@ function videoClipLabel(clip, asset) {
|
|
|
2082
2814
|
if (clip.name) return clip.name;
|
|
2083
2815
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
2084
2816
|
if (clip.type === "color") return clip.color;
|
|
2085
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2817
|
+
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _76 => _76.name]), () => ( _optionalChain([asset, 'optionalAccess', _77 => _77.src, 'access', _78 => _78.replace, 'call', _79 => _79(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
2086
2818
|
const speed = videoClipSpeed(clip);
|
|
2087
2819
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
2088
2820
|
}
|
|
@@ -2099,7 +2831,8 @@ function planVideoClipDrag(project, input) {
|
|
|
2099
2831
|
const start = Math.max(0, raw + (shifts.length ? shifts.reduce((a, b) => Math.abs(b) < Math.abs(a) ? b : a) : 0));
|
|
2100
2832
|
const target = _nullishCoalesce(input.track, () => ( track.id));
|
|
2101
2833
|
const moved = Math.abs(start - clip.start) > 1e-6 || target !== track.id;
|
|
2102
|
-
|
|
2834
|
+
const mode = input.placement && input.placement !== "refuse" ? { mode: input.placement } : {};
|
|
2835
|
+
return { start, duration: clip.duration, track: target, edit: moved ? { op: "move_clip", clip: clip.id, start, ...target !== track.id ? { track: target } : {}, ...mode } : null };
|
|
2103
2836
|
}
|
|
2104
2837
|
const minimum = 1e3 / project.fps;
|
|
2105
2838
|
if (input.mode === "trim-start") {
|
|
@@ -2117,13 +2850,17 @@ function videoDropTracks(project, clipId) {
|
|
|
2117
2850
|
if (!at) return [];
|
|
2118
2851
|
return project.tracks.filter((t) => t.kind === at.track.kind && !t.locked).map((t) => t.id);
|
|
2119
2852
|
}
|
|
2853
|
+
function videoDragLabel(edit) {
|
|
2854
|
+
if (edit.op === "move_clip") return edit.mode === "insert" ? "Insert clip" : edit.mode === "overwrite" ? "Overwrite clip" : "Move clip";
|
|
2855
|
+
return edit.op === "trim_clip" && edit.ripple ? "Ripple trim" : "Trim clip";
|
|
2856
|
+
}
|
|
2120
2857
|
function videoAssetDropEdit(project, assetId, options = {}) {
|
|
2121
2858
|
const asset = videoAssetMap(project).get(assetId);
|
|
2122
2859
|
if (!asset) return null;
|
|
2123
2860
|
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
2124
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access',
|
|
2861
|
+
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _80 => _80.tracks, 'access', _81 => _81.find, 'call', _82 => _82((t) => t.kind === kind && !t.locked), 'optionalAccess', _83 => _83.id])));
|
|
2125
2862
|
if (!track) return null;
|
|
2126
|
-
return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {} };
|
|
2863
|
+
return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
|
|
2127
2864
|
}
|
|
2128
2865
|
function videoAssetKindOf(typeOrName) {
|
|
2129
2866
|
const s = typeOrName.toLowerCase().replace(/[?#].*$/, "");
|
|
@@ -2162,6 +2899,99 @@ function probeVideoMedia(src, kind, timeout = 15e3) {
|
|
|
2162
2899
|
el.src = src;
|
|
2163
2900
|
});
|
|
2164
2901
|
}
|
|
2902
|
+
var KEY_LABELS = {
|
|
2903
|
+
opacity: { label: "Opacity", step: 0.05 },
|
|
2904
|
+
volume: { label: "Volume", step: 0.05 },
|
|
2905
|
+
x: { label: "X", step: 1 },
|
|
2906
|
+
y: { label: "Y", step: 1 },
|
|
2907
|
+
scale: { label: "Scale", step: 0.05 },
|
|
2908
|
+
rotation: { label: "Rotation", step: 1 }
|
|
2909
|
+
};
|
|
2910
|
+
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _84 => _84.transform, 'optionalAccess', _85 => _85[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
2911
|
+
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
2912
|
+
function videoKeyframeRows(project, clipId, time) {
|
|
2913
|
+
const at = findVideoClip(project, clipId);
|
|
2914
|
+
if (!at) return [];
|
|
2915
|
+
const { clip } = at;
|
|
2916
|
+
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
2917
|
+
const visual = !(_optionalChain([asset, 'optionalAccess', _86 => _86.kind]) === "audio" || at.track.kind === "audio");
|
|
2918
|
+
const audible = clip.type === "media" && (_optionalChain([asset, 'optionalAccess', _87 => _87.kind]) === "audio" || _optionalChain([asset, 'optionalAccess', _88 => _88.kind]) === "video" && asset.hasAudio !== false);
|
|
2919
|
+
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
2920
|
+
const clipTime = playheadInClip(clip, time);
|
|
2921
|
+
return properties.map((property) => {
|
|
2922
|
+
const prop = animatableOf(clip, property);
|
|
2923
|
+
const keys = isVideoKeyframes(prop) ? prop.keys : [];
|
|
2924
|
+
const limits = VIDEO_KEYFRAME_LIMITS[property];
|
|
2925
|
+
return {
|
|
2926
|
+
property,
|
|
2927
|
+
...KEY_LABELS[property],
|
|
2928
|
+
value: Math.round(videoValueAt(prop, clipTime, limits.default) * 1e3) / 1e3,
|
|
2929
|
+
keyed: keys.length > 0,
|
|
2930
|
+
keys: keys.length,
|
|
2931
|
+
onKey: keys.some((k) => Math.abs(k.t - clipTime) < 0.5),
|
|
2932
|
+
clipTime,
|
|
2933
|
+
...limits.min !== void 0 ? { min: limits.min } : {},
|
|
2934
|
+
...limits.max !== void 0 ? { max: limits.max } : {}
|
|
2935
|
+
};
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
function plainValueEdit(clip, property, value) {
|
|
2939
|
+
if (!VIDEO_TRANSFORM_PROPERTIES.includes(property))
|
|
2940
|
+
return value === void 0 ? { op: "update_clip", clip: clip.id, remove: [property] } : { op: "update_clip", clip: clip.id, set: { [property]: value } };
|
|
2941
|
+
const transform = { ...clip.transform, [property]: value };
|
|
2942
|
+
if (value === void 0) delete transform[property];
|
|
2943
|
+
return Object.keys(transform).length ? { op: "update_clip", clip: clip.id, set: { transform } } : { op: "update_clip", clip: clip.id, remove: ["transform"] };
|
|
2944
|
+
}
|
|
2945
|
+
function videoKeyframeValueEdit(project, clipId, property, time, value) {
|
|
2946
|
+
const at = findVideoClip(project, clipId);
|
|
2947
|
+
if (!at) return null;
|
|
2948
|
+
const prop = animatableOf(at.clip, property);
|
|
2949
|
+
if (!isVideoKeyframes(prop)) return plainValueEdit(at.clip, property, value);
|
|
2950
|
+
if (value === void 0) return null;
|
|
2951
|
+
return { op: "set_keyframes", clip: clipId, property, keys: setVideoKey(prop, { t: playheadInClip(at.clip, time), v: value }, VIDEO_KEYFRAME_LIMITS[property].default).keys };
|
|
2952
|
+
}
|
|
2953
|
+
function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
2954
|
+
const at = findVideoClip(project, clipId);
|
|
2955
|
+
if (!at) return null;
|
|
2956
|
+
const prop = animatableOf(at.clip, property);
|
|
2957
|
+
const t = playheadInClip(at.clip, time);
|
|
2958
|
+
const fallback = VIDEO_KEYFRAME_LIMITS[property].default;
|
|
2959
|
+
if (isVideoKeyframes(prop)) {
|
|
2960
|
+
const key = prop.keys.find((k) => Math.abs(k.t - t) < 0.5);
|
|
2961
|
+
if (key) {
|
|
2962
|
+
const rest = removeVideoKey(prop, key.t);
|
|
2963
|
+
return isVideoKeyframes(rest) ? { op: "set_keyframes", clip: clipId, property, keys: rest.keys } : plainValueEdit(at.clip, property, rest === fallback ? void 0 : rest);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
const v = videoValueAt(prop, t, fallback);
|
|
2967
|
+
return { op: "set_keyframes", clip: clipId, property, keys: isVideoKeyframes(prop) ? setVideoKey(prop, { t, v }, fallback).keys : [{ t, v }] };
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
|
|
2971
|
+
|
|
2972
|
+
|
|
2973
|
+
|
|
2974
|
+
|
|
2975
|
+
|
|
2976
|
+
|
|
2977
|
+
|
|
2978
|
+
|
|
2979
|
+
|
|
2980
|
+
|
|
2981
|
+
|
|
2982
|
+
|
|
2983
|
+
|
|
2984
|
+
|
|
2985
|
+
|
|
2986
|
+
|
|
2987
|
+
|
|
2988
|
+
|
|
2989
|
+
|
|
2990
|
+
|
|
2991
|
+
|
|
2992
|
+
|
|
2993
|
+
|
|
2994
|
+
|
|
2165
2995
|
|
|
2166
2996
|
|
|
2167
2997
|
|
|
@@ -2230,5 +3060,5 @@ function probeVideoMedia(src, kind, timeout = 15e3) {
|
|
|
2230
3060
|
|
|
2231
3061
|
|
|
2232
3062
|
|
|
2233
|
-
exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.checkVideoProject = checkVideoProject; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoClip = findVideoClip; exports.findVideoGaps = findVideoGaps; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortedVideoClips = sortedVideoClips; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoClipAfter = videoClipAfter; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSpeed = videoClipSpeed; exports.videoClipTint = videoClipTint; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDropTracks = videoDropTracks; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoIssueKey = videoIssueKey; exports.videoOverlap = videoOverlap; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTitleFrame = videoTitleFrame; exports.videoWipeRect = videoWipeRect;
|
|
3063
|
+
exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.checkVideoProject = checkVideoProject; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoClip = findVideoClip; exports.findVideoGaps = findVideoGaps; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.isVideoKeyframes = isVideoKeyframes; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoClipAfter = videoClipAfter; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSpeed = videoClipSpeed; exports.videoClipTint = videoClipTint; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoOverlap = videoOverlap; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTransformAt = videoTransformAt; exports.videoValueAt = videoValueAt; exports.videoWipeRect = videoWipeRect;
|
|
2234
3064
|
//# sourceMappingURL=index.cjs.map
|