@vosjs/studio-core 0.11.1 → 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);
@@ -5638,7 +5734,7 @@ function layoutOf(doc) {
5638
5734
  frame: LAYOUT_FRAME_FIELDS.filter((k) => frame[k] !== void 0),
5639
5735
  clips: (doc.overlays ?? []).filter(isStageClip).map((o) => o.id),
5640
5736
  lean: restLeanOf(doc) !== null,
5641
- hold: (doc.segments.at(-1)?.hold ?? 0) > 0
5737
+ freeze: trailingFreeze(doc) !== null
5642
5738
  };
5643
5739
  }
5644
5740
  function copyLayout(from, to, opts = {}) {
@@ -5700,11 +5796,33 @@ function copyLayout(from, to, opts = {}) {
5700
5796
  notes.push("lean kept: this take has tilt spans of its own");
5701
5797
  }
5702
5798
  }
5703
- const hold = from.segments.at(-1)?.hold;
5799
+ const freeze = trailingFreeze(from);
5704
5800
  const last = doc.segments.at(-1);
5705
- 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
+ }
5706
5819
  return { doc, notes };
5707
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
+ }
5708
5826
  var shiftBy = (at, templateEnd, takeEnd) => at === "start" ? 0 : at === "end" ? takeEnd - templateEnd : at;
5709
5827
  function clipsFrom(doc, from) {
5710
5828
  return {
@@ -6676,33 +6794,44 @@ function effectiveSegments(doc) {
6676
6794
  if (isRecordingDoc(doc) && doc.segments.length) return doc.segments;
6677
6795
  return [{ in: 0, out: anchorSourceDuration(doc) }];
6678
6796
  }
6679
- var outputLen = (seg, speeds) => totalDuration6(splitBySpeed3([seg], speeds));
6680
- 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 = []) => {
6681
6805
  const starts = [];
6682
6806
  let acc = 0;
6683
- for (const s of segments) {
6807
+ segments.forEach((s, i) => {
6684
6808
  starts.push(acc);
6685
- acc += outputLen(s, speeds);
6686
- }
6809
+ acc += outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i));
6810
+ });
6687
6811
  return starts;
6688
6812
  };
