@pixerate/editor 0.85.0 → 0.87.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.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5;
2
2
 
3
3
  var _chunkWAJQKWQUcjs = require('../chunk-WAJQKWQU.cjs');
4
4
 
@@ -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;
@@ -1955,6 +2124,379 @@ var VideoAudioPeaksCache = (_class2 = class {
1955
2124
  }
1956
2125
  }, _class2);
1957
2126
 
2127
+ // src/video-editor/scopes.ts
2128
+ var VIDEO_SCOPE_KINDS = ["histogram", "waveform", "vectorscope"];
2129
+ var videoLuma = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
2130
+ function videoChroma(r, g, b) {
2131
+ const y = videoLuma(r, g, b);
2132
+ return [(b - y) / 1.8556, (r - y) / 1.5748];
2133
+ }
2134
+ function eachPixel(pixels, maxPixels, visit) {
2135
+ const { width, height, data } = pixels;
2136
+ if (!(width > 0 && height > 0) || data.length < width * height * 4) throw new Error(`The pixels are not ${width} \xD7 ${height} RGBA: there are ${data.length} values, and ${width * height * 4} are needed.`);
2137
+ const step = Math.max(1, Math.sqrt(width * height / Math.max(1, _nullishCoalesce(maxPixels, () => ( 262144)))));
2138
+ let count = 0;
2139
+ for (let fy = step / 2; fy < height; fy += step) {
2140
+ const y = Math.floor(fy);
2141
+ for (let fx = step / 2; fx < width; fx += step) {
2142
+ const x = Math.floor(fx);
2143
+ const i = (y * width + x) * 4;
2144
+ visit(data[i], data[i + 1], data[i + 2], x, y);
2145
+ count++;
2146
+ }
2147
+ }
2148
+ return count;
2149
+ }
2150
+ var binOf = (v, bins) => Math.min(bins - 1, Math.floor((v + 1e-6) / 256 * bins));
2151
+ function videoHistogram(pixels, options = {}) {
2152
+ const bins = Math.max(1, Math.min(256, Math.round(_nullishCoalesce(options.bins, () => ( 256)))));
2153
+ const out = { bins, r: Array(bins).fill(0), g: Array(bins).fill(0), b: Array(bins).fill(0), luma: Array(bins).fill(0), total: 0 };
2154
+ out.total = eachPixel(pixels, options.maxPixels, (r, g, b) => {
2155
+ out.r[binOf(r, bins)]++;
2156
+ out.g[binOf(g, bins)]++;
2157
+ out.b[binOf(b, bins)]++;
2158
+ out.luma[binOf(videoLuma(r, g, b), bins)]++;
2159
+ });
2160
+ return out;
2161
+ }
2162
+ function videoLumaWaveform(pixels, options = {}) {
2163
+ const columns = Math.max(1, Math.min(pixels.width, Math.round(_nullishCoalesce(options.columns, () => ( Math.min(pixels.width, 256))))));
2164
+ const levels = Math.max(2, Math.min(256, Math.round(_nullishCoalesce(options.levels, () => ( 128)))));
2165
+ const data = new Uint32Array(columns * levels);
2166
+ let peak = 0;
2167
+ eachPixel(pixels, options.maxPixels, (r, g, b, x) => {
2168
+ const c = Math.min(columns - 1, Math.floor(x / pixels.width * columns));
2169
+ const i = c * levels + binOf(videoLuma(r, g, b), levels);
2170
+ if (++data[i] > peak) peak = data[i];
2171
+ });
2172
+ return { columns, levels, data, peak };
2173
+ }
2174
+ var videoVectorscopePoint = (cb, cr, size) => [(cb + 0.5) * (size - 1), (0.5 - cr) * (size - 1)];
2175
+ function videoVectorscope(pixels, options = {}) {
2176
+ const size = Math.max(16, Math.min(512, Math.round(_nullishCoalesce(options.size, () => ( 128)))));
2177
+ const data = new Uint32Array(size * size);
2178
+ let peak = 0;
2179
+ eachPixel(pixels, options.maxPixels, (r, g, b) => {
2180
+ const [cb, cr] = videoChroma(r / 255, g / 255, b / 255);
2181
+ const [x, y] = videoVectorscopePoint(cb, cr, size);
2182
+ const i = Math.round(y) * size + Math.round(x);
2183
+ if (++data[i] > peak) peak = data[i];
2184
+ });
2185
+ return { size, data, peak };
2186
+ }
2187
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
2188
+ var hex2 = (v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0");
2189
+ function videoExposureStats(pixels, options = {}) {
2190
+ const low = (_nullishCoalesce(options.shadowLevel, () => ( 0.02))) * 255;
2191
+ const high = (_nullishCoalesce(options.highlightLevel, () => ( 0.98))) * 255;
2192
+ const lumaBins = new Uint32Array(256);
2193
+ let sumY = 0;
2194
+ let sumR = 0;
2195
+ let sumG = 0;
2196
+ let sumB = 0;
2197
+ let sumSat = 0;
2198
+ let maxSat = 0;
2199
+ let shadows = 0;
2200
+ let highlights = 0;
2201
+ let cr = 0;
2202
+ let cg = 0;
2203
+ let cbl = 0;
2204
+ const total = eachPixel(pixels, options.maxPixels, (r, g, b) => {
2205
+ const y = videoLuma(r, g, b);
2206
+ lumaBins[Math.min(255, Math.round(y))]++;
2207
+ sumY += y;
2208
+ sumR += r;
2209
+ sumG += g;
2210
+ sumB += b;
2211
+ const [u, v] = videoChroma(r / 255, g / 255, b / 255);
2212
+ const sat = Math.min(1, Math.hypot(u, v) / 0.5);
2213
+ sumSat += sat;
2214
+ if (sat > maxSat) maxSat = sat;
2215
+ if (y <= low) shadows++;
2216
+ if (y >= high) highlights++;
2217
+ if (r >= high) cr++;
2218
+ if (g >= high) cg++;
2219
+ if (b >= high) cbl++;
2220
+ });
2221
+ const percentile = (p) => {
2222
+ const target = p * total;
2223
+ let seen = 0;
2224
+ for (let i = 0; i < 256; i++) {
2225
+ seen += lumaBins[i];
2226
+ if (seen >= target && seen > 0) return i / 255;
2227
+ }
2228
+ return 1;
2229
+ };
2230
+ let min = 0;
2231
+ while (min < 255 && !lumaBins[min]) min++;
2232
+ let max = 255;
2233
+ while (max > 0 && !lumaBins[max]) max--;
2234
+ return {
2235
+ pixels: total,
2236
+ luma: { mean: round3(sumY / total / 255), median: round3(percentile(0.5)), min: round3(min / 255), max: round3(max / 255), p1: round3(percentile(0.01)), p99: round3(percentile(0.99)) },
2237
+ clipped: { shadows: round3(shadows / total), highlights: round3(highlights / total), r: round3(cr / total), g: round3(cg / total), b: round3(cbl / total) },
2238
+ saturation: { mean: round3(sumSat / total), max: round3(maxSat) },
2239
+ average: `#${hex2(sumR / total)}${hex2(sumG / total)}${hex2(sumB / total)}`
2240
+ };
2241
+ }
2242
+ function videoExposureNotes(stats) {
2243
+ const notes = [];
2244
+ const pct = (v2) => `${Math.round(v2 * 1e3) / 10}%`;
2245
+ if (stats.clipped.shadows >= 0.05) notes.push(`${pct(stats.clipped.shadows)} of the picture is crushed to black; raise exposure or lower contrast.`);
2246
+ if (stats.clipped.highlights >= 0.02) notes.push(`${pct(stats.clipped.highlights)} of the picture is blown to white; lower exposure.`);
2247
+ else {
2248
+ const channels = ["r", "g", "b"].filter((k) => stats.clipped[k] >= 0.05);
2249
+ if (channels.length) notes.push(`The ${channels.map((k) => ({ r: "red", g: "green", b: "blue" })[k]).join(" and ")} channel${channels.length > 1 ? "s clip" : " clips"} in ${pct(Math.max(...channels.map((k) => stats.clipped[k])))} of the picture; lower exposure or saturation.`);
2250
+ }
2251
+ if (stats.luma.mean < 0.2 && stats.clipped.shadows < 0.05) notes.push(`The picture is dark (mean luma ${stats.luma.mean}); raise exposure if it is not meant to be.`);
2252
+ if (stats.luma.mean > 0.75 && stats.clipped.highlights < 0.02) notes.push(`The picture is bright (mean luma ${stats.luma.mean}); lower exposure if it is not meant to be.`);
2253
+ if (stats.luma.p99 - stats.luma.p1 < 0.3 && stats.pixels > 1) notes.push(`The picture is flat (most of it lies between luma ${stats.luma.p1} and ${stats.luma.p99}); raise contrast.`);
2254
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(stats.average.slice(i, i + 2), 16) / 255);
2255
+ const [u, v] = videoChroma(r, g, b);
2256
+ if (Math.hypot(u, v) > 0.04 && stats.saturation.mean < 0.35) {
2257
+ const warm = v - u;
2258
+ notes.push(`The picture has a ${Math.abs(warm) > Math.abs(v + u) * 0.5 ? warm > 0 ? "warm (orange)" : "cool (blue)" : v > 0 ? "magenta" : "green"} cast (average ${stats.average}); adjust temperature and tint.`);
2259
+ }
2260
+ return notes;
2261
+ }
2262
+ var videoVectorscopeAngle = (cb, cr) => (Math.atan2(cr, cb) * 180 / Math.PI % 360 + 360) % 360;
2263
+ function summarizeVideoScopes(pixels, options = {}) {
2264
+ const stats = videoExposureStats(pixels, options);
2265
+ const bins = Math.max(2, Math.min(64, Math.round(_nullishCoalesce(options.bins, () => ( 16)))));
2266
+ const h = videoHistogram(pixels, { ...options, bins });
2267
+ const share = (a) => a.map((v) => round3(v / h.total));
2268
+ const columns = Math.max(1, Math.min(32, Math.round(_nullishCoalesce(options.columns, () => ( 8)))));
2269
+ const w = videoLumaWaveform(pixels, { ...options, columns, levels: 256 });
2270
+ const waveform = Array.from({ length: w.columns }, (_, c) => {
2271
+ const counts = w.data.subarray(c * 256, c * 256 + 256);
2272
+ const n2 = counts.reduce((a, b) => a + b, 0);
2273
+ let lo = 0;
2274
+ while (lo < 255 && !counts[lo]) lo++;
2275
+ let hi = 255;
2276
+ while (hi > 0 && !counts[hi]) hi--;
2277
+ let seen = 0;
2278
+ let median = 0;
2279
+ for (let i = 0; i < 256; i++) {
2280
+ seen += counts[i];
2281
+ if (seen >= n2 / 2 && n2) {
2282
+ median = i;
2283
+ break;
2284
+ }
2285
+ }
2286
+ return { start: round3(c / w.columns), end: round3((c + 1) / w.columns), min: round3(lo / 255), median: round3(median / 255), max: round3(hi / 255) };
2287
+ }).filter((_, c) => w.data.subarray(c * 256, c * 256 + 256).some((v) => v > 0));
2288
+ const chroma = [];
2289
+ eachPixel(pixels, Math.min(_nullishCoalesce(options.maxPixels, () => ( 65536)), 65536), (r, g, b) => {
2290
+ const [u, v] = videoChroma(r / 255, g / 255, b / 255);
2291
+ chroma.push([u, v, Math.hypot(u, v)]);
2292
+ });
2293
+ chroma.sort((a, b) => b[2] - a[2]);
2294
+ const top = chroma.slice(0, Math.max(1, Math.ceil(chroma.length / 10)));
2295
+ const mu = top.reduce((s, c) => s + c[0], 0) / top.length;
2296
+ const mv = top.reduce((s, c) => s + c[1], 0) / top.length;
2297
+ const strength = Math.min(1, top.reduce((s, c) => s + c[2], 0) / top.length / 0.5);
2298
+ const coherence = Math.hypot(mu, mv) / Math.max(1e-9, top.reduce((s, c) => s + c[2], 0) / top.length);
2299
+ return {
2300
+ stats,
2301
+ notes: videoExposureNotes(stats),
2302
+ histogram: { bins, luma: share(h.luma), r: share(h.r), g: share(h.g), b: share(h.b) },
2303
+ waveform,
2304
+ vectorscope: { hue: strength < 0.02 ? null : Math.round(videoVectorscopeAngle(mu, mv)), strength: round3(strength), spread: round3(1 - coherence) }
2305
+ };
2306
+ }
2307
+ var VIDEO_VECTORSCOPE_TARGETS = [
2308
+ ["R", [0.75, 0, 0]],
2309
+ ["Mg", [0.75, 0, 0.75]],
2310
+ ["B", [0, 0, 0.75]],
2311
+ ["Cy", [0, 0.75, 0.75]],
2312
+ ["G", [0, 0.75, 0]],
2313
+ ["Yl", [0.75, 0.75, 0]]
2314
+ ].map(([name, rgb]) => {
2315
+ const [cb, cr] = videoChroma(rgb[0], rgb[1], rgb[2]);
2316
+ return { name, rgb: [...rgb], cb, cr };
2317
+ });
2318
+ function canvas(width, height) {
2319
+ const data = new Uint8ClampedArray(width * height * 4);
2320
+ for (let i = 0; i < data.length; i += 4) {
2321
+ data[i] = 12;
2322
+ data[i + 1] = 14;
2323
+ data[i + 2] = 18;
2324
+ data[i + 3] = 255;
2325
+ }
2326
+ return { width, height, data };
2327
+ }
2328
+ function plot(img, x, y, rgb, alpha = 1, add = false) {
2329
+ const xi = Math.round(x);
2330
+ const yi = Math.round(y);
2331
+ if (xi < 0 || yi < 0 || xi >= img.width || yi >= img.height) return;
2332
+ const i = (yi * img.width + xi) * 4;
2333
+ for (let k = 0; k < 3; k++) img.data[i + k] = add ? img.data[i + k] + rgb[k] * alpha : img.data[i + k] * (1 - alpha) + rgb[k] * alpha;
2334
+ }
2335
+ var GRID = [70, 76, 88];
2336
+ var glow = (count, peak) => count ? 0.25 + 0.75 * (Math.log1p(count) / Math.log1p(Math.max(1, peak))) : 0;
2337
+ function videoHistogramImage(h, width = 256, height = 128) {
2338
+ const img = canvas(width, height);
2339
+ for (const q of [0.25, 0.5, 0.75]) for (let y = 0; y < height; y++) plot(img, q * (width - 1), y, GRID);
2340
+ const inner = (a) => a.length > 2 ? a.slice(1, -1) : a;
2341
+ const peak = Math.max(1, ...inner(h.r), ...inner(h.g), ...inner(h.b), ...inner(h.luma));
2342
+ const height01 = (v) => Math.min(1, Math.sqrt(v / peak));
2343
+ for (let x = 0; x < width; x++) {
2344
+ const bin = Math.min(h.bins - 1, Math.floor(x / width * h.bins));
2345
+ const channels = [
2346
+ [h.r, [200, 40, 40]],
2347
+ [h.g, [40, 180, 40]],
2348
+ [h.b, [50, 80, 220]]
2349
+ ];
2350
+ for (const [a, rgb] of channels) {
2351
+ const top = height - height01(a[bin]) * height;
2352
+ for (let y = Math.ceil(top); y < height; y++) plot(img, x, y, rgb, 0.8, true);
2353
+ }
2354
+ plot(img, x, height - 1 - height01(h.luma[bin]) * (height - 1), [255, 255, 255]);
2355
+ }
2356
+ return img;
2357
+ }
2358
+ function videoWaveformImage(w, width = 256, height = 128) {
2359
+ const img = canvas(width, height);
2360
+ for (const q of [0, 0.25, 0.5, 0.75, 1]) for (let x = 0; x < width; x++) plot(img, x, (1 - q) * (height - 1), GRID);
2361
+ for (let x = 0; x < width; x++) {
2362
+ const c = Math.min(w.columns - 1, Math.floor(x / width * w.columns));
2363
+ for (let y = 0; y < height; y++) {
2364
+ const level = Math.min(w.levels - 1, Math.floor((height - 1 - y) / height * w.levels));
2365
+ const a = glow(w.data[c * w.levels + level], w.peak);
2366
+ if (a) plot(img, x, y, [120, 255, 140], a);
2367
+ }
2368
+ }
2369
+ return img;
2370
+ }
2371
+ function videoVectorscopeImage(v, size = v.size) {
2372
+ const img = canvas(size, size);
2373
+ const c = (size - 1) / 2;
2374
+ for (let a = 0; a < 720; a++) {
2375
+ const t = a / 720 * 2 * Math.PI;
2376
+ plot(img, c + Math.cos(t) * c, c - Math.sin(t) * c, GRID);
2377
+ }
2378
+ const skin = 123 * Math.PI / 180;
2379
+ for (let r = 0; r < c; r++) plot(img, c + Math.cos(skin) * r, c - Math.sin(skin) * r, [140, 110, 90]);
2380
+ for (const t of VIDEO_VECTORSCOPE_TARGETS) {
2381
+ const [x, y] = videoVectorscopePoint(t.cb, t.cr, size);
2382
+ const rgb = t.rgb.map((k) => 80 + k * 200);
2383
+ for (let k = -3; k <= 3; k++) {
2384
+ plot(img, x + k, y - 3, rgb);
2385
+ plot(img, x + k, y + 3, rgb);
2386
+ plot(img, x - 3, y + k, rgb);
2387
+ plot(img, x + 3, y + k, rgb);
2388
+ }
2389
+ }
2390
+ for (let y = 0; y < size; y++)
2391
+ for (let x = 0; x < size; x++) {
2392
+ const sy = Math.min(v.size - 1, Math.floor(y / size * v.size));
2393
+ const sx = Math.min(v.size - 1, Math.floor(x / size * v.size));
2394
+ const a = glow(v.data[sy * v.size + sx], v.peak);
2395
+ if (!a) continue;
2396
+ const cb = sx / (v.size - 1) - 0.5;
2397
+ const cr = 0.5 - sy / (v.size - 1);
2398
+ const r = Math.min(1, Math.max(0, 0.6 + 1.5748 * cr));
2399
+ const g = Math.min(1, Math.max(0, 0.6 - 0.1873 * cb - 0.4681 * cr));
2400
+ const b = Math.min(1, Math.max(0, 0.6 + 1.8556 * cb));
2401
+ plot(img, x, y, [r * 255, g * 255, b * 255], a);
2402
+ }
2403
+ return img;
2404
+ }
2405
+ function videoScopeImage(kind, pixels, width = 256, height = 128, options = {}) {
2406
+ if (kind === "histogram") return videoHistogramImage(videoHistogram(pixels, { ...options, bins: Math.min(256, width) }), width, height);
2407
+ if (kind === "waveform") return videoWaveformImage(videoLumaWaveform(pixels, { ...options, columns: Math.min(pixels.width, width), levels: Math.min(256, height) }), width, height);
2408
+ const size = Math.min(width, height);
2409
+ return videoVectorscopeImage(videoVectorscope(pixels, { ...options, size }), size);
2410
+ }
2411
+ var VideoPreviewPixels = (_class3 = class {constructor() { _class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this); }
2412
+ __init6() {this.reader = null}
2413
+ __init7() {this.drawnAt = null}
2414
+ __init8() {this.listeners = /* @__PURE__ */ new Set()}
2415
+ /** Gives the canvas to read (the latest preview wins). Returns a function that takes it away again. */
2416
+ attach(reader) {
2417
+ this.reader = reader;
2418
+ this.drawnAt = null;
2419
+ return () => {
2420
+ if (this.reader !== reader) return;
2421
+ this.reader = null;
2422
+ this.drawnAt = null;
2423
+ this.emit();
2424
+ };
2425
+ }
2426
+ /** Whether a preview is attached. */
2427
+ get attached() {
2428
+ return this.reader !== null;
2429
+ }
2430
+ /** The preview drew the frame at `time`; `complete` is false while a picture was missing or stale. */
2431
+ drawn(time, complete) {
2432
+ this.drawnAt = { time, complete };
2433
+ this.emit();
2434
+ }
2435
+ /** The last frame drawn, with its pixels; `null` without a preview. Throws when the canvas cannot be read. */
2436
+ read() {
2437
+ if (!this.reader) return null;
2438
+ try {
2439
+ return this.reader();
2440
+ } catch (e) {
2441
+ const reason = e instanceof Error ? e.message : String(e);
2442
+ throw new Error(`The preview's pixels cannot be read (${reason}). Media from another origin must be served with CORS headers (Access-Control-Allow-Origin) for its pixels to be measured.`);
2443
+ }
2444
+ }
2445
+ /** Called after each frame the preview draws. */
2446
+ subscribe(listener) {
2447
+ this.listeners.add(listener);
2448
+ return () => {
2449
+ this.listeners.delete(listener);
2450
+ };
2451
+ }
2452
+ /**
2453
+ * The frame at `time` once it is complete: now, if the last frame drawn was it, else the next
2454
+ * complete drawing of it. Fails after `timeout` ms (default 5000).
2455
+ */
2456
+ next(time, timeout = 5e3) {
2457
+ const matches = () => this.drawnAt && this.drawnAt.complete && Math.abs(this.drawnAt.time - time) < 0.5;
2458
+ return new Promise((resolve, reject) => {
2459
+ const done = (fn) => {
2460
+ clearTimeout(timer);
2461
+ off();
2462
+ fn();
2463
+ };
2464
+ const check = () => {
2465
+ if (!this.reader) return done(() => reject(new Error("The preview was closed before it drew the frame.")));
2466
+ if (!matches()) return;
2467
+ try {
2468
+ const snap = this.read();
2469
+ done(() => snap ? resolve(snap) : reject(new Error("The preview has no pixels to read.")));
2470
+ } catch (e) {
2471
+ done(() => reject(e));
2472
+ }
2473
+ };
2474
+ const off = this.subscribe(check);
2475
+ const timer = setTimeout(() => done(() => reject(new Error(`The preview did not finish drawing ${Math.round(time)} ms within ${timeout} ms; its media may still be loading or may have failed to load. Try again, or check the asset sources.`))), timeout);
2476
+ check();
2477
+ });
2478
+ }
2479
+ emit() {
2480
+ for (const l of [...this.listeners]) l();
2481
+ }
2482
+ }, _class3);
2483
+ function readVideoScopesPanel(feed, kinds = VIDEO_SCOPE_KINDS, options = {}) {
2484
+ let snap;
2485
+ try {
2486
+ snap = feed.read();
2487
+ } catch (e) {
2488
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
2489
+ }
2490
+ if (!snap) return { ok: false, message: feed.attached ? "The preview has not drawn a frame yet." : "Show the preview to see its scopes." };
2491
+ const width = _nullishCoalesce(options.width, () => ( 256));
2492
+ const height = _nullishCoalesce(options.height, () => ( 128));
2493
+ const opts = { maxPixels: _nullishCoalesce(options.maxPixels, () => ( 65536)) };
2494
+ const images = {};
2495
+ for (const kind of kinds) images[kind] = videoScopeImage(kind, snap.pixels, width, height, opts);
2496
+ const stats = videoExposureStats(snap.pixels, opts);
2497
+ return { ok: true, time: snap.time, complete: snap.complete, images, stats, notes: videoExposureNotes(stats) };
2498
+ }
2499
+
1958
2500
  // src/video-editor/transcript.ts
