@pixerate/editor 0.85.0 → 0.86.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.
@@ -340,6 +340,105 @@ function videoGradeFfmpegFilters(grade) {
340
340
  });
341
341
  }
342
342
 
343
+ // src/video-editor/remap.ts
344
+ function videoClipSourceTime(clip, clipTime) {
345
+ const base = _nullishCoalesce(clip.in, () => ( 0));
346
+ return clip.timeRemap ? base + videoValueAt(clip.timeRemap, clipTime, 0) : base + clipTime * (_nullishCoalesce(clip.speed, () => ( 1)));
347
+ }
348
+ function videoClipSpeedAt(clip, clipTime) {
349
+ if (clip.type !== "media") return 1;
350
+ if (!clip.timeRemap) return _nullishCoalesce(clip.speed, () => ( 1));
351
+ const t = Math.min(Math.max(clipTime, 0), Math.max(0, clip.duration - 1));
352
+ return Math.max(0, videoClipSourceTime(clip, t + 1) - videoClipSourceTime(clip, t));
353
+ }
354
+ function videoSpeedRampKeys(segments) {
355
+ if (!segments.length) throw new Error("Give at least one stretch { duration, speed }.");
356
+ const keys = [];
357
+ let t = 0;
358
+ let v = 0;
359
+ segments.forEach((s, i) => {
360
+ if (!(s.duration > 0)) throw new Error(`Stretch ${i + 1} needs a duration above 0 ms.`);
361
+ for (const [name, x] of [["speed", s.speed], ["toSpeed", s.toSpeed]])
362
+ if (x !== void 0 && !(x >= 0 && x <= 20)) throw new Error(`Stretch ${i + 1}'s ${name} must be from 0 (a freeze) to 20; got ${x}.`);
363
+ const a = s.speed;
364
+ const b = _nullishCoalesce(s.toSpeed, () => ( s.speed));
365
+ const avg = (a + b) / 2;
366
+ let ease = "linear";
367
+ if (a !== b && avg > 0) {
368
+ ease = [1 / 3, 2 * a / (3 * (a + b)), 2 / 3, 1 - 2 * b / (3 * (a + b))].map((n2) => Math.round(n2 * 1e6) / 1e6);
369
+ }
370
+ keys.push({ t, v, ...ease === "linear" ? {} : { ease } });
371
+ t += s.duration;
372
+ v += avg * s.duration;
373
+ });
374
+ keys.push({ t, v });
375
+ return { keys: mergeLinear(keys), duration: t, source: v };
376
+ }
377
+ function mergeLinear(keys) {
378
+ const out = [keys[0]];
379
+ for (let i = 1; i < keys.length; i++) {
380
+ const prev = out[out.length - 1];
381
+ const k = keys[i];
382
+ const before = out.length > 1 ? out[out.length - 2] : null;
383
+ if (before && !before.ease && !prev.ease) {
384
+ const s1 = (prev.v - before.v) / (prev.t - before.t);
385
+ const s2 = (k.v - prev.v) / (k.t - prev.t);
386
+ if (Math.abs(s1 - s2) < 1e-9) {
387
+ out[out.length - 1] = k;
388
+ continue;
389
+ }
390
+ }
391
+ out.push(k);
392
+ }
393
+ return out;
394
+ }
395
+ function checkVideoTimeRemap(remap, path = "timeRemap") {
396
+ const { keys } = remap;
397
+ if (!keys.length) throw new Error(`${path} needs at least one key.`);
398
+ keys.forEach((k, i) => {
399
+ if (k.v < 0) throw new Error(`${path}.keys[${i}].v must be at least 0 (ms after the in point).`);
400
+ const next = keys[i + 1];
401
+ if (!next) return;
402
+ if (next.v < k.v - 1e-6) throw new Error(`${path}.keys[${i + 1}] goes back in the source (${next.v} ms after ${k.v} ms); a ramp only plays forwards, so make each value at least the one before.`);
403
+ if (!videoEaseBezier(k.ease)) return;
404
+ let prev = k.v;
405
+ for (let n2 = 1; n2 <= 32; n2++) {
406
+ const v = videoValueAt(remap, k.t + (next.t - k.t) * n2 / 32);
407
+ if (v < prev - 1e-3) throw new Error(`${path}.keys[${i}].ease overshoots, which would play backwards; use a curve that only moves forwards (or linear).`);
408
+ prev = v;
409
+ }
410
+ });
411
+ }
412
+ function videoTimeRemapSpan(remap) {
413
+ return { from: remap.keys[0].v, to: remap.keys[remap.keys.length - 1].v };
414
+ }
415
+ function videoTimeRemapBreakpoints(remap, step) {
416
+ const { points, holds } = videoKeyframeBreakpoints(remap, step);
417
+ const out = [];
418
+ points.forEach(([t, v], i) => {
419
+ out.push({ t, v });
420
+ if (holds.has(i) && points[i + 1]) out.push({ t: points[i + 1][0], v });
421
+ });
422
+ return out;
423
+ }
424
+ function videoSpeedRampEdit(clip, segments, ripple = true) {
425
+ const ramp = videoSpeedRampKeys(segments);
426
+ const offset = videoClipSourceTime(clip, 0) - (_nullishCoalesce(clip.in, () => ( 0)));
427
+ return { op: "set_time_remap", clip: clip.id, keys: ramp.keys.map((k) => ({ ...k, v: k.v + offset })), duration: ramp.duration, ripple };
428
+ }
429
+ function videoFreezeFrameEdit(clip, at, duration, ripple = true) {
430
+ if (at < 0 || at > clip.duration) throw new Error(`${Math.round(at)} ms is outside clip "${clip.id}" (0\u2013${clip.duration} ms of clip time).`);
431
+ const remap = _nullishCoalesce(clip.timeRemap, () => ( { keys: [{ t: 0, v: 0 }, { t: clip.duration, v: clip.duration * (_nullishCoalesce(clip.speed, () => ( 1))) }] }));
432
+ const [left, right] = splitVideoAnimatable(remap, at);
433
+ const keys = [...left.keys.slice(0, -1), { ...left.keys[left.keys.length - 1], ease: "linear" }, ...right.keys.map((k) => ({ ...k, t: k.t + at + duration }))];
434
+ return { op: "set_time_remap", clip: clip.id, keys, duration: clip.duration + duration, ripple };
435
+ }
436
+ var videoSlowMotionRamp = (duration) => [
437
+ { duration: Math.max(1, duration / 2), speed: 1 },
438
+ { duration: 500, speed: 1, toSpeed: 0.3 },
439
+ { duration: 1500, speed: 0.3 }
440
+ ];
441
+
343
442
  // src/video-editor/parse.ts
344
443
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
345
444
  var fail = (path, what) => {
@@ -408,6 +507,18 @@ function parseVideoTransform(v, path) {
408
507
  rotation: parseVideoAnimatable(v.rotation, `${path}.rotation`, { optional: true })
409
508
  });
410
509
  }
