@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.
@@ -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', _4 => _4.transform, 'optionalAccess', _5 => _5[p]])) }))
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', _6 => _6.transform, 'optionalAccess', _7 => _7[p]]), set: (v) => c.transform && (c.transform[p] = v) });
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', _8 => _8.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
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', _9 => _9(this.p), 'access', _10 => _10.get, 'call', _11 => _11(clip.asset), 'optionalAccess', _12 => _12.kind]) === "image") return;
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', _13 => _13.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
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', _14 => _14.remove, 'optionalAccess', _15 => _15.includes, 'call', _16 => _16("speed")]) ? 1 : was;
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', _17 => _17.id]), clip.id, _optionalChain([after, 'optionalAccess', _18 => _18.id])].filter((x) => !!x), track: track.id, delta });
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', _19 => _19.transform, 'optionalAccess', _20 => _20[e.property]]) : c[e.property];
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', _21 => _21.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
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', _22 => _22.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _23 => _23.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _24 => _24.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _25 => _25.rotation]), clipTime, 0) };
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', _26 => _26.fadeInCurve]), (time - clip.start) / clip.fadeIn);
1409
- if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _27 => _27.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
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', _28 => _28.transition]) ? next.transition.duration / 2 : 0;
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', _29 => _29.type]) === "dip" && incoming.progress < 0.5) continue;
1440
- if (_optionalChain([outgoing, 'optionalAccess', _30 => _30.type]) === "dip" && outgoing.progress >= 0.5) continue;
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
- let opacity = videoValueAt(clip.opacity, clipTime, 1) * fade;
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 dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
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 rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, clip.transform, clipTime);
1467
- const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([clip, 'access', _32 => _32.transform, 'optionalAccess', _33 => _33.rotation]), clipTime, 0) };
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- body.push(attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner);
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', _34 => _34.name]), () => ( _optionalChain([asset, 'optionalAccess', _35 => _35.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
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 === "wipe" ? "W001" : "D"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
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', _36 => _36.name]), () => ( _optionalChain([asset, 'optionalAccess', _37 => _37.src]))), () => ( clip.asset))}`);
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 clip of sortedVideoClips(track)) {
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
- if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}`);
1977
- if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}`);
1978
- if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}`);
1979
- const after = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && _optionalChain([c, 'access', _38 => _38.transition, 'optionalAccess', _39 => _39.type]) === "dip");
1980
- if (after) chain2.push(`afade=t=out:st=${sec(span - after.transition.duration / 2)}:d=${sec(after.transition.duration / 2)}`);
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', _40 => _40[p]])));
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', _41 => _41[axis]]);
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', _42 => _42.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _43 => _43.scale]), () => ( 1))))}`,
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', _44 => _44.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
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
- if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _45 => _45.scale]))) {
2035
- const s = expr(placed.scale, onChain("t"), 1, "scale");
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
- const rotation = _optionalChain([placed, 'optionalAccess', _46 => _46.rotation]);
2052
- if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=none:ow='hypot(iw,ih)':oh=ow`);
2053
- else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=none:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
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', _47 => _47[axis]]);
2059
- return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2'`;
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
- graph.push(`[${cur}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
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', _48 => _48.get, 'call', _49 => _49(c.asset), 'optionalAccess', _50 => _50.kind]) === "video" && _optionalChain([assets, 'access', _51 => _51.get, 'call', _52 => _52(c.asset), 'optionalAccess', _53 => _53.hasAudio]) === void 0)))
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', _54 => _54.getContext, 'optionalCall', _55 => _55("2d")]), () => ( null));
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', _56 => _56.media, 'access', _57 => _57.removeAttribute, 'optionalCall', _58 => _58("src")]);
2168
- _optionalChain([entry, 'access', _59 => _59.media, 'access', _60 => _60.load, 'optionalCall', _61 => _61()]);
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', _62 => _62.speed]), () => ( 1)), sound ? sound.gain : null);
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
- __init10() {this.sourceFor = (layer) => {
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', _63 => _63.options, 'access', _64 => _64.onFrame, 'optionalCall', _65 => _65(frame)]);
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
- __init11() {this.selection = []}
2596
+ __init14() {this.selection = []}
2254
2597
 
2255
- __init12() {this.error = null}
2256
- __init13() {this.revision = 0}
2257
- __init14() {this.applying = false}
2598
+ __init15() {this.error = null}
2599
+ __init16() {this.revision = 0}
2600
+ __init17() {this.applying = false}
2258
2601
 
2259
- __init15() {this.listeners = /* @__PURE__ */ new Set()}
2260
- __init16() {this.unsubscribers = []}
2602
+ __init18() {this.listeners = /* @__PURE__ */ new Set()}
2603
+ __init19() {this.unsubscribers = []}
2261
2604
 
2262
- constructor(options = {}) {;_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_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);
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
- __init17() {this.getState = () => this.state}
2288
- __init18() {this.subscribe = (listener) => {
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', _66 => _66.onChange, 'optionalCall', _67 => _67(project)]);
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', _68 => _68.filter, 'call', _69 => _69((id) => findVideoClip(this.project, id))]), () => ( []));
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) or wipe (an edge travels across revealing this clip)." },
2533
- duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut. A crossfade or wipe needs half of it in media past the cut on both sides (handles); a dip needs none." },
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', _70 => _70.created, 'optionalAccess', _71 => _71.length]) ? { created: o.created } : {},
2681
- ..._optionalChain([o, 'access', _72 => _72.trimmed, 'optionalAccess', _73 => _73.length]) ? { trimmed: o.trimmed } : {},
2682
- ..._optionalChain([o, 'access', _74 => _74.removed, 'optionalAccess', _75 => _75.length]) ? { removed: o.removed } : {},
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', _76 => _76.transform, 'optionalAccess', _77 => _77[property]]) : _optionalChain([c, 'optionalAccess', _78 => _78[property]]);
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). A crossfade or wipe 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.",
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', _79 => _79.name]), () => ( _optionalChain([asset, 'optionalAccess', _80 => _80.src, 'access', _81 => _81.replace, 'call', _82 => _82(/^.*[\\/]/, "")]))), () => ( clip.asset));
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', _83 => _83.tracks, 'access', _84 => _84.find, 'call', _85 => _85((t) => t.kind === kind && !t.locked), 'optionalAccess', _86 => _86.id])));
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', _87 => _87.transform, 'optionalAccess', _88 => _88[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
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', _89 => _89.kind]) === "audio" || at.track.kind === "audio");
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
- 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_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_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.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.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; 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.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.videoHourFrames = videoHourFrames; 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.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect;
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