@vosjs/studio-core 0.11.0 → 0.12.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;
@@ -540,28 +573,67 @@ function exitSeconds(e) {
540
573
  const house = e.kind === "fade" ? CARD_FADE_SECONDS : END_CARD_RECEDE;
541
574
  return Math.max(0.1, Math.min(3, 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;
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;
561
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);
@@ -704,9 +776,29 @@ function migrateMotion(doc) {
704
776
  const segments = Array.isArray(doc.segments) ? doc.segments : [];
705
777
  const overlays = Array.isArray(doc.overlays) ? doc.overlays : [];
706
778
  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);
779
+ const heldSegments = segments.some((s) => s.hold !== void 0);
780
+ const legacy = frame.entrance !== void 0 || doc.endCard !== void 0 || heldSegments || overlays.some(hasLegacyClipMotion) || objects.some((o) => o.animation !== void 0);
708
781
  if (!legacy) return doc;
709
782
  const out = { ...doc };
783
+ if (heldSegments) {
784
+ const holds = legacyHoldFreezes(segments);
785
+ const own = Array.isArray(doc.freeze) ? doc.freeze : [];
786
+ const taken = new Set(own.map((f) => f.id));
787
+ let n = own.length;
788
+ const migrated = holds.map((h) => {
789
+ let id = `f${n++}`;
790
+ while (taken.has(id)) id = `f${n++}`;
791
+ taken.add(id);
792
+ return { id, at: h.at, seconds: h.seconds };
793
+ });
794
+ out.freeze = [...own, ...migrated].sort((a, b) => a.at - b.at);
795
+ out.segments = segments.map((s) => {
796
+ if (s.hold === void 0) return s;
797
+ const next = { ...s };
798
+ delete next.hold;
799
+ return next;
800
+ });
801
+ }
710
802
  const nextFrame = { ...frame };
711
803
  if (frame.entrance !== void 0) {
712
804
  const e = frame.entrance;
@@ -730,7 +822,10 @@ function migrateMotion(doc) {
730
822
  const card = doc.endCard;
731
823
  if (card && segments.length) {
732
824
  const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
733
- const rated = withHolds(segments, splitBySpeed(segments, doc.speed ?? []));
825
+ const rated = withFreezes(
826
+ splitBySpeed(segments, doc.speed ?? []),
827
+ docFreezes(doc)
828
+ );
734
829
  const endStart = totalDuration(rated);
735
830
  nextOverlays = [...nextOverlays, ...endCardClips(card, doc, endStart)];
736
831
  nextFrame.anim = {
@@ -767,12 +862,12 @@ function outputEnd(doc, footageEnd) {
767
862
  }
768
863
 
769
864
  // src/docVersion.ts
770
- var DOC_SCHEMA_VERSION = 3;
865
+ var DOC_SCHEMA_VERSION = 4;
771
866
  function migrateHostedDoc(raw) {
772
867
  const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
773
868
  if (version >= DOC_SCHEMA_VERSION) return raw;
774
869
  let doc = raw;
775
- if (version < 3 && doc.source && typeof doc.source === "object") {
870
+ if (version < 4 && doc.source && typeof doc.source === "object") {
776
871
  doc = migrateMotion(doc);
777
872
  }
778
873
  return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
@@ -3222,7 +3317,8 @@ var CAM_PAN_EASE = ZOOM_PAN_EASE;
3222
3317
  var PROGRAM_DURATION = 1;
3223
3318
  function ratedSegments(doc) {
3224
3319
  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 ?? []));
3320
+ const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
3321
+ return withFreezes(splitBySpeed2(segs, doc.speed ?? []), freezes);
3226
3322
  }
3227
3323
  function durationSec(doc, rated) {
3228
3324
  const trimmed = totalDuration2(rated);
@@ -3960,6 +4056,15 @@ var ON_FRAME = `(ctx, content, dt) => {
3960
4056
  // range); the paused/seek path needs nothing \u2014 mapTime already lands srcT.
3961
4057
  var srcRate = TL.rateAt ? TL.rateAt(d.segments || [], t) : 1
3962
4058
  var playRate = Math.min(16, Math.max(0.0625, srcRate))
4059
+ // Where the FOOTAGE ends in output seconds. The output may outlast it
4060
+ // (clips placed after the footage), and a hold is a freeze piece whose
4061
+ // rate is tiny: in both the element must stay PAUSED on its frame. An
4062
+ // ended element's play() rewinds it to zero and the drift guard then
4063
+ // seeks it back, which reads as frames jumping back and forth; a hold
4064
+ // crawling at the clamped 1/16 speed toward that same end was the same
4065
+ // bug in slow motion.
4066
+ var footEnd = TL.totalDuration ? TL.totalDuration(d.segments || []) : Infinity
4067
+ var frozen = srcRate < 0.01 || t >= footEnd - 1e-3
3963
4068
  // Drive a <video> to the on-screen SOURCE moment: play natively while playing (drift
3964
4069
  // correction covers cut-boundary jumps), else step to the exact frame (registering a
3965
4070
  // decode promise so the deterministic export awaits it). Shared by the screen video
@@ -3967,7 +4072,7 @@ var ON_FRAME = `(ctx, content, dt) => {
3967
4072
  ${FRAME_STEP_SRC}
3968
4073
  function syncVid(vid) {
3969
4074
  try {
3970
- if (playing) {
4075
+ if (playing && !frozen) {
3971
4076
  if (vid.playbackRate !== playRate) {
3972
4077
  vid.playbackRate = playRate
3973
4078
  // Resampled (tape-style) speed, matching the export's offline mix.
@@ -5629,7 +5734,7 @@ function layoutOf(doc) {
5629
5734
  frame: LAYOUT_FRAME_FIELDS.filter((k) => frame[k] !== void 0),
5630
5735
  clips: (doc.overlays ?? []).filter(isStageClip).map((o) => o.id),
5631
5736
  lean: restLeanOf(doc) !== null,
5632
- hold: (doc.segments.at(-1)?.hold ?? 0) > 0
5737
+ freeze: trailingFreeze(doc) !== null
5633
5738
  };
5634
5739
  }
5635
5740
  function copyLayout(from, to, opts = {}) {
@@ -5691,11 +5796,33 @@ function copyLayout(from, to, opts = {}) {
5691
5796
  notes.push("lean kept: this take has tilt spans of its own");
5692
5797
  }
5693
5798
  }
5694
- const hold = from.segments.at(-1)?.hold;
5799
+ const freeze = trailingFreeze(from);
5695
5800
  const last = doc.segments.at(-1);
5696
- if (typeof hold === "number" && hold > 0 && last) last.hold = hold;
5801
+ if (freeze && last) {
5802
+ const own2 = (doc.freeze ?? []).filter(
5803
+ (f) => Math.abs(f.at - last.out) > 1e-9
5804
+ );
5805
+ const taken = new Set(own2.map((f) => f.id));
5806
+ let n = 0;
5807
+ while (taken.has(`f${n}`)) n++;
5808
+ doc.freeze = [
5809
+ ...own2,
5810
+ { id: `f${n}`, at: last.out, seconds: freeze.seconds }
5811
+ ].sort((a, b) => a.at - b.at);
5812
+ doc.segments = doc.segments.map((s) => {
5813
+ if (s.hold === void 0) return s;
5814
+ const next = { ...s };
5815
+ delete next.hold;
5816
+ return next;
5817
+ });
5818
+ }
5697
5819
  return { doc, notes };
5698
5820
  }
5821
+ function trailingFreeze(doc) {
5822
+ const last = doc.segments.at(-1);
5823
+ if (!last) return null;
5824
+ return docFreezes(doc).find((f) => Math.abs(f.at - last.out) < 1e-9) ?? null;
5825
+ }
5699
5826
  var shiftBy = (at, templateEnd, takeEnd) => at === "start" ? 0 : at === "end" ? takeEnd - templateEnd : at;
5700
5827
  function clipsFrom(doc, from) {
5701
5828
  return {
@@ -6667,33 +6794,44 @@ function effectiveSegments(doc) {
6667
6794
  if (isRecordingDoc(doc) && doc.segments.length) return doc.segments;
6668
6795
  return [{ in: 0, out: anchorSourceDuration(doc) }];
6669
6796
  }
6670
- var outputLen = (seg, speeds) => totalDuration6(splitBySpeed3([seg], speeds));
6671
- var segmentStarts = (segments, speeds) => {
6797
+ var segmentPieces = (seg, speeds, freezes, leading) => {
6798
+ const own = freezes.filter(
6799
+ (f) => f.at <= seg.out + 1e-9 && (leading ? f.at >= seg.in - 1e-9 : f.at > seg.in + 1e-9)
6800
+ );
6801
+ return withFreezes(splitBySpeed3([seg], speeds), own);
6802
+ };
6803
+ var outputLen = (seg, speeds, freezes = [], leading = true) => totalDuration6(segmentPieces(seg, speeds, freezes, leading));
6804
+ var segmentStarts = (segments, speeds, freezes = []) => {
6672
6805
  const starts = [];
6673
6806
  let acc = 0;
6674
- for (const s of segments) {
6807
+ segments.forEach((s, i) => {
6675
6808
  starts.push(acc);
6676
- acc += outputLen(s, speeds);
6677
- }
6809
+ acc += outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i));
6810
+ });
6678
6811
  return starts;
6679
6812
  };
6813
+ var contiguous = (segments, i) => i > 0 && Math.abs(segments[i - 1].out - segments[i].in) < 1e-9;
6680
6814
  var videoLane = {
6681
6815
  id: "video",
6682
6816
  label: "Video",
6683
6817
  items(doc) {
6684
6818
  const segments = effectiveSegments(doc);
6685
6819
  const speeds = doc.speed ?? [];
6686
- const starts = segmentStarts(segments, speeds);
6820
+ const freezes = docFreezes(doc);
6821
+ const starts = segmentStarts(segments, speeds, freezes);
6687
6822
  return segments.map((s, i) => ({
6688
6823
  id: `seg-${i}`,
6689
6824
  kind: "clip",
6690
- t: starts[i],
6691
- duration: outputLen(s, speeds)
6825
+ t: round10(starts[i]),
6826
+ duration: round10(
6827
+ outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
6828
+ )
6692
6829
  }));
6693
6830
  },
6694
6831
  gesture(doc, g) {
6695
6832
  const segments = effectiveSegments(doc);
6696
6833
  const speeds = doc.speed ?? [];
6834
+ const freezes = docFreezes(doc);
6697
6835
  const sourceDuration = anchorSourceDuration(doc);
6698
6836
  switch (g.type) {
6699
6837
  case "move": {
@@ -6705,7 +6843,7 @@ var videoLane = {
6705
6843
  let acc = 0;
6706
6844
  let insert = others.length;
6707
6845
  for (let i = 0; i < others.length; i++) {
6708
- const dur = outputLen(others[i], speeds);
6846
+ const dur = outputLen(others[i], speeds, freezes);
6709
6847
  if (g.t < acc + dur / 2) {
6710
6848
  insert = i;
6711
6849
  break;
@@ -6723,10 +6861,15 @@ var videoLane = {
6723
6861
  const index = segIndex(g.id);
6724
6862
  if (index < 0 || index >= segments.length) return null;
6725
6863
  const seg = segments[index];
6726
- const starts = segmentStarts(segments, speeds);
6864
+ const starts = segmentStarts(segments, speeds, freezes);
6727
6865
  const fullMap = splitBySpeed3([{ in: 0, out: sourceDuration }], speeds);
6728
6866
  const edgeSrc = g.edge === "start" ? seg.in : seg.out;
6729
- const edgeOutNow = g.edge === "start" ? starts[index] : starts[index] + outputLen(seg, speeds);
6867
+ const edgeOutNow = g.edge === "start" ? starts[index] : starts[index] + outputLen(
6868
+ seg,
6869
+ speeds,
6870
+ freezes.filter((f) => f.at < seg.out - 1e-9),
6871
+ index === 0 || !contiguous(segments, index)
6872
+ );
6730
6873
  const anchorOut = sourceToTimeline3(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
6731
6874
  const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
6732
6875
  const next = trimSegment(
@@ -6741,13 +6884,31 @@ var videoLane = {
6741
6884
  };
6742
6885
  }
6743
6886
  case "create": {
6744
- const starts = segmentStarts(segments, speeds);
6887
+ const starts = segmentStarts(segments, speeds, freezes);
6745
6888
  const index = segments.findIndex(
6746
- (s2, i) => g.t >= starts[i] && g.t < starts[i] + outputLen(s2, speeds)
6889
+ (s2, i) => g.t >= starts[i] && g.t < starts[i] + outputLen(
6890
+ s2,
6891
+ speeds,
6892
+ freezes,
6893
+ i === 0 || !contiguous(segments, i)
6894
+ )
6747
6895
  );
6748
6896
  if (index < 0) return null;
6749
6897
  const s = segments[index];
6750
- const sourceT = mapTime2(splitBySpeed3([s], speeds), g.t - starts[index]);
6898
+ const leading = index === 0 || !contiguous(segments, index);
6899
+ const local = g.t - starts[index];
6900
+ for (const m of freezeOutputExtents(
6901
+ splitBySpeed3([s], speeds),
6902
+ freezes.filter(
6903
+ (f) => f.at <= s.out + 1e-9 && (leading ? f.at >= s.in - 1e-9 : f.at > s.in + 1e-9)
6904
+ )
6905
+ ).values()) {
6906
+ if (local >= m.t - 1e-9 && local <= m.t + m.duration + 1e-9)
6907
+ return null;
6908
+ }
6909
+ const sourceT = round10(
6910
+ mapTime2(segmentPieces(s, speeds, freezes, leading), local)
6911
+ );
6751
6912
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
6752
6913
  const next = [
6753
6914
  ...segments.slice(0, index),
@@ -6773,15 +6934,154 @@ var videoLane = {
6773
6934
  magnets(doc) {
6774
6935
  const segments = effectiveSegments(doc);
6775
6936
  const speeds = doc.speed ?? [];
6776
- const starts = segmentStarts(segments, speeds);
6937
+ const freezes = docFreezes(doc);
6938
+ const starts = segmentStarts(segments, speeds, freezes);
6939
+ const n = segments.length;
6777
6940
  return [
6778
6941
  ...starts,
6779
- ...starts.length ? [
6780
- starts[starts.length - 1] + outputLen(segments[segments.length - 1], speeds)
6942
+ ...n ? [
6943
+ starts[n - 1] + outputLen(
6944
+ segments[n - 1],
6945
+ speeds,
6946
+ freezes,
6947
+ n === 1 || !contiguous(segments, n - 1)
6948
+ )
6781
6949
  ] : []
6782
- ];
6950
+ ].map(round10);
6951
+ }
6952
+ };
6953
+ function nextFreezeId(doc) {
6954
+ const taken = new Set((doc.freeze ?? []).map((f) => f.id));
6955
+ let n = 0;
6956
+ while (taken.has(`f${n}`)) n++;
6957
+ return `f${n}`;
6958
+ }
6959
+ var FREEZE_GAP = 0.05;
6960
+ var freezeLane = {
6961
+ id: "freeze",
6962
+ label: "Freeze",
6963
+ items(doc) {
6964
+ const freezes = docFreezes(doc);
6965
+ if (!freezes.length) return [];
6966
+ const segs = effectiveSegments(doc);
6967
+ const marks = freezeOutputExtents(
6968
+ splitBySpeed3(segs, doc.speed ?? []),
6969
+ freezes
6970
+ );
6971
+ return freezes.flatMap((f) => {
6972
+ const m = marks.get(f.id);
6973
+ return m ? [
6974
+ {
6975
+ id: f.id,
6976
+ kind: "clip",
6977
+ t: round10(m.t),
6978
+ duration: round10(m.duration),
6979
+ label: `${trimZeros(f.seconds)}s`
6980
+ }
6981
+ ] : [];
6982
+ });
6983
+ },
6984
+ gesture(doc, g) {
6985
+ const freezes = docFreezes(doc);
6986
+ const segs = effectiveSegments(doc);
6987
+ const speeds = doc.speed ?? [];
6988
+ const sourceDuration = anchorSourceDuration(doc);
6989
+ const kept = (t) => segs.some((s) => t >= s.in - 1e-9 && t <= s.out + 1e-9);
6990
+ const clear = (t, except) => !freezes.some((f2) => f2.id !== except && Math.abs(f2.at - t) < FREEZE_GAP);
6991
+ if (g.type === "create") {
6992
+ const rated = ratedSegments(doc);
6993
+ for (const m of freezeOutputExtents(
6994
+ splitBySpeed3(segs, speeds),
6995
+ freezes
6996
+ ).values()) {
6997
+ if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
6998
+ }
6999
+ if (g.t > totalDuration6(rated) + 1e-9) return null;
7000
+ const srcT = round10(mapTime2(rated, Math.max(0, g.t)));
7001
+ if (!kept(srcT) || !clear(srcT)) return null;
7002
+ const id = nextFreezeId(doc);
7003
+ return (d) => {
7004
+ d.freeze = [
7005
+ ...d.freeze ?? [],
7006
+ { id, at: srcT, seconds: FREEZE_DEFAULT_SECONDS }
7007
+ ].sort((a, b) => a.at - b.at);
7008
+ };
7009
+ }
7010
+ const f = freezes.find((x) => x.id === g.id);
7011
+ if (!f) return null;
7012
+ const own = (d) => {
7013
+ const list = d.freeze ?? [];
7014
+ let mine = list.find((x) => x.id === f.id);
7015
+ if (!mine) {
7016
+ const id = nextFreezeId(d);
7017
+ mine = { id, at: f.at, seconds: f.seconds };
7018
+ d.freeze = [...list, mine];
7019
+ d.segments = d.segments.map((sg) => {
7020
+ if (sg.hold === void 0 || Math.abs(sg.out - f.at) > 1e-9) return sg;
7021
+ const next = { ...sg };
7022
+ delete next.hold;
7023
+ return next;
7024
+ });
7025
+ }
7026
+ return mine;
7027
+ };
7028
+ const base = withFreezes(
7029
+ splitBySpeed3(segs, speeds),
7030
+ freezes.filter((x) => x.id !== f.id)
7031
+ );
7032
+ if (g.type === "move" || g.type === "resize" && g.edge === "start") {
7033
+ const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
7034
+ const mine = marks.get(f.id);
7035
+ const endOut = mine ? mine.t + mine.duration : null;
7036
+ const srcT = round10(
7037
+ Math.min(sourceDuration, mapTime2(base, Math.max(0, g.t)))
7038
+ );
7039
+ if (!kept(srcT) || !clear(srcT, f.id)) return null;
7040
+ return (d) => {
7041
+ const x = own(d);
7042
+ if (!x) return;
7043
+ x.at = srcT;
7044
+ if (g.type === "resize" && endOut !== null) {
7045
+ const after = freezeOutputExtents(
7046
+ splitBySpeed3(effectiveSegments(d), d.speed ?? []),
7047
+ docFreezes({
7048
+ ...d,
7049
+ freeze: (d.freeze ?? []).map(
7050
+ (y) => y.id === x.id ? { ...y, seconds: FREEZE_SECONDS_MIN } : y
7051
+ )
7052
+ })
7053
+ ).get(x.id);
7054
+ if (after) x.seconds = clampFreezeSeconds(endOut - after.t);
7055
+ }
7056
+ d.freeze.sort((a, b) => a.at - b.at);
7057
+ };
7058
+ }
7059
+ if (g.type === "resize") {
7060
+ const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
7061
+ const mine = marks.get(f.id);
7062
+ if (!mine) return null;
7063
+ const seconds = clampFreezeSeconds(g.t - mine.t);
7064
+ if (Math.abs(seconds - f.seconds) < 1e-6) return null;
7065
+ return (d) => {
7066
+ const x = own(d);
7067
+ if (x) x.seconds = seconds;
7068
+ };
7069
+ }
7070
+ if (g.type === "remove") {
7071
+ return (d) => {
7072
+ const x = own(d);
7073
+ d.freeze = (d.freeze ?? []).filter((y) => y !== x);
7074
+ };
7075
+ }
7076
+ return null;
7077
+ },
7078
+ magnets(doc) {
7079
+ return freezeLane.items(doc).flatMap((it) => [it.t, it.t + (it.duration ?? 0)]);
6783
7080
  }
6784
7081
  };
7082
+ function trimZeros(n) {
7083
+ return String(Math.round(n * 100) / 100);
7084
+ }
6785
7085
  var zoomLane = {
6786
7086
  id: "zoom",
6787
7087
  label: "Zoom",
@@ -7136,7 +7436,8 @@ var camLane = {
7136
7436
  if (!doc.source.camKey || !doc.cam.visible) return [];
7137
7437
  const segments = effectiveSegments(doc);
7138
7438
  const speeds = doc.speed ?? [];
7139
- const starts = segmentStarts(segments, speeds);
7439
+ const freezes = docFreezes(doc);
7440
+ const starts = segmentStarts(segments, speeds, freezes);
7140
7441
  const first = segments[0];
7141
7442
  const last = segments[segments.length - 1];
7142
7443
  const win = doc.cam.window ?? { in: first.in, out: last.out };
@@ -7145,11 +7446,16 @@ var camLane = {
7145
7446
  const a = Math.max(s.in, win.in);
7146
7447
  const b = Math.min(s.out, win.out);
7147
7448
  if (b - a <= 1e-6) return;
7449
+ const leading = i === 0 || !contiguous(segments, i);
7148
7450
  items.push({
7149
7451
  id: `cam-${i}`,
7150
7452
  kind: "clip",
7151
- t: round10(starts[i] + outputLen({ in: s.in, out: a }, speeds)),
7152
- duration: round10(outputLen({ in: a, out: b }, speeds))
7453
+ t: round10(
7454
+ starts[i] + outputLen({ in: s.in, out: a }, speeds, freezes, leading)
7455
+ ),
7456
+ duration: round10(
7457
+ outputLen({ in: a, out: b }, speeds, freezes, a === s.in && leading)
7458
+ )
7153
7459
  });
7154
7460
  });
7155
7461
  return items;
@@ -7205,12 +7511,15 @@ var micLane = {
7205
7511
  if (!voiceKey(doc)) return [];
7206
7512
  const segments = effectiveSegments(doc);
7207
7513
  const speeds = doc.speed ?? [];
7208
- const starts = segmentStarts(segments, speeds);
7514
+ const freezes = docFreezes(doc);
7515
+ const starts = segmentStarts(segments, speeds, freezes);
7209
7516
  return segments.map((s, i) => ({
7210
7517
  id: `mic-${i}`,
7211
7518
  kind: "clip",
7212
- t: starts[i],
7213
- duration: outputLen(s, speeds)
7519
+ t: round10(starts[i]),
7520
+ duration: round10(
7521
+ outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
7522
+ )
7214
7523
  }));
7215
7524
  },
7216
7525
  gesture() {
@@ -7877,6 +8186,9 @@ export {
7877
8186
  FRAME_BORDER_COLOR_DEFAULT,
7878
8187
  FRAME_BORDER_DEFAULT,
7879
8188
  FRAME_BORDER_WIDTH_DEFAULT,
8189
+ FREEZE_DEFAULT_SECONDS,
8190
+ FREEZE_SECONDS_MAX,
8191
+ FREEZE_SECONDS_MIN,
7880
8192
  HOUSE_GRADIENT,
7881
8193
  IDLE_KINDS,
7882
8194
  LAYOUT_FRAME_FIELDS,
@@ -7967,6 +8279,7 @@ export {
7967
8279
  clampCamFrac,
7968
8280
  clampCamSize,
7969
8281
  clampFocus,
8282
+ clampFreezeSeconds,
7970
8283
  clampSpeedRate,
7971
8284
  clampTiltDeg,
7972
8285
  clampZoomLevel,
@@ -7984,6 +8297,7 @@ export {
7984
8297
  destinationById,
7985
8298
  destinationsForChannel,
7986
8299
  docCardLayout,
8300
+ docFreezes,
7987
8301
  docOutputDuration,
7988
8302
  docRestTime,
7989
8303
  docToCropSpace,
@@ -8007,6 +8321,8 @@ export {
8007
8321
  focusBounds,
8008
8322
  followFocusEvents,
8009
8323
  frameGeometry,
8324
+ freezeLane,
8325
+ freezeOutputExtents,
8010
8326
  hexToRgbTriplet,
8011
8327
  houseLook,
8012
8328
  idleGaps,
@@ -8054,6 +8370,7 @@ export {
8054
8370
  pageDisplayUrl,
8055
8371
  parsePoseId,
8056
8372
  pickStyle,
8373
+ placeFreezes,
8057
8374
  planAutoSpeed,
8058
8375
  planAutoTilt,
8059
8376
  planAutoZoom,
@@ -8089,6 +8406,7 @@ export {
8089
8406
  videoLane,
8090
8407
  voiceKey,
8091
8408
  withBackdrop,
8409
+ withFreezes,
8092
8410
  withHolds,
8093
8411
  withoutRejected,
8094
8412
  wrapProgramLength,