510
+ function parseVideoTimeRemap(v, speed, path) {
511
+ if (v === void 0) return void 0;
512
+ const remap = parseVideoAnimatable(v, path, { optional: true, min: 0 });
513
+ if (typeof remap === "number" || remap === void 0) return fail(path, "must be keyframes { keys: [{ t, v, ease }] } (v: ms of source after the in point)");
514
+ if (speed !== void 0 && speed !== 1) fail(path, 'cannot be used with a speed; a ramp sets the speed itself (remove "speed")');
515
+ try {
516
+ checkVideoTimeRemap(remap, path);
517
+ } catch (e) {
518
+ throw new Error(e.message.replace(/\.?$/, "."));
519
+ }
520
+ return remap;
521
+ }
411
522
  function parseVideoClipAnimation(v, path) {
412
523
  if (v === void 0) return void 0;
413
524
  if (!isObject(v)) return fail(path, "must be an object { effect, duration }");
@@ -494,6 +605,7 @@ function parseVideoClip(v, path = "clip") {
494
605
  speed: num(v.speed, `${path}.speed`, { optional: true, min: 0.05, max: 20 }),
495
606
  volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
496
607
  muted: bool(v.muted, `${path}.muted`),
608
+ timeRemap: parseVideoTimeRemap(v.timeRemap, v.speed, `${path}.timeRemap`),
497
609
  fadeInCurve: oneOf(v.fadeInCurve, VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
498
610
  fadeOutCurve: oneOf(v.fadeOutCurve, VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
499
611
  });
@@ -589,6 +701,7 @@ var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.spe
589
701
  function videoClipSourceRange(clip) {
590
702
  if (clip.type !== "media") return null;
591
703
  const start = _nullishCoalesce(clip.in, () => ( 0));
704
+ if (_optionalChain([clip, 'access', _2 => _2.timeRemap, 'optionalAccess', _3 => _3.keys, 'access', _4 => _4.length])) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
592
705
  return { in: start, out: start + clip.duration * videoClipSpeed(clip) };
593
706
  }
594
707
  var videoFrameDuration = (fps) => 1e3 / fps;
@@ -669,13 +782,13 @@ function videoClipHandles(clip, assets) {
669
782
  const speed = videoClipSpeed(clip);
670
783
  const range = videoClipSourceRange(clip);
671
784
  const before = range.in / speed;
672
- if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _2 => _2.kind]) === "image" ? Infinity : before, after: Infinity };
785
+ if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _5 => _5.kind]) === "image" ? Infinity : before, after: Infinity };
673
786
  return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
674
787
  }
675
788
  function videoClipOverhang(track, clip) {
676
789
  const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
677
790
  const next = videoClipAfter(track, clip);
678
- const tail = _optionalChain([next, 'optionalAccess', _3 => _3.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
791
+ const tail = _optionalChain([next, 'optionalAccess', _6 => _6.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
679
792
  return { lead, tail };
680
793
  }
681
794
  function videoSnapPoints(project, options = {}) {
@@ -759,7 +872,7 @@ function videoClipAnimated(clip, project) {
759
872
  const spans = videoClipAnimationSpans(clip);
760
873
  const out = { ...clip, transform: { ...clip.transform } };
761
874
  for (const prop of PROPS) {
762
- const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _4 => _4.transform, 'optionalAccess', _5 => _5[prop]]);
875
+ const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _7 => _7.transform, 'optionalAccess', _8 => _8[prop]]);
763
876
  if (isVideoKeyframes(current)) continue;
764
877
  const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
765
878
  const away = (spec) => {
@@ -796,7 +909,7 @@ function videoClipKeyframes(clip) {
796
909
  const out = [
797
910
  { property: "opacity", keys: videoKeysOf(clip.opacity) },
798
911
  { property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
799
- ...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _6 => _6.transform, 'optionalAccess', _7 => _7[p]])) }))
912
+ ...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _9 => _9.transform, 'optionalAccess', _10 => _10[p]])) }))
800
913
  ];
801
914
  return out.filter((o) => o.keys.length);
802
915
  }
@@ -853,7 +966,7 @@ function checkVideoProject(project) {
853
966
  for (const which of ["animateIn", "animateOut"]) {
854
967
  const a = clip[which];
855
968
  if (!a) continue;
856
- const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _8 => _8.transform, 'optionalAccess', _9 => _9[p]])).length);
969
+ const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _11 => _11.transform, 'optionalAccess', _12 => _12[p]])).length);
857
970
  if (keyed.length)
858
971
  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.` });
859
972
  }
@@ -928,6 +1041,7 @@ var VIDEO_EDIT_OPS = [
928
1041
  "extract_range",
929
1042
  "split_at",
930
1043
  "set_keyframes",
1044
+ "set_time_remap",
931
1045
  "close_gaps",
932
1046
  "add_marker",
933
1047
  "update_marker",
@@ -955,13 +1069,16 @@ function mergeSpans(spans) {
955
1069
  }
956
1070
  return out;
957
1071
  }
1072
+ var isRamped = (clip) => clip.type === "media" && !!clip.timeRemap;
958
1073
  function clipAnimatables(clip) {
959
1074
  const c = clip;
960
1075
  const out = [
961
1076
  { get: () => c.opacity, set: (v) => c.opacity = v },
962
- { get: () => c.volume, set: (v) => c.volume = v }
1077
+ { get: () => c.volume, set: (v) => c.volume = v },
1078
+ // A speed ramp is keys in clip time too, so trims and splits keep it on the same frames of source.
1079
+ { get: () => c.timeRemap, set: (v) => c.timeRemap = v }
963
1080
  ];
964
- 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) });
1081
+ for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _13 => _13.transform, 'optionalAccess', _14 => _14[p]]), set: (v) => c.transform && (c.transform[p] = v) });
965
1082
  return out;
966
1083
  }
967
1084
  function retimeClipKeys(clip, fn) {
@@ -970,7 +1087,7 @@ function retimeClipKeys(clip, fn) {
970
1087
  if (isVideoKeyframes(v)) a.set(fn(v));
971
1088
  }
972
1089
  }
973
- 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"];
1090
+ var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
974
1091
  var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
975
1092
  var Editor = (_class = class {
976
1093
  constructor(p) {;_class.prototype.__init.call(this);
@@ -1009,7 +1126,7 @@ var Editor = (_class = class {
1009
1126
  kindFits(track, clip) {
1010
1127
  const asset = clip.type === "media" ? this.asset(clip.asset) : null;
1011
1128
  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.`);
1012
- 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.`);
1129
+ if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _15 => _15.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
1013
1130
  }
1014
1131
  /** Moves every clip on `track` starting at or after `from` by `delta`. */
1015
1132
  shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
@@ -1051,13 +1168,13 @@ var Editor = (_class = class {
1051
1168
  }
1052
1169
  /** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
1053
1170
  moveIn(clip, delta) {
1054
- 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;
1171
+ if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _16 => _16(this.p), 'access', _17 => _17.get, 'call', _18 => _18(clip.asset), 'optionalAccess', _19 => _19.kind]) === "image") return;
1055
1172
  clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
1056
1173
  if (clip.in < 1e-9) delete clip.in;
1057
1174
  }
1058
1175
  /** Moves a clip's head `delta` later in time (start, in point and keys); the caller sets start and duration. */
1059
1176
  trimHead(clip, delta) {
1060
- this.moveIn(clip, delta);
1177
+ if (!isRamped(clip)) this.moveIn(clip, delta);
1061
1178
  retimeClipKeys(clip, (v) => retimeVideoAnimatable(v, -delta));
1062
1179
  }
1063
1180
  /** Cuts `clip` at `at` (inside it): the left part keeps the id, the right part is returned. */
@@ -1074,7 +1191,7 @@ var Editor = (_class = class {
1074
1191
  if (isVideoKeyframes(l)) leftValues[i].set(l);
1075
1192
  if (isVideoKeyframes(rr)) r.set(rr);
1076
1193
  });
1077
- this.moveIn(right, at - clip.start);
1194
+ if (!isRamped(right)) this.moveIn(right, at - clip.start);
1078
1195
  clip.duration = at - clip.start;
1079
1196
  delete clip.fadeOut;
1080
1197
  delete clip.animateOut;
@@ -1286,7 +1403,7 @@ var Editor = (_class = class {
1286
1403
  if (e.edge === "start") {
1287
1404
  const delta = e.to - clip.start;
1288
1405
  if (clip.duration - delta < minimum - VIDEO_TIME_EPSILON) refuse(`Trimming the start to ${formatVideoSeconds(e.to)} would leave "${clip.id}" shorter than a frame; it ends at ${formatVideoSeconds(end)}.`);
1289
- if (clip.type === "media") {
1406
+ if (clip.type === "media" && !isRamped(clip)) {
1290
1407
  const sourceIn = (_nullishCoalesce(clip.in, () => ( 0))) + delta * speed;
1291
1408
  if (sourceIn < -VIDEO_TIME_EPSILON) refuse(`"${clip.id}" starts ${Math.round((_nullishCoalesce(clip.in, () => ( 0))) / speed)} ms into its source, so its start can move at most that far earlier (to ${formatVideoSeconds(clip.start - (_nullishCoalesce(clip.in, () => ( 0))) / speed)}).`);
1292
1409
  clip.in = Math.max(0, sourceIn);
@@ -1314,10 +1431,10 @@ var Editor = (_class = class {
1314
1431
  } else {
1315
1432
  const duration = e.to - clip.start;
1316
1433
  if (duration < minimum - VIDEO_TIME_EPSILON) refuse(`Trimming the end to ${formatVideoSeconds(e.to)} would leave "${clip.id}" shorter than a frame; it starts at ${formatVideoSeconds(clip.start)}.`);
1317
- if (clip.type === "media") {
1434
+ if (clip.type === "media" && !isRamped(clip)) {
1318
1435
  const asset = assets.get(clip.asset);
1319
1436
  const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
1320
- if (_optionalChain([asset, 'optionalAccess', _17 => _17.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
1437
+ if (_optionalChain([asset, 'optionalAccess', _20 => _20.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
1321
1438
  refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
1322
1439
  }
1323
1440
  if (e.ripple) {
@@ -1370,10 +1487,12 @@ var Editor = (_class = class {
1370
1487
  for (const k of ["id", "type"]) if (k in set) refuse(`A clip's ${k} cannot be changed; delete it and add a new one.`);
