@vosjs/studio-core 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,6 +8,13 @@ var TRANSITION_SPEED_MULT = {
8
8
  function transitionMult(t) {
9
9
  return t !== void 0 && t in TRANSITION_SPEED_MULT ? TRANSITION_SPEED_MULT[t] : 1;
10
10
  }
11
+ var FREEZE_SECONDS_MIN = 0.25;
12
+ var FREEZE_SECONDS_MAX = 30;
13
+ var FREEZE_DEFAULT_SECONDS = 2;
14
+ function clampFreezeSeconds(seconds) {
15
+ const s = Math.min(FREEZE_SECONDS_MAX, Math.max(FREEZE_SECONDS_MIN, seconds));
16
+ return Math.round(s * 1e3) / 1e3;
17
+ }
11
18
  var SPEED_RATE_MIN = 0.1;
12
19
  var SPEED_RATE_MAX = 16;
13
20
  var SPEED_SPAN_MIN = 0.25;
@@ -506,17 +513,43 @@ function lastLayerEnd(doc) {
506
513
  import { splitBySpeed, totalDuration } from "@vosjs/timeline";
507
514
  function docRestTime(doc) {
508
515
  if (doc.endCard) return null;
509
- const last = doc.segments.at(-1);
510
- const hold = last?.hold;
511
- if (typeof hold !== "number" || !(hold > 0)) return null;
512
- const segs = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
513
- const total = totalDuration(
514
- withHolds(segs, splitBySpeed(segs, doc.speed ?? []))
516
+ const freezes = docFreezes(doc);
517
+ if (!freezes.length) return null;
518
+ const segs = docSegmentsOf(doc);
519
+ const marks = freezeOutputExtents(
520
+ splitBySpeed(segs, doc.speed ?? []),
521
+ freezes
515
522
  );
516
- const rest = total - hold;
517
- if (!(rest >= 0)) return null;
523
+ let rest = null;
524
+ for (const m of marks.values()) if (rest === null || m.t > rest) rest = m.t;
525
+ if (rest === null || !(rest >= 0)) return null;
518
526
  return Math.round(rest * 1e3) / 1e3;
519
527
  }
528
+ function docSegmentsOf(doc) {
529
+ const segs = Array.isArray(doc.segments) ? doc.segments : [];
530
+ if (segs.length) return segs;
531
+ const ms = doc.source?.meta?.durationMs;
532
+ return [{ in: 0, out: typeof ms === "number" ? ms / 1e3 : 0 }];
533
+ }
534
+ function docFreezes(doc) {
535
+ const own = Array.isArray(doc.freeze) ? doc.freeze : [];
536
+ const legacy = legacyHoldFreezes(
537
+ Array.isArray(doc.segments) ? doc.segments : []
538
+ );
539
+ const all = [...own, ...legacy].filter(
540
+ (f) => typeof f.at === "number" && Number.isFinite(f.at) && typeof f.seconds === "number" && f.seconds > 0
541
+ );
542
+ return all.sort((a, b) => a.at - b.at);
543
+ }
544
+ function legacyHoldFreezes(segments) {
545
+ const out = [];
546
+ segments.forEach((s, i) => {
547
+ const hold = s.hold;
548
+ if (typeof hold === "number" && hold > 0)
549
+ out.push({ id: `h${i}`, at: s.out, seconds: hold });
550
+ });
551
+ return out;
552
+ }
520
553
  var ENTRANCE_SECONDS = 1.2;
521
554
  var END_CARD_SECONDS = 2.5;
522
555
  var END_CARD_RECEDE = 0.7;
@@ -538,30 +571,69 @@ function entranceSeconds(e) {
538
571
  function exitSeconds(e) {
539
572
  if (!e || e.kind === "none") return 0;
540
573
  const house = e.kind === "fade" ? CARD_FADE_SECONDS : END_CARD_RECEDE;
541
- return Math.max(0.1, Math.min(3, e.seconds ?? house));
574
+ return Math.max(0.1, Math.min(8, e.seconds ?? house));
542
575
  }
543
- function withHolds(docSegments, rated) {
544
- const out = [];
545
- const pending = docSegments.filter((s) => typeof s.hold === "number" && s.hold > 0).map((s) => ({ out: s.out, hold: s.hold, used: false }));
576
+ function placeFreezes(rated, freezes) {
577
+ const marks = /* @__PURE__ */ new Map();
578
+ const pending = [...freezes].filter((f) => f.seconds > 0).sort((a, b) => a.at - b.at);
579
+ const pieces = [];
580
+ let acc = 0;
581
+ const freezePiece = (f) => {
582
+ const at = Math.max(HOLD_SOURCE_SPAN, f.at);
583
+ marks.set(f.id, { t: acc, duration: f.seconds });
584
+ pieces.push({
585
+ in: at - HOLD_SOURCE_SPAN,
586
+ out: at,
587
+ rate: HOLD_SOURCE_SPAN / f.seconds
588
+ });
589
+ acc += f.seconds;
590
+ };
591
+ const push2 = (p) => {
592
+ pieces.push(p);
593
+ acc += Math.max(0, p.out - p.in) / (p.rate ?? 1);
594
+ };
546
595
  for (let i = 0; i < rated.length; i++) {
547
- const piece = rated[i];
548
- out.push(piece);
596
+ let piece = rated[i];
549
597
  const next = rated[i + 1];
550
- for (const p of pending) {
551
- if (p.used) continue;
552
- const endsHere = Math.abs(piece.out - p.out) < 1e-9;
553
- const nextContinues = next && Math.abs(next.in - p.out) < 1e-9 && next.out > p.out;
554
- if (endsHere && !nextContinues) {
555
- out.push({
556
- in: p.out - HOLD_SOURCE_SPAN,
557
- out: p.out,
558
- rate: HOLD_SOURCE_SPAN / p.hold
559
- });
560
- p.used = true;
598
+ while (pending.length) {
599
+ const f = pending[0];
600
+ if (f.at > piece.out + 1e-9) break;
601
+ if (f.at < piece.in - 1e-9) {
602
+ pending.shift();
603
+ continue;
561
604
  }
605
+ if (f.at <= piece.in + 1e-9) {
606
+ pending.shift();
607
+ freezePiece(f);
608
+ continue;
609
+ }
610
+ if (f.at < piece.out - 1e-9) {
611
+ push2({ ...piece, out: f.at });
612
+ pending.shift();
613
+ freezePiece(f);
614
+ piece = { ...piece, in: f.at };
615
+ continue;
616
+ }
617
+ const continues = next && Math.abs(next.in - piece.out) < 1e-9 && next.out > piece.out;
618
+ if (continues) break;
619
+ push2(piece);
620
+ piece = null;
621
+ pending.shift();
622
+ freezePiece(f);
623
+ break;
562
624
  }
625
+ if (piece) push2(piece);
563
626
  }
564
- return out;
627
+ return { pieces, marks };
628
+ }
629
+ function withFreezes(rated, freezes) {
630
+ return placeFreezes(rated, freezes).pieces;
631
+ }
632
+ function freezeOutputExtents(rated, freezes) {
633
+ return placeFreezes(rated, freezes).marks;
634
+ }
635
+ function withHolds(docSegments, rated) {
636
+ return withFreezes(rated, legacyHoldFreezes(docSegments));
565
637
  }
566
638
  function entranceTiltKeyframes(e) {
567
639
  const s = entranceSeconds(e);
@@ -591,8 +663,9 @@ function prependEntrance(track, head) {
591
663
  return { keyframes: sortKeyframes([...head, ...rest]) };
592
664
  }
593
665
  function exitPose(e) {
594
- return e?.kind === "fade" ? [1, 0, 0] : [0.9, -0.02, 0.22];
666
+ return e?.kind === "fade" ? [1, 0, 0] : [0.9, -0.02, 0];
595
667
  }
668
+ var RECEDE_MID = [0.91, -0.018, 0.8];
596
669
  function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
597
670
  const keyframes = [];
598
671
  const s = entranceSeconds(enter);
@@ -602,16 +675,36 @@ function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
602
675
  keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
603
676
  }
604
677
  const x = exitSeconds(exit);
678
+ let last = [1, 0, 1];
605
679
  if (x > 0 && footageEnd > 0) {
606
- const room = outputEnd2 - footageEnd;
607
- const t0 = Math.max(s, room > 1e-6 ? footageEnd : footageEnd - x);
608
- const t1 = room > 1e-6 ? t0 + Math.min(x, Math.max(0.1, room)) : t0 + x;
680
+ const t0 = round3(Math.max(s, footageEnd - x));
681
+ const t1 = round3(Math.max(t0 + 0.05, footageEnd));
682
+ last = exitPose(exit);
609
683
  keyframes.push({ t: t0, value: [1, 0, 1], ease: "none" });
610
- keyframes.push({ t: t1, value: exitPose(exit), ease: "power2.out" });
684
+ if (exit?.kind !== "fade") {
685
+ keyframes.push({
686
+ t: round3(t0 + (t1 - t0) * 0.45),
687
+ value: [...RECEDE_MID],
688
+ ease: "power2.out"
689
+ });
690
+ keyframes.push({ t: t1, value: last, ease: "power1.in" });
691
+ } else {
692
+ keyframes.push({ t: t1, value: last, ease: "power2.out" });
693
+ }
694
+ }
695
+ if (outputEnd2 > footageEnd + 1e-6 && footageEnd > 0) {
696
+ if (!keyframes.some((k) => Math.abs(k.t - footageEnd) < 1e-9))
697
+ keyframes.push({ t: footageEnd, value: [...last], ease: "none" });
698
+ keyframes.push({
699
+ t: round3(footageEnd + 1e-3),
700
+ value: [last[0], last[1], 0],
701
+ ease: "none"
702
+ });
611
703
  }
612
704
  if (!keyframes.length) return void 0;
613
705
  return { keyframes: sortKeyframes(keyframes) };
614
706
  }
707
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
615
708
  function endCardClips(card, doc, endStart) {
616
709
  const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
617
710
  const anim = { enter: "rise", exit: "none" };
@@ -704,9 +797,29 @@ function migrateMotion(doc) {
704
797
  const segments = Array.isArray(doc.segments) ? doc.segments : [];
705
798
  const overlays = Array.isArray(doc.overlays) ? doc.overlays : [];
706
799
  const objects = Array.isArray(doc.objects) ? doc.objects : [];
707
- const legacy = frame.entrance !== void 0 || doc.endCard !== void 0 || overlays.some(hasLegacyClipMotion) || objects.some((o) => o.animation !== void 0);
800
+ const heldSegments = segments.some((s) => s.hold !== void 0);
801
+ const legacy = frame.entrance !== void 0 || doc.endCard !== void 0 || heldSegments || overlays.some(hasLegacyClipMotion) || objects.some((o) => o.animation !== void 0);
708
802
  if (!legacy) return doc;
709
803
  const out = { ...doc };
804
+ if (heldSegments) {
805
+ const holds = legacyHoldFreezes(segments);
806
+ const own = Array.isArray(doc.freeze) ? doc.freeze : [];
807
+ const taken = new Set(own.map((f) => f.id));
808
+ let n = own.length;
809
+ const migrated = holds.map((h) => {
810
+ let id = `f${n++}`;
811
+ while (taken.has(id)) id = `f${n++}`;
812
+ taken.add(id);
813
+ return { id, at: h.at, seconds: h.seconds };
814
+ });
815
+ out.freeze = [...own, ...migrated].sort((a, b) => a.at - b.at);
816
+ out.segments = segments.map((s) => {
817
+ if (s.hold === void 0) return s;
818
+ const next = { ...s };
819
+ delete next.hold;
820
+ return next;
821
+ });
822
+ }
710
823
  const nextFrame = { ...frame };
711
824
  if (frame.entrance !== void 0) {
712
825
  const e = frame.entrance;
@@ -730,15 +843,25 @@ function migrateMotion(doc) {
730
843
  const card = doc.endCard;
731
844
  if (card && segments.length) {
732
845
  const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
733
- const rated = withHolds(segments, splitBySpeed(segments, doc.speed ?? []));
846
+ const segsNow = out.segments ?? segments;
847
+ const lastSeg = segsNow.at(-1);
848
+ const freezesNow = docFreezes({ segments: segsNow, freeze: out.freeze });
849
+ const rated = withFreezes(
850
+ splitBySpeed(segsNow, doc.speed ?? []),
851
+ freezesNow
852
+ );
734
853
  const endStart = totalDuration(rated);
854
+ const taken = new Set(freezesNow.map((f) => f.id));
855
+ let n = 0;
856
+ while (taken.has(`f${n}`)) n++;
857
+ out.freeze = [
858
+ ...freezesNow,
859
+ { id: `f${n}`, at: lastSeg.out, seconds, from: END_CARD_FROM }
860
+ ].sort((a, b) => a.at - b.at);
735
861
  nextOverlays = [...nextOverlays, ...endCardClips(card, doc, endStart)];
736
862
  nextFrame.anim = {
737
863
  ...nextFrame.anim ?? {},
738
- exit: {
739
- kind: "recede",
740
- seconds: Math.min(END_CARD_RECEDE, seconds * 0.4)
741
- }
864
+ exit: { kind: "recede", seconds }
742
865
  };
743
866
  }
744
867
  delete out.endCard;
@@ -767,12 +890,12 @@ function outputEnd(doc, footageEnd) {
767
890
  }
768
891
 
769
892
  // src/docVersion.ts
770
- var DOC_SCHEMA_VERSION = 3;
893
+ var DOC_SCHEMA_VERSION = 4;
771
894
  function migrateHostedDoc(raw) {
772
895
  const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
773
896
  if (version >= DOC_SCHEMA_VERSION) return raw;
774
897
  let doc = raw;
775
- if (version < 3 && doc.source && typeof doc.source === "object") {
898
+ if (version < 4 && doc.source && typeof doc.source === "object") {
776
899
  doc = migrateMotion(doc);
777
900
  }
778
901
  return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
@@ -1815,14 +1938,14 @@ function resolveOverlayFx(clip, clipDuration) {
1815
1938
  const maxTotal = Math.max(dur, clipDuration * 0.9);
1816
1939
  st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
1817
1940
  }
1818
- const round11 = (v) => Math.round(v * 1e4) / 1e4;
1941
+ const round12 = (v) => Math.round(v * 1e4) / 1e4;
1819
1942
  return {
1820
1943
  k: spec.fx,
1821
1944
  u: unit,
1822
1945
  d: FX_DIR[spec.direction ?? "forward"] ?? 0,
1823
- st: round11(st),
1824
- dur: round11(dur),
1825
- tt: round11(st * (upperN - 1) + dur),
1946
+ st: round12(st),
1947
+ dur: round12(dur),
1948
+ tt: round12(st * (upperN - 1) + dur),
1826
1949
  units,
1827
1950
  n
1828
1951
  };
@@ -2037,7 +2160,7 @@ var CURSOR_IDLE_HOLD = 1;
2037
2160
  var CURSOR_IDLE_FADE_OUT = 0.35;
2038
2161
  var CURSOR_IDLE_FADE_IN = 0.18;
2039
2162
  var CURSOR_IDLE_EPS_FRAC = 25e-4;
2040
- var round3 = (n) => Math.round(n * 1e3) / 1e3;
2163
+ var round4 = (n) => Math.round(n * 1e3) / 1e3;
2041
2164
  function cursorIdleFade(track, o) {
2042
2165
  const moves = track.filter((e) => e.type === "move" || e.type === "down" || e.type === "up").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y }));
2043
2166
  if (moves.length === 0) return [];
@@ -2089,7 +2212,7 @@ function emit(keys, s, e) {
2089
2212
  push(keys, e + CURSOR_IDLE_FADE_IN, 1);
2090
2213
  }
2091
2214
  function push(keys, t, a) {
2092
- const rt = round3(t);
2215
+ const rt = round4(t);
2093
2216
  if (keys.length > 0 && rt <= keys[keys.length - 1].t) {
2094
2217
  keys[keys.length - 1].a = a;
2095
2218
  return;
@@ -3147,7 +3270,7 @@ function gatedRect(rect, x, y, space) {
3147
3270
  return null;
3148
3271
  const m = RECT_CONTAIN_MARGIN;
3149
3272
  const inside = x >= rect.x - m && x <= rect.x + rect.w + m && y >= rect.y - m && y <= rect.y + rect.h + m;
3150
- return inside ? [round4(rect.x), round4(rect.y), round4(rect.w), round4(rect.h)] : null;
3273
+ return inside ? [round5(rect.x), round5(rect.y), round5(rect.w), round5(rect.h)] : null;
3151
3274
  }
3152
3275
  function extractClicks(track, segments, opts) {
3153
3276
  const out = [];
@@ -3172,11 +3295,11 @@ function extractClicks(track, segments, opts) {
3172
3295
  const upSrc = st + Math.max(pressLen, 0.02);
3173
3296
  const up = keptOutputEnd(segments, st, upSrc) ?? ot + CLICK_SYNTH_RELEASE;
3174
3297
  const click = {
3175
- ot: round4(ot),
3176
- up: round4(Math.max(up, ot + 0.02)),
3177
- st: round4(st),
3178
- x: round4(e.x),
3179
- y: round4(e.y),
3298
+ ot: round5(ot),
3299
+ up: round5(Math.max(up, ot + 0.02)),
3300
+ st: round5(st),
3301
+ x: round5(e.x),
3302
+ y: round5(e.y),
3180
3303
  b: button
3181
3304
  };
3182
3305
  if (opts.rects && e.rect) {
@@ -3195,7 +3318,7 @@ function hexToRgbTriplet(hex) {
3195
3318
  const n = parseInt(h, 16);
3196
3319
  return [n >> 16, n >> 8 & 255, n & 255];
3197
3320
  }
3198
- function round4(v) {
3321
+ function round5(v) {
3199
3322
  return Math.round(v * 1e3) / 1e3;
3200
3323
  }
3201
3324
 
@@ -3222,7 +3345,8 @@ var CAM_PAN_EASE = ZOOM_PAN_EASE;
3222
3345
  var PROGRAM_DURATION = 1;
3223
3346
  function ratedSegments(doc) {
3224
3347
  const segs = isRecordingDoc(doc) ? doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }] : [{ in: 0, out: programDuration(doc) }];
3225
- return withHolds(segs, splitBySpeed2(segs, doc.speed ?? []));
3348
+ const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
3349
+ return withFreezes(splitBySpeed2(segs, doc.speed ?? []), freezes);
3226
3350
  }
3227
3351
  function durationSec(doc, rated) {
3228
3352
  const trimmed = totalDuration2(rated);
@@ -3257,7 +3381,7 @@ function trackEmitter() {
3257
3381
  if (tt <= prev.t + 1e-6 && sameVec(prev.value, value)) return prev.t;
3258
3382
  if (tt <= prev.t + 1e-6) tt = prev.t + 1e-3;
3259
3383
  }
3260
- keyframes.push({ t: round5(tt), value: value.map(round5), ease });
3384
+ keyframes.push({ t: round6(tt), value: value.map(round6), ease });
3261
3385
  return tt;
3262
3386
  };
3263
3387
  return { keyframes, push: push2 };
@@ -4753,10 +4877,13 @@ var ON_FRAME = `(ctx, content, dt) => {
4753
4877
  card.mesh.scale.x = cpv[0]; card.mesh.scale.y = cpv[0]
4754
4878
  card.mesh.position.y = cpv[1] * cpH
4755
4879
  if (card.mesh.material) card.mesh.material.opacity = cpv[2]
4756
- } else if (card.mesh.scale && card.mesh.position && ((card.mesh.scale.x !== undefined && card.mesh.scale.x !== 1) || (card.mesh.position.y !== undefined && card.mesh.position.y !== 0))) {
4880
+ // Gone past its clip: the mesh leaves the scene, not just its paint.
4881
+ card.mesh.visible = cpv[2] > 0.001
4882
+ } else if (card.mesh.scale && card.mesh.position && ((card.mesh.scale.x !== undefined && card.mesh.scale.x !== 1) || (card.mesh.position.y !== undefined && card.mesh.position.y !== 0) || card.mesh.visible === false)) {
4757
4883
  card.mesh.scale.x = 1; card.mesh.scale.y = 1
4758
4884
  card.mesh.position.y = 0
4759
4885
  if (card.mesh.material) card.mesh.material.opacity = 1
4886
+ card.mesh.visible = true
4760
4887
  }
4761
4888
  var tilted = rx * rx + ry * ry > 1e-6
4762
4889
  if (card.texture && card.texture.generateMipmaps !== tilted) {
@@ -4788,14 +4915,14 @@ function studioLayerData(layers, duration) {
4788
4915
  // needs a decoded recording, which a sync lowering can't produce.
4789
4916
  audio: (layers.audio ?? []).map((c) => ({
4790
4917
  key: c.key,
4791
- start: round5(c.start),
4792
- in: round5(c.in),
4793
- out: round5(c.out),
4794
- gain: round5(c.gain),
4918
+ start: round6(c.start),
4919
+ in: round6(c.in),
4920
+ out: round6(c.out),
4921
+ gain: round6(c.gain),
4795
4922
  loop: !!c.loop,
4796
- len: round5(clipLength(c)),
4923
+ len: round6(clipLength(c)),
4797
4924
  duck: !!c.duck,
4798
- env: clipEnvelope(c).map((p) => ({ t: round5(p.t), g: round5(p.g) }))
4925
+ env: clipEnvelope(c).map((p) => ({ t: round6(p.t), g: round6(p.g) }))
4799
4926
  })),
4800
4927
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
4801
4928
  ...layers.objects && layers.objects.length ? {
@@ -4808,17 +4935,17 @@ function studioLayerData(layers, duration) {
4808
4935
  } : o.asset.kind === "text3d" ? resolveText3dAsset(o.asset) : { kind: "gltf", key: o.asset.key },
4809
4936
  ...o.span ? {
4810
4937
  span: {
4811
- start: round5(o.span.start),
4812
- duration: round5(o.span.duration)
4938
+ start: round6(o.span.start),
4939
+ duration: round6(o.span.duration)
4813
4940
  }
4814
4941
  } : {},
4815
- x: round5(o.transform3d.x),
4816
- y: round5(o.transform3d.y),
4817
- z: round5(o.transform3d.z),
4818
- rx: round5(o.transform3d.rx),
4819
- ry: round5(o.transform3d.ry),
4820
- rz: round5(o.transform3d.rz),
4821
- scale: round5(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
4942
+ x: round6(o.transform3d.x),
4943
+ y: round6(o.transform3d.y),
4944
+ z: round6(o.transform3d.z),
4945
+ rx: round6(o.transform3d.rx),
4946
+ ry: round6(o.transform3d.ry),
4947
+ rz: round6(o.transform3d.rz),
4948
+ scale: round6(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
4822
4949
  anim: idleKey(o),
4823
4950
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
4824
4951
  // sampled at t − span.start. Omitted when absent — parity.
@@ -4845,12 +4972,12 @@ function studioLayerData(layers, duration) {
4845
4972
  const base = {
4846
4973
  id: o.id,
4847
4974
  kind: o.kind,
4848
- start: round5(o.start),
4849
- dur: round5(Math.max(OVERLAY_MIN_DURATION, o.duration)),
4850
- x: round5(o.transform.x),
4851
- y: round5(o.transform.y),
4852
- scale: round5(o.transform.scale || 1),
4853
- rot: round5(o.transform.rotation || 0),
4975
+ start: round6(o.start),
4976
+ dur: round6(Math.max(OVERLAY_MIN_DURATION, o.duration)),
4977
+ x: round6(o.transform.x),
4978
+ y: round6(o.transform.y),
4979
+ scale: round6(o.transform.scale || 1),
4980
+ rot: round6(o.transform.rotation || 0),
4854
4981
  enter: enterKey(o),
4855
4982
  exit: exitKey(o),
4856
4983
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
@@ -4871,7 +4998,7 @@ function studioLayerData(layers, duration) {
4871
4998
  return {
4872
4999
  ...base,
4873
5000
  key: o.key,
4874
- w: round5(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
5001
+ w: round6(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
4875
5002
  radius: o.radius ?? OVERLAY_MEDIA_DEFAULT_RADIUS,
4876
5003
  opacity: o.opacity ?? 1,
4877
5004
  loop: !!o.loop,
@@ -4881,7 +5008,7 @@ function studioLayerData(layers, duration) {
4881
5008
  ...o.shadow ? { shadow: o.shadow } : {},
4882
5009
  ...o.border && o.border.width > 0 ? {
4883
5010
  border: {
4884
- width: round5(o.border.width),
5011
+ width: round6(o.border.width),
4885
5012
  color: o.border.color
4886
5013
  }
4887
5014
  } : {}
@@ -4901,11 +5028,11 @@ function studioLayerData(layers, duration) {
4901
5028
  // Style-v2 fields bake only when non-default (byte parity for
4902
5029
  // older docs); ON_FRAME reads them unconditionally.
4903
5030
  ...st.fontStyle === "italic" ? { sty: "italic" } : {},
4904
- ...o.maxWidth ? { mw: round5(o.maxWidth) } : {},
4905
- ...st.letterSpacing ? { ls: round5(st.letterSpacing) } : {},
4906
- ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round5(st.lineHeight) } : {},
5031
+ ...o.maxWidth ? { mw: round6(o.maxWidth) } : {},
5032
+ ...st.letterSpacing ? { ls: round6(st.letterSpacing) } : {},
5033
+ ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round6(st.lineHeight) } : {},
4907
5034
  ...st.align !== "center" ? { align: st.align } : {},
4908
- ...st.stroke ? { stroke: { c: st.stroke.color, w: round5(st.stroke.width) } } : {},
5035
+ ...st.stroke ? { stroke: { c: st.stroke.color, w: round6(st.stroke.width) } } : {},
4909
5036
  // Hosted face behind an override: ON_FRAME lazy-loads it so a
4910
5037
  // live family/weight edit paints without a LOAD (SETUP only
4911
5038
  // runs on cold load).
@@ -4918,10 +5045,10 @@ function studioLayerData(layers, duration) {
4918
5045
  ...bx ? {
4919
5046
  box: {
4920
5047
  c: bx.color,
4921
- o: round5(bx.opacity),
4922
- px: round5(bx.padX),
4923
- py: round5(bx.padY),
4924
- r: round5(bx.radius)
5048
+ o: round6(bx.opacity),
5049
+ px: round6(bx.padX),
5050
+ py: round6(bx.padY),
5051
+ r: round6(bx.radius)
4925
5052
  }
4926
5053
  } : {},
4927
5054
  // Entrance animation: segmentation + timing normalized
@@ -4996,16 +5123,16 @@ function lowerToComposition(input) {
4996
5123
  // Rated segments (speed spans pre-intersected): ON_FRAME's mapTime and the
4997
5124
  // export's audio splice read the rate straight off each segment.
4998
5125
  segments: rated.map((seg) => ({
4999
- in: round5(seg.in),
5000
- out: round5(seg.out),
5001
- ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round5(seg.rate) } : {}
5126
+ in: round6(seg.in),
5127
+ out: round6(seg.out),
5128
+ ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {}
5002
5129
  })),
5003
5130
  frame: doc.frame,
5004
5131
  micGain: doc.micGain ?? 1,
5005
5132
  cursor: smoothed.map((p) => ({
5006
- t: round5(p.t),
5007
- x: round5(p.x),
5008
- y: round5(p.y)
5133
+ t: round6(p.t),
5134
+ x: round6(p.x),
5135
+ y: round6(p.y)
5009
5136
  })),
5010
5137
  cursorStyle: doc.cursor,
5011
5138
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
@@ -5090,7 +5217,7 @@ function clickFxData(doc, rated) {
5090
5217
  }
5091
5218
  };
5092
5219
  }
5093
- function round5(v) {
5220
+ function round6(v) {
5094
5221
  return Math.round(v * 1e3) / 1e3;
5095
5222
  }
5096
5223
 
@@ -5180,8 +5307,8 @@ function planAutoSpeed(track, opts) {
5180
5307
  accepted.sort((a, b) => a.in - b.in);
5181
5308
  return accepted.map((c, i) => ({
5182
5309
  id: `s${i}`,
5183
- in: round6(c.in),
5184
- out: round6(c.out),
5310
+ in: round7(c.in),
5311
+ out: round7(c.out),
5185
5312
  rate: clampSpeedRate(c.rate),
5186
5313
  source: "auto"
5187
5314
  }));
@@ -5234,7 +5361,7 @@ function collectRuns(evs, gap, minLen, emit2) {
5234
5361
  }
5235
5362
  flush();
5236
5363
  }
5237
- function round6(v) {
5364
+ function round7(v) {
5238
5365
  return Math.round(v * 1e3) / 1e3;
5239
5366
  }
5240
5367
 
@@ -5411,16 +5538,16 @@ function momentsFromDoc(doc, plan, opts = {}) {
5411
5538
  return {
5412
5539
  id: `m${String(i + 1).padStart(2, "0")}`,
5413
5540
  kind: d.kind,
5414
- source: { in: round7(d.in), out: round7(d.out) },
5415
- output: output ? { in: round7(output.start), out: round7(output.end) } : null,
5416
- at: d.at === null ? null : round7(d.at),
5417
- outputAt: outAt ? round7(outAt.start) : null,
5418
- focus: d.focus ? { cx: round7(d.focus.cx), cy: round7(d.focus.cy) } : null,
5541
+ source: { in: round8(d.in), out: round8(d.out) },
5542
+ output: output ? { in: round8(output.start), out: round8(output.end) } : null,
5543
+ at: d.at === null ? null : round8(d.at),
5544
+ outputAt: outAt ? round8(outAt.start) : null,
5545
+ focus: d.focus ? { cx: round8(d.focus.cx), cy: round8(d.focus.cy) } : null,
5419
5546
  rect: d.rect ? {
5420
- x: round7(d.rect.x),
5421
- y: round7(d.rect.y),
5422
- w: round7(d.rect.w),
5423
- h: round7(d.rect.h)
5547
+ x: round8(d.rect.x),
5548
+ y: round8(d.rect.y),
5549
+ w: round8(d.rect.w),
5550
+ h: round8(d.rect.h)
5424
5551
  } : null,
5425
5552
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
5426
5553
  ...d.pings !== void 0 ? { pings: d.pings } : {},
@@ -5440,7 +5567,7 @@ function activityOf(bins, d) {
5440
5567
  sum += bins[i];
5441
5568
  n++;
5442
5569
  }
5443
- return n ? round7(sum / n) : null;
5570
+ return n ? round8(sum / n) : null;
5444
5571
  }
5445
5572
  function saidOver(transcript, d) {
5446
5573
  if (!transcript?.length) return null;
@@ -5449,7 +5576,7 @@ function saidOver(transcript, d) {
5449
5576
  const text = transcript.filter((s) => s.start < hi && s.end > lo).map((s) => s.text.trim()).filter(Boolean).join(" ");
5450
5577
  return text || null;
5451
5578
  }
5452
- function round7(v) {
5579
+ function round8(v) {
5453
5580
  return Math.round(v * 1e3) / 1e3;
5454
5581
  }
5455
5582
 
@@ -5638,7 +5765,7 @@ function layoutOf(doc) {
5638
5765
  frame: LAYOUT_FRAME_FIELDS.filter((k) => frame[k] !== void 0),
5639
5766
  clips: (doc.overlays ?? []).filter(isStageClip).map((o) => o.id),
5640
5767
  lean: restLeanOf(doc) !== null,
5641
- hold: (doc.segments.at(-1)?.hold ?? 0) > 0
5768
+ freeze: trailingFreeze(doc) !== null
5642
5769
  };
5643
5770
  }
5644
5771
  function copyLayout(from, to, opts = {}) {
@@ -5700,11 +5827,33 @@ function copyLayout(from, to, opts = {}) {
5700
5827
  notes.push("lean kept: this take has tilt spans of its own");
5701
5828
  }
5702
5829
  }
5703
- const hold = from.segments.at(-1)?.hold;
5830
+ const freeze = trailingFreeze(from);
5704
5831
  const last = doc.segments.at(-1);
5705
- if (typeof hold === "number" && hold > 0 && last) last.hold = hold;
5832
+ if (freeze && last) {
5833
+ const own2 = (doc.freeze ?? []).filter(
5834
+ (f) => Math.abs(f.at - last.out) > 1e-9
5835
+ );
5836
+ const taken = new Set(own2.map((f) => f.id));
5837
+ let n = 0;
5838
+ while (taken.has(`f${n}`)) n++;
5839
+ doc.freeze = [
5840
+ ...own2,
5841
+ { id: `f${n}`, at: last.out, seconds: freeze.seconds }
5842
+ ].sort((a, b) => a.at - b.at);
5843
+ doc.segments = doc.segments.map((s) => {
5844
+ if (s.hold === void 0) return s;
5845
+ const next = { ...s };
5846
+ delete next.hold;
5847
+ return next;
5848
+ });
5849
+ }
5706
5850
  return { doc, notes };
5707
5851
  }
5852
+ function trailingFreeze(doc) {
5853
+ const last = doc.segments.at(-1);
5854
+ if (!last) return null;
5855
+ return docFreezes(doc).find((f) => Math.abs(f.at - last.out) < 1e-9) ?? null;
5856
+ }
5708
5857
  var shiftBy = (at, templateEnd, takeEnd) => at === "start" ? 0 : at === "end" ? takeEnd - templateEnd : at;
5709
5858
  function clipsFrom(doc, from) {
5710
5859
  return {
@@ -5715,12 +5864,14 @@ function clipsFrom(doc, from) {
5715
5864
  }
5716
5865
  function dropTemplate(doc, from) {
5717
5866
  const has = clipsFrom(doc, from);
5718
- if (!has.overlays.length && !has.objects.length && !has.audio.length)
5867
+ const freezes = (doc.freeze ?? []).some((f) => f.from === from);
5868
+ if (!has.overlays.length && !has.objects.length && !has.audio.length && !freezes)
5719
5869
  return doc;
5720
5870
  const out = { ...doc };
5721
5871
  if (doc.overlays) out.overlays = doc.overlays.filter((o) => o.from !== from);
5722
5872
  if (doc.objects) out.objects = doc.objects.filter((o) => o.from !== from);
5723
5873
  out.audio = doc.audio.filter((a) => a.from !== from);
5874
+ if (doc.freeze) out.freeze = doc.freeze.filter((f) => f.from !== from);
5724
5875
  return out;
5725
5876
  }
5726
5877
  function applyTemplate(template, take, opts) {
@@ -5728,8 +5879,22 @@ function applyTemplate(template, take, opts) {
5728
5879
  const notes = [];
5729
5880
  const base = dropTemplate(take, from);
5730
5881
  const doc = structuredClone(base);
5882
+ const tail = trailingFreeze(template);
5883
+ const lastSeg = doc.segments.at(-1);
5884
+ if (tail && opts.at === "end" && lastSeg) {
5885
+ const own2 = (doc.freeze ?? []).filter(
5886
+ (f) => Math.abs(f.at - lastSeg.out) > 1e-9
5887
+ );
5888
+ const taken = new Set(own2.map((f) => f.id));
5889
+ let n = 0;
5890
+ while (taken.has(`f${n}`)) n++;
5891
+ doc.freeze = [
5892
+ ...own2,
5893
+ { id: `f${n}`, at: lastSeg.out, seconds: tail.seconds, from }
5894
+ ].sort((a, b) => a.at - b.at);
5895
+ }
5731
5896
  const templateEnd = totalDuration4(ratedSegments(template));
5732
- const takeEnd = totalDuration4(ratedSegments(take));
5897
+ const takeEnd = totalDuration4(ratedSegments(doc));
5733
5898
  const shift = shiftBy(opts.at, templateEnd, takeEnd);
5734
5899
  const own = new Map((take.overlays ?? []).map((o) => [o.id, o]));
5735
5900
  const overlays = (doc.overlays ?? []).filter(
@@ -5893,8 +6058,8 @@ function buildDigest(input) {
5893
6058
  return {
5894
6059
  digestVersion: DIGEST_VERSION,
5895
6060
  take: {
5896
- sourceDuration: round8(meta.durationMs / 1e3),
5897
- outputDuration: round8(input.outputDuration),
6061
+ sourceDuration: round9(meta.durationMs / 1e3),
6062
+ outputDuration: round9(input.outputDuration),
5898
6063
  width: meta.width,
5899
6064
  height: meta.height,
5900
6065
  captureWidth: meta.captureWidth ?? null,
@@ -5943,7 +6108,7 @@ function buildDigest(input) {
5943
6108
  function outputDurationOf(doc) {
5944
6109
  return docOutputDuration(doc);
5945
6110
  }
5946
- function round8(v) {
6111
+ function round9(v) {
5947
6112
  return Math.round(v * 1e3) / 1e3;
5948
6113
  }
5949
6114
 
@@ -5969,20 +6134,20 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
5969
6134
  continue;
5970
6135
  }
5971
6136
  if (srcIn - s.in >= MIN_REMAINDER)
5972
- kept.push({ ...s, out: round9(srcIn), source: "manual" });
6137
+ kept.push({ ...s, out: round10(srcIn), source: "manual" });
5973
6138
  if (s.out - srcOut >= MIN_REMAINDER)
5974
6139
  kept.push({
5975
6140
  ...s,
5976
6141
  id: mintId(used),
5977
- in: round9(srcOut),
6142
+ in: round10(srcOut),
5978
6143
  source: "manual"
5979
6144
  });
5980
6145
  }
5981
6146
  if (rate != null)
5982
6147
  kept.push({
5983
6148
  id: mintId(used),
5984
- in: round9(srcIn),
5985
- out: round9(srcOut),
6149
+ in: round10(srcIn),
6150
+ out: round10(srcOut),
5986
6151
  rate: clampSpeedRate(rate),
5987
6152
  source: "manual"
5988
6153
  });
@@ -5998,9 +6163,9 @@ function removeSourceRange(segments, srcIn, srcOut) {
5998
6163
  next.push(seg);
5999
6164
  continue;
6000
6165
  }
6001
- if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round9(srcIn) });
6166
+ if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round10(srcIn) });
6002
6167
  if (seg.out - srcOut >= MIN_SEGMENT)
6003
- next.push({ ...seg, in: round9(srcOut) });
6168
+ next.push({ ...seg, in: round10(srcOut) });
6004
6169
  }
6005
6170
  return next.length ? next : [...segments];
6006
6171
  }
@@ -6018,8 +6183,8 @@ function zoomSpanForRange(zoom, srcIn, srcOut) {
6018
6183
  while (used.has(`u${n}`)) n++;
6019
6184
  return {
6020
6185
  id: `u${n}`,
6021
- in: round9(start),
6022
- out: round9(end),
6186
+ in: round10(start),
6187
+ out: round10(end),
6023
6188
  level: DEFAULT_ZOOM_LEVEL,
6024
6189
  cx: 0.5,
6025
6190
  cy: 0.5,
@@ -6033,7 +6198,7 @@ function mintId(used) {
6033
6198
  used.add(id);
6034
6199
  return id;
6035
6200
  }
6036
- function round9(v) {
6201
+ function round10(v) {
6037
6202
  return Math.round(v * 1e3) / 1e3;
6038
6203
  }
6039
6204
 
@@ -6676,33 +6841,44 @@ function effectiveSegments(doc) {
6676
6841
  if (isRecordingDoc(doc) && doc.segments.length) return doc.segments;
6677
6842
  return [{ in: 0, out: anchorSourceDuration(doc) }];
6678
6843
  }
6679
- var outputLen = (seg, speeds) => totalDuration6(splitBySpeed3([seg], speeds));
6680
- var segmentStarts = (segments, speeds) => {
6844
+ var segmentPieces = (seg, speeds, freezes, leading) => {
6845
+ const own = freezes.filter(
6846
+ (f) => f.at <= seg.out + 1e-9 && (leading ? f.at >= seg.in - 1e-9 : f.at > seg.in + 1e-9)
6847
+ );
6848
+ return withFreezes(splitBySpeed3([seg], speeds), own);
6849
+ };
6850
+ var outputLen = (seg, speeds, freezes = [], leading = true) => totalDuration6(segmentPieces(seg, speeds, freezes, leading));
6851
+ var segmentStarts = (segments, speeds, freezes = []) => {
6681
6852
  const starts = [];
6682
6853
  let acc = 0;
6683
- for (const s of segments) {
6854
+ segments.forEach((s, i) => {
6684
6855
  starts.push(acc);
6685
- acc += outputLen(s, speeds);
6686
- }
6856
+ acc += outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i));
6857
+ });
6687
6858
  return starts;
6688
6859
  };
6860
+ var contiguous = (segments, i) => i > 0 && Math.abs(segments[i - 1].out - segments[i].in) < 1e-9;
6689
6861
  var videoLane = {
6690
6862
  id: "video",
6691
6863
  label: "Video",
6692
6864
  items(doc) {
6693
6865
  const segments = effectiveSegments(doc);
6694
6866
  const speeds = doc.speed ?? [];
6695
- const starts = segmentStarts(segments, speeds);
6867
+ const freezes = docFreezes(doc);
6868
+ const starts = segmentStarts(segments, speeds, freezes);
6696
6869
  return segments.map((s, i) => ({
6697
6870
  id: `seg-${i}`,
6698
6871
  kind: "clip",
6699
- t: starts[i],
6700
- duration: outputLen(s, speeds)
6872
+ t: round11(starts[i]),
6873
+ duration: round11(
6874
+ outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
6875
+ )
6701
6876
  }));
6702
6877
  },
6703
6878
  gesture(doc, g) {
6704
6879
  const segments = effectiveSegments(doc);
6705
6880
  const speeds = doc.speed ?? [];
6881
+ const freezes = docFreezes(doc);
6706
6882
  const sourceDuration = anchorSourceDuration(doc);
6707
6883
  switch (g.type) {
6708
6884
  case "move": {
@@ -6714,7 +6890,7 @@ var videoLane = {
6714
6890
  let acc = 0;
6715
6891
  let insert = others.length;
6716
6892
  for (let i = 0; i < others.length; i++) {
6717
- const dur = outputLen(others[i], speeds);
6893
+ const dur = outputLen(others[i], speeds, freezes);
6718
6894
  if (g.t < acc + dur / 2) {
6719
6895
  insert = i;
6720
6896
  break;
@@ -6732,10 +6908,15 @@ var videoLane = {
6732
6908
  const index = segIndex(g.id);
6733
6909
  if (index < 0 || index >= segments.length) return null;
6734
6910
  const seg = segments[index];
6735
- const starts = segmentStarts(segments, speeds);
6911
+ const starts = segmentStarts(segments, speeds, freezes);
6736
6912
  const fullMap = splitBySpeed3([{ in: 0, out: sourceDuration }], speeds);
6737
6913
  const edgeSrc = g.edge === "start" ? seg.in : seg.out;
6738
- const edgeOutNow = g.edge === "start" ? starts[index] : starts[index] + outputLen(seg, speeds);
6914
+ const edgeOutNow = g.edge === "start" ? starts[index] : starts[index] + outputLen(
6915
+ seg,
6916
+ speeds,
6917
+ freezes.filter((f) => f.at < seg.out - 1e-9),
6918
+ index === 0 || !contiguous(segments, index)
6919
+ );
6739
6920
  const anchorOut = sourceToTimeline3(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
6740
6921
  const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
6741
6922
  const next = trimSegment(
@@ -6750,13 +6931,31 @@ var videoLane = {
6750
6931
  };
6751
6932
  }
6752
6933
  case "create": {
6753
- const starts = segmentStarts(segments, speeds);
6934
+ const starts = segmentStarts(segments, speeds, freezes);
6754
6935
  const index = segments.findIndex(
6755
- (s2, i) => g.t >= starts[i] && g.t < starts[i] + outputLen(s2, speeds)
6936
+ (s2, i) => g.t >= starts[i] && g.t < starts[i] + outputLen(
6937
+ s2,
6938
+ speeds,
6939
+ freezes,
6940
+ i === 0 || !contiguous(segments, i)
6941
+ )
6756
6942
  );
6757
6943
  if (index < 0) return null;
6758
6944
  const s = segments[index];
6759
- const sourceT = mapTime2(splitBySpeed3([s], speeds), g.t - starts[index]);
6945
+ const leading = index === 0 || !contiguous(segments, index);
6946
+ const local = g.t - starts[index];
6947
+ for (const m of freezeOutputExtents(
6948
+ splitBySpeed3([s], speeds),
6949
+ freezes.filter(
6950
+ (f) => f.at <= s.out + 1e-9 && (leading ? f.at >= s.in - 1e-9 : f.at > s.in + 1e-9)
6951
+ )
6952
+ ).values()) {
6953
+ if (local >= m.t - 1e-9 && local <= m.t + m.duration + 1e-9)
6954
+ return null;
6955
+ }
6956
+ const sourceT = round11(
6957
+ mapTime2(segmentPieces(s, speeds, freezes, leading), local)
6958
+ );
6760
6959
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
6761
6960
  const next = [
6762
6961
  ...segments.slice(0, index),
@@ -6782,15 +6981,154 @@ var videoLane = {
6782
6981
  magnets(doc) {
6783
6982
  const segments = effectiveSegments(doc);
6784
6983
  const speeds = doc.speed ?? [];
6785
- const starts = segmentStarts(segments, speeds);
6984
+ const freezes = docFreezes(doc);
6985
+ const starts = segmentStarts(segments, speeds, freezes);
6986
+ const n = segments.length;
6786
6987
  return [
6787
6988
  ...starts,
6788
- ...starts.length ? [
6789
- starts[starts.length - 1] + outputLen(segments[segments.length - 1], speeds)
6989
+ ...n ? [
6990
+ starts[n - 1] + outputLen(
6991
+ segments[n - 1],
6992
+ speeds,
6993
+ freezes,
6994
+ n === 1 || !contiguous(segments, n - 1)
6995
+ )
6790
6996
  ] : []
6791
- ];
6997
+ ].map(round11);
6998
+ }
6999
+ };
7000
+ function nextFreezeId(doc) {
7001
+ const taken = new Set((doc.freeze ?? []).map((f) => f.id));
7002
+ let n = 0;
7003
+ while (taken.has(`f${n}`)) n++;
7004
+ return `f${n}`;
7005
+ }
7006
+ var FREEZE_GAP = 0.05;
7007
+ var freezeLane = {
7008
+ id: "freeze",
7009
+ label: "Freeze",
7010
+ items(doc) {
7011
+ const freezes = docFreezes(doc);
7012
+ if (!freezes.length) return [];
7013
+ const segs = effectiveSegments(doc);
7014
+ const marks = freezeOutputExtents(
7015
+ splitBySpeed3(segs, doc.speed ?? []),
7016
+ freezes
7017
+ );
7018
+ return freezes.flatMap((f) => {
7019
+ const m = marks.get(f.id);
7020
+ return m ? [
7021
+ {
7022
+ id: f.id,
7023
+ kind: "clip",
7024
+ t: round11(m.t),
7025
+ duration: round11(m.duration),
7026
+ label: `${trimZeros(f.seconds)}s`
7027
+ }
7028
+ ] : [];
7029
+ });
7030
+ },
7031
+ gesture(doc, g) {
7032
+ const freezes = docFreezes(doc);
7033
+ const segs = effectiveSegments(doc);
7034
+ const speeds = doc.speed ?? [];
7035
+ const sourceDuration = anchorSourceDuration(doc);
7036
+ const kept = (t) => segs.some((s) => t >= s.in - 1e-9 && t <= s.out + 1e-9);
7037
+ const clear = (t, except) => !freezes.some((f2) => f2.id !== except && Math.abs(f2.at - t) < FREEZE_GAP);
7038
+ if (g.type === "create") {
7039
+ const rated = ratedSegments(doc);
7040
+ for (const m of freezeOutputExtents(
7041
+ splitBySpeed3(segs, speeds),
7042
+ freezes
7043
+ ).values()) {
7044
+ if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
7045
+ }
7046
+ if (g.t > totalDuration6(rated) + 1e-9) return null;
7047
+ const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
7048
+ if (!kept(srcT) || !clear(srcT)) return null;
7049
+ const id = nextFreezeId(doc);
7050
+ return (d) => {
7051
+ d.freeze = [
7052
+ ...d.freeze ?? [],
7053
+ { id, at: srcT, seconds: FREEZE_DEFAULT_SECONDS }
7054
+ ].sort((a, b) => a.at - b.at);
7055
+ };
7056
+ }
7057
+ const f = freezes.find((x) => x.id === g.id);
7058
+ if (!f) return null;
7059
+ const own = (d) => {
7060
+ const list = d.freeze ?? [];
7061
+ let mine = list.find((x) => x.id === f.id);
7062
+ if (!mine) {
7063
+ const id = nextFreezeId(d);
7064
+ mine = { id, at: f.at, seconds: f.seconds };
7065
+ d.freeze = [...list, mine];
7066
+ d.segments = d.segments.map((sg) => {
7067
+ if (sg.hold === void 0 || Math.abs(sg.out - f.at) > 1e-9) return sg;
7068
+ const next = { ...sg };
7069
+ delete next.hold;
7070
+ return next;
7071
+ });
7072
+ }
7073
+ return mine;
7074
+ };
7075
+ const base = withFreezes(
7076
+ splitBySpeed3(segs, speeds),
7077
+ freezes.filter((x) => x.id !== f.id)
7078
+ );
7079
+ if (g.type === "move" || g.type === "resize" && g.edge === "start") {
7080
+ const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
7081
+ const mine = marks.get(f.id);
7082
+ const endOut = mine ? mine.t + mine.duration : null;
7083
+ const srcT = round11(
7084
+ Math.min(sourceDuration, mapTime2(base, Math.max(0, g.t)))
7085
+ );
7086
+ if (!kept(srcT) || !clear(srcT, f.id)) return null;
7087
+ return (d) => {
7088
+ const x = own(d);
7089
+ if (!x) return;
7090
+ x.at = srcT;
7091
+ if (g.type === "resize" && endOut !== null) {
7092
+ const after = freezeOutputExtents(
7093
+ splitBySpeed3(effectiveSegments(d), d.speed ?? []),
7094
+ docFreezes({
7095
+ ...d,
7096
+ freeze: (d.freeze ?? []).map(
7097
+ (y) => y.id === x.id ? { ...y, seconds: FREEZE_SECONDS_MIN } : y
7098
+ )
7099
+ })
7100
+ ).get(x.id);
7101
+ if (after) x.seconds = clampFreezeSeconds(endOut - after.t);
7102
+ }
7103
+ d.freeze.sort((a, b) => a.at - b.at);
7104
+ };
7105
+ }
7106
+ if (g.type === "resize") {
7107
+ const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
7108
+ const mine = marks.get(f.id);
7109
+ if (!mine) return null;
7110
+ const seconds = clampFreezeSeconds(g.t - mine.t);
7111
+ if (Math.abs(seconds - f.seconds) < 1e-6) return null;
7112
+ return (d) => {
7113
+ const x = own(d);
7114
+ if (x) x.seconds = seconds;
7115
+ };
7116
+ }
7117
+ if (g.type === "remove") {
7118
+ return (d) => {
7119
+ const x = own(d);
7120
+ d.freeze = (d.freeze ?? []).filter((y) => y !== x);
7121
+ };
7122
+ }
7123
+ return null;
7124
+ },
7125
+ magnets(doc) {
7126
+ return freezeLane.items(doc).flatMap((it) => [it.t, it.t + (it.duration ?? 0)]);
6792
7127
  }
6793
7128
  };
7129
+ function trimZeros(n) {
7130
+ return String(Math.round(n * 100) / 100);
7131
+ }
6794
7132
  var zoomLane = {
6795
7133
  id: "zoom",
6796
7134
  label: "Zoom",
@@ -6802,8 +7140,8 @@ var zoomLane = {
6802
7140
  {
6803
7141
  id: z.id,
6804
7142
  kind: "clip",
6805
- t: round10(ext.start),
6806
- duration: round10(ext.end - ext.start),
7143
+ t: round11(ext.start),
7144
+ duration: round11(ext.end - ext.start),
6807
7145
  label: `${z.level.toFixed(2)}\xD7`
6808
7146
  }
6809
7147
  ];
@@ -6827,8 +7165,8 @@ var zoomLane = {
6827
7165
  ...d.zoom,
6828
7166
  {
6829
7167
  id,
6830
- in: round10(srcT),
6831
- out: round10(srcT + len),
7168
+ in: round11(srcT),
7169
+ out: round11(srcT + len),
6832
7170
  level: DEFAULT_ZOOM_LEVEL,
6833
7171
  cx,
6834
7172
  cy,
@@ -6859,8 +7197,8 @@ var zoomLane = {
6859
7197
  return (d) => {
6860
7198
  const z = d.zoom.find((x) => x.id === g.id);
6861
7199
  if (!z) return;
6862
- z.in = round10(newIn);
6863
- z.out = round10(newIn + len);
7200
+ z.in = round11(newIn);
7201
+ z.out = round11(newIn + len);
6864
7202
  z.source = "manual";
6865
7203
  d.zoom.sort((a, b) => a.in - b.in);
6866
7204
  };
@@ -6889,8 +7227,8 @@ var zoomLane = {
6889
7227
  return (d) => {
6890
7228
  const z = d.zoom.find((x) => x.id === g.id);
6891
7229
  if (!z) return;
6892
- z.in = round10(next.in);
6893
- z.out = round10(next.out);
7230
+ z.in = round11(next.in);
7231
+ z.out = round11(next.out);
6894
7232
  z.source = "manual";
6895
7233
  };
6896
7234
  }
@@ -6912,8 +7250,8 @@ function cursorFocusAt(doc, srcT) {
6912
7250
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
6913
7251
  const clamp015 = (v) => Math.max(0, Math.min(1, v));
6914
7252
  return {
6915
- cx: round10(clamp015(best.x / width)),
6916
- cy: round10(clamp015(best.y / height))
7253
+ cx: round11(clamp015(best.x / width)),
7254
+ cy: round11(clamp015(best.y / height))
6917
7255
  };
6918
7256
  }
6919
7257
  var tiltLane = {
@@ -6927,8 +7265,8 @@ var tiltLane = {
6927
7265
  {
6928
7266
  id: z.id,
6929
7267
  kind: "clip",
6930
- t: round10(ext.start),
6931
- duration: round10(ext.end - ext.start),
7268
+ t: round11(ext.start),
7269
+ duration: round11(ext.end - ext.start),
6932
7270
  label: `${formatDeg(z.rx)}\xB0/${formatDeg(z.ry)}\xB0`
6933
7271
  }
6934
7272
  ];
@@ -6951,8 +7289,8 @@ var tiltLane = {
6951
7289
  ...d.tilt ?? [],
6952
7290
  {
6953
7291
  id,
6954
- in: round10(srcT),
6955
- out: round10(srcT + len),
7292
+ in: round11(srcT),
7293
+ out: round11(srcT + len),
6956
7294
  rx: DEFAULT_TILT_POSE.rx,
6957
7295
  ry: DEFAULT_TILT_POSE.ry,
6958
7296
  source: "manual"
@@ -6982,8 +7320,8 @@ var tiltLane = {
6982
7320
  return (d) => {
6983
7321
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
6984
7322
  if (!z || !d.tilt) return;
6985
- z.in = round10(newIn);
6986
- z.out = round10(newIn + len);
7323
+ z.in = round11(newIn);
7324
+ z.out = round11(newIn + len);
6987
7325
  z.source = "manual";
6988
7326
  d.tilt.sort((a, b) => a.in - b.in);
6989
7327
  };
@@ -7012,8 +7350,8 @@ var tiltLane = {
7012
7350
  return (d) => {
7013
7351
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
7014
7352
  if (!z) return;
7015
- z.in = round10(next.in);
7016
- z.out = round10(next.out);
7353
+ z.in = round11(next.in);
7354
+ z.out = round11(next.out);
7017
7355
  z.source = "manual";
7018
7356
  };
7019
7357
  }
@@ -7040,8 +7378,8 @@ var camMoveLane = {
7040
7378
  {
7041
7379
  id: z.id,
7042
7380
  kind: "clip",
7043
- t: round10(ext.start),
7044
- duration: round10(ext.end - ext.start),
7381
+ t: round11(ext.start),
7382
+ duration: round11(ext.end - ext.start),
7045
7383
  label: z.size != null ? `${Math.round(z.size * 100)}%` : "Move"
7046
7384
  }
7047
7385
  ];
@@ -7065,8 +7403,8 @@ var camMoveLane = {
7065
7403
  ...d.camMotion ?? [],
7066
7404
  {
7067
7405
  id,
7068
- in: round10(srcT),
7069
- out: round10(srcT + len),
7406
+ in: round11(srcT),
7407
+ out: round11(srcT + len),
7070
7408
  ...DEFAULT_CAM_POSE,
7071
7409
  source: "manual"
7072
7410
  }
@@ -7095,8 +7433,8 @@ var camMoveLane = {
7095
7433
  return (d) => {
7096
7434
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
7097
7435
  if (!z || !d.camMotion) return;
7098
- z.in = round10(newIn);
7099
- z.out = round10(newIn + len);
7436
+ z.in = round11(newIn);
7437
+ z.out = round11(newIn + len);
7100
7438
  z.source = "manual";
7101
7439
  d.camMotion.sort((a, b) => a.in - b.in);
7102
7440
  };
@@ -7125,8 +7463,8 @@ var camMoveLane = {
7125
7463
  return (d) => {
7126
7464
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
7127
7465
  if (!z) return;
7128
- z.in = round10(next.in);
7129
- z.out = round10(next.out);
7466
+ z.in = round11(next.in);
7467
+ z.out = round11(next.out);
7130
7468
  z.source = "manual";
7131
7469
  };
7132
7470
  }
@@ -7145,7 +7483,8 @@ var camLane = {
7145
7483
  if (!doc.source.camKey || !doc.cam.visible) return [];
7146
7484
  const segments = effectiveSegments(doc);
7147
7485
  const speeds = doc.speed ?? [];
7148
- const starts = segmentStarts(segments, speeds);
7486
+ const freezes = docFreezes(doc);
7487
+ const starts = segmentStarts(segments, speeds, freezes);
7149
7488
  const first = segments[0];
7150
7489
  const last = segments[segments.length - 1];
7151
7490
  const win = doc.cam.window ?? { in: first.in, out: last.out };
@@ -7154,11 +7493,16 @@ var camLane = {
7154
7493
  const a = Math.max(s.in, win.in);
7155
7494
  const b = Math.min(s.out, win.out);
7156
7495
  if (b - a <= 1e-6) return;
7496
+ const leading = i === 0 || !contiguous(segments, i);
7157
7497
  items.push({
7158
7498
  id: `cam-${i}`,
7159
7499
  kind: "clip",
7160
- t: round10(starts[i] + outputLen({ in: s.in, out: a }, speeds)),
7161
- duration: round10(outputLen({ in: a, out: b }, speeds))
7500
+ t: round11(
7501
+ starts[i] + outputLen({ in: s.in, out: a }, speeds, freezes, leading)
7502
+ ),
7503
+ duration: round11(
7504
+ outputLen({ in: a, out: b }, speeds, freezes, a === s.in && leading)
7505
+ )
7162
7506
  });
7163
7507
  });
7164
7508
  return items;
@@ -7185,7 +7529,7 @@ var camLane = {
7185
7529
  Math.max(first.in, last.out - span)
7186
7530
  );
7187
7531
  return (d) => {
7188
- d.cam.window = { in: round10(newIn), out: round10(newIn + span) };
7532
+ d.cam.window = { in: round11(newIn), out: round11(newIn + span) };
7189
7533
  };
7190
7534
  }
7191
7535
  const overlapping = eff.map((s, i) => ({
@@ -7198,8 +7542,8 @@ var camLane = {
7198
7542
  const next = g.edge === "start" ? { in: Math.min(sourceT, current.out - 0.05), out: current.out } : { in: current.in, out: Math.max(sourceT, current.in + 0.05) };
7199
7543
  return (d) => {
7200
7544
  d.cam.window = {
7201
- in: round10(Math.max(0, next.in)),
7202
- out: round10(Math.min(sourceDuration, next.out))
7545
+ in: round11(Math.max(0, next.in)),
7546
+ out: round11(Math.min(sourceDuration, next.out))
7203
7547
  };
7204
7548
  };
7205
7549
  },
@@ -7214,12 +7558,15 @@ var micLane = {
7214
7558
  if (!voiceKey(doc)) return [];
7215
7559
  const segments = effectiveSegments(doc);
7216
7560
  const speeds = doc.speed ?? [];
7217
- const starts = segmentStarts(segments, speeds);
7561
+ const freezes = docFreezes(doc);
7562
+ const starts = segmentStarts(segments, speeds, freezes);
7218
7563
  return segments.map((s, i) => ({
7219
7564
  id: `mic-${i}`,
7220
7565
  kind: "clip",
7221
- t: starts[i],
7222
- duration: outputLen(s, speeds)
7566
+ t: round11(starts[i]),
7567
+ duration: round11(
7568
+ outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
7569
+ )
7223
7570
  }));
7224
7571
  },
7225
7572
  gesture() {
@@ -7273,8 +7620,8 @@ var speedLane = {
7273
7620
  {
7274
7621
  id: sp.id,
7275
7622
  kind: "clip",
7276
- t: round10(start),
7277
- duration: round10(end - start),
7623
+ t: round11(start),
7624
+ duration: round11(end - start),
7278
7625
  label: `${sp.rate}\xD7`
7279
7626
  }
7280
7627
  ];
@@ -7297,8 +7644,8 @@ var speedLane = {
7297
7644
  ...d.speed ?? [],
7298
7645
  {
7299
7646
  id,
7300
- in: round10(srcT),
7301
- out: round10(srcT + len),
7647
+ in: round11(srcT),
7648
+ out: round11(srcT + len),
7302
7649
  rate: DEFAULT_SPEED_RATE,
7303
7650
  source: "manual"
7304
7651
  }
@@ -7328,8 +7675,8 @@ var speedLane = {
7328
7675
  return (d) => {
7329
7676
  const s = d.speed?.find((x) => x.id === g.id);
7330
7677
  if (!s) return;
7331
- s.in = round10(newIn);
7332
- s.out = round10(newIn + len);
7678
+ s.in = round11(newIn);
7679
+ s.out = round11(newIn + len);
7333
7680
  s.source = "manual";
7334
7681
  d.speed.sort((a, b) => a.in - b.in);
7335
7682
  };
@@ -7358,8 +7705,8 @@ var speedLane = {
7358
7705
  return (d) => {
7359
7706
  const s = d.speed?.find((x) => x.id === g.id);
7360
7707
  if (!s) return;
7361
- s.in = round10(next.in);
7362
- s.out = round10(next.out);
7708
+ s.in = round11(next.in);
7709
+ s.out = round11(next.out);
7363
7710
  s.source = "manual";
7364
7711
  };
7365
7712
  }
@@ -7378,8 +7725,8 @@ var audioLane = {
7378
7725
  return doc.audio.map((c) => ({
7379
7726
  id: c.id,
7380
7727
  kind: "clip",
7381
- t: round10(c.start),
7382
- duration: round10(clipLength(c)),
7728
+ t: round11(c.start),
7729
+ duration: round11(clipLength(c)),
7383
7730
  label: c.name
7384
7731
  }));
7385
7732
  },
@@ -7390,7 +7737,7 @@ var audioLane = {
7390
7737
  const start = Math.max(0, g.t);
7391
7738
  return (d) => {
7392
7739
  const c = d.audio.find((x) => x.id === g.id);
7393
- if (c) c.start = round10(start);
7740
+ if (c) c.start = round11(start);
7394
7741
  };
7395
7742
  }
7396
7743
  if (g.type === "resize") {
@@ -7403,8 +7750,8 @@ var audioLane = {
7403
7750
  return (d) => {
7404
7751
  const c = d.audio.find((x) => x.id === g.id);
7405
7752
  if (!c) return;
7406
- c.start = round10(newStart2);
7407
- c.loopLen = round10(end - newStart2);
7753
+ c.start = round11(newStart2);
7754
+ c.loopLen = round11(end - newStart2);
7408
7755
  };
7409
7756
  }
7410
7757
  const delta = g.t - clip.start;
@@ -7413,22 +7760,22 @@ var audioLane = {
7413
7760
  return (d) => {
7414
7761
  const c = d.audio.find((x) => x.id === g.id);
7415
7762
  if (!c) return;
7416
- c.in = round10(newIn);
7417
- c.start = round10(newStart);
7763
+ c.in = round11(newIn);
7764
+ c.start = round11(newStart);
7418
7765
  };
7419
7766
  }
7420
7767
  if (clip.loop) {
7421
7768
  const newLen = Math.max(0.1, g.t - clip.start);
7422
7769
  return (d) => {
7423
7770
  const c = d.audio.find((x) => x.id === g.id);
7424
- if (c) c.loopLen = round10(newLen);
7771
+ if (c) c.loopLen = round11(newLen);
7425
7772
  };
7426
7773
  }
7427
7774
  const span = Math.max(0.05, g.t - clip.start);
7428
7775
  const newOut = Math.min(clip.in + span, clip.duration);
7429
7776
  return (d) => {
7430
7777
  const c = d.audio.find((x) => x.id === g.id);
7431
- if (c) c.out = round10(Math.max(c.in + 0.05, newOut));
7778
+ if (c) c.out = round11(Math.max(c.in + 0.05, newOut));
7432
7779
  };
7433
7780
  }
7434
7781
  if (g.type === "remove") {
@@ -7441,8 +7788,8 @@ var audioLane = {
7441
7788
  },
7442
7789
  magnets(doc) {
7443
7790
  return doc.audio.flatMap((c) => [
7444
- round10(c.start),
7445
- round10(c.start + clipLength(c))
7791
+ round11(c.start),
7792
+ round11(c.start + clipLength(c))
7446
7793
  ]);
7447
7794
  }
7448
7795
  };
@@ -7460,7 +7807,7 @@ function poseItems(clipId, start, duration, motion) {
7460
7807
  return (motion ?? []).map((p, i) => ({
7461
7808
  id: `${clipId}::k${i}`,
7462
7809
  kind: "keyframe",
7463
- t: round10(start + Math.min(Math.max(0, p.at), duration))
7810
+ t: round11(start + Math.min(Math.max(0, p.at), duration))
7464
7811
  }));
7465
7812
  }
7466
7813
  var overlaysLane = {
@@ -7471,8 +7818,8 @@ var overlaysLane = {
7471
7818
  {
7472
7819
  id: o.id,
7473
7820
  kind: "clip",
7474
- t: round10(o.start),
7475
- duration: round10(o.duration),
7821
+ t: round11(o.start),
7822
+ duration: round11(o.duration),
7476
7823
  label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "image" ? "Image" : "Video"
7477
7824
  },
7478
7825
  // Pose diamonds render after the clips, so they sit on top.
@@ -7492,7 +7839,7 @@ var overlaysLane = {
7492
7839
  return (d) => {
7493
7840
  const o = (d.overlays ?? []).find((x) => x.id === kf.clipId);
7494
7841
  const p = o?.motion?.[kf.index];
7495
- if (p) p.at = round10(at);
7842
+ if (p) p.at = round11(at);
7496
7843
  };
7497
7844
  }
7498
7845
  if (g.type === "remove") {
@@ -7521,8 +7868,8 @@ var overlaysLane = {
7521
7868
  {
7522
7869
  id,
7523
7870
  kind: "text",
7524
- start: round10(start),
7525
- duration: round10(len),
7871
+ start: round11(start),
7872
+ duration: round11(len),
7526
7873
  text: "Title",
7527
7874
  preset: "title",
7528
7875
  // Frame fractions: centered, lower-third — at any aspect ratio.
@@ -7537,7 +7884,7 @@ var overlaysLane = {
7537
7884
  const start = Math.max(0, g.t);
7538
7885
  return (d) => {
7539
7886
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
7540
- if (o) o.start = round10(start);
7887
+ if (o) o.start = round11(start);
7541
7888
  };
7542
7889
  }
7543
7890
  if (g.type === "resize") {
@@ -7547,14 +7894,14 @@ var overlaysLane = {
7547
7894
  return (d) => {
7548
7895
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
7549
7896
  if (!o) return;
7550
- o.start = round10(newStart);
7551
- o.duration = round10(end - newStart);
7897
+ o.start = round11(newStart);
7898
+ o.duration = round11(end - newStart);
7552
7899
  };
7553
7900
  }
7554
7901
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.start);
7555
7902
  return (d) => {
7556
7903
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
7557
- if (o) o.duration = round10(newLen);
7904
+ if (o) o.duration = round11(newLen);
7558
7905
  };
7559
7906
  }
7560
7907
  return (d) => {
@@ -7563,8 +7910,8 @@ var overlaysLane = {
7563
7910
  },
7564
7911
  magnets(doc) {
7565
7912
  return (doc.overlays ?? []).flatMap((o) => [
7566
- round10(o.start),
7567
- round10(o.start + o.duration)
7913
+ round11(o.start),
7914
+ round11(o.start + o.duration)
7568
7915
  ]);
7569
7916
  }
7570
7917
  };
@@ -7577,8 +7924,8 @@ var objectsLane = {
7577
7924
  {
7578
7925
  id: o.id,
7579
7926
  kind: "clip",
7580
- t: round10(o.span?.start ?? 0),
7581
- duration: round10(o.span?.duration ?? outDur),
7927
+ t: round11(o.span?.start ?? 0),
7928
+ duration: round11(o.span?.duration ?? outDur),
7582
7929
  label: o.asset.kind === "primitive" ? o.asset.shape : o.asset.kind === "text3d" ? o.asset.text : "model"
7583
7930
  },
7584
7931
  // Pose diamonds: clip-local `at` from the span start (0 span-less).
@@ -7606,7 +7953,7 @@ var objectsLane = {
7606
7953
  return (d) => {
7607
7954
  const o = (d.objects ?? []).find((x) => x.id === kf.clipId);
7608
7955
  const p = o?.motion?.[kf.index];
7609
- if (p) p.at = round10(at);
7956
+ if (p) p.at = round11(at);
7610
7957
  };
7611
7958
  }
7612
7959
  if (g.type === "remove") {
@@ -7627,7 +7974,7 @@ var objectsLane = {
7627
7974
  const start = Math.max(0, g.t);
7628
7975
  return (d) => {
7629
7976
  const o = (d.objects ?? []).find((x) => x.id === g.id);
7630
- if (o?.span) o.span.start = round10(start);
7977
+ if (o?.span) o.span.start = round11(start);
7631
7978
  };
7632
7979
  }
7633
7980
  if (g.type === "resize") {
@@ -7638,14 +7985,14 @@ var objectsLane = {
7638
7985
  return (d) => {
7639
7986
  const o = (d.objects ?? []).find((x) => x.id === g.id);
7640
7987
  if (!o?.span) return;
7641
- o.span.start = round10(newStart);
7642
- o.span.duration = round10(end - newStart);
7988
+ o.span.start = round11(newStart);
7989
+ o.span.duration = round11(end - newStart);
7643
7990
  };
7644
7991
  }
7645
7992
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.span.start);
7646
7993
  return (d) => {
7647
7994
  const o = (d.objects ?? []).find((x) => x.id === g.id);
7648
- if (o?.span) o.span.duration = round10(newLen);
7995
+ if (o?.span) o.span.duration = round11(newLen);
7649
7996
  };
7650
7997
  }
7651
7998
  return (d) => {
@@ -7654,7 +8001,7 @@ var objectsLane = {
7654
8001
  },
7655
8002
  magnets(doc) {
7656
8003
  return (doc.objects ?? []).flatMap(
7657
- (o) => o.span ? [round10(o.span.start), round10(o.span.start + o.span.duration)] : []
8004
+ (o) => o.span ? [round11(o.span.start), round11(o.span.start + o.span.duration)] : []
7658
8005
  );
7659
8006
  }
7660
8007
  };
@@ -7681,7 +8028,7 @@ function nextSpeedId(doc) {
7681
8028
  function segIndex(id) {
7682
8029
  return Number(id.replace("seg-", ""));
7683
8030
  }
7684
- function round10(v) {
8031
+ function round11(v) {
7685
8032
  return Math.round(v * 1e3) / 1e3;
7686
8033
  }
7687
8034
 
@@ -7886,6 +8233,9 @@ export {
7886
8233
  FRAME_BORDER_COLOR_DEFAULT,
7887
8234
  FRAME_BORDER_DEFAULT,
7888
8235
  FRAME_BORDER_WIDTH_DEFAULT,
8236
+ FREEZE_DEFAULT_SECONDS,
8237
+ FREEZE_SECONDS_MAX,
8238
+ FREEZE_SECONDS_MIN,
7889
8239
  HOUSE_GRADIENT,
7890
8240
  IDLE_KINDS,
7891
8241
  LAYOUT_FRAME_FIELDS,
@@ -7976,6 +8326,7 @@ export {
7976
8326
  clampCamFrac,
7977
8327
  clampCamSize,
7978
8328
  clampFocus,
8329
+ clampFreezeSeconds,
7979
8330
  clampSpeedRate,
7980
8331
  clampTiltDeg,
7981
8332
  clampZoomLevel,
@@ -7993,6 +8344,7 @@ export {
7993
8344
  destinationById,
7994
8345
  destinationsForChannel,
7995
8346
  docCardLayout,
8347
+ docFreezes,
7996
8348
  docOutputDuration,
7997
8349
  docRestTime,
7998
8350
  docToCropSpace,
@@ -8016,6 +8368,8 @@ export {
8016
8368
  focusBounds,
8017
8369
  followFocusEvents,
8018
8370
  frameGeometry,
8371
+ freezeLane,
8372
+ freezeOutputExtents,
8019
8373
  hexToRgbTriplet,
8020
8374
  houseLook,
8021
8375
  idleGaps,
@@ -8063,6 +8417,7 @@ export {
8063
8417
  pageDisplayUrl,
8064
8418
  parsePoseId,
8065
8419
  pickStyle,
8420
+ placeFreezes,
8066
8421
  planAutoSpeed,
8067
8422
  planAutoTilt,
8068
8423
  planAutoZoom,
@@ -8098,6 +8453,7 @@ export {
8098
8453
  videoLane,
8099
8454
  voiceKey,
8100
8455
  withBackdrop,
8456
+ withFreezes,
8101
8457
  withHolds,
8102
8458
  withoutRejected,
8103
8459
  wrapProgramLength,