1959
2501
  var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1960
2502
  function cueTime(s) {
@@ -2020,8 +2562,8 @@ function videoTranscriptTrack(project, track) {
2020
2562
  return track;
2021
2563
  }
2022
2564
  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));
2565
+ 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]));
2566
+ return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _46 => _46.id]), () => ( null));
2025
2567
  }
2026
2568
  function videoTimelineTranscript(project, options = {}) {
2027
2569
  const id = videoTranscriptTrack(project, options.track);
@@ -2031,8 +2573,8 @@ function videoTimelineTranscript(project, options = {}) {
2031
2573
  const out = [];
2032
2574
  for (const clip of sortedVideoClips(track)) {
2033
2575
  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;
2576
+ const words = _optionalChain([assets, 'access', _47 => _47.get, 'call', _48 => _48(clip.asset), 'optionalAccess', _49 => _49.transcript]);
2577
+ if (!_optionalChain([words, 'optionalAccess', _50 => _50.length])) continue;
2036
2578
  const speed = videoClipSpeed(clip);
2037
2579
  const from = _nullishCoalesce(clip.in, () => ( 0));
2038
2580
  const to = from + clip.duration * speed;
@@ -2276,7 +2818,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
2276
2818
  }
2277
2819
 
2278
2820
  // 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";
2821
+ 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
2822
  function toVideoEdl(project, options = {}) {
2281
2823
  const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
2282
2824
  if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
@@ -2311,7 +2853,7 @@ function toVideoEdl(project, options = {}) {
2311
2853
  lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2312
2854
  } else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
2313
2855
  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))}`);
2856
+ 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
2857
  if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
2316
2858
  if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
2317
2859
  lines.push("");
@@ -2344,6 +2886,7 @@ function parseVideoLoudnessMeasurement(output) {
2344
2886
  return m;
2345
2887
  }
2346
2888
  var sec = (ms) => String(Math.round(ms) / 1e3);
2889
+ var fmtMs = (v) => String(Math.round(v * 100) / 100);
2347
2890
  var FFMPEG_BLEND = {
2348
2891
  multiply: "multiply",
2349
2892
  screen: "screen",
@@ -2403,6 +2946,58 @@ function buildFfmpeg(project, options, measure) {
2403
2946
  let cur = "base";
2404
2947
  let usesDrawtext = false;
2405
2948
  const animated = /* @__PURE__ */ new Set();
2949
+ const ramped = /* @__PURE__ */ new Set();
2950
+ const rampedPicture = (remap, lead, end) => {
2951
+ const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
2952
+ const first = pts[0];
2953
+ const last = pts[pts.length - 1];
2954
+ const s = `(T*1000+${fmtMs(first.v)})`;
2955
+ let e = fmtMs(last.t - first.t);
2956
+ for (let i = pts.length - 2; i >= 0; i--) {
2957
+ const a = pts[i];
2958
+ const b = pts[i + 1];
2959
+ if (b.v - a.v < 1e-6) continue;
2960
+ 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})`;
2961
+ }
2962
+ const before = lead + first.t;
2963
+ const after = Math.max(0, end - last.t);
2964
+ 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)}`];
2965
+ };
2966
+ const rampedSound = (remap, input2, output, lead, end) => {
2967
+ const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
2968
+ const first = pts[0];
2969
+ const last = pts[pts.length - 1];
2970
+ const n2 = audio.length;
2971
+ const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
2972
+ const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
2973
+ const pieces = [];
2974
+ const out = [];
2975
+ const parts = [];
2976
+ if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
2977
+ const played = [];
2978
+ for (let i = 0; i + 1 < pts.length; i++) {
2979
+ const dt = pts[i + 1].t - pts[i].t;
2980
+ if (dt < 0.5) continue;
2981
+ const speed = (pts[i + 1].v - pts[i].v) / dt;
2982
+ if (speed < 0.1) parts.push(silence(dt));
2983
+ else {
2984
+ played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
2985
+ parts.push(`#${played.length - 1}`);
2986
+ }
2987
+ }
2988
+ if (end - last.t > 0.5) parts.push(silence(end - last.t));
2989
+ if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
2990
+ parts.forEach((p, k2) => {
2991
+ const label = `[r${n2}p${k2}]`;
2992
+ if (p.startsWith("#")) {
2993
+ const piece = played[Number(p.slice(1))];
2994
+ 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}`);
2995
+ } else out.push(`${p}${label}`);
2996
+ pieces.push(label);
2997
+ });
2998
+ out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
2999
+ return out;
3000
+ };
2406
3001
  const blended = /* @__PURE__ */ new Set();
2407
3002
  let k = 0;
2408
3003
  const step = 2e3 / fps;
@@ -2415,7 +3010,10 @@ function buildFfmpeg(project, options, measure) {
2415
3010
  const src = options.resolve ? options.resolve(asset) : asset.src;
2416
3011
  const span = clip.duration + lead + tail;
2417
3012
  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]);
3013
+ else if (clip.timeRemap) {
3014
+ const { from, to } = videoTimeRemapSpan(clip.timeRemap);
3015
+ inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
3016
+ } else inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) - lead * speed), "-t", sec(span * speed), "-i", src]);
2419
3017
  return inputs.length - 1;
2420
3018
  };
2421
3019
  for (const track of [...project.tracks].reverse()) {
@@ -2437,7 +3035,13 @@ function buildFfmpeg(project, options, measure) {
2437
3035
  const ai = _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
2438
3036
  const trackGain = _nullishCoalesce(track.volume, () => ( 1));
2439
3037
  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];
3038
+ let source2 = `[${ai}:a]`;
3039
+ if (clip.timeRemap) {
3040
+ source2 = `[r${audio.length}]`;
3041
+ graph.push(...rampedSound(clip.timeRemap, `${ai}:a`, source2, lead, clip.duration + tail));
3042
+ ramped.add("sound");
3043
+ }
3044
+ const chain2 = [...clip.timeRemap ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
2441
3045
  const before2 = clip.transition ? videoClipBefore(track, clip) : null;
2442
3046
  const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
2443
3047
  const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
@@ -2445,11 +3049,11 @@ function buildFfmpeg(project, options, measure) {
2445
3049
  if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
2446
3050
  if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
2447
3051
  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}`);
3052
+ 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
3053
  if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
2450
3054
  if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
2451
3055
  chain2.push(`adelay=${Math.round(t0)}:all=1`);
2452
- graph.push(`[${ai}:a]${chain2.join(",")}[a${audio.length}]`);
3056
+ graph.push(`${source2}${chain2.join(",")}[a${audio.length}]`);
2453
3057
  audio.push(`[a${audio.length}]`);
2454
3058
  }