1371
1488
  for (const k of _nullishCoalesce(e.remove, () => ( []))) if (!OPTIONAL_CLIP_FIELDS.includes(k)) refuse(`"${k}" cannot be removed; removable fields are ${OPTIONAL_CLIP_FIELDS.join(", ")}.`);
1372
1489
  if ("speed" in set && (typeof set.speed !== "number" || !(set.speed > 0))) refuse('"speed" must be a number greater than 0 (1 = normal speed).');
1490
+ if ("speed" in set && isRamped(at.clip)) refuse(`Clip "${at.clip.id}" has a speed ramp, which sets its speed; change the ramp, or remove it first.`);
1491
+ if ("timeRemap" in set) refuse("Set a speed ramp with set_time_remap, which checks it against the media and moves later clips if asked.");
1373
1492
  let stretch = 1;
1374
1493
  if (at.clip.type === "media" && !("duration" in set)) {
1375
1494
  const was = videoClipSpeed(at.clip);
1376
- const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _18 => _18.remove, 'optionalAccess', _19 => _19.includes, 'call', _20 => _20("speed")]) ? 1 : was;
1495
+ const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _21 => _21.remove, 'optionalAccess', _22 => _22.includes, 'call', _23 => _23("speed")]) ? 1 : was;
1377
1496
  if (now !== was) {
1378
1497
  set.duration = at.clip.duration * was / now;
1379
1498
  stretch = was / now;
@@ -1389,6 +1508,56 @@ var Editor = (_class = class {
1389
1508
  at.track.clips.sort((a, b) => a.start - b.start);
1390
1509
  return this.outcomes.push({ op: e.op, clip: clip.id, track: at.track.id });
1391
1510
  }
1511
+ case "set_time_remap": {
1512
+ const { track, clip } = this.clip(e.clip);
1513
+ this.unlocked(track);
1514
+ if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to ramp; only video and audio clips change speed.`);
1515
+ const asset = this.asset(clip.asset);
1516
+ if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no motion to ramp.`);
1517
+ const media = clip;
1518
+ const end = videoClipEnd(media);
1519
+ const duration = _nullishCoalesce(e.duration, () => ( media.duration));
1520
+ if (!(duration >= 1e3 / this.p.fps - VIDEO_TIME_EPSILON)) refuse(`"duration" must be at least a frame (${Math.ceil(1e3 / this.p.fps)} ms).`);
1521
+ if (e.keys === null) {
1522
+ if (!media.timeRemap) refuse(`Clip "${clip.id}" has no speed ramp to remove.`);
1523
+ const from = (_nullishCoalesce(media.in, () => ( 0))) + media.timeRemap.keys[0].v;
1524
+ if (asset.duration !== void 0 && from + duration > asset.duration + VIDEO_TIME_EPSILON)
1525
+ refuse(`At normal speed from ${Math.round(from)} ms, "${clip.id}" would run past the end of "${asset.id}" (${asset.duration} ms); give a duration of at most ${Math.floor(asset.duration - from)} ms.`);
1526
+ delete media.timeRemap;
1527
+ media.in = from;
1528
+ if (media.in < 1e-9) delete media.in;
1529
+ } else {
1530
+ if (!Array.isArray(e.keys) || !e.keys.length) refuse('"keys" must be a list of at least one { t, v, ease } (or null to remove the ramp).');
1531
+ const remap = parseVideoTimeRemap({ keys: e.keys }, void 0, "keys");
1532
+ remap.keys = sortVideoKeys(remap.keys);
1533
+ checkVideoTimeRemap(remap, "keys");
1534
+ const span = videoTimeRemapSpan(remap);
1535
+ if (asset.duration !== void 0 && (_nullishCoalesce(media.in, () => ( 0))) + span.to > asset.duration + VIDEO_TIME_EPSILON)
1536
+ refuse(`The ramp plays ${Math.round(span.to)} ms of source from the in point (${_nullishCoalesce(media.in, () => ( 0))} ms), past the end of "${asset.id}" (${asset.duration} ms); it can use at most ${Math.floor(asset.duration - (_nullishCoalesce(media.in, () => ( 0))))} ms.`);
1537
+ media.timeRemap = remap;
1538
+ delete media.speed;
1539
+ }
1540
+ const newEnd = media.start + duration;
1541
+ const outcome = { op: e.op, clip: clip.id, track: track.id };
1542
+ if (Math.abs(newEnd - end) > VIDEO_TIME_EPSILON) {
1543
+ if (e.ripple) {
1544
+ const tracks = this.rippleTracks([track]);
1545
+ if (newEnd < end) {
1546
+ media.duration = duration;
1547
+ this.close(tracks, newEnd, end);
1548
+ } else {
1549
+ const created = this.open(tracks, end, newEnd - end, /* @__PURE__ */ new Set([clip.id]));
1550
+ media.duration = duration;
1551
+ if (created.length) outcome.created = created;
1552
+ }
1553
+ outcome.rippled = tracks.map((t) => t.id);
1554
+ } else {
1555
+ if (newEnd > end) this.fits(track, end, newEnd - end, /* @__PURE__ */ new Set([clip.id]), `Lengthening "${clip.id}"`);
1556
+ media.duration = duration;
1557
+ }
1558
+ }
1559
+ return this.outcomes.push(outcome);
1560
+ }
1392
1561
  case "set_transition": {
1393
1562
  const { track, clip } = this.clip(e.clip);
1394
1563
  this.unlocked(track);
@@ -1469,7 +1638,7 @@ var Editor = (_class = class {
1469
1638
  this.trimHead(after, delta);
1470
1639
  }
1471
1640
  track.clips.sort((a, b) => a.start - b.start);
1472
- 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 });
1641
+ return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _24 => _24.id]), clip.id, _optionalChain([after, 'optionalAccess', _25 => _25.id])].filter((x) => !!x), track: track.id, delta });
1473
1642
  }
1474
1643
  case "lift_range":