6813
+ var contiguous = (segments, i) => i > 0 && Math.abs(segments[i - 1].out - segments[i].in) < 1e-9;
6689
6814
  var videoLane = {
6690
6815
  id: "video",
6691
6816
  label: "Video",
6692
6817
  items(doc) {
6693
6818
  const segments = effectiveSegments(doc);
6694
6819
  const speeds = doc.speed ?? [];
6695
- const starts = segmentStarts(segments, speeds);
6820
+ const freezes = docFreezes(doc);
6821
+ const starts = segmentStarts(segments, speeds, freezes);
6696
6822
  return segments.map((s, i) => ({
6697
6823
  id: `seg-${i}`,
6698
6824
  kind: "clip",
6699
- t: starts[i],
6700
- duration: outputLen(s, speeds)
6825
+ t: round10(starts[i]),
6826
+ duration: round10(
6827
+ outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
6828
+ )
6701
6829
  }));
6702
6830
  },
6703
6831
  gesture(doc, g) {
6704
6832
  const segments = effectiveSegments(doc);
6705
6833
  const speeds = doc.speed ?? [];
6834
+ const freezes = docFreezes(doc);
6706
6835
  const sourceDuration = anchorSourceDuration(doc);
6707
6836
  switch (g.type) {
6708
6837
  case "move": {
@@ -6714,7 +6843,7 @@ var videoLane = {
6714
6843
  let acc = 0;
6715
6844
  let insert = others.length;
6716
6845
  for (let i = 0; i < others.length; i++) {
6717
- const dur = outputLen(others[i], speeds);
6846
+ const dur = outputLen(others[i], speeds, freezes);
6718
6847
  if (g.t < acc + dur / 2) {
6719
6848
  insert = i;
6720
6849
  break;
@@ -6732,10 +6861,15 @@ var videoLane = {
6732
6861
  const index = segIndex(g.id);
6733
6862
  if (index < 0 || index >= segments.length) return null;
6734
6863
  const seg = segments[index];
6735
- const starts = segmentStarts(segments, speeds);
6864
+ const starts = segmentStarts(segments, speeds, freezes);
6736
6865
  const fullMap = splitBySpeed3([{ in: 0, out: sourceDuration }], speeds);
6737
6866
  const edgeSrc = g.edge === "start" ? seg.in : seg.out;
6738
- 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
+ );
6739
6873
  const anchorOut = sourceToTimeline3(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
6740
6874
  const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
6741
6875
  const next = trimSegment(
@@ -6750,13 +6884,31 @@ var videoLane = {
6750
6884
  };
6751
6885
  }
6752
6886
  case "create": {
6753
- const starts = segmentStarts(segments, speeds);
6887
+ const starts = segmentStarts(segments, speeds, freezes);
6754
6888
  const index = segments.findIndex(
6755
- (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
+ )
6756
6895
  );
6757
6896
  if (index < 0) return null;
6758
6897
  const s = segments[index];
6759
- 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
+ );
6760
6912
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
6761
6913
  const next = [
6762
6914
  ...segments.slice(0, index),
@@ -6782,15 +6934,154 @@ var videoLane = {
6782
6934
  magnets(doc) {
6783
6935
  const segments = effectiveSegments(doc);
6784
6936
  const speeds = doc.speed ?? [];
6785
- const starts = segmentStarts(segments, speeds);
6937
+ const freezes = docFreezes(doc);
6938
+ const starts = segmentStarts(segments, speeds, freezes);
6939
+ const n = segments.length;
6786
6940
  return [
6787
6941
  ...starts,
6788
- ...starts.length ? [
6789
- 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
+ )
6790
6949
  ] : []
6791
- ];
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)]);
6792
7080
  }
6793
7081
  };
7082
+ function trimZeros(n) {
7083
+ return String(Math.round(n * 100) / 100);
7084
+ }
6794
7085
  var zoomLane = {
6795
7086
  id: "zoom",
6796
7087
  label: "Zoom",
@@ -7145,7 +7436,8 @@ var camLane = {
7145
7436
  if (!doc.source.camKey || !doc.cam.visible) return [];
7146
7437
  const segments = effectiveSegments(doc);
7147
7438
  const speeds = doc.speed ?? [];
7148
- const starts = segmentStarts(segments, speeds);
7439
+ const freezes = docFreezes(doc);
7440
+ const starts = segmentStarts(segments, speeds, freezes);
7149
7441
  const first = segments[0];
7150
7442
  const last = segments[segments.length - 1];
7151
7443
  const win = doc.cam.window ?? { in: first.in, out: last.out };
@@ -7154,11 +7446,16 @@ var camLane = {
7154
7446
  const a = Math.max(s.in, win.in);
7155
7447
  const b = Math.min(s.out, win.out);
7156
7448
  if (b - a <= 1e-6) return;
7449
+ const leading = i === 0 || !contiguous(segments, i);
7157
7450
  items.push({
7158
7451
  id: `cam-${i}`,
7159
7452
  kind: "clip",
7160
- t: round10(starts[i] + outputLen({ in: s.in, out: a }, speeds)),
7161
- 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
+ )
7162
7459
  });
7163
7460
  });
7164
7461
  return items;
@@ -7214,12 +7511,15 @@ var micLane = {
7214
7511
  if (!voiceKey(doc)) return [];
7215
7512
  const segments = effectiveSegments(doc);
7216
7513
  const speeds = doc.speed ?? [];
7217
- const starts = segmentStarts(segments, speeds);
7514
+ const freezes = docFreezes(doc);
7515
+ const starts = segmentStarts(segments, speeds, freezes);
7218
7516
  return segments.map((s, i) => ({
7219
7517
  id: `mic-${i}`,
7220
7518
  kind: "clip",
7221
- t: starts[i],
7222
- duration: outputLen(s, speeds)
7519
+ t: round10(starts[i]),
7520
+ duration: round10(
7521
+ outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
7522
+ )
7223
7523
  }));
7224
7524
  },
7225
7525
  gesture() {
@@ -7886,6 +8186,9 @@ export {
7886
8186
  FRAME_BORDER_COLOR_DEFAULT,
7887
8187
  FRAME_BORDER_DEFAULT,
7888
8188
  FRAME_BORDER_WIDTH_DEFAULT,
8189
+ FREEZE_DEFAULT_SECONDS,
8190
+ FREEZE_SECONDS_MAX,
8191
+ FREEZE_SECONDS_MIN,
7889
8192
  HOUSE_GRADIENT,
7890
8193
  IDLE_KINDS,
7891
8194
  LAYOUT_FRAME_FIELDS,
@@ -7976,6 +8279,7 @@ export {
7976
8279
  clampCamFrac,
7977
8280
  clampCamSize,
7978
8281
  clampFocus,
8282
+ clampFreezeSeconds,
7979
8283
  clampSpeedRate,
7980
8284
  clampTiltDeg,
7981
8285
  clampZoomLevel,
@@ -7993,6 +8297,7 @@ export {
7993
8297
  destinationById,
7994
8298
  destinationsForChannel,
7995
8299
  docCardLayout,
8300
+ docFreezes,
7996
8301
  docOutputDuration,
7997
8302
  docRestTime,
7998
8303
  docToCropSpace,
@@ -8016,6 +8321,8 @@ export {
8016
8321
  focusBounds,
8017
8322
  followFocusEvents,
8018
8323
  frameGeometry,
8324
+ freezeLane,
8325
+ freezeOutputExtents,
8019
8326
  hexToRgbTriplet,
8020
8327
  houseLook,
8021
8328
  idleGaps,
@@ -8063,6 +8370,7 @@ export {
8063
8370
  pageDisplayUrl,
8064
8371
  parsePoseId,
8065
8372
  pickStyle,
8373
+ placeFreezes,
8066
8374
  planAutoSpeed,
8067
8375
  planAutoTilt,
8068
8376
  planAutoZoom,
@@ -8098,6 +8406,7 @@ export {
8098
8406
  videoLane,
8099
8407
  voiceKey,
8100
8408
  withBackdrop,
8409
+ withFreezes,
8101
8410
  withHolds,
8102
8411
  withoutRejected,
8103
8412
  wrapProgramLength,