2455
3059
  if (measure || track.kind !== "video" || track.hidden) continue;
@@ -2458,7 +3062,7 @@ function buildFfmpeg(project, options, measure) {
2458
3062
  const onTimeline = `(t*1000-${Math.round(clip.start)})`;
2459
3063
  const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
2460
3064
  const tf = clip.transform;
2461
- const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _54 => _54[p]])));
3065
+ const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _57 => _57[p]])));
2462
3066
  if (clip.type === "text" && options.titleImage) {
2463
3067
  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
3068
  index = inputs.length - 1;
@@ -2467,7 +3071,7 @@ function buildFfmpeg(project, options, measure) {
2467
3071
  const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
2468
3072
  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
3073
  const coord = (axis) => {
2470
- const v = _optionalChain([tf, 'optionalAccess', _55 => _55[axis]]);
3074
+ const v = _optionalChain([tf, 'optionalAccess', _58 => _58[axis]]);
2471
3075
  const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
2472
3076
  return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
2473
3077
  };
@@ -2475,7 +3079,7 @@ function buildFfmpeg(project, options, measure) {
2475
3079
  const opts = [
2476
3080
  `text=${ffmpegFilterValue(clip.text)}`,
2477
3081
  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))))}`,
3082
+ 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
3083
  `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
2480
3084
  coord("x"),