1475
1644
  case "extract_range": {
@@ -1500,7 +1669,7 @@ var Editor = (_class = class {
1500
1669
  const limits = VIDEO_KEYFRAME_LIMITS[e.property];
1501
1670
  const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
1502
1671
  const c = clip;
1503
- const current = transform ? _optionalChain([c, 'access', _23 => _23.transform, 'optionalAccess', _24 => _24[e.property]]) : c[e.property];
1672
+ const current = transform ? _optionalChain([c, 'access', _26 => _26.transform, 'optionalAccess', _27 => _27[e.property]]) : c[e.property];
1504
1673
  let value;
1505
1674
  if (e.keys === null) {
1506
1675
  const v = videoValueAt(current, 0, limits.default);
@@ -1582,7 +1751,7 @@ function applyVideoEdits(project, edits) {
1582
1751
  editor.run(edit);
1583
1752
  } catch (error) {
1584
1753
  const message = error.message;
1585
- 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."}` };
1754
+ return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _28 => _28.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
1586
1755
  }
1587
1756
  }
1588
1757
  let next;
@@ -1601,7 +1770,7 @@ function applyVideoEdits(project, edits) {
1601
1770
 
1602
1771
  // src/video-editor/evaluate.ts
1603
1772
  function videoTransformAt(transform, clipTime = 0) {
1604
- 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) };
1773
+ return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _29 => _29.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _30 => _30.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _31 => _31.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _32 => _32.rotation]), clipTime, 0) };
1605
1774
  }
1606
1775
  function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
1607
1776
  let w = size.width;
@@ -1648,8 +1817,8 @@ function videoFadeCurveGain(curve, x) {
1648
1817
  function fadeGain(clip, time, sound = false) {
1649
1818
  let g = 1;
1650
1819
  const shape = sound && clip.type === "media" ? clip : void 0;
1651
- if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _30 => _30.fadeInCurve]), (time - clip.start) / clip.fadeIn);
1652
- if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _31 => _31.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
1820
+ if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _33 => _33.fadeInCurve]), (time - clip.start) / clip.fadeIn);
1821
+ if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _34 => _34.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
1653
1822
  return g;
1654
1823
  }
1655
1824
  function videoClipSoundGain(clip, clipTime) {
@@ -1662,7 +1831,7 @@ function activeOnTrack(track, time) {
1662
1831
  const before = clip.transition ? videoClipBefore(track, clip) : null;
1663
1832
  const lead = before ? clip.transition.duration / 2 : 0;
1664
1833
  const next = videoClipAfter(track, clip);
1665
- const tail = _optionalChain([next, 'optionalAccess', _32 => _32.transition]) ? next.transition.duration / 2 : 0;
1834
+ const tail = _optionalChain([next, 'optionalAccess', _35 => _35.transition]) ? next.transition.duration / 2 : 0;
1666
1835
  if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
1667
1836
  const a = { clip };
1668
1837
  if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
@@ -1671,7 +1840,7 @@ function activeOnTrack(track, time) {
1671
1840
  }
1672
1841
  return out;
1673
1842
  }
1674
- var sourceTimeAt = (clip, time) => clip.type === "media" ? (_nullishCoalesce(clip.in, () => ( 0))) + (time - clip.start) * videoClipSpeed(clip) : 0;
1843
+ var sourceTimeAt = (clip, time) => clip.type === "media" ? videoClipSourceTime(clip, time - clip.start) : 0;
1675
1844
  function videoFrameAt(project, time) {
1676
1845
  const assets = videoAssetMap(project);
1677
1846
  const layers = [];
@@ -1679,8 +1848,8 @@ function videoFrameAt(project, time) {
1679
1848
  for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
1680
1849
  const track = project.tracks[ti];
1681
1850
  for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
1682
- if (_optionalChain([incoming, 'optionalAccess', _33 => _33.type]) === "dip" && incoming.progress < 0.5) continue;
1683
- if (_optionalChain([outgoing, 'optionalAccess', _34 => _34.type]) === "dip" && outgoing.progress >= 0.5) continue;
1851
+ if (_optionalChain([incoming, 'optionalAccess', _36 => _36.type]) === "dip" && incoming.progress < 0.5) continue;
1852
+ if (_optionalChain([outgoing, 'optionalAccess', _37 => _37.type]) === "dip" && outgoing.progress >= 0.5) continue;
1684
1853
  const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
1685
1854
  const fade = fadeGain(clip, inside);
1686
1855
  const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
@@ -1691,7 +1860,7 @@ function videoFrameAt(project, time) {
1691
1860
  const audible = videoAssetHasSound(asset);
1692
1861
  if (audible && clip.type === "media" && !clip.muted && !track.muted) {
1693
1862
  const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fadeGain(clip, inside, true) * mix;
1694
- audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeed(clip) });
1863
+ audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
1695
1864
  }
1696
1865
  if (track.kind !== "video" || track.hidden) continue;
1697
1866
  if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
@@ -1711,8 +1880,8 @@ function videoFrameAt(project, time) {
1711
1880
  shift = videoTransitionEase(incoming.progress) - 1;
1712
1881
  } else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
1713
1882
  }
1714
- if (_optionalChain([outgoing, 'optionalAccess', _35 => _35.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
1715
- if (_optionalChain([outgoing, 'optionalAccess', _36 => _36.type]) === "push") {
1883
+ if (_optionalChain([outgoing, 'optionalAccess', _38 => _38.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
1884
+ if (_optionalChain([outgoing, 'optionalAccess', _39 => _39.type]) === "push") {
1716
1885
  shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
1717
1886
  shift = videoTransitionEase(outgoing.progress);
1718
1887
  }
@@ -1722,7 +1891,7 @@ function videoFrameAt(project, time) {
1722
1891
  const across = shiftDirection === "left" || shiftDirection === "right";
1723
1892
  const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
1724
1893
  const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
1725
- 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) };
1894
+ const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _40 => _40.transform, 'optionalAccess', _41 => _41.rotation]), clipTime, 0) };
1726
1895
  if (wipe) layer.wipe = wipe;
1727
1896
  if (iris) layer.iris = iris;
1728
1897
  if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
@@ -2020,8 +2189,8 @@ function videoTranscriptTrack(project, track) {
2020
2189
  return track;
2021
2190
  }
2022
2191
  const assets = videoAssetMap(project);
2023
- const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _39 => _39.get, 'call', _40 => _40(c.asset), 'optionalAccess', _41 => _41.transcript, 'optionalAccess', _42 => _42.length]));
2024
- return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _43 => _43.id]), () => ( null));
2192
+ const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _42 => _42.get, 'call', _43 => _43(c.asset), 'optionalAccess', _44 => _44.transcript, 'optionalAccess', _45 => _45.length]));
2193
+ return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _46 => _46.id]), () => ( null));
2025
2194
  }
2026
2195
  function videoTimelineTranscript(project, options = {}) {
2027
2196
  const id = videoTranscriptTrack(project, options.track);
@@ -2031,8 +2200,8 @@ function videoTimelineTranscript(project, options = {}) {
2031
2200
  const out = [];
2032
2201
  for (const clip of sortedVideoClips(track)) {
2033
2202
  if (clip.type !== "media") continue;
2034
- const words = _optionalChain([assets, 'access', _44 => _44.get, 'call', _45 => _45(clip.asset), 'optionalAccess', _46 => _46.transcript]);
2035
- if (!_optionalChain([words, 'optionalAccess', _47 => _47.length])) continue;
2203
+ const words = _optionalChain([assets, 'access', _47 => _47.get, 'call', _48 => _48(clip.asset), 'optionalAccess', _49 => _49.transcript]);
2204
+ if (!_optionalChain([words, 'optionalAccess', _50 => _50.length])) continue;
2036
2205
  const speed = videoClipSpeed(clip);
2037
2206
  const from = _nullishCoalesce(clip.in, () => ( 0));
2038
2207
  const to = from + clip.duration * speed;
@@ -2276,7 +2445,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
2276
2445
  }
2277
2446
 
2278
2447
  // src/video-editor/export.ts
2279
- var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _48 => _48.name]), () => ( _optionalChain([asset, 'optionalAccess', _49 => _49.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
2448
+ var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _51 => _51.name]), () => ( _optionalChain([asset, 'optionalAccess', _52 => _52.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
2280
2449
  function toVideoEdl(project, options = {}) {
2281
2450
  const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
2282
2451
  if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
@@ -2311,7 +2480,7 @@ function toVideoEdl(project, options = {}) {
2311
2480
  lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2312
2481
  } else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2313
2482
  if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
2314
- if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _50 => _50.name]), () => ( _optionalChain([asset, 'optionalAccess', _51 => _51.src]))), () => ( clip.asset))}`);
2483
+ if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _53 => _53.name]), () => ( _optionalChain([asset, 'optionalAccess', _54 => _54.src]))), () => ( clip.asset))}`);
2315
2484
  if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
