@pixerate/editor 0.83.0 → 0.84.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 +556 -90
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +247 -8
- package/dist/video-editor/index.d.ts +247 -8
- package/dist/video-editor/index.js +503 -37
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +24 -6
|
@@ -13,8 +13,11 @@ require('../chunk-2FHLHZAL.cjs');
|
|
|
13
13
|
|
|
14
14
|
// src/video-editor/types.ts
|
|
15
15
|
var VIDEO_FITS = ["contain", "cover", "fill", "none"];
|
|
16
|
-
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe"];
|
|
16
|
+
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe", "push", "slide", "iris"];
|
|
17
17
|
var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
18
|
+
var videoTransitionAudioCurve = (t) => _nullishCoalesce(t.audioCurve, () => ( (t.type === "dip" ? "linear" : "equal-power")));
|
|
19
|
+
var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
|
|
20
|
+
var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
|
|
18
21
|
var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
|
|
19
22
|
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
20
23
|
var VIDEO_CLIP_TYPES = ["media", "text", "color"];
|
|
@@ -207,6 +210,136 @@ function videoKeyframeFfmpegExpr(prop, time, fallback, step) {
|
|
|
207
210
|
return `if(lt(${time},${fmt(points[0][0])}),${fmt(points[0][1])},${expr})`;
|
|
208
211
|
}
|
|
209
212
|
|
|
213
|
+
// src/video-editor/grade.ts
|
|
214
|
+
var VIDEO_GRADE_CONTROLS = [
|
|
215
|
+
{ key: "exposure", label: "Exposure", min: -3, max: 3, step: 0.1, description: "Exposure in stops: +1 doubles every channel, -1 halves it (default 0)." },
|
|
216
|
+
{ key: "contrast", label: "Contrast", min: -1, max: 1, step: 0.05, description: "Contrast around mid grey, -1 (flat grey) to 1 (twice as steep); default 0." },
|
|
217
|
+
{ key: "saturation", label: "Saturation", min: -1, max: 1, step: 0.05, description: "Saturation, -1 (black and white) to 1 (twice as saturated); default 0." },
|
|
218
|
+
{ key: "hue", label: "Hue", min: -180, max: 180, step: 1, description: "Hue rotation in degrees (default 0)." },
|
|
219
|
+
{ key: "temperature", label: "Temperature", min: -1, max: 1, step: 0.05, description: "White balance, -1 (cool, bluer) to 1 (warm, more orange); default 0." },
|
|
220
|
+
{ key: "tint", label: "Tint", min: -1, max: 1, step: 0.05, description: "White balance, -1 (greener) to 1 (more magenta); default 0." }
|
|
221
|
+
];
|
|
222
|
+
var VIDEO_GRADE_PRESETS = {
|
|
223
|
+
warm: { temperature: 0.4, saturation: 0.1 },
|
|
224
|
+
cool: { temperature: -0.4, tint: -0.05 },
|
|
225
|
+
punchy: { contrast: 0.25, saturation: 0.3 },
|
|
226
|
+
faded: { exposure: 0.15, contrast: -0.3, saturation: -0.3 },
|
|
227
|
+
"black-and-white": { saturation: -1, contrast: 0.15 },
|
|
228
|
+
"bleach-bypass": { saturation: -0.6, contrast: 0.4 },
|
|
229
|
+
"day-for-night": { exposure: -1.5, saturation: -0.4, temperature: -0.6 }
|
|
230
|
+
};
|
|
231
|
+
var VIDEO_GRADE_PRESET_NAMES = Object.keys(VIDEO_GRADE_PRESETS);
|
|
232
|
+
var clampTo = (c, v) => Math.min(c.max, Math.max(c.min, v));
|
|
233
|
+
function normalizeVideoGrade(grade) {
|
|
234
|
+
if (!grade) return void 0;
|
|
235
|
+
const out = {};
|
|
236
|
+
for (const c of VIDEO_GRADE_CONTROLS) {
|
|
237
|
+
const v = grade[c.key];
|
|
238
|
+
if (typeof v === "number" && Number.isFinite(v) && v !== 0) out[c.key] = Math.round(clampTo(c, v) * 1e3) / 1e3;
|
|
239
|
+
}
|
|
240
|
+
return Object.keys(out).length ? out : void 0;
|
|
241
|
+
}
|
|
242
|
+
function videoGradePreset(name, amount = 1) {
|
|
243
|
+
const preset = VIDEO_GRADE_PRESETS[name];
|
|
244
|
+
if (!preset) throw new Error(`There is no grade preset "${name}"; the presets are ${VIDEO_GRADE_PRESET_NAMES.join(", ")}.`);
|
|
245
|
+
const k = Math.min(1, Math.max(0, amount));
|
|
246
|
+
return normalizeVideoGrade(Object.fromEntries(Object.entries(preset).map(([key, v]) => [key, v * k])));
|
|
247
|
+
}
|
|
248
|
+
function videoGradeSteps(grade) {
|
|
249
|
+
const g = normalizeVideoGrade(grade);
|
|
250
|
+
if (!g) return [];
|
|
251
|
+
const steps = [];
|
|
252
|
+
if (g.exposure) steps.push({ kind: "brightness", amount: 2 ** g.exposure });
|
|
253
|
+
if (g.contrast) steps.push({ kind: "contrast", amount: 1 + g.contrast });
|
|
254
|
+
if (g.saturation) steps.push({ kind: "saturate", amount: 1 + g.saturation });
|
|
255
|
+
if (g.hue) steps.push({ kind: "hue-rotate", degrees: g.hue });
|
|
256
|
+
if (g.temperature || g.tint) {
|
|
257
|
+
const t = _nullishCoalesce(g.temperature, () => ( 0));
|
|
258
|
+
steps.push({ kind: "gains", r: 1 + 0.25 * t, g: 1 - 0.25 * (_nullishCoalesce(g.tint, () => ( 0))), b: 1 - 0.25 * t });
|
|
259
|
+
}
|
|
260
|
+
return steps;
|
|
261
|
+
}
|
|
262
|
+
function videoGradeStepMatrix(step) {
|
|
263
|
+
switch (step.kind) {
|
|
264
|
+
case "brightness":
|
|
265
|
+
return [
|
|
266
|
+
[step.amount, 0, 0, 0],
|
|
267
|
+
[0, step.amount, 0, 0],
|
|
268
|
+
[0, 0, step.amount, 0]
|
|
269
|
+
];
|
|
270
|
+
case "contrast": {
|
|
271
|
+
const c = step.amount;
|
|
272
|
+
const o = 0.5 - 0.5 * c;
|
|
273
|
+
return [
|
|
274
|
+
[c, 0, 0, o],
|
|
275
|
+
[0, c, 0, o],
|
|
276
|
+
[0, 0, c, o]
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
case "gains":
|
|
280
|
+
return [
|
|
281
|
+
[step.r, 0, 0, 0],
|
|
282
|
+
[0, step.g, 0, 0],
|
|
283
|
+
[0, 0, step.b, 0]
|
|
284
|
+
];
|
|
285
|
+
case "saturate": {
|
|
286
|
+
const s = step.amount;
|
|
287
|
+
return [
|
|
288
|
+
[0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0],
|
|
289
|
+
[0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0],
|
|
290
|
+
[0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0]
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
case "hue-rotate": {
|
|
294
|
+
const a = step.degrees * Math.PI / 180;
|
|
295
|
+
const cos = Math.cos(a);
|
|
296
|
+
const sin = Math.sin(a);
|
|
297
|
+
return [
|
|
298
|
+
[0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928, 0],
|
|
299
|
+
[0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.14, 0.072 - cos * 0.072 - sin * 0.283, 0],
|
|
300
|
+
[0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072, 0]
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function applyVideoGrade(grade, rgb) {
|
|
306
|
+
let v = rgb.map((x) => x / 255);
|
|
307
|
+
for (const step of videoGradeSteps(grade)) {
|
|
308
|
+
const m = videoGradeStepMatrix(step);
|
|
309
|
+
v = m.map((row) => Math.min(1, Math.max(0, row[0] * v[0] + row[1] * v[1] + row[2] * v[2] + row[3])));
|
|
310
|
+
}
|
|
311
|
+
return v.map((x) => Math.round(x * 255));
|
|
312
|
+
}
|
|
313
|
+
var fmt2 = (v) => String(Math.round(v * 1e5) / 1e5);
|
|
314
|
+
function videoGradeStepSvgValues(step) {
|
|
315
|
+
return videoGradeStepMatrix(step).map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
|
|
316
|
+
}
|
|
317
|
+
function videoGradeSvgFilter(id, grade) {
|
|
318
|
+
const steps = videoGradeSteps(grade);
|
|
319
|
+
return `<filter id="${id}" color-interpolation-filters="sRGB" x="0" y="0" width="100%" height="100%">${steps.map((s) => `<feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/>`).join("")}</filter>`;
|
|
320
|
+
}
|
|
321
|
+
var videoGradeGainsFilterId = (step) => `pixerate-wb-${[step.r, step.g, step.b].map((v) => Math.round(v * 1e3)).join("-")}`;
|
|
322
|
+
function videoGradeCssFilter(grade) {
|
|
323
|
+
return videoGradeSteps(grade).map((s) => s.kind === "brightness" ? `brightness(${fmt2(s.amount)})` : s.kind === "contrast" ? `contrast(${fmt2(s.amount)})` : s.kind === "saturate" ? `saturate(${fmt2(s.amount)})` : s.kind === "hue-rotate" ? `hue-rotate(${fmt2(s.degrees)}deg)` : `url(#${videoGradeGainsFilterId(s)})`).join(" ");
|
|
324
|
+
}
|
|
325
|
+
function videoGradeSvgDefs(grade) {
|
|
326
|
+
return videoGradeSteps(grade).filter((s) => s.kind === "gains").map((s) => {
|
|
327
|
+
const id = videoGradeGainsFilterId(s);
|
|
328
|
+
return { id, markup: `<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/></filter>` };
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function videoGradeFfmpegFilters(grade) {
|
|
332
|
+
return videoGradeSteps(grade).map((step) => {
|
|
333
|
+
const m = videoGradeStepMatrix(step);
|
|
334
|
+
if (step.kind === "brightness" || step.kind === "contrast") {
|
|
335
|
+
const lut = (i) => `clip(val*${fmt2(m[i][i])}+${fmt2(m[i][3] * 255)},0,255)`;
|
|
336
|
+
return `lutrgb=r='${lut(0)}':g='${lut(1)}':b='${lut(2)}'`;
|
|
337
|
+
}
|
|
338
|
+
const k = ["r", "g", "b"];
|
|
339
|
+
return `colorchannelmixer=${m.flatMap((row, i) => row.slice(0, 3).map((v, j) => `${k[i]}${k[j]}=${fmt2(v)}`)).join(":")}`;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
210
343
|
// src/video-editor/parse.ts
|
|
211
344
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
212
345
|
var fail = (path, what) => {
|
|
@@ -275,6 +408,22 @@ function parseVideoTransform(v, path) {
|
|
|
275
408
|
rotation: parseVideoAnimatable(v.rotation, `${path}.rotation`, { optional: true })
|
|
276
409
|
});
|
|
277
410
|
}
|
|
411
|
+
function parseVideoClipAnimation(v, path) {
|
|
412
|
+
if (v === void 0) return void 0;
|
|
413
|
+
if (!isObject(v)) return fail(path, "must be an object { effect, duration }");
|
|
414
|
+
return compact({
|
|
415
|
+
effect: oneOf(v.effect, VIDEO_CLIP_ANIMATION_EFFECTS, `${path}.effect`, false),
|
|
416
|
+
duration: num(v.duration, `${path}.duration`, { optional: true, positive: true })
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function parseVideoGrade(v, path) {
|
|
420
|
+
if (v === void 0) return void 0;
|
|
421
|
+
if (!isObject(v)) return fail(path, "must be an object such as { exposure, contrast, saturation }");
|
|
422
|
+
const known = new Set(VIDEO_GRADE_CONTROLS.map((c) => c.key));
|
|
423
|
+
const unknown = Object.keys(v).find((k) => !known.has(k));
|
|
424
|
+
if (unknown) fail(`${path}.${unknown}`, `is not a grade setting; use ${[...known].join(", ")}`);
|
|
425
|
+
return compact(Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, num(v[c.key], `${path}.${c.key}`, { optional: true, min: c.min, max: c.max })])));
|
|
426
|
+
}
|
|
278
427
|
function parseVideoTransition(v, path) {
|
|
279
428
|
if (v === void 0) return void 0;
|
|
280
429
|
if (!isObject(v)) return fail(path, "must be an object { type, duration }");
|
|
@@ -282,7 +431,8 @@ function parseVideoTransition(v, path) {
|
|
|
282
431
|
type: oneOf(v.type, VIDEO_TRANSITION_TYPES, `${path}.type`, false),
|
|
283
432
|
duration: num(v.duration, `${path}.duration`, { positive: true }),
|
|
284
433
|
color: str(v.color, `${path}.color`, true),
|
|
285
|
-
direction: oneOf(v.direction, VIDEO_WIPE_DIRECTIONS, `${path}.direction`)
|
|
434
|
+
direction: oneOf(v.direction, VIDEO_WIPE_DIRECTIONS, `${path}.direction`),
|
|
435
|
+
audioCurve: oneOf(v.audioCurve, VIDEO_FADE_CURVES, `${path}.audioCurve`)
|
|
286
436
|
});
|
|
287
437
|
}
|
|
288
438
|
function parseVideoAsset(v, path = "asset") {
|
|
@@ -313,6 +463,10 @@ function parseVideoClip(v, path = "clip") {
|
|
|
313
463
|
opacity: parseVideoAnimatable(v.opacity, `${path}.opacity`, { optional: true, min: 0, max: 1 }),
|
|
314
464
|
transform: parseVideoTransform(v.transform, `${path}.transform`),
|
|
315
465
|
fit: oneOf(v.fit, VIDEO_FITS, `${path}.fit`),
|
|
466
|
+
blend: oneOf(v.blend, VIDEO_BLEND_MODES, `${path}.blend`),
|
|
467
|
+
grade: parseVideoGrade(v.grade, `${path}.grade`),
|
|
468
|
+
animateIn: parseVideoClipAnimation(v.animateIn, `${path}.animateIn`),
|
|
469
|
+
animateOut: parseVideoClipAnimation(v.animateOut, `${path}.animateOut`),
|
|
316
470
|
fadeIn: num(v.fadeIn, `${path}.fadeIn`, { optional: true, min: 0 }),
|
|
317
471
|
fadeOut: num(v.fadeOut, `${path}.fadeOut`, { optional: true, min: 0 }),
|
|
318
472
|
transition: parseVideoTransition(v.transition, `${path}.transition`)
|
|
@@ -563,12 +717,72 @@ function videoClipsAt(project, time) {
|
|
|
563
717
|
return out;
|
|
564
718
|
}
|
|
565
719
|
|
|
720
|
+
// src/video-editor/clip-animation.ts
|
|
721
|
+
var VIDEO_CLIP_ANIMATION_DURATION = 500;
|
|
722
|
+
var EFFECTS = {
|
|
723
|
+
fade: { easeIn: "linear", easeOut: "linear" },
|
|
724
|
+
rise: { y: 0.06, easeIn: "ease-out", easeOut: "ease-in" },
|
|
725
|
+
drop: { y: -0.06, easeIn: "ease-out", easeOut: "ease-in" },
|
|
726
|
+
"slide-left": { x: 0.15, easeIn: "ease-out", easeOut: "ease-in" },
|
|
727
|
+
"slide-right": { x: -0.15, easeIn: "ease-out", easeOut: "ease-in" },
|
|
728
|
+
zoom: { scale: 0.6, easeIn: "ease-out", easeOut: "ease-in" },
|
|
729
|
+
// Overshoots past full size, then settles (a "back" curve).
|
|
730
|
+
pop: { scale: 0.3, easeIn: [0.34, 1.56, 0.64, 1], easeOut: [0.36, 0, 0.66, -0.56] }
|
|
731
|
+
};
|
|
732
|
+
var PROPS = ["opacity", "x", "y", "scale"];
|
|
733
|
+
function videoClipAnimationProperties(animation) {
|
|
734
|
+
const spec = EFFECTS[animation.effect];
|
|
735
|
+
return PROPS.filter((p) => p === "opacity" || spec[p] !== void 0);
|
|
736
|
+
}
|
|
737
|
+
function videoClipAnimationSpans(clip) {
|
|
738
|
+
const a = clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0;
|
|
739
|
+
const b = clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0;
|
|
740
|
+
const k = a + b > clip.duration ? clip.duration / (a + b) : 1;
|
|
741
|
+
return { in: a * k, out: b * k };
|
|
742
|
+
}
|
|
743
|
+
function videoClipAnimated(clip, project) {
|
|
744
|
+
if (!clip.animateIn && !clip.animateOut) return clip;
|
|
745
|
+
const spans = videoClipAnimationSpans(clip);
|
|
746
|
+
const out = { ...clip, transform: { ...clip.transform } };
|
|
747
|
+
for (const prop of PROPS) {
|
|
748
|
+
const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _4 => _4.transform, 'optionalAccess', _5 => _5[prop]]);
|
|
749
|
+
if (isVideoKeyframes(current)) continue;
|
|
750
|
+
const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
|
|
751
|
+
const away = (spec) => {
|
|
752
|
+
if (prop === "opacity") return 0;
|
|
753
|
+
const v = spec[prop];
|
|
754
|
+
if (v === void 0) return void 0;
|
|
755
|
+
return prop === "scale" ? base * v : base + v * (prop === "x" ? project.width : project.height);
|
|
756
|
+
};
|
|
757
|
+
const keys = [];
|
|
758
|
+
if (clip.animateIn) {
|
|
759
|
+
const spec = EFFECTS[clip.animateIn.effect];
|
|
760
|
+
const from = away(spec);
|
|
761
|
+
if (from !== void 0) keys.push({ t: 0, v: from, ease: spec.easeIn }, { t: spans.in, v: base });
|
|
762
|
+
}
|
|
763
|
+
if (clip.animateOut) {
|
|
764
|
+
const spec = EFFECTS[clip.animateOut.effect];
|
|
765
|
+
const to = away(spec);
|
|
766
|
+
if (to !== void 0) {
|
|
767
|
+
const start = clip.duration - spans.out;
|
|
768
|
+
if (!keys.length || keys[keys.length - 1].t < start) keys.push({ t: start, v: base, ease: spec.easeOut });
|
|
769
|
+
else keys[keys.length - 1] = { ...keys[keys.length - 1], ease: spec.easeOut };
|
|
770
|
+
keys.push({ t: clip.duration, v: to });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (!keys.length) continue;
|
|
774
|
+
if (prop === "opacity") out.opacity = { keys };
|
|
775
|
+
else out.transform[prop] = { keys };
|
|
776
|
+
}
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
|
|
566
780
|
// src/video-editor/check.ts
|
|
567
781
|
function videoClipKeyframes(clip) {
|
|
568
782
|
const out = [
|
|
569
783
|
{ property: "opacity", keys: videoKeysOf(clip.opacity) },
|
|
570
784
|
{ property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
|
|
571
|
-
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access',
|
|
785
|
+
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _6 => _6.transform, 'optionalAccess', _7 => _7[p]])) }))
|
|
572
786
|
];
|
|
573
787
|
return out.filter((o) => o.keys.length);
|
|
574
788
|
}
|
|
@@ -581,6 +795,8 @@ var VIDEO_ISSUE_CODES = [
|
|
|
581
795
|
"transition-too-long",
|
|
582
796
|
"transition-handles",
|
|
583
797
|
"fades-too-long",
|
|
798
|
+
"animations-too-long",
|
|
799
|
+
"animation-keyed",
|
|
584
800
|
"keyframes-outside-clip",
|
|
585
801
|
"unknown-duration",
|
|
586
802
|
"unused-asset",
|
|
@@ -617,6 +833,16 @@ function checkVideoProject(project) {
|
|
|
617
833
|
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.` });
|
|
618
834
|
if ((_nullishCoalesce(clip.fadeIn, () => ( 0))) + (_nullishCoalesce(clip.fadeOut, () => ( 0))) > clip.duration + VIDEO_TIME_EPSILON)
|
|
619
835
|
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.` });
|
|
836
|
+
const anim = (clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0) + (clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0);
|
|
837
|
+
if (anim > clip.duration + VIDEO_TIME_EPSILON)
|
|
838
|
+
issues.push({ code: "animations-too-long", severity: "warning", ...at, message: `Clip "${clip.id}"'s entrance and exit (${anim} ms together) are longer than the clip (${clip.duration} ms), so both are shortened; give them shorter durations.` });
|
|
839
|
+
for (const which of ["animateIn", "animateOut"]) {
|
|
840
|
+
const a = clip[which];
|
|
841
|
+
if (!a) continue;
|
|
842
|
+
const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _8 => _8.transform, 'optionalAccess', _9 => _9[p]])).length);
|
|
843
|
+
if (keyed.length)
|
|
844
|
+
issues.push({ code: "animation-keyed", severity: "note", ...at, message: `Clip "${clip.id}"'s ${a.effect} ${which === "animateIn" ? "entrance" : "exit"} leaves ${keyed.join(", ")} alone, because the clip keys ${keyed.length > 1 ? "them" : "it"} itself; remove those keyframes or the animation.` });
|
|
845
|
+
}
|
|
620
846
|
for (const { property, keys } of videoClipKeyframes(clip)) {
|
|
621
847
|
const outside = keys.filter((k) => k.t < -VIDEO_TIME_EPSILON || k.t > clip.duration + VIDEO_TIME_EPSILON);
|
|
622
848
|
if (outside.length)
|
|
@@ -721,7 +947,7 @@ function clipAnimatables(clip) {
|
|
|
721
947
|
{ get: () => c.opacity, set: (v) => c.opacity = v },
|
|
722
948
|
{ get: () => c.volume, set: (v) => c.volume = v }
|
|
723
949
|
];
|
|
724
|
-
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access',
|
|
950
|
+
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _10 => _10.transform, 'optionalAccess', _11 => _11[p]]), set: (v) => c.transform && (c.transform[p] = v) });
|
|
725
951
|
return out;
|
|
726
952
|
}
|
|
727
953
|
function retimeClipKeys(clip, fn) {
|
|
@@ -730,7 +956,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
730
956
|
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
731
957
|
}
|
|
732
958
|
}
|
|
733
|
-
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
959
|
+
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
734
960
|
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
735
961
|
var Editor = (_class = class {
|
|
736
962
|
constructor(p) {;_class.prototype.__init.call(this);
|
|
@@ -769,7 +995,7 @@ var Editor = (_class = class {
|
|
|
769
995
|
kindFits(track, clip) {
|
|
770
996
|
const asset = clip.type === "media" ? this.asset(clip.asset) : null;
|
|
771
997
|
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.`);
|
|
772
|
-
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess',
|
|
998
|
+
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _12 => _12.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
|
|
773
999
|
}
|
|
774
1000
|
/** Moves every clip on `track` starting at or after `from` by `delta`. */
|
|
775
1001
|
shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
|
|
@@ -811,7 +1037,7 @@ var Editor = (_class = class {
|
|
|
811
1037
|
}
|
|
812
1038
|
/** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
|
|
813
1039
|
moveIn(clip, delta) {
|
|
814
|
-
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call',
|
|
1040
|
+
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _13 => _13(this.p), 'access', _14 => _14.get, 'call', _15 => _15(clip.asset), 'optionalAccess', _16 => _16.kind]) === "image") return;
|
|
815
1041
|
clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
|
|
816
1042
|
if (clip.in < 1e-9) delete clip.in;
|
|
817
1043
|
}
|
|
@@ -827,6 +1053,7 @@ var Editor = (_class = class {
|
|
|
827
1053
|
delete right.transition;
|
|
828
1054
|
delete right.fadeIn;
|
|
829
1055
|
delete right.fadeInCurve;
|
|
1056
|
+
delete right.animateIn;
|
|
830
1057
|
const leftValues = clipAnimatables(clip);
|
|
831
1058
|
clipAnimatables(right).forEach((r, i) => {
|
|
832
1059
|
const [l, rr] = splitVideoAnimatable(leftValues[i].get(), at - clip.start);
|
|
@@ -836,6 +1063,7 @@ var Editor = (_class = class {
|
|
|
836
1063
|
this.moveIn(right, at - clip.start);
|
|
837
1064
|
clip.duration = at - clip.start;
|
|
838
1065
|
delete clip.fadeOut;
|
|
1066
|
+
delete clip.animateOut;
|
|
839
1067
|
if (clip.type === "media") delete clip.fadeOutCurve;
|
|
840
1068
|
track.clips.push(right);
|
|
841
1069
|
track.clips.sort((a, b) => a.start - b.start);
|
|
@@ -1075,7 +1303,7 @@ var Editor = (_class = class {
|
|
|
1075
1303
|
if (clip.type === "media") {
|
|
1076
1304
|
const asset = assets.get(clip.asset);
|
|
1077
1305
|
const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
|
|
1078
|
-
if (_optionalChain([asset, 'optionalAccess',
|
|
1306
|
+
if (_optionalChain([asset, 'optionalAccess', _17 => _17.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
1079
1307
|
refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
|
|
1080
1308
|
}
|
|
1081
1309
|
if (e.ripple) {
|
|
@@ -1131,7 +1359,7 @@ var Editor = (_class = class {
|
|
|
1131
1359
|
let stretch = 1;
|
|
1132
1360
|
if (at.clip.type === "media" && !("duration" in set)) {
|
|
1133
1361
|
const was = videoClipSpeed(at.clip);
|
|
1134
|
-
const now = "speed" in set ? set.speed : _optionalChain([e, 'access',
|
|
1362
|
+
const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _18 => _18.remove, 'optionalAccess', _19 => _19.includes, 'call', _20 => _20("speed")]) ? 1 : was;
|
|
1135
1363
|
if (now !== was) {
|
|
1136
1364
|
set.duration = at.clip.duration * was / now;
|
|
1137
1365
|
stretch = was / now;
|
|
@@ -1227,7 +1455,7 @@ var Editor = (_class = class {
|
|
|
1227
1455
|
this.trimHead(after, delta);
|
|
1228
1456
|
}
|
|
1229
1457
|
track.clips.sort((a, b) => a.start - b.start);
|
|
1230
|
-
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess',
|
|
1458
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _21 => _21.id]), clip.id, _optionalChain([after, 'optionalAccess', _22 => _22.id])].filter((x) => !!x), track: track.id, delta });
|
|
1231
1459
|
}
|
|
1232
1460
|
case "lift_range":
|
|
1233
1461
|
case "extract_range": {
|
|
@@ -1258,7 +1486,7 @@ var Editor = (_class = class {
|
|
|
1258
1486
|
const limits = VIDEO_KEYFRAME_LIMITS[e.property];
|
|
1259
1487
|
const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
|
|
1260
1488
|
const c = clip;
|
|
1261
|
-
const current = transform ? _optionalChain([c, 'access',
|
|
1489
|
+
const current = transform ? _optionalChain([c, 'access', _23 => _23.transform, 'optionalAccess', _24 => _24[e.property]]) : c[e.property];
|
|
1262
1490
|
let value;
|
|
1263
1491
|
if (e.keys === null) {
|
|
1264
1492
|
const v = videoValueAt(current, 0, limits.default);
|
|
@@ -1340,7 +1568,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1340
1568
|
editor.run(edit);
|
|
1341
1569
|
} catch (error) {
|
|
1342
1570
|
const message = error.message;
|
|
1343
|
-
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1571
|
+
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _25 => _25.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
|
|
1344
1572
|
}
|
|
1345
1573
|
}
|
|
1346
1574
|
let next;
|
|
@@ -1359,7 +1587,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1359
1587
|
|
|
1360
1588
|
// src/video-editor/evaluate.ts
|
|
1361
1589
|
function videoTransformAt(transform, clipTime = 0) {
|
|
1362
|
-
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess',
|
|
1590
|
+
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _26 => _26.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _27 => _27.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _28 => _28.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _29 => _29.rotation]), clipTime, 0) };
|
|
1363
1591
|
}
|
|
1364
1592
|
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
1365
1593
|
let w = size.width;
|
|
@@ -1387,6 +1615,7 @@ function videoTitleFrame(project, clip, clipTime = 0) {
|
|
|
1387
1615
|
return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
|
|
1388
1616
|
}
|
|
1389
1617
|
var clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
1618
|
+
var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI * clamp01(progress))) / 2;
|
|
1390
1619
|
function videoFadeCurveGain(curve, x) {
|
|
1391
1620
|
const t = clamp01(x);
|
|
1392
1621
|
switch (curve) {
|
|
@@ -1405,8 +1634,8 @@ function videoFadeCurveGain(curve, x) {
|
|
|
1405
1634
|
function fadeGain(clip, time, sound = false) {
|
|
1406
1635
|
let g = 1;
|
|
1407
1636
|
const shape = sound && clip.type === "media" ? clip : void 0;
|
|
1408
|
-
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1409
|
-
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1637
|
+
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _30 => _30.fadeInCurve]), (time - clip.start) / clip.fadeIn);
|
|
1638
|
+
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _31 => _31.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
|
|
1410
1639
|
return g;
|
|
1411
1640
|
}
|
|
1412
1641
|
function videoClipSoundGain(clip, clipTime) {
|
|
@@ -1419,7 +1648,7 @@ function activeOnTrack(track, time) {
|
|
|
1419
1648
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1420
1649
|
const lead = before ? clip.transition.duration / 2 : 0;
|
|
1421
1650
|
const next = videoClipAfter(track, clip);
|
|
1422
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
1651
|
+
const tail = _optionalChain([next, 'optionalAccess', _32 => _32.transition]) ? next.transition.duration / 2 : 0;
|
|
1423
1652
|
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
1424
1653
|
const a = { clip };
|
|
1425
1654
|
if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
@@ -1436,12 +1665,12 @@ function videoFrameAt(project, time) {
|
|
|
1436
1665
|
for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
|
|
1437
1666
|
const track = project.tracks[ti];
|
|
1438
1667
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
1439
|
-
if (_optionalChain([incoming, 'optionalAccess',
|
|
1440
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1668
|
+
if (_optionalChain([incoming, 'optionalAccess', _33 => _33.type]) === "dip" && incoming.progress < 0.5) continue;
|
|
1669
|
+
if (_optionalChain([outgoing, 'optionalAccess', _34 => _34.type]) === "dip" && outgoing.progress >= 0.5) continue;
|
|
1441
1670
|
const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
|
|
1442
1671
|
const fade = fadeGain(clip, inside);
|
|
1443
|
-
const level = (t, into) => t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress;
|
|
1444
|
-
const mix = (incoming ? level(incoming, true) : 1) * (outgoing ? level(outgoing, false) : 1);
|
|
1672
|
+
const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
|
|
1673
|
+
const mix = (incoming ? level(incoming, incoming.transition, true) : 1) * (outgoing ? level(outgoing, outgoing.next.transition, false) : 1);
|
|
1445
1674
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
1446
1675
|
const sourceTime = sourceTimeAt(clip, time);
|
|
1447
1676
|
const clipTime = time - clip.start;
|
|
@@ -1452,20 +1681,39 @@ function videoFrameAt(project, time) {
|
|
|
1452
1681
|
}
|
|
1453
1682
|
if (track.kind !== "video" || track.hidden) continue;
|
|
1454
1683
|
if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
|
|
1455
|
-
|
|
1684
|
+
const shown = videoClipAnimated(clip, project);
|
|
1685
|
+
let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
|
|
1456
1686
|
let wipe;
|
|
1457
1687
|
let dip = null;
|
|
1688
|
+
let iris;
|
|
1689
|
+
let shift = 0;
|
|
1690
|
+
let shiftDirection = "right";
|
|
1458
1691
|
if (incoming) {
|
|
1459
1692
|
if (incoming.type === "crossfade") opacity *= incoming.progress;
|
|
1460
1693
|
else if (incoming.type === "wipe") wipe = { direction: _nullishCoalesce(incoming.transition.direction, () => ( "right")), progress: incoming.progress };
|
|
1461
|
-
else
|
|
1694
|
+
else if (incoming.type === "iris") iris = { progress: incoming.progress };
|
|
1695
|
+
else if (incoming.type === "push" || incoming.type === "slide") {
|
|
1696
|
+
shiftDirection = _nullishCoalesce(incoming.transition.direction, () => ( "right"));
|
|
1697
|
+
shift = videoTransitionEase(incoming.progress) - 1;
|
|
1698
|
+
} else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
|
|
1699
|
+
}
|
|
1700
|
+
if (_optionalChain([outgoing, 'optionalAccess', _35 => _35.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1701
|
+
if (_optionalChain([outgoing, 'optionalAccess', _36 => _36.type]) === "push") {
|
|
1702
|
+
shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
|
|
1703
|
+
shift = videoTransitionEase(outgoing.progress);
|
|
1462
1704
|
}
|
|
1463
|
-
if (_optionalChain([outgoing, 'optionalAccess', _31 => _31.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1464
1705
|
const size = clip.type === "media" ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
|
|
1465
1706
|
const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill")));
|
|
1466
|
-
const
|
|
1467
|
-
const
|
|
1707
|
+
const placed = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, shown.transform, clipTime);
|
|
1708
|
+
const across = shiftDirection === "left" || shiftDirection === "right";
|
|
1709
|
+
const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
|
|
1710
|
+
const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
|
|
1711
|
+
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _37 => _37.transform, 'optionalAccess', _38 => _38.rotation]), clipTime, 0) };
|
|
1468
1712
|
if (wipe) layer.wipe = wipe;
|
|
1713
|
+
if (iris) layer.iris = iris;
|
|
1714
|
+
if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
|
|
1715
|
+
const grade = normalizeVideoGrade(clip.grade);
|
|
1716
|
+
if (grade) layer.grade = grade;
|
|
1469
1717
|
if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
|
|
1470
1718
|
if (clip.type === "text") layer.text = videoTextStyle(clip);
|
|
1471
1719
|
if (clip.type === "color") layer.color = clip.color;
|
|
@@ -1696,6 +1944,7 @@ var VideoAudioPeaksCache = (_class2 = class {
|
|
|
1696
1944
|
// src/video-editor/render.ts
|
|
1697
1945
|
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1698
1946
|
var n = (v) => String(Math.round(v * 100) / 100);
|
|
1947
|
+
var videoIrisRadius = (frame, progress) => progress * Math.hypot(frame.width / 2, frame.height / 2);
|
|
1699
1948
|
function videoWipeRect(rect, wipe) {
|
|
1700
1949
|
const { direction, progress } = wipe;
|
|
1701
1950
|
const w = rect.width * progress;
|
|
@@ -1713,6 +1962,11 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
|
1713
1962
|
const { rect } = layer;
|
|
1714
1963
|
const attrs = [];
|
|
1715
1964
|
if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
|
|
1965
|
+
if (layer.blend) attrs.push(`style="mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}"`);
|
|
1966
|
+
if (layer.grade) {
|
|
1967
|
+
defs.push(videoGradeSvgFilter(`g${i}`, layer.grade));
|
|
1968
|
+
attrs.push(`filter="url(#g${i})"`);
|
|
1969
|
+
}
|
|
1716
1970
|
if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
|
|
1717
1971
|
if (layer.wipe) {
|
|
1718
1972
|
const r = videoWipeRect(rect, layer.wipe);
|
|
@@ -1744,7 +1998,12 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
|
1744
1998
|
inner = `<rect ${box} fill="#334155" stroke="#94a3b8" stroke-width="2"/><text x="${n(rect.x + rect.width / 2)}" y="${n(rect.y + rect.height / 2)}" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="${n(size)}" fill="#e2e8f0">${esc(label)}</text>`;
|
|
1745
1999
|
}
|
|
1746
2000
|
}
|
|
1747
|
-
|
|
2001
|
+
let item = attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner;
|
|
2002
|
+
if (layer.iris) {
|
|
2003
|
+
defs.push(`<clipPath id="i${i}"><circle cx="${n(frame.width / 2)}" cy="${n(frame.height / 2)}" r="${n(videoIrisRadius(frame, layer.iris.progress))}"/></clipPath>`);
|
|
2004
|
+
item = `<g clip-path="url(#i${i})">${item}</g>`;
|
|
2005
|
+
}
|
|
2006
|
+
body.push(item);
|
|
1748
2007
|
});
|
|
1749
2008
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(frame.width)} ${n(frame.height)}" width="${n(frame.width)}" height="${n(frame.height)}">${defs.length ? `<defs>${defs.join("")}</defs>` : ""}<rect width="100%" height="100%" fill="${esc(frame.background)}"/>${body.join("")}</svg>`;
|
|
1750
2009
|
}
|
|
@@ -1768,7 +2027,7 @@ function videoContactSheetSvg(project, times, options = {}) {
|
|
|
1768
2027
|
});
|
|
1769
2028
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(W)} ${n(H)}" width="${n(W)}" height="${n(H)}"><rect width="100%" height="100%" fill="#f8fafc"/>${cells.join("")}</svg>`;
|
|
1770
2029
|
}
|
|
1771
|
-
function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
2030
|
+
function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
1772
2031
|
ctx.save();
|
|
1773
2032
|
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
1774
2033
|
ctx.globalAlpha = 1;
|
|
@@ -1778,6 +2037,18 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
|
1778
2037
|
const { rect } = layer;
|
|
1779
2038
|
ctx.save();
|
|
1780
2039
|
ctx.globalAlpha = layer.opacity;
|
|
2040
|
+
if (layer.iris) {
|
|
2041
|
+
ctx.beginPath();
|
|
2042
|
+
ctx.arc(frame.width / 2, frame.height / 2, videoIrisRadius(frame, layer.iris.progress), 0, 2 * Math.PI);
|
|
2043
|
+
ctx.clip();
|
|
2044
|
+
}
|
|
2045
|
+
if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
|
|
2046
|
+
if (layer.grade) {
|
|
2047
|
+
const defs = videoGradeSvgDefs(layer.grade);
|
|
2048
|
+
if (options.defineSvgFilter) for (const d of defs) options.defineSvgFilter(d.id, d.markup);
|
|
2049
|
+
const css = videoGradeCssFilter(layer.grade);
|
|
2050
|
+
ctx.filter = options.defineSvgFilter ? css : css.replace(/\s*url\([^)]*\)/g, "").trim() || "none";
|
|
2051
|
+
}
|
|
1781
2052
|
if (layer.rotation) {
|
|
1782
2053
|
ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
1783
2054
|
ctx.rotate(layer.rotation * Math.PI / 180);
|
|
@@ -1826,7 +2097,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
|
1826
2097
|
}
|
|
1827
2098
|
|
|
1828
2099
|
// src/video-editor/export.ts
|
|
1829
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2100
|
+
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _39 => _39.name]), () => ( _optionalChain([asset, 'optionalAccess', _40 => _40.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
1830
2101
|
function toVideoEdl(project, options = {}) {
|
|
1831
2102
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
1832
2103
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
@@ -1858,10 +2129,10 @@ function toVideoEdl(project, options = {}) {
|
|
|
1858
2129
|
const outReel = before.type === "media" ? reelName(assets.get(before.asset)) : "BL";
|
|
1859
2130
|
const at = sourceAt(before, recIn);
|
|
1860
2131
|
lines.push(row(n2, outReel, "C", at, at, recIn, recIn));
|
|
1861
|
-
lines.push(row(n2, reel, `${t.type === "
|
|
2132
|
+
lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1862
2133
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1863
2134
|
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
1864
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2135
|
+
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _41 => _41.name]), () => ( _optionalChain([asset, 'optionalAccess', _42 => _42.src]))), () => ( clip.asset))}`);
|
|
1865
2136
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
1866
2137
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
1867
2138
|
lines.push("");
|
|
@@ -1894,6 +2165,20 @@ function parseVideoLoudnessMeasurement(output) {
|
|
|
1894
2165
|
return m;
|
|
1895
2166
|
}
|
|
1896
2167
|
var sec = (ms) => String(Math.round(ms) / 1e3);
|
|
2168
|
+
var FFMPEG_BLEND = {
|
|
2169
|
+
multiply: "multiply",
|
|
2170
|
+
screen: "screen",
|
|
2171
|
+
overlay: "overlay",
|
|
2172
|
+
darken: "darken",
|
|
2173
|
+
lighten: "lighten",
|
|
2174
|
+
"color-dodge": "dodge",
|
|
2175
|
+
"color-burn": "burn",
|
|
2176
|
+
"hard-light": "hardlight",
|
|
2177
|
+
"soft-light": "softlight",
|
|
2178
|
+
difference: "difference",
|
|
2179
|
+
exclusion: "exclusion",
|
|
2180
|
+
add: "addition"
|
|
2181
|
+
};
|
|
1897
2182
|
var AFADE_CURVES = { linear: "tri", "equal-power": "qsin", "s-curve": "hsin", exponential: "cub", logarithmic: "ipar" };
|
|
1898
2183
|
var afadeCurve = (curve) => curve && curve !== "linear" ? `:curve=${AFADE_CURVES[curve]}` : "";
|
|
1899
2184
|
var even = (v) => Math.max(2, 2 * Math.round(v / 2));
|
|
@@ -1939,6 +2224,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1939
2224
|
let cur = "base";
|
|
1940
2225
|
let usesDrawtext = false;
|
|
1941
2226
|
const animated = /* @__PURE__ */ new Set();
|
|
2227
|
+
const blended = /* @__PURE__ */ new Set();
|
|
1942
2228
|
let k = 0;
|
|
1943
2229
|
const step = 2e3 / fps;
|
|
1944
2230
|
const expr = (prop, clipTime, fallback, what) => {
|
|
@@ -1954,7 +2240,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1954
2240
|
return inputs.length - 1;
|
|
1955
2241
|
};
|
|
1956
2242
|
for (const track of [...project.tracks].reverse()) {
|
|
1957
|
-
for (const
|
|
2243
|
+
for (const raw of sortedVideoClips(track)) {
|
|
2244
|
+
const clip = videoClipAnimated(raw, project);
|
|
1958
2245
|
const { lead, tail } = videoClipOverhang(track, clip);
|
|
1959
2246
|
const t0 = clip.start - lead;
|
|
1960
2247
|
const span = clip.duration + lead + tail;
|
|
@@ -1973,11 +2260,13 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1973
2260
|
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}`;
|
|
1974
2261
|
const chain2 = ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip)), volume];
|
|
1975
2262
|
const before2 = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (
|
|
2263
|
+
const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
|
|
2264
|
+
const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
|
|
2265
|
+
const outCurve = next2 ? afadeCurve(videoTransitionAudioCurve(next2.transition)) : "";
|
|
2266
|
+
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
|
|
2267
|
+
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
|
|
2268
|
+
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
|
|
2269
|
+
if (_optionalChain([next2, 'optionalAccess', _43 => _43.transition, 'access', _44 => _44.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
|
|
1981
2270
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
1982
2271
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
1983
2272
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
@@ -1990,7 +2279,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1990
2279
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
1991
2280
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
1992
2281
|
const tf = clip.transform;
|
|
1993
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2282
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _45 => _45[p]])));
|
|
1994
2283
|
if (clip.type === "text" && options.titleImage) {
|
|
1995
2284
|
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 })]);
|
|
1996
2285
|
index = inputs.length - 1;
|
|
@@ -1999,7 +2288,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1999
2288
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
2000
2289
|
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("*");
|
|
2001
2290
|
const coord = (axis) => {
|
|
2002
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
2291
|
+
const v = _optionalChain([tf, 'optionalAccess', _46 => _46[axis]]);
|
|
2003
2292
|
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
2004
2293
|
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
2005
2294
|
};
|
|
@@ -2007,7 +2296,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2007
2296
|
const opts = [
|
|
2008
2297
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
2009
2298
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
2010
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2299
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _47 => _47.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _48 => _48.scale]), () => ( 1))))}`,
|
|
2011
2300
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
2012
2301
|
coord("x"),
|
|
2013
2302
|
coord("y"),
|
|
@@ -2015,7 +2304,9 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2015
2304
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
2016
2305
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
2017
2306
|
];
|
|
2018
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
2307
|
+
if (_optionalChain([tf, 'optionalAccess', _49 => _49.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
2308
|
+
if (videoGradeFfmpegFilters(clip.grade).length) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
|
|
2309
|
+
if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
|
|
2019
2310
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
2020
2311
|
cur = next;
|
|
2021
2312
|
continue;
|
|
@@ -2031,10 +2322,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2031
2322
|
source = `[${index}:v]`;
|
|
2032
2323
|
chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2033
2324
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
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`);
|
|
2037
|
-
}
|
|
2325
|
+
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
2326
|
+
if (grade.length) chain.push("format=rgba", ...grade);
|
|
2038
2327
|
chain.push("format=yuva420p");
|
|
2039
2328
|
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)'`);
|
|
2040
2329
|
else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
|
|
@@ -2048,18 +2337,50 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2048
2337
|
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}))`;
|
|
2049
2338
|
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
|
|
2050
2339
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2340
|
+
if (lead && clip.transition.type === "iris") {
|
|
2341
|
+
const r = videoIrisRadius({ width: W, height: H }, 1);
|
|
2342
|
+
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
|
|
2343
|
+
}
|
|
2344
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _50 => _50.rotation]);
|
|
2345
|
+
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
|
|
2346
|
+
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
2347
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _51 => _51.scale]))) {
|
|
2348
|
+
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
2349
|
+
chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
|
|
2350
|
+
}
|
|
2054
2351
|
chain.push(`setpts=PTS+${sec(t0)}/TB`);
|
|
2055
2352
|
const cx = rect.x + rect.width / 2;
|
|
2056
2353
|
const cy = rect.y + rect.height / 2;
|
|
2354
|
+
const shifts = { x: [], y: [] };
|
|
2355
|
+
const shiftBy = (direction, from, d, entering) => {
|
|
2356
|
+
const dir = _nullishCoalesce(direction, () => ( "right"));
|
|
2357
|
+
const axis = dir === "left" || dir === "right" ? "x" : "y";
|
|
2358
|
+
const size2 = axis === "x" ? W : H;
|
|
2359
|
+
const sign = dir === "left" || dir === "up" ? -1 : 1;
|
|
2360
|
+
const ease = `(1-cos(PI*clip((t-${sec(from)})/${sec(d)},0,1)))/2`;
|
|
2361
|
+
shifts[axis].push(`${sign * size2}*(${ease}${entering ? "-1" : ""})`);
|
|
2362
|
+
};
|
|
2363
|
+
const into = lead ? clip.transition.type : void 0;
|
|
2364
|
+
if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
|
|
2365
|
+
const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
|
|
2366
|
+
if (_optionalChain([after, 'optionalAccess', _52 => _52.transition, 'access', _53 => _53.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
|
|
2057
2367
|
const at = (axis, c) => {
|
|
2058
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
2059
|
-
|
|
2368
|
+
const v = _optionalChain([placed, 'optionalAccess', _54 => _54[axis]]);
|
|
2369
|
+
const extra = shifts[axis].map((s) => `+${s}`).join("");
|
|
2370
|
+
return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
|
|
2060
2371
|
};
|
|
2061
2372
|
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
|
|
2062
|
-
|
|
2373
|
+
if (clip.blend && clip.blend !== "normal") {
|
|
2374
|
+
graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[t${next}]`);
|
|
2375
|
+
graph.push(`[t${next}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable},format=gbrap,split[f${next}][g${next}]`);
|
|
2376
|
+
graph.push(`[${cur}]format=gbrap,split[c${next}][d${next}]`);
|
|
2377
|
+
const layerFirst = clip.blend === "color-dodge" || clip.blend === "color-burn";
|
|
2378
|
+
graph.push(`${layerFirst ? `[f${next}][c${next}]` : `[c${next}][f${next}]`}blend=all_mode=${FFMPEG_BLEND[clip.blend]}[b${next}]`);
|
|
2379
|
+
graph.push(`[g${next}]alphaextract[m${next}]`);
|
|
2380
|
+
graph.push(`[b${next}][m${next}]alphamerge[x${next}]`);
|
|
2381
|
+
graph.push(`[d${next}][x${next}]overlay=format=gbrp,format=yuva420p[${next}]`);
|
|
2382
|
+
blended.add(clip.blend);
|
|
2383
|
+
} else graph.push(`[${cur}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
|
|
2063
2384
|
cur = next;
|
|
2064
2385
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
2065
2386
|
if (before && clip.transition.type === "dip") {
|
|
@@ -2078,10 +2399,11 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2078
2399
|
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
2079
2400
|
else if (options.loudness && !options.loudness.measured)
|
|
2080
2401
|
notes.push("Loudness is normalised in one pass, which adjusts the level as it goes; for a fixed gain that keeps the dynamics, run toVideoLoudnessMeasure first and pass its result as loudness.measured.");
|
|
2081
|
-
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
2402
|
+
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _55 => _55.get, 'call', _56 => _56(c.asset), 'optionalAccess', _57 => _57.kind]) === "video" && _optionalChain([assets, 'access', _58 => _58.get, 'call', _59 => _59(c.asset), 'optionalAccess', _60 => _60.hasAudio]) === void 0)))
|
|
2082
2403
|
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.');
|
|
2083
2404
|
if (animated.size)
|
|
2084
2405
|
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)" : ""}.`);
|
|
2406
|
+
if (blended.has("soft-light")) notes.push("ffmpeg's soft-light blend uses a slightly different formula from the browser's, so the render can differ a little from the preview.");
|
|
2085
2407
|
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.");
|
|
2086
2408
|
const filterComplex = graph.join(";");
|
|
2087
2409
|
if (measure) {
|
|
@@ -2112,11 +2434,11 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2112
2434
|
|
|
2113
2435
|
// src/video-editor/preview.ts
|
|
2114
2436
|
var VideoPreviewEngine = (_class3 = class {
|
|
2115
|
-
constructor(source, canvas, options = {}) {;_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);
|
|
2437
|
+
constructor(source, canvas, options = {}) {;_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);
|
|
2116
2438
|
this.source = source;
|
|
2117
2439
|
this.options = options;
|
|
2118
2440
|
this.canvas = canvas;
|
|
2119
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access',
|
|
2441
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _61 => _61.getContext, 'optionalCall', _62 => _62("2d")]), () => ( null));
|
|
2120
2442
|
this.unsubscribe = source.subscribe(() => this.update());
|
|
2121
2443
|
this.update();
|
|
2122
2444
|
}
|
|
@@ -2129,6 +2451,8 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2129
2451
|
|
|
2130
2452
|
__init8() {this.scheduled = false}
|
|
2131
2453
|
__init9() {this.destroyed = false}
|
|
2454
|
+
__init10() {this.filterHost = null}
|
|
2455
|
+
__init11() {this.filters = /* @__PURE__ */ new Set()}
|
|
2132
2456
|
createMedia(kind) {
|
|
2133
2457
|
if (this.options.createMedia) return this.options.createMedia(kind);
|
|
2134
2458
|
const el = document.createElement(kind);
|
|
@@ -2164,8 +2488,8 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2164
2488
|
entry.media.pause();
|
|
2165
2489
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
2166
2490
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
2167
|
-
_optionalChain([entry, 'access',
|
|
2168
|
-
_optionalChain([entry, 'access',
|
|
2491
|
+
_optionalChain([entry, 'access', _63 => _63.media, 'access', _64 => _64.removeAttribute, 'optionalCall', _65 => _65("src")]);
|
|
2492
|
+
_optionalChain([entry, 'access', _66 => _66.media, 'access', _67 => _67.load, 'optionalCall', _68 => _68()]);
|
|
2169
2493
|
this.media.delete(entry.clip);
|
|
2170
2494
|
}
|
|
2171
2495
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -2198,7 +2522,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2198
2522
|
for (const layer of frame.layers) {
|
|
2199
2523
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
2200
2524
|
const sound = sounding.get(layer.clip);
|
|
2201
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
2525
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _69 => _69.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
2202
2526
|
sounding.delete(layer.clip);
|
|
2203
2527
|
}
|
|
2204
2528
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -2213,7 +2537,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2213
2537
|
if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
|
|
2214
2538
|
});
|
|
2215
2539
|
}
|
|
2216
|
-
|
|
2540
|
+
__init12() {this.sourceFor = (layer) => {
|
|
2217
2541
|
if (layer.assetKind === "image" && layer.src) {
|
|
2218
2542
|
const img = this.image(layer.src);
|
|
2219
2543
|
return img.complete ? img : null;
|
|
@@ -2230,12 +2554,31 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2230
2554
|
const h = Math.max(1, Math.round(frame.height * resolution));
|
|
2231
2555
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
2232
2556
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
2233
|
-
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution);
|
|
2234
|
-
_optionalChain([this, 'access',
|
|
2235
|
-
}
|
|
2557
|
+
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
|
|
2558
|
+
_optionalChain([this, 'access', _70 => _70.options, 'access', _71 => _71.onFrame, 'optionalCall', _72 => _72(frame)]);
|
|
2559
|
+
}
|
|
2560
|
+
__init13() {this.defineSvgFilter = (id, markup) => {
|
|
2561
|
+
if (this.options.defineSvgFilter === null) return;
|
|
2562
|
+
if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
|
|
2563
|
+
if (this.filters.has(id) || typeof document === "undefined") return;
|
|
2564
|
+
if (!this.filterHost) {
|
|
2565
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2566
|
+
svg.setAttribute("width", "0");
|
|
2567
|
+
svg.setAttribute("height", "0");
|
|
2568
|
+
svg.setAttribute("aria-hidden", "true");
|
|
2569
|
+
svg.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden");
|
|
2570
|
+
document.body.appendChild(svg);
|
|
2571
|
+
this.filterHost = svg;
|
|
2572
|
+
}
|
|
2573
|
+
this.filterHost.insertAdjacentHTML("beforeend", markup);
|
|
2574
|
+
this.filters.add(id);
|
|
2575
|
+
}}
|
|
2236
2576
|
/** Stops following the editor and releases every media element. */
|
|
2237
2577
|
destroy() {
|
|
2238
2578
|
this.destroyed = true;
|
|
2579
|
+
_optionalChain([this, 'access', _73 => _73.filterHost, 'optionalAccess', _74 => _74.remove, 'call', _75 => _75()]);
|
|
2580
|
+
this.filterHost = null;
|
|
2581
|
+
this.filters.clear();
|
|
2239
2582
|
this.unsubscribe();
|
|
2240
2583
|
for (const entry of [...this.media.values()]) this.release(entry);
|
|
2241
2584
|
this.images.clear();
|
|
@@ -2250,16 +2593,16 @@ var VideoEditorController = (_class4 = class {
|
|
|
2250
2593
|
|
|
2251
2594
|
|
|
2252
2595
|
|
|
2253
|
-
|
|
2596
|
+
__init14() {this.selection = []}
|
|
2254
2597
|
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2598
|
+
__init15() {this.error = null}
|
|
2599
|
+
__init16() {this.revision = 0}
|
|
2600
|
+
__init17() {this.applying = false}
|
|
2258
2601
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2602
|
+
__init18() {this.listeners = /* @__PURE__ */ new Set()}
|
|
2603
|
+
__init19() {this.unsubscribers = []}
|
|
2261
2604
|
|
|
2262
|
-
constructor(options = {}) {;_class4.prototype.
|
|
2605
|
+
constructor(options = {}) {;_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);_class4.prototype.__init19.call(this);_class4.prototype.__init20.call(this);_class4.prototype.__init21.call(this);
|
|
2263
2606
|
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
2264
2607
|
this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
|
|
2265
2608
|
this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
|
|
@@ -2284,8 +2627,8 @@ var VideoEditorController = (_class4 = class {
|
|
|
2284
2627
|
this.player.pause();
|
|
2285
2628
|
this.unsubscribers.splice(0).forEach((u) => u());
|
|
2286
2629
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2630
|
+
__init20() {this.getState = () => this.state}
|
|
2631
|
+
__init21() {this.subscribe = (listener) => {
|
|
2289
2632
|
this.connect();
|
|
2290
2633
|
this.listeners.add(listener);
|
|
2291
2634
|
return () => {
|
|
@@ -2341,7 +2684,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2341
2684
|
this.applying = false;
|
|
2342
2685
|
}
|
|
2343
2686
|
this.notify();
|
|
2344
|
-
_optionalChain([this, 'access',
|
|
2687
|
+
_optionalChain([this, 'access', _76 => _76.onChange, 'optionalCall', _77 => _77(project)]);
|
|
2345
2688
|
}
|
|
2346
2689
|
commit(label, next) {
|
|
2347
2690
|
const before = this.project;
|
|
@@ -2405,7 +2748,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2405
2748
|
}
|
|
2406
2749
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
2407
2750
|
select(clips, options = {}) {
|
|
2408
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
2751
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _78 => _78.filter, 'call', _79 => _79((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
2409
2752
|
let next;
|
|
2410
2753
|
if (options.add) {
|
|
2411
2754
|
next = [...this.selection];
|
|
@@ -2529,19 +2872,37 @@ var TRANSFORM = object(
|
|
|
2529
2872
|
);
|
|
2530
2873
|
var TRANSITION = object(
|
|
2531
2874
|
{
|
|
2532
|
-
type: { type: "string", enum: [...VIDEO_TRANSITION_TYPES], description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it)
|
|
2533
|
-
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut.
|
|
2875
|
+
type: { type: "string", enum: [...VIDEO_TRANSITION_TYPES], description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it), wipe (an edge travels across revealing this clip), push (this clip slides in and pushes the one before out), slide (this clip slides in over the one before) or iris (a circle opens from the centre)." },
|
|
2876
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut. Every type but dip needs half of it in media past the cut on both sides (handles); a dip needs none." },
|
|
2534
2877
|
color: COLOR("For dip: the colour passed through (default black)"),
|
|
2535
|
-
direction: { type: "string", enum: [...VIDEO_WIPE_DIRECTIONS], description: "For wipe: the way the edge travels (default right)." }
|
|
2878
|
+
direction: { type: "string", enum: [...VIDEO_WIPE_DIRECTIONS], description: "For wipe, push and slide: the way the edge or picture travels (default right, coming in from the left)." },
|
|
2879
|
+
audioCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fades through it: default equal-power for every type but dip (steady loudness while the two sounds overlap), linear for dip; or s-curve, exponential, logarithmic." }
|
|
2536
2880
|
},
|
|
2537
2881
|
["type", "duration"],
|
|
2538
2882
|
"A transition from the clip that ends where this one starts on the same track."
|
|
2539
2883
|
);
|
|
2884
|
+
var GRADE = object(
|
|
2885
|
+
Object.fromEntries(VIDEO_GRADE_CONTROLS.map((c) => [c.key, { type: "number", minimum: c.min, maximum: c.max, description: c.description }])),
|
|
2886
|
+
[],
|
|
2887
|
+
"Colour correction of the picture, applied in order: exposure, contrast, saturation, hue, white balance (temperature, tint). Leave a setting out for no change; the object replaces the clip's whole grade."
|
|
2888
|
+
);
|
|
2889
|
+
var ANIMATION = (when) => object(
|
|
2890
|
+
{
|
|
2891
|
+
effect: { type: "string", enum: [...VIDEO_CLIP_ANIMATION_EFFECTS], description: "fade; rise (up into place) or drop (down into place); slide-left / slide-right (moving that way); zoom (grows into place); pop (grows past its size and settles). Each also fades; an exit plays the movement backwards." },
|
|
2892
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms (default 500)." }
|
|
2893
|
+
},
|
|
2894
|
+
["effect"],
|
|
2895
|
+
when
|
|
2896
|
+
);
|
|
2540
2897
|
var CLIP_COMMON = {
|
|
2541
2898
|
name: { type: "string", description: "A label for the clip." },
|
|
2542
2899
|
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1). A number replaces any keyframes; set_video_keyframes animates it." },
|
|
2543
2900
|
transform: TRANSFORM,
|
|
2544
2901
|
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)." },
|
|
2902
|
+
grade: GRADE,
|
|
2903
|
+
animateIn: ANIMATION("How the picture enters, from the clip's start (on top of any fade in). Titles usually rise, pop or fade in."),
|
|
2904
|
+
animateOut: ANIMATION("How the picture leaves, ending at the clip's end."),
|
|
2905
|
+
blend: { type: "string", enum: [...VIDEO_BLEND_MODES], description: "How the picture mixes with what is beneath it, as CSS mix-blend-mode (default normal): multiply darkens (shadows, textures), screen and add lighten (light leaks, flares, glows), overlay and soft-light add contrast, difference and exclusion invert." },
|
|
2545
2906
|
fadeIn: MS("Fade up from nothing (picture and sound) over this long"),
|
|
2546
2907
|
fadeOut: MS("Fade down to nothing over this long")
|
|
2547
2908
|
};
|
|
@@ -2677,14 +3038,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2677
3038
|
return null;
|
|
2678
3039
|
};
|
|
2679
3040
|
const placed = (o) => ({
|
|
2680
|
-
..._optionalChain([o, 'access',
|
|
2681
|
-
..._optionalChain([o, 'access',
|
|
2682
|
-
..._optionalChain([o, 'access',
|
|
3041
|
+
..._optionalChain([o, 'access', _80 => _80.created, 'optionalAccess', _81 => _81.length]) ? { created: o.created } : {},
|
|
3042
|
+
..._optionalChain([o, 'access', _82 => _82.trimmed, 'optionalAccess', _83 => _83.length]) ? { trimmed: o.trimmed } : {},
|
|
3043
|
+
..._optionalChain([o, 'access', _84 => _84.removed, 'optionalAccess', _85 => _85.length]) ? { removed: o.removed } : {},
|
|
2683
3044
|
...o.rippled ? { rippled: o.rippled } : {}
|
|
2684
3045
|
});
|
|
2685
3046
|
const keysOut = (p, clip, property) => {
|
|
2686
3047
|
const c = clipOut(p, clip);
|
|
2687
|
-
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess',
|
|
3048
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _86 => _86.transform, 'optionalAccess', _87 => _87[property]]) : _optionalChain([c, 'optionalAccess', _88 => _88[property]]);
|
|
2688
3049
|
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
2689
3050
|
};
|
|
2690
3051
|
const getProject = {
|
|
@@ -2702,7 +3063,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2702
3063
|
const getFrame = {
|
|
2703
3064
|
name: "get_video_frame",
|
|
2704
3065
|
title: "What plays at a moment",
|
|
2705
|
-
description: "Returns what plays at a time without changing anything: the pictures back to front (clip, track, asset, source time in ms, opacity, placement rect, rotation, any wipe) and the sounds (clip, source time, gain after volumes, fades and transitions).",
|
|
3066
|
+
description: "Returns what plays at a time without changing anything: the pictures back to front (clip, track, asset, source time in ms, opacity, placement rect, rotation, any wipe or iris) and the sounds (clip, source time, gain after volumes, fades and transitions).",
|
|
2706
3067
|
inputSchema: object({ time: MS("Time from the start (default: the playhead)") }),
|
|
2707
3068
|
annotations: { title: "What plays at a moment", ...READ },
|
|
2708
3069
|
handler: ({ time }) => {
|
|
@@ -2724,7 +3085,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2724
3085
|
const checkProject = {
|
|
2725
3086
|
name: "check_video_project",
|
|
2726
3087
|
title: "Check the video project",
|
|
2727
|
-
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.",
|
|
3088
|
+
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 or animations longer than the clip, animations a clip's own keyframes override, keyframes outside their clip, assets with no duration, unused assets.",
|
|
2728
3089
|
inputSchema: object({}),
|
|
2729
3090
|
annotations: { title: "Check the video project", ...READ },
|
|
2730
3091
|
handler: () => {
|
|
@@ -2956,12 +3317,12 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2956
3317
|
const updateClip = {
|
|
2957
3318
|
name: "update_video_clip",
|
|
2958
3319
|
title: "Change a clip",
|
|
2959
|
-
description: `Changes a clip's properties (one undo step): opacity, transform, fit, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for titles text and style; for colours the colour. Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
|
|
3320
|
+
description: `Changes a clip's properties (one undo step): opacity, transform, fit, blend mode, colour grade, entrance and exit animations, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for titles text and style; for colours the colour. Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
|
|
2960
3321
|
inputSchema: object(
|
|
2961
3322
|
{
|
|
2962
3323
|
clip: CLIP_ID,
|
|
2963
3324
|
set: object({ ...CLIP_COMMON, ...MEDIA_FIELDS, asset: { ...ASSET_ID, description: "For media: play another asset (same in point)." }, ...TEXT_FIELDS, color: COLOR("Text colour, or a colour clip's fill") }, [], "Fields to replace (only those that fit the clip's type)."),
|
|
2964
|
-
remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background"] }, description: "Fields to reset to their defaults." }
|
|
3325
|
+
remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background"] }, description: "Fields to reset to their defaults." }
|
|
2965
3326
|
},
|
|
2966
3327
|
["clip"]
|
|
2967
3328
|
),
|
|
@@ -2969,17 +3330,60 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2969
3330
|
handler: ({ clip, set, remove }) => {
|
|
2970
3331
|
const current = clipOut(project(), clip);
|
|
2971
3332
|
if (current && set) {
|
|
2972
|
-
const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset", "fadeInCurve", "fadeOutCurve"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
|
|
3333
|
+
const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset", "fadeInCurve", "fadeOutCurve"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
|
|
2973
3334
|
const wrong = Object.keys(set).filter((k) => !allowed.has(k));
|
|
2974
3335
|
if (wrong.length) throw new Error(`${wrong.map((k) => `"${k}"`).join(", ")} cannot be set on a ${current.type} clip; it takes ${[...allowed].join(", ")}.`);
|
|
2975
3336
|
}
|
|
2976
3337
|
return apply({ op: "update_clip", clip, set, remove }, "Change clip", (r) => ({ clip: clipOut(r.project, clip) }));
|
|
2977
3338
|
}
|
|
2978
3339
|
};
|
|
3340
|
+
const gradeTargets = (ids) => {
|
|
3341
|
+
const p = project();
|
|
3342
|
+
for (const id of ids) {
|
|
3343
|
+
const at = findVideoClip(p, id);
|
|
3344
|
+
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3345
|
+
const { clip } = at;
|
|
3346
|
+
if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _89 => _89.assets, 'access', _90 => _90.find, 'call', _91 => _91((a) => a.id === clip.asset), 'optionalAccess', _92 => _92.kind]) === "audio")
|
|
3347
|
+
throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
const applyGradePreset = {
|
|
3351
|
+
name: "apply_video_grade_preset",
|
|
3352
|
+
title: "Apply a colour look",
|
|
3353
|
+
description: "Gives clips a named colour look at a strength (one undo step), replacing their grades: warm, cool, punchy (more contrast and colour), faded (lifted and muted), black-and-white, bleach-bypass (desaturated, contrasty), day-for-night (dark and blue). Returns each clip's resulting grade; tweak it with update_video_clip set.grade.",
|
|
3354
|
+
inputSchema: object(
|
|
3355
|
+
{
|
|
3356
|
+
clips: { type: "array", items: CLIP_ID, minItems: 1, description: "Clips on video tracks to grade." },
|
|
3357
|
+
preset: { type: "string", enum: [...VIDEO_GRADE_PRESET_NAMES], description: "The look." },
|
|
3358
|
+
amount: { type: "number", minimum: 0, maximum: 1, description: "Strength, 0 (none) to 1 (full, the default); 0.5 is a subtler version." }
|
|
3359
|
+
},
|
|
3360
|
+
["clips", "preset"]
|
|
3361
|
+
),
|
|
3362
|
+
annotations: { title: "Apply a colour look", ...WRITE_IDEMPOTENT },
|
|
3363
|
+
handler: ({ clips, preset, amount }) => {
|
|
3364
|
+
gradeTargets(clips);
|
|
3365
|
+
const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
|
|
3366
|
+
const edits = clips.map((clip) => grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] });
|
|
3367
|
+
return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _93 => _93(r.project, id), 'optionalAccess', _94 => _94.clip, 'access', _95 => _95.grade]), () => ( null)) })) }));
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
const copyGrade = {
|
|
3371
|
+
name: "copy_video_grade",
|
|
3372
|
+
title: "Copy a colour grade",
|
|
3373
|
+
description: "Gives clips the same colour grade as another clip (one undo step), to match shots in a scene; a source with no grade clears theirs. Returns the grade copied and the clips changed.",
|
|
3374
|
+
inputSchema: object({ from: { ...CLIP_ID, description: "The clip whose grade to copy." }, to: { type: "array", items: CLIP_ID, minItems: 1, description: "Clips on video tracks to give it to." } }, ["from", "to"]),
|
|
3375
|
+
annotations: { title: "Copy a colour grade", ...WRITE_IDEMPOTENT },
|
|
3376
|
+
handler: ({ from, to }) => {
|
|
3377
|
+
gradeTargets([from, ...to]);
|
|
3378
|
+
const grade = findVideoClip(project(), from).clip.grade;
|
|
3379
|
+
const edits = to.map((clip) => grade ? { op: "update_clip", clip, set: { grade: { ...grade } } } : { op: "update_clip", clip, remove: ["grade"] });
|
|
3380
|
+
return apply(edits, "Copy grade", () => ({ grade: _nullishCoalesce(grade, () => ( null)), clips: to }));
|
|
3381
|
+
}
|
|
3382
|
+
};
|
|
2979
3383
|
const setTransition = {
|
|
2980
3384
|
name: "set_video_transition",
|
|
2981
3385
|
title: "Set a transition",
|
|
2982
|
-
description: "Sets (or with null removes) the transition into a clip from the clip that ends where it starts on the same track (one undo step).
|
|
3386
|
+
description: "Sets (or with null removes) the transition into a clip from the clip that ends where it starts on the same track (one undo step). Every type but dip plays half before and half after the cut, so both clips need that much media past the cut; when they do not, the error says the longest that fits. Returns the clip.",
|
|
2983
3387
|
inputSchema: object({ clip: { ...CLIP_ID, description: "The incoming clip (the one after the cut)." }, transition: { anyOf: [TRANSITION, { type: "null" }], description: "The transition, or null to remove it." } }, ["clip", "transition"]),
|
|
2984
3388
|
annotations: { title: "Set a transition", ...WRITE_IDEMPOTENT },
|
|
2985
3389
|
handler: ({ clip, transition }) => apply({ op: "set_transition", clip, transition }, transition ? "Set transition" : "Remove transition", (r) => ({ clip: clipOut(r.project, clip) }))
|
|
@@ -3194,7 +3598,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3194
3598
|
}
|
|
3195
3599
|
};
|
|
3196
3600
|
const reads = [getProject, getFrame, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
3197
|
-
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];
|
|
3601
|
+
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
3198
3602
|
return {
|
|
3199
3603
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
3200
3604
|
namespace: options.namespace,
|
|
@@ -3230,6 +3634,39 @@ function videoClipShowsWaveform(clip, asset, mode = "audio") {
|
|
|
3230
3634
|
if (clip.type !== "media" || mode === "none" || !videoAssetHasSound(asset)) return false;
|
|
3231
3635
|
return mode === "all" || asset.kind === "audio";
|
|
3232
3636
|
}
|
|
3637
|
+
var VIDEO_CLIP_ANIMATION_LABELS = {
|
|
3638
|
+
fade: "Fade",
|
|
3639
|
+
rise: "Rise",
|
|
3640
|
+
drop: "Drop",
|
|
3641
|
+
"slide-left": "Slide left",
|
|
3642
|
+
"slide-right": "Slide right",
|
|
3643
|
+
zoom: "Zoom",
|
|
3644
|
+
pop: "Pop"
|
|
3645
|
+
};
|
|
3646
|
+
var VIDEO_TRANSITION_LABELS = {
|
|
3647
|
+
crossfade: "Crossfade",
|
|
3648
|
+
dip: "Dip to colour",
|
|
3649
|
+
wipe: "Wipe",
|
|
3650
|
+
push: "Push",
|
|
3651
|
+
slide: "Slide",
|
|
3652
|
+
iris: "Iris"
|
|
3653
|
+
};
|
|
3654
|
+
var videoTransitionHasDirection = (type) => type === "wipe" || type === "push" || type === "slide";
|
|
3655
|
+
var VIDEO_BLEND_MODE_LABELS = {
|
|
3656
|
+
normal: "Normal",
|
|
3657
|
+
multiply: "Multiply",
|
|
3658
|
+
screen: "Screen",
|
|
3659
|
+
overlay: "Overlay",
|
|
3660
|
+
darken: "Darken",
|
|
3661
|
+
lighten: "Lighten",
|
|
3662
|
+
"color-dodge": "Colour dodge",
|
|
3663
|
+
"color-burn": "Colour burn",
|
|
3664
|
+
"hard-light": "Hard light",
|
|
3665
|
+
"soft-light": "Soft light",
|
|
3666
|
+
difference: "Difference",
|
|
3667
|
+
exclusion: "Exclusion",
|
|
3668
|
+
add: "Add"
|
|
3669
|
+
};
|
|
3233
3670
|
var VIDEO_FADE_CURVE_LABELS = {
|
|
3234
3671
|
linear: "Linear",
|
|
3235
3672
|
"equal-power": "Equal power",
|
|
@@ -3247,7 +3684,7 @@ function videoClipLabel(clip, asset) {
|
|
|
3247
3684
|
if (clip.name) return clip.name;
|
|
3248
3685
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
3249
3686
|
if (clip.type === "color") return clip.color;
|
|
3250
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
3687
|
+
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _96 => _96.name]), () => ( _optionalChain([asset, 'optionalAccess', _97 => _97.src, 'access', _98 => _98.replace, 'call', _99 => _99(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
3251
3688
|
const speed = videoClipSpeed(clip);
|
|
3252
3689
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
3253
3690
|
}
|
|
@@ -3291,7 +3728,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
|
|
|
3291
3728
|
const asset = videoAssetMap(project).get(assetId);
|
|
3292
3729
|
if (!asset) return null;
|
|
3293
3730
|
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
3294
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access',
|
|
3731
|
+
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _100 => _100.tracks, 'access', _101 => _101.find, 'call', _102 => _102((t) => t.kind === kind && !t.locked), 'optionalAccess', _103 => _103.id])));
|
|
3295
3732
|
if (!track) return null;
|
|
3296
3733
|
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 } : {} };
|
|
3297
3734
|
}
|
|
@@ -3340,14 +3777,14 @@ var KEY_LABELS = {
|
|
|
3340
3777
|
scale: { label: "Scale", step: 0.05 },
|
|
3341
3778
|
rotation: { label: "Rotation", step: 1 }
|
|
3342
3779
|
};
|
|
3343
|
-
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access',
|
|
3780
|
+
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _104 => _104.transform, 'optionalAccess', _105 => _105[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
3344
3781
|
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
3345
3782
|
function videoKeyframeRows(project, clipId, time) {
|
|
3346
3783
|
const at = findVideoClip(project, clipId);
|
|
3347
3784
|
if (!at) return [];
|
|
3348
3785
|
const { clip } = at;
|
|
3349
3786
|
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
3350
|
-
const visual = !(_optionalChain([asset, 'optionalAccess',
|
|
3787
|
+
const visual = !(_optionalChain([asset, 'optionalAccess', _106 => _106.kind]) === "audio" || at.track.kind === "audio");
|
|
3351
3788
|
const audible = clip.type === "media" && videoAssetHasSound(asset);
|
|
3352
3789
|
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
3353
3790
|
const clipTime = playheadInClip(clip, time);
|
|
@@ -3518,5 +3955,34 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
|
3518
3955
|
|
|
3519
3956
|
|
|
3520
3957
|
|
|
3521
|
-
|
|
3958
|
+
|
|
3959
|
+
|
|
3960
|
+
|
|
3961
|
+
|
|
3962
|
+
|
|
3963
|
+
|
|
3964
|
+
|
|
3965
|
+
|
|
3966
|
+
|
|
3967
|
+
|
|
3968
|
+
|
|
3969
|
+
|
|
3970
|
+
|
|
3971
|
+
|
|
3972
|
+
|
|
3973
|
+
|
|
3974
|
+
|
|
3975
|
+
|
|
3976
|
+
|
|
3977
|
+
|
|
3978
|
+
|
|
3979
|
+
|
|
3980
|
+
|
|
3981
|
+
|
|
3982
|
+
|
|
3983
|
+
|
|
3984
|
+
|
|
3985
|
+
|
|
3986
|
+
|
|
3987
|
+
exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; 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_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoGaps = findVideoGaps; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; 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.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSpeed = videoClipSpeed; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; 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.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect;
|
|
3522
3988
|
//# sourceMappingURL=index.cjs.map
|