2481
3085
  coord("y"),
@@ -2483,7 +3087,7 @@ function buildFfmpeg(project, options, measure) {
2483
3087
  ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
2484
3088
  `enable='between(t,${sec(clip.start)},${sec(end)})'`
2485
3089
  ];
2486
- if (_optionalChain([tf, 'optionalAccess', _58 => _58.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
3090
+ if (_optionalChain([tf, 'optionalAccess', _61 => _61.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
2487
3091
  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
3092
  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
3093
  graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
@@ -2499,7 +3103,8 @@ function buildFfmpeg(project, options, measure) {
2499
3103
  if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
2500
3104
  else {
2501
3105
  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)}`);
3106
+ if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
3107
+ else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
2503
3108
  }
2504
3109
  const grade = videoGradeFfmpegFilters(clip.grade);
2505
3110
  if (grade.length) chain.push("format=rgba", ...grade);
@@ -2520,10 +3125,10 @@ function buildFfmpeg(project, options, measure) {
2520
3125
  const r = videoIrisRadius({ width: W, height: H }, 1);
2521
3126
  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
3127
  }
2523
- const rotation = _optionalChain([placed, 'optionalAccess', _59 => _59.rotation]);
3128
+ const rotation = _optionalChain([placed, 'optionalAccess', _62 => _62.rotation]);
2524
3129
  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
3130
  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]))) {
3131
+ if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _63 => _63.scale]))) {
2527
3132
  const s = expr(placed.scale, onChain("t"), 1, "scale");
2528
3133
  chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
2529
3134
  }
@@ -2542,9 +3147,9 @@ function buildFfmpeg(project, options, measure) {
2542
3147
  const into = lead ? clip.transition.type : void 0;
2543
3148
  if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
2544
3149
  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);
3150
+ if (_optionalChain([after, 'optionalAccess', _64 => _64.transition, 'access', _65 => _65.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
2546
3151
  const at = (axis, c) => {
2547
- const v = _optionalChain([placed, 'optionalAccess', _63 => _63[axis]]);
3152
+ const v = _optionalChain([placed, 'optionalAccess', _66 => _66[axis]]);
2548
3153
  const extra = shifts[axis].map((s) => `+${s}`).join("");
2549
3154
  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
3155
  };
@@ -2578,11 +3183,12 @@ function buildFfmpeg(project, options, measure) {
2578
3183
  if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
2579
3184
  else if (options.loudness && !options.loudness.measured)
2580
3185
  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)))
3186
+ 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
3187
  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
3188
  if (animated.size)
2584
3189
  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
3190
  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.");
3191
+ 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
3192
  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
3193
  const filterComplex = graph.join(";");
2588
3194
  if (measure) {
@@ -2612,26 +3218,40 @@ function buildFfmpeg(project, options, measure) {
2612
3218
  }
2613
3219
 
2614
3220
  // src/video-editor/preview.ts
2615
- var VideoPreviewEngine = (_class3 = class {
2616
- constructor(source, canvas, options = {}) {;_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);
3221
+ var VideoPreviewEngine = (_class4 = class {
3222
+ constructor(source, canvas2, options = {}) {;_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);
2617
3223
  this.source = source;
2618
3224
  this.options = options;
2619
- this.canvas = canvas;
2620
- this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _70 => _70.getContext, 'optionalCall', _71 => _71("2d")]), () => ( null));
3225
+ this.canvas = canvas2;
3226
+ this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _73 => _73.getContext, 'optionalCall', _74 => _74("2d")]), () => ( null));
2621
3227
  this.unsubscribe = source.subscribe(() => this.update());
3228
+ const pixels = options.pixels !== void 0 ? options.pixels : source.previewPixels;
3229
+ this.detachPixels = pixels ? pixels.attach(() => this.readPixels()) : () => {
3230
+ };
3231
+ this.pixelsTarget = _nullishCoalesce(pixels, () => ( null));
2622
3232
  this.update();
2623
3233
  }
2624
3234
 
2625
3235
 
2626
3236
 
2627
3237
 
2628
- __init6() {this.media = /* @__PURE__ */ new Map()}
2629
- __init7() {this.images = /* @__PURE__ */ new Map()}
3238
+ __init9() {this.media = /* @__PURE__ */ new Map()}
3239
+ __init10() {this.images = /* @__PURE__ */ new Map()}
2630
3240
 
2631
- __init8() {this.scheduled = false}
2632
- __init9() {this.destroyed = false}
2633
- __init10() {this.filterHost = null}
2634
- __init11() {this.filters = /* @__PURE__ */ new Set()}
3241
+ __init11() {this.scheduled = false}
3242
+ __init12() {this.destroyed = false}
3243
+ __init13() {this.filterHost = null}
3244
+ __init14() {this.filters = /* @__PURE__ */ new Set()}
3245
+
3246
+
3247
+ __init15() {this.drawn = null}
3248
+ __init16() {this.complete = true}
3249
+ /** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
3250
+ readPixels() {
3251
+ const ctx = this.ctx;
3252
+ if (!_optionalChain([ctx, 'optionalAccess', _75 => _75.getImageData]) || !this.drawn) return null;
3253
+ return { ...this.drawn, pixels: ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) };
3254
+ }
2635
3255
  createMedia(kind) {
2636
3256
  if (this.options.createMedia) return this.options.createMedia(kind);
2637
3257
  const el = document.createElement(kind);
@@ -2667,8 +3287,8 @@ var VideoPreviewEngine = (_class3 = class {
2667
3287
  entry.media.pause();
2668
3288
  entry.media.removeEventListener("seeked", entry.onReady);
2669
3289
  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()]);
3290
+ _optionalChain([entry, 'access', _76 => _76.media, 'access', _77 => _77.removeAttribute, 'optionalCall', _78 => _78("src")]);
3291
+ _optionalChain([entry, 'access', _79 => _79.media, 'access', _80 => _80.load, 'optionalCall', _81 => _81()]);
2672
3292
  this.media.delete(entry.clip);
2673
3293
  }
2674
3294
  /** Brings the media elements in step with the editor and draws the frame. */
@@ -2682,13 +3302,14 @@ var VideoPreviewEngine = (_class3 = class {
2682
3302
  const { media } = this.entry(clip, src, kind);
2683
3303
  active.add(clip);
2684
3304
  const target = sourceTime / 1e3;
2685
- if (Math.abs(media.playbackRate - speed) > 1e-6) media.playbackRate = speed;
3305
+ const frozen = speed < 0.0625;
3306
+ if (!frozen && Math.abs(media.playbackRate - speed) > 1e-6) media.playbackRate = speed;
2686
3307
  if (gain === null) media.muted = true;
2687
3308
  else {
2688
3309
  media.muted = gain <= 0;
2689
3310
  media.volume = Math.min(1, Math.max(0, gain));
2690
3311
  }
2691
- if (playing) {
3312
+ if (playing && !frozen) {
2692
3313
  if (media.paused || Math.abs(media.currentTime - target) > tolerance) media.currentTime = target;
2693
3314
  if (media.paused) void Promise.resolve(media.play()).catch(() => {
2694
3315
  });
@@ -2701,7 +3322,7 @@ var VideoPreviewEngine = (_class3 = class {
2701
3322
  for (const layer of frame.layers) {
2702
3323
  if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
2703
3324
  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);
3325
+ sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _82 => _82.speed]), () => ( 1)), sound ? sound.gain : null);
2705
3326
  sounding.delete(layer.clip);
2706
3327
  }
2707
3328
  for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
@@ -2716,13 +3337,15 @@ var VideoPreviewEngine = (_class3 = class {
2716
3337
  if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
2717
3338
  });
2718
3339
  }
2719
- __init12() {this.sourceFor = (layer) => {
3340
+ __init17() {this.sourceFor = (layer) => {
2720
3341
  if (layer.assetKind === "image" && layer.src) {
2721
3342
  const img = this.image(layer.src);
2722
3343
  return img.complete ? img : null;
2723
3344
  }
2724
3345
  const entry = this.media.get(layer.clip);
2725
- return entry && entry.media.readyState >= 2 ? entry.media : null;
3346
+ if (!entry || entry.media.readyState < 2) return null;
3347
+ if (entry.media.seeking) this.complete = false;
3348
+ return entry.media;
2726
3349
  }}
2727
3350
  draw(frame) {
2728
3351
  if (!this.ctx) return;
@@ -2733,10 +3356,18 @@ var VideoPreviewEngine = (_class3 = class {
2733
3356
  const h = Math.max(1, Math.round(frame.height * resolution));
2734
3357
  if (this.canvas.width !== w) this.canvas.width = w;
2735
3358
  if (this.canvas.height !== h) this.canvas.height = h;
2736
- 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)]);
3359
+ this.complete = true;
3360
+ const sourceFor = (layer) => {
3361
+ const source = this.sourceFor(layer);
3362
+ if (!source) this.complete = false;
3363
+ return source;
3364
+ };
3365
+ drawVideoFrame(this.ctx, frame, sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
3366
+ this.drawn = { time: frame.time, complete: this.complete };
3367
+ _optionalChain([this, 'access', _83 => _83.options, 'access', _84 => _84.onFrame, 'optionalCall', _85 => _85(frame)]);
3368
+ _optionalChain([this, 'access', _86 => _86.pixelsTarget, 'optionalAccess', _87 => _87.drawn, 'call', _88 => _88(frame.time, this.complete)]);
2738
3369
  }
2739
- __init13() {this.defineSvgFilter = (id, markup) => {
3370
+ __init18() {this.defineSvgFilter = (id, markup) => {
2740
3371
  if (this.options.defineSvgFilter === null) return;
2741
3372
  if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
2742
3373
  if (this.filters.has(id) || typeof document === "undefined") return;
@@ -2755,33 +3386,36 @@ var VideoPreviewEngine = (_class3 = class {
2755
3386
  /** Stops following the editor and releases every media element. */
2756
3387
  destroy() {
2757
3388
  this.destroyed = true;
2758
- _optionalChain([this, 'access', _82 => _82.filterHost, 'optionalAccess', _83 => _83.remove, 'call', _84 => _84()]);
3389
+ this.detachPixels();
3390
+ _optionalChain([this, 'access', _89 => _89.filterHost, 'optionalAccess', _90 => _90.remove, 'call', _91 => _91()]);
2759
3391
  this.filterHost = null;
2760
3392
  this.filters.clear();
2761
3393
  this.unsubscribe();
2762
3394
  for (const entry of [...this.media.values()]) this.release(entry);
2763
3395
  this.images.clear();
2764
3396
  }
2765
- }, _class3);
3397
+ }, _class4);
2766
3398
 
2767
3399
  // src/video-editor/controller.ts
2768
- var VideoEditorController = (_class4 = class {
3400
+ var VideoEditorController = (_class5 = class {
2769
3401
 
2770
3402
 
2771
3403
  /** Sound levels per asset, loaded on demand for waveforms and agents. */
2772
3404
 
3405
+ /** The pixels the preview last drew, for scopes and `get_video_scopes` (a `VideoPreviewEngine` fills it). */
3406
+ __init19() {this.previewPixels = new VideoPreviewPixels()}
2773
3407
 
2774
3408
 
2775
- __init14() {this.selection = []}
3409
+ __init20() {this.selection = []}
2776
3410
 
2777
- __init15() {this.error = null}
2778
- __init16() {this.revision = 0}
2779
- __init17() {this.applying = false}
3411
+ __init21() {this.error = null}
3412
+ __init22() {this.revision = 0}
3413
+ __init23() {this.applying = false}
2780
3414
 
2781
- __init18() {this.listeners = /* @__PURE__ */ new Set()}
2782
- __init19() {this.unsubscribers = []}
3415
+ __init24() {this.listeners = /* @__PURE__ */ new Set()}
3416
+ __init25() {this.unsubscribers = []}
2783
3417
 
2784
- constructor(options = {}) {;_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);_class4.prototype.__init19.call(this);_class4.prototype.__init20.call(this);_class4.prototype.__init21.call(this);
3418
+ constructor(options = {}) {;_class5.prototype.__init19.call(this);_class5.prototype.__init20.call(this);_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);_class5.prototype.__init25.call(this);_class5.prototype.__init26.call(this);_class5.prototype.__init27.call(this);
2785
3419
  this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
2786
3420
  this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
2787
3421
  this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
@@ -2806,8 +3440,8 @@ var VideoEditorController = (_class4 = class {
2806
3440
  this.player.pause();
2807
3441
  this.unsubscribers.splice(0).forEach((u) => u());
2808
3442
  }
2809
- __init20() {this.getState = () => this.state}
2810
- __init21() {this.subscribe = (listener) => {
3443
+ __init26() {this.getState = () => this.state}
3444
+ __init27() {this.subscribe = (listener) => {
2811
3445
  this.connect();
2812
3446
  this.listeners.add(listener);
2813
3447
  return () => {
@@ -2863,7 +3497,7 @@ var VideoEditorController = (_class4 = class {
2863
3497
  this.applying = false;
2864
3498
  }
2865
3499
  this.notify();
2866
- _optionalChain([this, 'access', _85 => _85.onChange, 'optionalCall', _86 => _86(project)]);
3500
+ _optionalChain([this, 'access', _92 => _92.onChange, 'optionalCall', _93 => _93(project)]);
2867
3501
  }
2868
3502
  commit(label, next) {
2869
3503
  const before = this.project;
@@ -2927,7 +3561,7 @@ var VideoEditorController = (_class4 = class {
2927
3561
  }
2928
3562
  /** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
2929
3563
  select(clips, options = {}) {
2930
- const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _87 => _87.filter, 'call', _88 => _88((id) => findVideoClip(this.project, id))]), () => ( []));
3564
+ const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _94 => _94.filter, 'call', _95 => _95((id) => findVideoClip(this.project, id))]), () => ( []));
2931
3565
  let next;
2932
3566
  if (options.add) {
2933
3567
  next = [...this.selection];
@@ -3008,7 +3642,7 @@ var VideoEditorController = (_class4 = class {
3008
3642
  toEdl(options = {}) {
3009
3643
  return toVideoEdl(this.project, options);
3010
3644
  }
3011
- }, _class4);
3645
+ }, _class5);
3012
3646
 
3013
3647
  // src/video-editor/tools.ts
3014
3648
  var round = (ms) => Math.round(ms * 10) / 10;
@@ -3221,14 +3855,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
3221
3855
  return null;
3222
3856
  };
3223
3857
  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 } : {},
3858
+ ..._optionalChain([o, 'access', _96 => _96.created, 'optionalAccess', _97 => _97.length]) ? { created: o.created } : {},
3859
+ ..._optionalChain([o, 'access', _98 => _98.trimmed, 'optionalAccess', _99 => _99.length]) ? { trimmed: o.trimmed } : {},
3860
+ ..._optionalChain([o, 'access', _100 => _100.removed, 'optionalAccess', _101 => _101.length]) ? { removed: o.removed } : {},
3227
3861
  ...o.rippled ? { rippled: o.rippled } : {}
3228
3862
  });
3229
3863
  const keysOut = (p, clip, property) => {
3230
3864
  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]]);
3865
+ const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _102 => _102.transform, 'optionalAccess', _103 => _103[property]]) : _optionalChain([c, 'optionalAccess', _104 => _104[property]]);
3232
3866
  return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
3233
3867
  };
3234
3868
  const getProject = {
@@ -3526,7 +4160,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3526
4160
  const at = findVideoClip(p, id);
3527
4161
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
3528
4162
  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")
4163
+ if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _105 => _105.assets, 'access', _106 => _106.find, 'call', _107 => _107((a) => a.id === clip.asset), 'optionalAccess', _108 => _108.kind]) === "audio")
3530
4164
  throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
3531
4165
  }
3532
4166
  };
@@ -3547,7 +4181,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3547
4181
  gradeTargets(clips);
3548
4182
  const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
3549
4183
  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)) })) }));
4184
+ return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _109 => _109(r.project, id), 'optionalAccess', _110 => _110.clip, 'access', _111 => _111.grade]), () => ( null)) })) }));
3551
4185
  }
3552
4186
  };
3553
4187
  const copyGrade = {
@@ -3614,7 +4248,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3614
4248
  handler: ({ track, from, to, limit }) => {
3615
4249
  const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
3616
4250
  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 };
4251
+ return { track: _nullishCoalesce(_optionalChain([words, 'access', _112 => _112[0], 'optionalAccess', _113 => _113.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
4252
  }
3619
4253
  };
3620
4254
  const findText = {
@@ -3671,6 +4305,83 @@ function createVideoEditorToolProvider(controller, options = {}) {
3671
4305
  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
4306
  }
3673
4307
  };
4308
+ const rampTarget = (id) => {
4309
+ const at = findVideoClip(project(), id);
4310
+ if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
4311
+ const { clip } = at;
4312
+ 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.`);
4313
+ if (_optionalChain([project, 'call', _114 => _114(), 'access', _115 => _115.assets, 'access', _116 => _116.find, 'call', _117 => _117((a) => a.id === clip.asset), 'optionalAccess', _118 => _118.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
4314
+ return clip;
4315
+ };
4316
+ const rampOut = (r, id) => {
4317
+ const outcome2 = r.outcomes[r.outcomes.length - 1];
4318
+ return { clip: clipOut(r.project, id), ...outcome2.rippled ? { rippled: outcome2.rippled } : {}, duration: videoProjectDuration(r.project) };
4319
+ };
4320
+ 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." };
4321
+ const setSpeedRamp = {
4322
+ name: "set_video_speed_ramp",
4323
+ title: "Ramp the speed",
4324
+ 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).`,
4325
+ inputSchema: object(
4326
+ {
4327
+ clip: CLIP_ID,
4328
+ segments: {
4329
+ type: "array",
4330
+ minItems: 1,
4331
+ maxItems: 100,
4332
+ description: "The stretches in order, from the clip's start.",
4333
+ items: object(
4334
+ {
4335
+ duration: { type: "number", exclusiveMinimum: 0, description: "Length of the stretch on the timeline, in ms." },
4336
+ 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." },
4337
+ 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." }
4338
+ },
4339
+ ["duration", "speed"],
4340
+ "One stretch."
4341
+ )
4342
+ },
4343
+ ripple: RIPPLE_RAMP
4344
+ },
4345
+ ["clip", "segments"]
4346
+ ),
4347
+ annotations: { title: "Ramp the speed", ...WRITE_IDEMPOTENT },
4348
+ handler: ({ clip, segments, ripple }) => {
4349
+ const edit = videoSpeedRampEdit(rampTarget(clip), segments, _nullishCoalesce(ripple, () => ( true)));
4350
+ return apply(edit, "Speed ramp", (r) => ({ ...rampOut(r, clip), source: Math.round(videoSpeedRampKeys(segments).source) }));
4351
+ }
4352
+ };
4353
+ const addFreeze = {
4354
+ name: "add_video_freeze_frame",
4355
+ title: "Add a freeze frame",
4356
+ 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.",
4357
+ inputSchema: object(
4358
+ {
4359
+ clip: CLIP_ID,
4360
+ at: MS("Timeline time of the frame to hold (inside the clip)"),
4361
+ duration: { type: "number", exclusiveMinimum: 0, description: "How long to hold it, in ms." },
4362
+ ripple: RIPPLE_RAMP
4363
+ },
4364
+ ["clip", "at", "duration"]
4365
+ ),
4366
+ annotations: { title: "Add a freeze frame", ...WRITE },
4367
+ handler: ({ clip, at, duration, ripple }) => {
4368
+ const c = rampTarget(clip);
4369
+ const t = at - c.start;
4370
+ 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.`);
4371
+ return apply(videoFreezeFrameEdit(c, t, duration, _nullishCoalesce(ripple, () => ( true))), "Freeze frame", (r) => rampOut(r, clip));
4372
+ }
4373
+ };
4374
+ const removeRamp = {
4375
+ name: "remove_video_speed_ramp",
4376
+ title: "Remove a speed ramp",
4377
+ 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.',
4378
+ inputSchema: object({ clip: CLIP_ID, duration: { type: "number", exclusiveMinimum: 0, description: "New length in ms (default: unchanged)." }, ripple: RIPPLE_RAMP }, ["clip"]),
4379
+ annotations: { title: "Remove a speed ramp", ...WRITE },
4380
+ handler: ({ clip, duration, ripple }) => {
4381
+ rampTarget(clip);
4382
+ return apply({ op: "set_time_remap", clip, keys: null, ...duration !== void 0 ? { duration } : {}, ripple: _nullishCoalesce(ripple, () => ( true)) }, "Remove speed ramp", (r) => rampOut(r, clip));
4383
+ }
4384
+ };
3674
4385
  const setTransition = {
3675
4386
  name: "set_video_transition",
3676
4387
  title: "Set a transition",
@@ -3781,7 +4492,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
3781
4492
  };
3782
4493
  const EDIT_OP = {
3783
4494
  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 }.`,
4495
+ 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
4496
  properties: { op: { type: "string", enum: VIDEO_EDIT_OPS.filter((o) => o !== "replace_project"), description: "Which edit." } },
3786
4497
  required: ["op"]
3787
4498
  };
@@ -3888,8 +4599,47 @@ function createVideoEditorToolProvider(controller, options = {}) {
3888
4599
  return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
3889
4600
  }
3890
4601
  };
3891
- 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];
4602
+ const pixelsAt = async (time) => {
4603
+ if (options.framePixels) {
4604
+ const px = await options.framePixels(time);
4605
+ if (!px) throw new Error(`The host could not render the frame at ${Math.round(time)} ms; use render_video_frame to look at it instead.`);
4606
+ return px;
4607
+ }
4608
+ const feed = controller.previewPixels;
4609
+ if (!feed.attached) throw new Error("No preview is showing this project, so there are no pixels to measure. The host must show a <VideoPreview> for this editor (or pass framePixels to the tool provider); until then use get_video_frame and render_video_frame.");
4610
+ const state = controller.getState();
4611
+ if (Math.abs(time - state.time) < 0.5) return (await feed.next(state.time, options.framePixelsTimeout)).pixels;
4612
+ const { time: before, playing } = state;
4613
+ if (playing) controller.pause();
4614
+ controller.seek(time);
4615
+ try {
4616
+ return (await feed.next(controller.getState().time, options.framePixelsTimeout)).pixels;
4617
+ } finally {
4618
+ controller.seek(before);
4619
+ if (playing) controller.play();
4620
+ }
4621
+ };
4622
+ const getScopes = {
4623
+ name: "get_video_scopes",
4624
+ title: "Measure a frame with scopes",
4625
+ description: "Measures the rendered picture at a moment as colour scopes would, without changing the project: luma (0 = black, 1 = white) mean, median, min, max and 1st / 99th percentiles; the share of pixels crushed to black or blown to white (and per channel); saturation; the average colour (a cast shows as a tint); a coarse luma and RGB histogram; the luma spread of vertical strips left to right (a waveform); the main hue and strength on a vectorscope; and notes on what to fix. Use it before and after grading (update_video_clip grade, apply_video_grade_preset) to check exposure and colour. Needs the pixels the host draws: fails with a reason when no preview is showing.",
4626
+ inputSchema: object({
4627
+ time: MS("Time from the start to measure (default: the playhead). Another time is shown briefly and the playhead put back"),
4628
+ bins: { type: "integer", minimum: 2, maximum: 64, description: "Histogram steps, darkest first (default 16)." },
4629
+ columns: { type: "integer", minimum: 1, maximum: 32, description: "Waveform strips across the picture, left to right (default 8)." }
4630
+ }),
4631
+ annotations: { title: "Measure a frame with scopes", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4632
+ handler: async ({ time, bins, columns }) => {
4633
+ const p = project();
4634
+ const end = videoProjectDuration(p);
4635
+ const t = _nullishCoalesce(time, () => ( controller.getState().time));
4636
+ if (time !== void 0 && time > end) throw new Error(`Time ${time} ms is past the end of the project (${end} ms).`);
4637
+ const pixels = await pixelsAt(t);
4638
+ return { time: t, timecode: formatVideoTimecode(t, p.fps), width: pixels.width, height: pixels.height, ...summarizeVideoScopes(pixels, { bins, columns }) };
4639
+ }
4640
+ };
4641
+ const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
4642
+ 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
4643
  return {
3894
4644
  id: _nullishCoalesce(options.id, () => ( "video")),
3895
4645
  namespace: options.namespace,
@@ -3975,7 +4725,7 @@ function videoClipLabel(clip, asset) {
3975
4725
  if (clip.name) return clip.name;
3976
4726
  if (clip.type === "text") return clip.text.split("\n")[0];
3977
4727
  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));
4728
+ const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _119 => _119.name]), () => ( _optionalChain([asset, 'optionalAccess', _120 => _120.src, 'access', _121 => _121.replace, 'call', _122 => _122(/^.*[\\/]/, "")]))), () => ( clip.asset));
3979
4729
  const speed = videoClipSpeed(clip);
3980
4730
  return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
3981
4731
  }
@@ -4019,7 +4769,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
4019
4769
  const asset = videoAssetMap(project).get(assetId);
4020
4770
  if (!asset) return null;
4021
4771
  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])));
4772
+ const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _123 => _123.tracks, 'access', _124 => _124.find, 'call', _125 => _125((t) => t.kind === kind && !t.locked), 'optionalAccess', _126 => _126.id])));
4023
4773
  if (!track) return null;
4024
4774
  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
4775
  }
@@ -4068,14 +4818,14 @@ var KEY_LABELS = {
4068
4818
  scale: { label: "Scale", step: 0.05 },
4069
4819
  rotation: { label: "Rotation", step: 1 }
4070
4820
  };
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;
4821
+ var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _127 => _127.transform, 'optionalAccess', _128 => _128[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
4072
4822
  var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
4073
4823
  function videoKeyframeRows(project, clipId, time) {
4074
4824
  const at = findVideoClip(project, clipId);
4075
4825
  if (!at) return [];
4076
4826
  const { clip } = at;
4077
4827
  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");
4828
+ const visual = !(_optionalChain([asset, 'optionalAccess', _129 => _129.kind]) === "audio" || at.track.kind === "audio");
4079
4829
  const audible = clip.type === "media" && videoAssetHasSound(asset);
4080
4830
  const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
4081
4831
  const clipTime = playheadInClip(clip, time);
@@ -4289,5 +5039,33 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
4289
5039
 
4290
5040
 
4291
5041
 
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;
5042
+
5043
+
5044
+
5045
+
5046
+
5047
+
5048
+
5049
+
5050
+
5051
+
5052
+
5053
+
5054
+
5055
+
5056
+
5057
+
5058
+
5059
+
5060
+
5061
+
5062
+
5063
+
5064
+
5065
+
5066
+
5067
+
5068
+
5069
+
5070
+ 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_SCOPE_KINDS = VIDEO_SCOPE_KINDS; 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_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; 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.readVideoScopesPanel = readVideoScopesPanel; 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.summarizeVideoScopes = summarizeVideoScopes; 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.videoChroma = videoChroma; 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.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; 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.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; 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.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; 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.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
4293
5071
  //# sourceMappingURL=index.cjs.map