2316
2485
  if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
2317
2486
  lines.push("");
@@ -2344,6 +2513,7 @@ function parseVideoLoudnessMeasurement(output) {
2344
2513
  return m;
2345
2514
  }
2346
2515
  var sec = (ms) => String(Math.round(ms) / 1e3);
2516
+ var fmtMs = (v) => String(Math.round(v * 100) / 100);
2347
2517
  var FFMPEG_BLEND = {
2348
2518
  multiply: "multiply",
2349
2519
  screen: "screen",
@@ -2403,6 +2573,58 @@ function buildFfmpeg(project, options, measure) {
2403
2573
  let cur = "base";
2404
2574
  let usesDrawtext = false;
2405
2575
  const animated = /* @__PURE__ */ new Set();
2576
+ const ramped = /* @__PURE__ */ new Set();
2577
+ const rampedPicture = (remap, lead, end) => {
2578
+ const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
2579
+ const first = pts[0];
2580
+ const last = pts[pts.length - 1];
2581
+ const s = `(T*1000+${fmtMs(first.v)})`;
2582
+ let e = fmtMs(last.t - first.t);
2583
+ for (let i = pts.length - 2; i >= 0; i--) {
2584
+ const a = pts[i];
2585
+ const b = pts[i + 1];
2586
+ if (b.v - a.v < 1e-6) continue;
2587
+ e = `if(lt(${s},${fmtMs(b.v)}),${fmtMs(a.t - first.t)}+(${s}-${fmtMs(a.v)})*${fmtMs((b.t - a.t) / (b.v - a.v))},${e})`;
2588
+ }
2589
+ const before = lead + first.t;
2590
+ const after = Math.max(0, end - last.t);
2591
+ return ["setpts=PTS-STARTPTS", `setpts='(${e})/1000/TB'`, `fps=${fps}`, ...before > 0.5 || after > 0.5 ? [`tpad=start_mode=clone:start_duration=${sec(before)}:stop_mode=clone:stop_duration=${sec(after + 1e3 / fps)}`] : [`tpad=stop_mode=clone:stop_duration=${sec(1e3 / fps)}`], `trim=duration=${sec(lead + end)}`];
2592
+ };
2593
+ const rampedSound = (remap, input2, output, lead, end) => {
2594
+ const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
2595
+ const first = pts[0];
2596
+ const last = pts[pts.length - 1];
2597
+ const n2 = audio.length;
2598
+ const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
2599
+ const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
2600
+ const pieces = [];
2601
+ const out = [];
2602
+ const parts = [];
2603
+ if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
2604
+ const played = [];
2605
+ for (let i = 0; i + 1 < pts.length; i++) {
2606
+ const dt = pts[i + 1].t - pts[i].t;
2607
+ if (dt < 0.5) continue;
2608
+ const speed = (pts[i + 1].v - pts[i].v) / dt;
2609
+ if (speed < 0.1) parts.push(silence(dt));
2610
+ else {
2611
+ played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
2612
+ parts.push(`#${played.length - 1}`);
2613
+ }
2614
+ }
2615
+ if (end - last.t > 0.5) parts.push(silence(end - last.t));
2616
+ if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
2617
+ parts.forEach((p, k2) => {
2618
+ const label = `[r${n2}p${k2}]`;
2619
+ if (p.startsWith("#")) {
2620
+ const piece = played[Number(p.slice(1))];
2621
+ out.push(`[r${n2}s${p.slice(1)}]atrim=start=${sec(piece.from)}:end=${sec(piece.to)},asetpts=PTS-STARTPTS,${atempo(piece.speed).join(",") || "anull"},${fmtA}${label}`);
2622
+ } else out.push(`${p}${label}`);
2623
+ pieces.push(label);
2624
+ });
2625
+ out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
2626
+ return out;
2627
+ };
2406
2628
  const blended = /* @__PURE__ */ new Set();
2407
2629
  let k = 0;
2408
2630
  const step = 2e3 / fps;
@@ -2415,7 +2637,10 @@ function buildFfmpeg(project, options, measure) {
2415
2637
  const src = options.resolve ? options.resolve(asset) : asset.src;
2416
2638
  const span = clip.duration + lead + tail;
2417
2639
  if (asset.kind === "image") inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", src]);
2418
- else inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) - lead * speed), "-t", sec(span * speed), "-i", src]);
2640
+ else if (clip.timeRemap) {
2641
+ const { from, to } = videoTimeRemapSpan(clip.timeRemap);
2642
+ inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
2643
+ } else inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) - lead * speed), "-t", sec(span * speed), "-i", src]);
2419
2644
  return inputs.length - 1;
2420
2645
  };
2421
2646
  for (const track of [...project.tracks].reverse()) {
@@ -2437,7 +2662,13 @@ function buildFfmpeg(project, options, measure) {
2437
2662
  const ai = _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
2438
2663
  const trackGain = _nullishCoalesce(track.volume, () => ( 1));
2439
2664
  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}`;
2440
- const chain2 = ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip)), volume];
2665
+ let source2 = `[${ai}:a]`;
2666
+ if (clip.timeRemap) {
2667
+ source2 = `[r${audio.length}]`;
2668
+ graph.push(...rampedSound(clip.timeRemap, `${ai}:a`, source2, lead, clip.duration + tail));
2669
+ ramped.add("sound");
2670
+ }
2671
+ const chain2 = [...clip.timeRemap ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
2441
2672
  const before2 = clip.transition ? videoClipBefore(track, clip) : null;
2442
2673
  const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
2443
2674
  const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
@@ -2445,11 +2676,11 @@ function buildFfmpeg(project, options, measure) {
2445
2676
  if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
2446
2677
  if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
2447
2678
  if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
2448
- if (_optionalChain([next2, 'optionalAccess', _52 => _52.transition, 'access', _53 => _53.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
2679
+ if (_optionalChain([next2, 'optionalAccess', _55 => _55.transition, 'access', _56 => _56.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
2449
2680
  if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
2450
2681
  if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
2451
2682
  chain2.push(`adelay=${Math.round(t0)}:all=1`);
2452
- graph.push(`[${ai}:a]${chain2.join(",")}[a${audio.length}]`);
2683
+ graph.push(`${source2}${chain2.join(",")}[a${audio.length}]`);
2453
2684
  audio.push(`[a${audio.length}]`);
2454
2685
  }
2455
2686
  if (measure || track.kind !== "video" || track.hidden) continue;
@@ -2458,7 +2689,7 @@ function buildFfmpeg(project, options, measure) {
2458
2689
  const onTimeline = `(t*1000-${Math.round(clip.start)})`;
2459
2690
  const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
2460
2691
  const tf = clip.transform;
2461
- const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _54 => _54[p]])));
2692
+ const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _57 => _57[p]])));
2462
2693
  if (clip.type === "text" && options.titleImage) {
2463
2694
  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 })]);
2464
2695
  index = inputs.length - 1;
@@ -2467,7 +2698,7 @@ function buildFfmpeg(project, options, measure) {
2467
2698
  const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
2468
2699
  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("*");
2469
2700
  const coord = (axis) => {
2470
- const v = _optionalChain([tf, 'optionalAccess', _55 => _55[axis]]);
2701
+ const v = _optionalChain([tf, 'optionalAccess', _58 => _58[axis]]);
2471
2702
  const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
2472
2703
  return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
2473
2704
  };
@@ -2475,7 +2706,7 @@ function buildFfmpeg(project, options, measure) {
2475
2706
  const opts = [
2476
2707
  `text=${ffmpegFilterValue(clip.text)}`,
2477
2708
  options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
2478
- isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _56 => _56.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _57 => _57.scale]), () => ( 1))))}`,
2709
+ isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _59 => _59.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _60 => _60.scale]), () => ( 1))))}`,
2479
2710
  `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
2480
2711
  coord("x"),
2481
2712
  coord("y"),
@@ -2483,7 +2714,7 @@ function buildFfmpeg(project, options, measure) {
2483
2714
  ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
2484
2715
  `enable='between(t,${sec(clip.start)},${sec(end)})'`
2485
2716
  ];
2486
- if (_optionalChain([tf, 'optionalAccess', _58 => _58.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
2717
+ if (_optionalChain([tf, 'optionalAccess', _61 => _61.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
2487
2718
  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.`);
2488
2719
  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.`);
2489
2720
  graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
@@ -2499,7 +2730,8 @@ function buildFfmpeg(project, options, measure) {
2499
2730
  if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
2500
2731
  else {
2501
2732
  source = `[${index}:v]`;
2502
- chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
2733
+ if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
2734
+ else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
2503
2735
  }
2504
2736
  const grade = videoGradeFfmpegFilters(clip.grade);
2505
2737
  if (grade.length) chain.push("format=rgba", ...grade);
@@ -2520,10 +2752,10 @@ function buildFfmpeg(project, options, measure) {
2520
2752
  const r = videoIrisRadius({ width: W, height: H }, 1);
2521
2753
  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)'`);
2522
2754
  }
2523
- const rotation = _optionalChain([placed, 'optionalAccess', _59 => _59.rotation]);
2755
+ const rotation = _optionalChain([placed, 'optionalAccess', _62 => _62.rotation]);
2524
2756
  if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
2525
2757
  else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
2526
- if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _60 => _60.scale]))) {
2758
+ if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _63 => _63.scale]))) {
2527
2759
  const s = expr(placed.scale, onChain("t"), 1, "scale");
2528
2760
  chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
2529
2761
  }
@@ -2542,9 +2774,9 @@ function buildFfmpeg(project, options, measure) {
2542
2774
  const into = lead ? clip.transition.type : void 0;
2543
2775
  if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
2544
2776
  const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
2545
- if (_optionalChain([after, 'optionalAccess', _61 => _61.transition, 'access', _62 => _62.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
2777
+ if (_optionalChain([after, 'optionalAccess', _64 => _64.transition, 'access', _65 => _65.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
2546
2778
  const at = (axis, c) => {
2547
- const v = _optionalChain([placed, 'optionalAccess', _63 => _63[axis]]);
2779
+ const v = _optionalChain([placed, 'optionalAccess', _66 => _66[axis]]);
2548
2780
  const extra = shifts[axis].map((s) => `+${s}`).join("");
2549
2781
  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}'`;
2550
2782
  };
@@ -2578,11 +2810,12 @@ function buildFfmpeg(project, options, measure) {
2578
2810
  if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
2579
2811
  else if (options.loudness && !options.loudness.measured)
2580
2812
  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.");
2581
- if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _64 => _64.get, 'call', _65 => _65(c.asset), 'optionalAccess', _66 => _66.kind]) === "video" && _optionalChain([assets, 'access', _67 => _67.get, 'call', _68 => _68(c.asset), 'optionalAccess', _69 => _69.hasAudio]) === void 0)))
2813
+ if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _67 => _67.get, 'call', _68 => _68(c.asset), 'optionalAccess', _69 => _69.kind]) === "video" && _optionalChain([assets, 'access', _70 => _70.get, 'call', _71 => _71(c.asset), 'optionalAccess', _72 => _72.hasAudio]) === void 0)))
2582
2814
  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.');
2583
2815
  if (animated.size)
2584
2816
  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)" : ""}.`);
2585
2817
  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.");
2818
+ if (ramped.has("sound")) notes.push("Sound through a speed ramp plays each stretch at its own speed, so a gliding change is heard in small steps (about every 80 ms), and freezes are silent.");
2586
2819
  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.");
2587
2820
  const filterComplex = graph.join(";");
2588
2821
  if (measure) {
@@ -2617,7 +2850,7 @@ var VideoPreviewEngine = (_class3 = class {
2617
2850
  this.source = source;
2618
2851
  this.options = options;
2619
2852
  this.canvas = canvas;
2620
- this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _70 => _70.getContext, 'optionalCall', _71 => _71("2d")]), () => ( null));
2853
+ this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _73 => _73.getContext, 'optionalCall', _74 => _74("2d")]), () => ( null));
2621
2854
  this.unsubscribe = source.subscribe(() => this.update());
2622
2855
  this.update();
2623
2856
  }
@@ -2667,8 +2900,8 @@ var VideoPreviewEngine = (_class3 = class {
2667
2900
  entry.media.pause();
2668
2901
  entry.media.removeEventListener("seeked", entry.onReady);
2669
2902
  entry.media.removeEventListener("loadeddata", entry.onReady);
2670
- _optionalChain([entry, 'access', _72 => _72.media, 'access', _73 => _73.removeAttribute, 'optionalCall', _74 => _74("src")]);
2671
- _optionalChain([entry, 'access', _75 => _75.media, 'access', _76 => _76.load, 'optionalCall', _77 => _77()]);
2903
+ _optionalChain([entry, 'access', _75 => _75.media, 'access', _76 => _76.removeAttribute, 'optionalCall', _77 => _77("src")]);
2904
+ _optionalChain([entry, 'access', _78 => _78.media, 'access', _79 => _79.load, 'optionalCall', _80 => _80()]);
2672
2905
  this.media.delete(entry.clip);
2673
2906
  }
2674
2907
  /** Brings the media elements in step with the editor and draws the frame. */
@@ -2682,13 +2915,14 @@ var VideoPreviewEngine = (_class3 = class {
2682
2915
  const { media } = this.entry(clip, src, kind);
2683
2916
  active.add(clip);
2684
2917
  const target = sourceTime / 1e3;
2685
- if (Math.abs(media.playbackRate - speed) > 1e-6) media.playbackRate = speed;
2918
+ const frozen = speed < 0.0625;
2919
+ if (!frozen && Math.abs(media.playbackRate - speed) > 1e-6) media.playbackRate = speed;
2686
2920
  if (gain === null) media.muted = true;
2687
2921
  else {
2688
2922
  media.muted = gain <= 0;
2689
2923
  media.volume = Math.min(1, Math.max(0, gain));
2690
2924
  }
2691
- if (playing) {
2925
+ if (playing && !frozen) {
2692
2926
  if (media.paused || Math.abs(media.currentTime - target) > tolerance) media.currentTime = target;
2693
2927
  if (media.paused) void Promise.resolve(media.play()).catch(() => {
2694
2928
  });
@@ -2701,7 +2935,7 @@ var VideoPreviewEngine = (_class3 = class {
2701
2935
  for (const layer of frame.layers) {
2702
2936
  if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
2703
2937
  const sound = sounding.get(layer.clip);
2704
- sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _78 => _78.speed]), () => ( 1)), sound ? sound.gain : null);
2938
+ sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _81 => _81.speed]), () => ( 1)), sound ? sound.gain : null);
2705
2939
  sounding.delete(layer.clip);
2706
2940
  }
2707
2941
  for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
@@ -2734,7 +2968,7 @@ var VideoPreviewEngine = (_class3 = class {
2734
2968
  if (this.canvas.width !== w) this.canvas.width = w;
2735
2969
  if (this.canvas.height !== h) this.canvas.height = h;
2736
2970
  drawVideoFrame(this.ctx, frame, this.sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
2737
- _optionalChain([this, 'access', _79 => _79.options, 'access', _80 => _80.onFrame, 'optionalCall', _81 => _81(frame)]);
2971
+ _optionalChain([this, 'access', _82 => _82.options, 'access', _83 => _83.onFrame, 'optionalCall', _84 => _84(frame)]);
2738
2972
  }
2739
2973
  __init13() {this.defineSvgFilter = (id, markup) => {
2740
2974
  if (this.options.defineSvgFilter === null) return;
@@ -2755,7 +2989,7 @@ var VideoPreviewEngine = (_class3 = class {
2755
2989
  /** Stops following the editor and releases every media element. */
2756
2990
  destroy() {
2757
2991
  this.destroyed = true;
2758
- _optionalChain([this, 'access', _82 => _82.filterHost, 'optionalAccess', _83 => _83.remove, 'call', _84 => _84()]);
2992
+ _optionalChain([this, 'access', _85 => _85.filterHost, 'optionalAccess', _86 => _86.remove, 'call', _87 => _87()]);
2759
2993
  this.filterHost = null;
2760
2994
  this.filters.clear();
2761
2995
  this.unsubscribe();
@@ -2863,7 +3097,7 @@ var VideoEditorController = (_class4 = class {
2863
3097
  this.applying = false;
2864
3098
  }
2865
3099
  this.notify();
2866
- _optionalChain([this, 'access', _85 => _85.onChange, 'optionalCall', _86 => _86(project)]);
3100
+ _optionalChain([this, 'access', _88 => _88.onChange, 'optionalCall', _89 => _89(project)]);
2867
3101
  }
2868
3102
  commit(label, next) {
2869
3103
  const before = this.project;
@@ -2927,7 +3161,7 @@ var VideoEditorController = (_class4 = class {
2927
3161
  }
2928
3162
  /** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
2929
3163
  select(clips, options = {}) {
2930
- const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _87 => _87.filter, 'call', _88 => _88((id) => findVideoClip(this.project, id))]), () => ( []));
3164
+ const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _90 => _90.filter, 'call', _91 => _91((id) => findVideoClip(this.project, id))]), () => ( []));
2931
3165
  let next;
2932
3166
  if (options.add) {
2933
3167
  next = [...this.selection];
@@ -3221,14 +3455,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
3221
3455
  return null;
3222
3456
  };
3223
3457
  const placed = (o) => ({
3224
- ..._optionalChain([o, 'access', _89 => _89.created, 'optionalAccess', _90 => _90.length]) ? { created: o.created } : {},
3225
- ..._optionalChain([o, 'access', _91 => _91.trimmed, 'optionalAccess', _92 => _92.length]) ? { trimmed: o.trimmed } : {},
3226
- ..._optionalChain([o, 'access', _93 => _93.removed, 'optionalAccess', _94 => _94.length]) ? { removed: o.removed } : {},
3458
+ ..._optionalChain([o, 'access', _92 => _92.created, 'optionalAccess', _93 => _93.length]) ? { created: o.created } : {},
3459
+ ..._optionalChain([o, 'access', _94 => _94.trimmed, 'optionalAccess', _95 => _95.length]) ? { trimmed: o.trimmed } : {},
3460
+ ..._optionalChain([o, 'access', _96 => _96.removed, 'optionalAccess', _97 => _97.length]) ? { removed: o.removed } : {},
3227
3461
  ...o.rippled ? { rippled: o.rippled } : {}
3228
3462
  });
3229
3463
  const keysOut = (p, clip, property) => {
3230
3464
  const c = clipOut(p, clip);
3231
- const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _95 => _95.transform, 'optionalAccess', _96 => _96[property]]) : _optionalChain([c, 'optionalAccess', _97 => _97[property]]);
3465
+ const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _98 => _98.transform, 'optionalAccess', _99 => _99[property]]) : _optionalChain([c, 'optionalAccess', _100 => _100[property]]);
3232
3466
  return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
3233
3467
  };
3234
3468
  const getProject = {
@@ -3526,7 +3760,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3526
3760
  const at = findVideoClip(p, id);
3527
3761
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
3528
3762
  const { clip } = at;
3529
- if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _98 => _98.assets, 'access', _99 => _99.find, 'call', _100 => _100((a) => a.id === clip.asset), 'optionalAccess', _101 => _101.kind]) === "audio")
3763
+ if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _101 => _101.assets, 'access', _102 => _102.find, 'call', _103 => _103((a) => a.id === clip.asset), 'optionalAccess', _104 => _104.kind]) === "audio")
3530
3764
  throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
3531
3765
  }
3532
3766
  };
@@ -3547,7 +3781,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3547
3781
  gradeTargets(clips);
3548
3782
  const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
3549
3783
  const edits = clips.map((clip) => grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] });
3550
- return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _102 => _102(r.project, id), 'optionalAccess', _103 => _103.clip, 'access', _104 => _104.grade]), () => ( null)) })) }));
3784
+ return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _105 => _105(r.project, id), 'optionalAccess', _106 => _106.clip, 'access', _107 => _107.grade]), () => ( null)) })) }));
3551
3785
  }
3552
3786
  };
3553
3787
  const copyGrade = {
@@ -3614,7 +3848,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3614
3848
  handler: ({ track, from, to, limit }) => {
3615
3849
  const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
3616
3850
  const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
3617
- return { track: _nullishCoalesce(_optionalChain([words, 'access', _105 => _105[0], 'optionalAccess', _106 => _106.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
3851
+ return { track: _nullishCoalesce(_optionalChain([words, 'access', _108 => _108[0], 'optionalAccess', _109 => _109.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
3618
3852
  }
3619
3853
  };
3620
3854
  const findText = {
@@ -3671,6 +3905,83 @@ function createVideoEditorToolProvider(controller, options = {}) {
3671
3905
  return apply(videoRemoveRangesEdits(merged, tracks), merged.length > 1 ? `Cut ${merged.length} stretches` : "Cut stretch", (r) => ({ removed: merged.length, time: round(before - videoProjectDuration(r.project)), duration: videoProjectDuration(r.project) }));
3672
3906
  }
3673
3907
  };
3908
+ const rampTarget = (id) => {
3909
+ const at = findVideoClip(project(), id);
3910
+ if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
3911
+ const { clip } = at;
3912
+ if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to change the speed of; use a video or audio clip.`);
3913
+ if (_optionalChain([project, 'call', _110 => _110(), 'access', _111 => _111.assets, 'access', _112 => _112.find, 'call', _113 => _113((a) => a.id === clip.asset), 'optionalAccess', _114 => _114.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
3914
+ return clip;
3915
+ };
3916
+ const rampOut = (r, id) => {
3917
+ const outcome2 = r.outcomes[r.outcomes.length - 1];
3918
+ return { clip: clipOut(r.project, id), ...outcome2.rippled ? { rippled: outcome2.rippled } : {}, duration: videoProjectDuration(r.project) };
3919
+ };
3920
+ const RIPPLE_RAMP = { type: "boolean", description: "Move later material on every sync-locked track by the change in length (default true); false refuses if the clip would overlap the next one." };
3921
+ const setSpeedRamp = {
3922
+ name: "set_video_speed_ramp",
3923
+ title: "Ramp the speed",
3924
+ description: `Plays a video or audio clip through stretches of speed, from its first frame: e.g. [{ duration: 1500, speed: 1 }, { duration: 500, speed: 1, toSpeed: 0.25 }, { duration: 2000, speed: 0.25 }] for a slow-motion ramp. "toSpeed" glides evenly to a new speed over the stretch; speed 0 is a freeze frame. The clip's length becomes the stretches' total (one undo step). Replaces any ramp or speed it had. Returns the clip (with its timeRemap keys and source range).`,
3925
+ inputSchema: object(
3926
+ {
3927
+ clip: CLIP_ID,
3928
+ segments: {
3929
+ type: "array",
3930
+ minItems: 1,
3931
+ maxItems: 100,
3932
+ description: "The stretches in order, from the clip's start.",
3933
+ items: object(
3934
+ {
3935
+ duration: { type: "number", exclusiveMinimum: 0, description: "Length of the stretch on the timeline, in ms." },
3936
+ speed: { type: "number", minimum: 0, maximum: 20, description: "Speed at its start: 1 normal, 0.5 half speed (slow motion), 2 double, 0 a freeze frame." },
3937
+ toSpeed: { type: "number", minimum: 0, maximum: 20, description: "Glide evenly to this speed by the stretch's end (default: stay at speed). A ramp between two speeds." }
3938
+ },
3939
+ ["duration", "speed"],
3940
+ "One stretch."
3941
+ )
3942
+ },
3943
+ ripple: RIPPLE_RAMP
3944
+ },
3945
+ ["clip", "segments"]
3946
+ ),
3947
+ annotations: { title: "Ramp the speed", ...WRITE_IDEMPOTENT },
3948
+ handler: ({ clip, segments, ripple }) => {
3949
+ const edit = videoSpeedRampEdit(rampTarget(clip), segments, _nullishCoalesce(ripple, () => ( true)));
3950
+ return apply(edit, "Speed ramp", (r) => ({ ...rampOut(r, clip), source: Math.round(videoSpeedRampKeys(segments).source) }));
3951
+ }
3952
+ };
3953
+ const addFreeze = {
3954
+ name: "add_video_freeze_frame",
3955
+ title: "Add a freeze frame",
3956
+ description: "Holds the frame a video clip shows at a timeline time for a while, then plays on (one undo step): the clip gets longer by the hold, and with ripple (default) everything after it on sync-locked tracks moves along. Works on clips with or without a speed ramp. Returns the clip.",
3957
+ inputSchema: object(
3958
+ {
3959
+ clip: CLIP_ID,
3960
+ at: MS("Timeline time of the frame to hold (inside the clip)"),
3961
+ duration: { type: "number", exclusiveMinimum: 0, description: "How long to hold it, in ms." },
3962
+ ripple: RIPPLE_RAMP
3963
+ },
3964
+ ["clip", "at", "duration"]
3965
+ ),
3966
+ annotations: { title: "Add a freeze frame", ...WRITE },
3967
+ handler: ({ clip, at, duration, ripple }) => {
3968
+ const c = rampTarget(clip);
3969
+ const t = at - c.start;
3970
+ if (t < 0 || t > c.duration) throw new Error(`${formatVideoSeconds(at)} is outside clip "${clip}" (${formatVideoSeconds(c.start)}\u2013${formatVideoSeconds(videoClipEnd(c))}); pick a time inside it.`);
3971
+ return apply(videoFreezeFrameEdit(c, t, duration, _nullishCoalesce(ripple, () => ( true))), "Freeze frame", (r) => rampOut(r, clip));
3972
+ }
3973
+ };
3974
+ const removeRamp = {
3975
+ name: "remove_video_speed_ramp",
3976
+ title: "Remove a speed ramp",
3977
+ description: 'Puts a ramped clip back to normal speed from the moment its ramp started (one undo step), keeping its length unless "duration" is given. Returns the clip.',
3978
+ inputSchema: object({ clip: CLIP_ID, duration: { type: "number", exclusiveMinimum: 0, description: "New length in ms (default: unchanged)." }, ripple: RIPPLE_RAMP }, ["clip"]),
3979
+ annotations: { title: "Remove a speed ramp", ...WRITE },
3980
+ handler: ({ clip, duration, ripple }) => {
3981
+ rampTarget(clip);
3982
+ return apply({ op: "set_time_remap", clip, keys: null, ...duration !== void 0 ? { duration } : {}, ripple: _nullishCoalesce(ripple, () => ( true)) }, "Remove speed ramp", (r) => rampOut(r, clip));
3983
+ }
3984
+ };
3674
3985
  const setTransition = {
3675
3986
  name: "set_video_transition",
3676
3987
  title: "Set a transition",
@@ -3781,7 +4092,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3781
4092
  };
3782
4093
  const EDIT_OP = {
3783
4094
  type: "object",
3784
- description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
4095
+ description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, set_time_remap { clip, keys (clip ms \u2192 source ms after the in point; null removes), duration, ripple }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
3785
4096
  properties: { op: { type: "string", enum: VIDEO_EDIT_OPS.filter((o) => o !== "replace_project"), description: "Which edit." } },
3786
4097
  required: ["op"]
3787
4098
  };
@@ -3889,7 +4200,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3889
4200
  }
3890
4201
  };
3891
4202
  const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
3892
- const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
4203
+ const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
3893
4204
  return {
3894
4205
  id: _nullishCoalesce(options.id, () => ( "video")),
3895
4206
  namespace: options.namespace,
@@ -3975,7 +4286,7 @@ function videoClipLabel(clip, asset) {
3975
4286
  if (clip.name) return clip.name;
3976
4287
  if (clip.type === "text") return clip.text.split("\n")[0];
3977
4288
  if (clip.type === "color") return clip.color;
3978
- const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _107 => _107.name]), () => ( _optionalChain([asset, 'optionalAccess', _108 => _108.src, 'access', _109 => _109.replace, 'call', _110 => _110(/^.*[\\/]/, "")]))), () => ( clip.asset));
4289
+ const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _115 => _115.name]), () => ( _optionalChain([asset, 'optionalAccess', _116 => _116.src, 'access', _117 => _117.replace, 'call', _118 => _118(/^.*[\\/]/, "")]))), () => ( clip.asset));
3979
4290
  const speed = videoClipSpeed(clip);
3980
4291
  return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
3981
4292
  }
@@ -4019,7 +4330,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
4019
4330
  const asset = videoAssetMap(project).get(assetId);
4020
4331
  if (!asset) return null;
4021
4332
  const kind = asset.kind === "audio" ? "audio" : "video";
4022
- const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _111 => _111.tracks, 'access', _112 => _112.find, 'call', _113 => _113((t) => t.kind === kind && !t.locked), 'optionalAccess', _114 => _114.id])));
4333
+ const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _119 => _119.tracks, 'access', _120 => _120.find, 'call', _121 => _121((t) => t.kind === kind && !t.locked), 'optionalAccess', _122 => _122.id])));
4023
4334
  if (!track) return null;
4024
4335
  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 } : {} };
4025
4336
  }
@@ -4068,14 +4379,14 @@ var KEY_LABELS = {
4068
4379
  scale: { label: "Scale", step: 0.05 },
4069
4380
  rotation: { label: "Rotation", step: 1 }
4070
4381
  };
4071
- var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _115 => _115.transform, 'optionalAccess', _116 => _116[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
4382
+ var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _123 => _123.transform, 'optionalAccess', _124 => _124[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
4072
4383
  var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
4073
4384
  function videoKeyframeRows(project, clipId, time) {
4074
4385
  const at = findVideoClip(project, clipId);
4075
4386
  if (!at) return [];
4076
4387
  const { clip } = at;
4077
4388
  const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
4078
- const visual = !(_optionalChain([asset, 'optionalAccess', _117 => _117.kind]) === "audio" || at.track.kind === "audio");
4389
+ const visual = !(_optionalChain([asset, 'optionalAccess', _125 => _125.kind]) === "audio" || at.track.kind === "audio");
4079
4390
  const audible = clip.type === "media" && videoAssetHasSound(asset);
4080
4391
  const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
4081
4392
  const clipTime = playheadInClip(clip, time);
@@ -4289,5 +4600,15 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
4289
4600
 
4290
4601
 
4291
4602
 
4292
- 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_FILLERS = VIDEO_DEFAULT_FILLERS; 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.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoWord = normalizeVideoWord; 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.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; 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.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
4603
+
4604
+
4605
+
4606
+
4607
+
4608
+
4609
+
4610
+
4611
+
4612
+
4613
+ 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_FILLERS = VIDEO_DEFAULT_FILLERS; 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.checkVideoTimeRemap = checkVideoTimeRemap; 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.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoWord = normalizeVideoWord; 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.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; 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.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSpeedAt = videoClipSpeedAt; 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.videoFreezeFrameEdit = videoFreezeFrameEdit; 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.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.videoSnapPoints = videoSnapPoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.videoTextStyle = videoTextStyle; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
4293
4614
  //# sourceMappingURL=index.cjs.map