@vosjs/studio-core 0.9.1 → 0.11.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
@@ -424,12 +424,358 @@ function projectFromArtifact(artifact, videoUrl, opts = {}) {
424
424
  return { doc, videoUrl };
425
425
  }
426
426
 
427
+ // src/lower/motion.ts
428
+ import { sortKeyframes } from "@vosjs/timeline";
429
+
430
+ // src/anim.ts
431
+ var CARD_ENTER_KINDS = [
432
+ "none",
433
+ "fade",
434
+ "rise",
435
+ "tilt-in",
436
+ "pull-out"
437
+ ];
438
+ var CARD_EXIT_KINDS = ["none", "fade", "recede"];
439
+ var TEXT_ENTER_KINDS = [
440
+ "none",
441
+ "fade",
442
+ "rise",
443
+ "pop",
444
+ "blur",
445
+ "typewriter"
446
+ ];
447
+ var TEXT_EXIT_KINDS = ["none", "fade", "rise"];
448
+ var MEDIA_ENTER_KINDS = ["none", "fade", "rise"];
449
+ var MEDIA_EXIT_KINDS = ["none", "fade", "rise"];
450
+ var IDLE_KINDS = ["spin", "float"];
451
+ var UNIT_KINDS = /* @__PURE__ */ new Set(["pop", "blur", "typewriter"]);
452
+ function animStep(step) {
453
+ if (step == null) return null;
454
+ if (typeof step === "string") return { kind: step };
455
+ return step;
456
+ }
457
+ function enterOf(anim) {
458
+ return animStep(anim?.enter);
459
+ }
460
+ function exitOf(anim) {
461
+ return animStep(anim?.exit);
462
+ }
463
+ function isUnitStep(step) {
464
+ if (!step) return false;
465
+ if (UNIT_KINDS.has(step.kind)) return true;
466
+ if (step.kind !== "fade" && step.kind !== "rise") return false;
467
+ return step.unit !== void 0 || step.stagger !== void 0 || step.seconds !== void 0 || step.direction !== void 0;
468
+ }
469
+ function overlayFxSpec(clip) {
470
+ const step = enterOf(clip.anim);
471
+ if (!isUnitStep(step) || !step || step.kind === "none") return null;
472
+ const kind = step.kind;
473
+ const fx = kind === "fade" || kind === "rise" || kind === "pop" || kind === "blur" || kind === "typewriter" ? kind : "rise";
474
+ return {
475
+ fx,
476
+ ...step.unit !== void 0 ? { unit: step.unit } : {},
477
+ ...step.direction !== void 0 ? { direction: step.direction } : {},
478
+ ...step.stagger !== void 0 ? { stagger: step.stagger } : {},
479
+ ...step.seconds !== void 0 ? { duration: step.seconds } : {}
480
+ };
481
+ }
482
+ var BLOCK_KINDS = /* @__PURE__ */ new Set(["none", "fade", "rise"]);
483
+ function enterKey(clip) {
484
+ const step = enterOf(clip.anim);
485
+ if (!step) return "rise";
486
+ return BLOCK_KINDS.has(step.kind) ? step.kind : "rise";
487
+ }
488
+ function exitKey(clip) {
489
+ const step = exitOf(clip.anim);
490
+ if (!step) return "fade";
491
+ return BLOCK_KINDS.has(step.kind) ? step.kind : "fade";
492
+ }
493
+ function idleKey(clip) {
494
+ const idle = clip.anim?.idle;
495
+ return idle === "spin" || idle === "float" ? idle : null;
496
+ }
497
+ function lastLayerEnd(doc) {
498
+ let end = 0;
499
+ for (const o of doc.overlays ?? []) end = Math.max(end, o.start + o.duration);
500
+ for (const o of doc.objects ?? [])
501
+ if (o.span) end = Math.max(end, o.span.start + o.span.duration);
502
+ return Number.isFinite(end) && end > 0 ? end : 0;
503
+ }
504
+
505
+ // src/lower/motion.ts
506
+ import { splitBySpeed, totalDuration } from "@vosjs/timeline";
507
+ function docRestTime(doc) {
508
+ 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 ?? []))
515
+ );
516
+ const rest = total - hold;
517
+ if (!(rest >= 0)) return null;
518
+ return Math.round(rest * 1e3) / 1e3;
519
+ }
520
+ var ENTRANCE_SECONDS = 1.2;
521
+ var END_CARD_SECONDS = 2.5;
522
+ var END_CARD_RECEDE = 0.7;
523
+ var CARD_FADE_SECONDS = 0.35;
524
+ var TILT_IN_POSE = [-9, 14];
525
+ var PULL_OUT_LEVEL = 1.32;
526
+ var END_CARD_FROM = "endcard";
527
+ var HOLD_SOURCE_SPAN = 2e-3;
528
+ function cardEnter(frame) {
529
+ return enterOf(frame.anim);
530
+ }
531
+ function cardExit(frame) {
532
+ return exitOf(frame.anim);
533
+ }
534
+ function entranceSeconds(e) {
535
+ if (!e || e.kind === "none") return 0;
536
+ return Math.max(0.2, Math.min(3, e.seconds ?? ENTRANCE_SECONDS));
537
+ }
538
+ function exitSeconds(e) {
539
+ if (!e || e.kind === "none") return 0;
540
+ const house = e.kind === "fade" ? CARD_FADE_SECONDS : END_CARD_RECEDE;
541
+ return Math.max(0.1, Math.min(3, e.seconds ?? house));
542
+ }
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 }));
546
+ for (let i = 0; i < rated.length; i++) {
547
+ const piece = rated[i];
548
+ out.push(piece);
549
+ 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;
561
+ }
562
+ }
563
+ }
564
+ return out;
565
+ }
566
+ function entranceTiltKeyframes(e) {
567
+ const s = entranceSeconds(e);
568
+ if (!s || e?.kind !== "tilt-in") return [];
569
+ return [
570
+ { t: 0, value: [...TILT_IN_POSE], ease: "none" },
571
+ { t: s, value: [0, 0], ease: "power3.out" }
572
+ ];
573
+ }
574
+ function entranceZoomKeyframes(e) {
575
+ const s = entranceSeconds(e);
576
+ if (!s) return [];
577
+ if (e?.kind === "pull-out")
578
+ return [
579
+ { t: 0, value: [PULL_OUT_LEVEL, 0.5, 0.42], ease: "none" },
580
+ { t: s, value: [1, 0.5, 0.5], ease: "power3.out" }
581
+ ];
582
+ return [
583
+ { t: 0, value: [1, 0.5, 0.5], ease: "none" },
584
+ { t: s, value: [1, 0.5, 0.5], ease: "none" }
585
+ ];
586
+ }
587
+ function prependEntrance(track, head) {
588
+ if (!head.length) return track;
589
+ const end = head[head.length - 1].t;
590
+ const rest = (track?.keyframes ?? []).filter((k) => k.t > end + 0.1);
591
+ return { keyframes: sortKeyframes([...head, ...rest]) };
592
+ }
593
+ function exitPose(e) {
594
+ return e?.kind === "fade" ? [1, 0, 0] : [0.9, -0.02, 0.22];
595
+ }
596
+ function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
597
+ const keyframes = [];
598
+ const s = entranceSeconds(enter);
599
+ if (s) {
600
+ const from = enter?.kind === "rise" ? [0.96, 0.08, 0.7] : enter?.kind === "pull-out" || enter?.kind === "fade" ? enter.kind === "fade" ? [1, 0, 0] : [1, 0, 1] : [0.94, 0.05, 0.7];
601
+ keyframes.push({ t: 0, value: from, ease: "none" });
602
+ keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
603
+ }
604
+ const x = exitSeconds(exit);
605
+ 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;
609
+ keyframes.push({ t: t0, value: [1, 0, 1], ease: "none" });
610
+ keyframes.push({ t: t1, value: exitPose(exit), ease: "power2.out" });
611
+ }
612
+ if (!keyframes.length) return void 0;
613
+ return { keyframes: sortKeyframes(keyframes) };
614
+ }
615
+ function endCardClips(card, doc, endStart) {
616
+ const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
617
+ const anim = { enter: "rise", exit: "none" };
618
+ const clips = [];
619
+ const text = (id, body, preset, y, delay) => ({
620
+ id,
621
+ kind: "text",
622
+ text: body,
623
+ preset,
624
+ start: endStart + delay,
625
+ duration: Math.max(0.3, seconds - delay),
626
+ transform: { x: 0.5, y, scale: 1, rotation: 0 },
627
+ anim,
628
+ from: END_CARD_FROM,
629
+ align: "center",
630
+ ...card.ink ? { color: card.ink } : {},
631
+ shadow: 0
632
+ });
633
+ if (card.headline?.trim())
634
+ clips.push(text("endcard-title", card.headline.trim(), "title", 0.44, 0.35));
635
+ if (card.sub?.trim())
636
+ clips.push(text("endcard-sub", card.sub.trim(), "caption", 0.57, 0.5));
637
+ const mark = card.mark;
638
+ const wide = !!mark && mark.aspect > 2.2;
639
+ if (mark && mark.key) {
640
+ const size = resolveExportSize(doc);
641
+ const designW = 1080 * size.width / Math.max(1, size.height);
642
+ const hPx = wide ? 44 : 30;
643
+ const clip = {
644
+ id: "endcard-markimg",
645
+ kind: "image",
646
+ key: mark.key,
647
+ width: Math.min(0.9, hPx * Math.max(0.2, mark.aspect) / designW),
648
+ radius: 0,
649
+ shadow: "none",
650
+ start: endStart + 0.65,
651
+ duration: Math.max(0.3, seconds - 0.65),
652
+ transform: { x: 0.5, y: wide ? 0.88 : 0.835, scale: 1, rotation: 0 },
653
+ anim,
654
+ from: END_CARD_FROM
655
+ };
656
+ clips.push(clip);
657
+ }
658
+ if (card.wordmark?.trim() && !wide)
659
+ clips.push(
660
+ text(
661
+ "endcard-mark",
662
+ card.wordmark.trim(),
663
+ "label",
664
+ mark && mark.key ? 0.895 : 0.88,
665
+ 0.65
666
+ )
667
+ );
668
+ return clips;
669
+ }
670
+ function expandEndCard(doc, outputDuration) {
671
+ const card = doc.endCard;
672
+ if (!card || !doc.segments.length) return { doc, endStart: null, seconds: 0 };
673
+ const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
674
+ const segments = doc.segments.map(
675
+ (s, i) => i === doc.segments.length - 1 ? { ...s, hold: (s.hold ?? 0) + seconds } : s
676
+ );
677
+ const endStart = outputDuration;
678
+ const overlays = [
679
+ ...doc.overlays ?? [],
680
+ ...endCardClips(card, doc, endStart)
681
+ ];
682
+ return { doc: { ...doc, segments, overlays }, endStart, seconds };
683
+ }
684
+ var hasLegacyClipMotion = (o) => o.enter !== void 0 || o.exit !== void 0 || o.kind === "text" && o.fx !== void 0;
685
+ function migrateClipAnim(o) {
686
+ const anim = { ...o.anim ?? {} };
687
+ if (o.kind === "text" && o.fx) {
688
+ const fx = o.fx;
689
+ anim.enter = {
690
+ kind: fx.fx,
691
+ unit: fx.unit ?? "block",
692
+ ...fx.direction !== void 0 ? { direction: fx.direction } : {},
693
+ ...fx.stagger !== void 0 ? { stagger: fx.stagger } : {},
694
+ ...fx.duration !== void 0 ? { seconds: fx.duration } : {}
695
+ };
696
+ } else if (o.enter !== void 0 && anim.enter === void 0) {
697
+ anim.enter = o.enter;
698
+ }
699
+ if (o.exit !== void 0 && anim.exit === void 0) anim.exit = o.exit;
700
+ return anim;
701
+ }
702
+ function migrateMotion(doc) {
703
+ const frame = doc.frame ?? {};
704
+ const segments = Array.isArray(doc.segments) ? doc.segments : [];
705
+ const overlays = Array.isArray(doc.overlays) ? doc.overlays : [];
706
+ 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);
708
+ if (!legacy) return doc;
709
+ const out = { ...doc };
710
+ const nextFrame = { ...frame };
711
+ if (frame.entrance !== void 0) {
712
+ const e = frame.entrance;
713
+ if (e && e.kind !== "none") {
714
+ nextFrame.anim = {
715
+ ...frame.anim ?? {},
716
+ enter: e.seconds !== void 0 ? { kind: e.kind, seconds: e.seconds } : e.kind
717
+ };
718
+ }
719
+ delete nextFrame.entrance;
720
+ }
721
+ let nextOverlays = overlays.map((o) => {
722
+ if (!hasLegacyClipMotion(o)) return o;
723
+ const next = { ...o, anim: migrateClipAnim(o) };
724
+ delete next.enter;
725
+ delete next.exit;
726
+ if (next.kind === "text") delete next.fx;
727
+ return next;
728
+ });
729
+ if (doc.endCard !== void 0) {
730
+ const card = doc.endCard;
731
+ if (card && segments.length) {
732
+ const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
733
+ const rated = withHolds(segments, splitBySpeed(segments, doc.speed ?? []));
734
+ const endStart = totalDuration(rated);
735
+ nextOverlays = [...nextOverlays, ...endCardClips(card, doc, endStart)];
736
+ nextFrame.anim = {
737
+ ...nextFrame.anim ?? {},
738
+ exit: {
739
+ kind: "recede",
740
+ seconds: Math.min(END_CARD_RECEDE, seconds * 0.4)
741
+ }
742
+ };
743
+ }
744
+ delete out.endCard;
745
+ }
746
+ const nextObjects = objects.map((o) => {
747
+ if (o.animation === void 0) return o;
748
+ const next = {
749
+ ...o,
750
+ anim: {
751
+ ...o.anim ?? {},
752
+ ...o.anim?.idle === void 0 ? { idle: o.animation ?? null } : {}
753
+ }
754
+ };
755
+ delete next.animation;
756
+ return next;
757
+ });
758
+ if (doc.frame !== void 0 || nextFrame.anim !== void 0)
759
+ out.frame = nextFrame;
760
+ if (doc.overlays !== void 0 || nextOverlays.length)
761
+ out.overlays = nextOverlays;
762
+ if (doc.objects !== void 0) out.objects = nextObjects;
763
+ return out;
764
+ }
765
+ function outputEnd(doc, footageEnd) {
766
+ return Math.max(footageEnd, lastLayerEnd(doc));
767
+ }
768
+
427
769
  // src/docVersion.ts
428
- var DOC_SCHEMA_VERSION = 2;
770
+ var DOC_SCHEMA_VERSION = 3;
429
771
  function migrateHostedDoc(raw) {
430
772
  const version = typeof raw.docSchemaVersion === "number" ? raw.docSchemaVersion : 0;
431
773
  if (version >= DOC_SCHEMA_VERSION) return raw;
432
- return { ...raw, docSchemaVersion: DOC_SCHEMA_VERSION };
774
+ let doc = raw;
775
+ if (version < 3 && doc.source && typeof doc.source === "object") {
776
+ doc = migrateMotion(doc);
777
+ }
778
+ return { ...doc, docSchemaVersion: DOC_SCHEMA_VERSION };
433
779
  }
434
780
 
435
781
  // src/doc/studioDoc.ts
@@ -1102,8 +1448,8 @@ import {
1102
1448
  segmentRate as segmentRate2,
1103
1449
  sortKeyframes as sortKeyframes2,
1104
1450
  sourceToTimeline as sourceToTimeline2,
1105
- splitBySpeed,
1106
- totalDuration
1451
+ splitBySpeed as splitBySpeed2,
1452
+ totalDuration as totalDuration2
1107
1453
  } from "@vosjs/timeline";
1108
1454
  import { timelineRuntimeCode as timelineRuntimeCode2 } from "@vosjs/timeline/bundle";
1109
1455
 
@@ -1452,7 +1798,7 @@ function overlaySegments(text, unit) {
1452
1798
  }
1453
1799
  var FX_DIR = { forward: 0, reverse: 1, center: 2 };
1454
1800
  function resolveOverlayFx(clip, clipDuration) {
1455
- const spec = clip.fx;
1801
+ const spec = overlayFxSpec(clip) ?? clip.fx ?? null;
1456
1802
  if (!spec) return null;
1457
1803
  const unit = spec.unit ?? "block";
1458
1804
  const units = overlaySegments(clip.text, unit);
@@ -2772,150 +3118,6 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
2772
3118
 
2773
3119
  }`;
2774
3120
 
2775
- // src/lower/motion.ts
2776
- import { sortKeyframes } from "@vosjs/timeline";
2777
- var ENTRANCE_SECONDS = 1.2;
2778
- var END_CARD_SECONDS = 2.5;
2779
- var END_CARD_RECEDE = 0.7;
2780
- var TILT_IN_POSE = [-9, 14];
2781
- var PULL_OUT_LEVEL = 1.32;
2782
- var HOLD_SOURCE_SPAN = 2e-3;
2783
- function entranceSeconds(e) {
2784
- if (!e || e.kind === "none") return 0;
2785
- return Math.max(0.2, Math.min(3, e.seconds ?? ENTRANCE_SECONDS));
2786
- }
2787
- function withHolds(docSegments, rated) {
2788
- const out = [];
2789
- const pending = docSegments.filter((s) => typeof s.hold === "number" && s.hold > 0).map((s) => ({ out: s.out, hold: s.hold, used: false }));
2790
- for (let i = 0; i < rated.length; i++) {
2791
- const piece = rated[i];
2792
- out.push(piece);
2793
- const next = rated[i + 1];
2794
- for (const p of pending) {
2795
- if (p.used) continue;
2796
- const endsHere = Math.abs(piece.out - p.out) < 1e-9;
2797
- const nextContinues = next && Math.abs(next.in - p.out) < 1e-9 && next.out > p.out;
2798
- if (endsHere && !nextContinues) {
2799
- out.push({
2800
- in: p.out - HOLD_SOURCE_SPAN,
2801
- out: p.out,
2802
- rate: HOLD_SOURCE_SPAN / p.hold
2803
- });
2804
- p.used = true;
2805
- }
2806
- }
2807
- }
2808
- return out;
2809
- }
2810
- function entranceTiltKeyframes(e) {
2811
- const s = entranceSeconds(e);
2812
- if (!s || e?.kind !== "tilt-in") return [];
2813
- return [
2814
- { t: 0, value: [...TILT_IN_POSE], ease: "none" },
2815
- { t: s, value: [0, 0], ease: "power3.out" }
2816
- ];
2817
- }
2818
- function entranceZoomKeyframes(e) {
2819
- const s = entranceSeconds(e);
2820
- if (!s) return [];
2821
- if (e?.kind === "pull-out")
2822
- return [
2823
- { t: 0, value: [PULL_OUT_LEVEL, 0.5, 0.42], ease: "none" },
2824
- { t: s, value: [1, 0.5, 0.5], ease: "power3.out" }
2825
- ];
2826
- return [
2827
- { t: 0, value: [1, 0.5, 0.5], ease: "none" },
2828
- { t: s, value: [1, 0.5, 0.5], ease: "none" }
2829
- ];
2830
- }
2831
- function prependEntrance(track, head) {
2832
- if (!head.length) return track;
2833
- const end = head[head.length - 1].t;
2834
- const rest = (track?.keyframes ?? []).filter((k) => k.t > end + 0.1);
2835
- return { keyframes: sortKeyframes([...head, ...rest]) };
2836
- }
2837
- function cardPoseTrack(e, endStart, endSeconds) {
2838
- const keyframes = [];
2839
- const s = entranceSeconds(e);
2840
- if (s) {
2841
- const from = e?.kind === "rise" ? [0.96, 0.08, 0.7] : e?.kind === "pull-out" ? [1, 0, 1] : [0.94, 0.05, 0.7];
2842
- keyframes.push({ t: 0, value: from, ease: "none" });
2843
- keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
2844
- }
2845
- if (endStart !== null && endSeconds > 0) {
2846
- const t0 = Math.max(endStart, s);
2847
- keyframes.push({ t: t0, value: [1, 0, 1], ease: "none" });
2848
- keyframes.push({
2849
- t: t0 + Math.min(END_CARD_RECEDE, endSeconds * 0.4),
2850
- value: [0.9, -0.02, 0.22],
2851
- ease: "power2.out"
2852
- });
2853
- }
2854
- if (!keyframes.length) return void 0;
2855
- return { keyframes: sortKeyframes(keyframes) };
2856
- }
2857
- function expandEndCard(doc, outputDuration) {
2858
- const card = doc.endCard;
2859
- if (!card || !doc.segments.length) return { doc, endStart: null, seconds: 0 };
2860
- const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
2861
- const segments = doc.segments.map(
2862
- (s, i) => i === doc.segments.length - 1 ? { ...s, hold: (s.hold ?? 0) + seconds } : s
2863
- );
2864
- const endStart = outputDuration;
2865
- const clips = [];
2866
- const text = (id, body, preset, y, delay) => ({
2867
- id,
2868
- kind: "text",
2869
- text: body,
2870
- preset,
2871
- start: endStart + delay,
2872
- duration: Math.max(0.3, seconds - delay),
2873
- transform: { x: 0.5, y, scale: 1, rotation: 0 },
2874
- enter: "rise",
2875
- exit: "none",
2876
- align: "center",
2877
- ...card.ink ? { color: card.ink } : {},
2878
- shadow: 0
2879
- });
2880
- if (card.headline?.trim())
2881
- clips.push(text("endcard-title", card.headline.trim(), "title", 0.44, 0.35));
2882
- if (card.sub?.trim())
2883
- clips.push(text("endcard-sub", card.sub.trim(), "caption", 0.57, 0.5));
2884
- const mark = card.mark;
2885
- const wide = !!mark && mark.aspect > 2.2;
2886
- if (mark && mark.key) {
2887
- const size = resolveExportSize(doc);
2888
- const designW = 1080 * size.width / Math.max(1, size.height);
2889
- const hPx = wide ? 44 : 30;
2890
- const clip = {
2891
- id: "endcard-markimg",
2892
- kind: "image",
2893
- key: mark.key,
2894
- width: Math.min(0.9, hPx * Math.max(0.2, mark.aspect) / designW),
2895
- radius: 0,
2896
- shadow: "none",
2897
- start: endStart + 0.65,
2898
- duration: Math.max(0.3, seconds - 0.65),
2899
- transform: { x: 0.5, y: wide ? 0.88 : 0.835, scale: 1, rotation: 0 },
2900
- enter: "rise",
2901
- exit: "none"
2902
- };
2903
- clips.push(clip);
2904
- }
2905
- if (card.wordmark?.trim() && !wide)
2906
- clips.push(
2907
- text(
2908
- "endcard-mark",
2909
- card.wordmark.trim(),
2910
- "label",
2911
- mark && mark.key ? 0.895 : 0.88,
2912
- 0.65
2913
- )
2914
- );
2915
- const overlays = [...doc.overlays ?? [], ...clips];
2916
- return { doc: { ...doc, segments, overlays }, endStart, seconds };
2917
- }
2918
-
2919
3121
  // src/lower/extractClicks.ts
2920
3122
  import { segmentRate, sourceToTimeline } from "@vosjs/timeline";
2921
3123
  var CLICK_FX_PRE = 0.06;
@@ -3020,10 +3222,10 @@ var CAM_PAN_EASE = ZOOM_PAN_EASE;
3020
3222
  var PROGRAM_DURATION = 1;
3021
3223
  function ratedSegments(doc) {
3022
3224
  const segs = isRecordingDoc(doc) ? doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }] : [{ in: 0, out: programDuration(doc) }];
3023
- return withHolds(segs, splitBySpeed(segs, doc.speed ?? []));
3225
+ return withHolds(segs, splitBySpeed2(segs, doc.speed ?? []));
3024
3226
  }
3025
3227
  function durationSec(doc, rated) {
3026
- const trimmed = totalDuration(rated);
3228
+ const trimmed = totalDuration2(rated);
3027
3229
  return trimmed > 0 ? trimmed : doc.source.meta.durationMs / 1e3;
3028
3230
  }
3029
3231
  function spanOutputExtent(segments, sIn, sOut) {
@@ -4608,7 +4810,7 @@ function studioLayerData(layers, duration) {
4608
4810
  ry: round5(o.transform3d.ry),
4609
4811
  rz: round5(o.transform3d.rz),
4610
4812
  scale: round5(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
4611
- anim: o.animation ?? null,
4813
+ anim: idleKey(o),
4612
4814
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
4613
4815
  // sampled at t − span.start. Omitted when absent — parity.
4614
4816
  ...(() => {
@@ -4640,8 +4842,8 @@ function studioLayerData(layers, duration) {
4640
4842
  y: round5(o.transform.y),
4641
4843
  scale: round5(o.transform.scale || 1),
4642
4844
  rot: round5(o.transform.rotation || 0),
4643
- enter: o.enter ?? "rise",
4644
- exit: o.exit ?? "fade",
4845
+ enter: enterKey(o),
4846
+ exit: exitKey(o),
4645
4847
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
4646
4848
  // track, sampled in ON_FRAME at t − start. Omitted when the clip
4647
4849
  // has no motion — data byte parity.
@@ -4729,11 +4931,10 @@ function studioLayerData(layers, duration) {
4729
4931
  };
4730
4932
  }
4731
4933
  function lowerToComposition(input) {
4732
- const before = durationSec(input, ratedSegments(input));
4733
- const expanded = expandEndCard(input, before);
4734
- const doc = expanded.doc;
4934
+ const doc = migrateMotion(input);
4735
4935
  const rated = ratedSegments(doc);
4736
- const duration = durationSec(doc, rated);
4936
+ const footageEnd = durationSec(doc, rated);
4937
+ const duration = outputEnd(doc, footageEnd);
4737
4938
  const fx = doc.cursor.clickFx;
4738
4939
  const smoothed = smoothCursor(doc.source.cursor, {
4739
4940
  factor: doc.cursor.smoothing,
@@ -4807,7 +5008,7 @@ function lowerToComposition(input) {
4807
5008
  // A pull-out entrance writes the track's head.
4808
5009
  zoomTrack: prependEntrance(
4809
5010
  zoomTrackFromDoc(zoomSpans, rated, zoomStyle),
4810
- entranceZoomKeyframes(doc.frame.entrance)
5011
+ entranceZoomKeyframes(cardEnter(doc.frame))
4811
5012
  ),
4812
5013
  // Tilt spans: OUTPUT-time [rx, ry] degree track. The rest pose is
4813
5014
  // FLAT — there is no static card tilt any more — and the motion constants
@@ -4816,18 +5017,19 @@ function lowerToComposition(input) {
4816
5017
  // spans and no tilt-in entrance — byte parity. The entrance is a
4817
5018
  // producer of this track, never a second pose.
4818
5019
  ...(() => {
4819
- const head = entranceTiltKeyframes(doc.frame.entrance);
5020
+ const head = entranceTiltKeyframes(cardEnter(doc.frame));
4820
5021
  const spans = doc.tilt && doc.tilt.length ? tiltTrackFromDoc(doc.tilt, rated, zoomStyle.tilt) : void 0;
4821
5022
  const track = prependEntrance(spans, head);
4822
5023
  return track ? { tiltTrack: track } : {};
4823
5024
  })(),
4824
- // The card's pose through the entrance and the end card: [scale, dy,
4825
- // opacity] in output seconds. Absent when neither exists.
5025
+ // The card's pose through its enter and its exit: [scale, dy, opacity]
5026
+ // in output seconds. Absent when the card carries neither.
4826
5027
  ...(() => {
4827
5028
  const track = cardPoseTrack(
4828
- doc.frame.entrance,
4829
- expanded.endStart,
4830
- expanded.seconds
5029
+ cardEnter(doc.frame),
5030
+ cardExit(doc.frame),
5031
+ footageEnd,
5032
+ duration
4831
5033
  );
4832
5034
  return track ? { cardPoseTrack: track } : {};
4833
5035
  })()
@@ -5278,6 +5480,92 @@ function zoomCoversRect(span, rect, layout, tol = 0.02) {
5278
5480
  return rect.x >= w.x0 - tol && rect.x + rect.w <= w.x1 + tol && rect.y >= w.y0 - tol && rect.y + rect.h <= w.y1 + tol;
5279
5481
  }
5280
5482
 
5483
+ // src/digest/style.ts
5484
+ import { totalDuration as totalDuration4 } from "@vosjs/timeline";
5485
+
5486
+ // src/audioBeds.ts
5487
+ import { totalDuration as totalDuration3 } from "@vosjs/timeline";
5488
+ var round32 = (v) => Math.round(v * 1e3) / 1e3;
5489
+ function docOutputDuration(doc) {
5490
+ const footage = totalDuration3(ratedSegments(doc));
5491
+ return isRecordingDoc(doc) ? outputEnd(migrateMotion(doc), footage) : footage;
5492
+ }
5493
+ function musicBedClip(input) {
5494
+ const track = round32(input.trackDuration);
5495
+ const output = round32(input.outputDuration);
5496
+ const clip = {
5497
+ id: input.id,
5498
+ key: input.key,
5499
+ name: input.name,
5500
+ start: 0,
5501
+ in: 0,
5502
+ out: track,
5503
+ duration: track,
5504
+ // Under speech, not over it; catalog tracks are loudness-normalized so
5505
+ // one default means the same thing across the library.
5506
+ gain: 0.5,
5507
+ fadeIn: 0,
5508
+ // A bed that just stops reads as a glitch; clamp so a very short cut
5509
+ // still spends most of its time at full level.
5510
+ fadeOut: output > 0 ? Math.min(1.5, round32(output / 4)) : 1.5,
5511
+ duck: input.hasMic || void 0
5512
+ };
5513
+ if (output <= 0) return clip;
5514
+ if (track >= output) {
5515
+ clip.out = output;
5516
+ } else {
5517
+ clip.loop = true;
5518
+ clip.loopLen = output;
5519
+ }
5520
+ return clip;
5521
+ }
5522
+ function isMusicBed(clip, outputDuration) {
5523
+ if (clip.start > 0.05) return false;
5524
+ if (clip.loop) return true;
5525
+ const placed = clip.out - clip.in;
5526
+ return Math.abs(placed - outputDuration) <= 1;
5527
+ }
5528
+ function refillAudioBeds(doc, prevDuration, nextDuration) {
5529
+ const EPS2 = 0.05;
5530
+ if (Math.abs(nextDuration - prevDuration) <= EPS2 || nextDuration <= 0) {
5531
+ return false;
5532
+ }
5533
+ let changed = false;
5534
+ for (const clip of doc.audio) {
5535
+ if (clip.start >= nextDuration) continue;
5536
+ const placed = clip.loop ? Math.max(clip.out - clip.in, clip.loopLen ?? clip.out - clip.in) : clip.out - clip.in;
5537
+ if (Math.abs(clip.start + placed - prevDuration) > EPS2) continue;
5538
+ const span = clip.out - clip.in;
5539
+ const nextLen = round32(nextDuration - clip.start);
5540
+ if (clip.loop) {
5541
+ if (nextLen >= span) {
5542
+ clip.loopLen = nextLen;
5543
+ } else {
5544
+ clip.loop = void 0;
5545
+ clip.loopLen = void 0;
5546
+ clip.out = round32(clip.in + nextLen);
5547
+ }
5548
+ } else {
5549
+ if (clip.in + nextLen <= clip.duration) {
5550
+ clip.out = round32(clip.in + nextLen);
5551
+ } else {
5552
+ clip.out = clip.duration;
5553
+ clip.loop = true;
5554
+ clip.loopLen = nextLen;
5555
+ }
5556
+ }
5557
+ changed = true;
5558
+ }
5559
+ return changed;
5560
+ }
5561
+ function voiceKey(doc) {
5562
+ if (!("source" in doc)) return null;
5563
+ const src = doc.source;
5564
+ if (src.micKey) return src.micKey;
5565
+ if (src.meta.hasAudio && !src.meta.hasMic) return src.videoKey;
5566
+ return null;
5567
+ }
5568
+
5281
5569
  // src/digest/style.ts
5282
5570
  var STYLE_FIELDS = [
5283
5571
  "zoomStyle",
@@ -5297,15 +5585,210 @@ function pickStyle(doc) {
5297
5585
  }
5298
5586
  return out;
5299
5587
  }
5300
- function copyStyle(from, to) {
5588
+ function copyStyle(from, to, opts = {}) {
5301
5589
  const next = structuredClone(to);
5302
5590
  const style = pickStyle(from);
5303
5591
  for (const k of STYLE_FIELDS) {
5304
5592
  if (k in style) next[k] = style[k];
5305
5593
  else delete next[k];
5306
5594
  }
5307
- return next;
5595
+ const styled = next;
5596
+ return opts.layout ? copyLayout(from, styled, { keys: opts.keys }).doc : styled;
5597
+ }
5598
+ var LAYOUT_FRAME_FIELDS = [
5599
+ "background",
5600
+ "padding",
5601
+ "radius",
5602
+ "shadow",
5603
+ "shadowContact",
5604
+ "shadowColor",
5605
+ "inset",
5606
+ "border",
5607
+ "borderWidth",
5608
+ "borderColor",
5609
+ "fit",
5610
+ "focus",
5611
+ "focusFollow",
5612
+ "parallax",
5613
+ "anim"
5614
+ ];
5615
+ var STAGE_CLIP_PREFIX = "stage-";
5616
+ var REST_TILT_ID = "rest";
5617
+ var isStageClip = (clip) => clip.id.startsWith(STAGE_CLIP_PREFIX);
5618
+ function restLeanOf(doc) {
5619
+ const spans = doc.tilt ?? [];
5620
+ const named = spans.find((z) => z.id === REST_TILT_ID || z.id === "stage");
5621
+ if (named) return { rx: named.rx, ry: named.ry };
5622
+ const total = anchorSourceDuration(doc);
5623
+ const whole = spans.find((z) => z.out - z.in >= total * 0.9);
5624
+ return whole ? { rx: whole.rx, ry: whole.ry } : null;
5625
+ }
5626
+ function layoutOf(doc) {
5627
+ const frame = doc.frame;
5628
+ return {
5629
+ frame: LAYOUT_FRAME_FIELDS.filter((k) => frame[k] !== void 0),
5630
+ clips: (doc.overlays ?? []).filter(isStageClip).map((o) => o.id),
5631
+ lean: restLeanOf(doc) !== null,
5632
+ hold: (doc.segments.at(-1)?.hold ?? 0) > 0
5633
+ };
5634
+ }
5635
+ function copyLayout(from, to, opts = {}) {
5636
+ const doc = structuredClone(to);
5637
+ const notes = [];
5638
+ const frame = doc.frame;
5639
+ const src = from.frame;
5640
+ for (const k of LAYOUT_FRAME_FIELDS) {
5641
+ if (src[k] !== void 0) frame[k] = structuredClone(src[k]);
5642
+ else delete frame[k];
5643
+ }
5644
+ if (from.frame.backgroundMedia)
5645
+ doc.frame.backgroundMedia = structuredClone(from.frame.backgroundMedia);
5646
+ const outDur = docOutputDuration(doc);
5647
+ const own = new Map((doc.overlays ?? []).map((o) => [o.id, o]));
5648
+ const merged = (doc.overlays ?? []).filter(
5649
+ (o) => !isStageClip(o)
5650
+ );
5651
+ const skippedMarks = [];
5652
+ for (const clip of (from.overlays ?? []).filter(isStageClip)) {
5653
+ const mine = own.get(clip.id);
5654
+ const next = structuredClone(clip);
5655
+ next.start = 0;
5656
+ next.duration = Math.max(0.3, Math.round(outDur * 1e3) / 1e3);
5657
+ if (next.kind === "text") {
5658
+ if (mine?.kind === "text") next.text = mine.text;
5659
+ } else if (opts.keys?.[clip.id]) {
5660
+ next.key = opts.keys[clip.id];
5661
+ } else if (mine && mine.kind !== "text") {
5662
+ next.key = mine.key;
5663
+ } else {
5664
+ skippedMarks.push(clip.id);
5665
+ continue;
5666
+ }
5667
+ merged.push(next);
5668
+ }
5669
+ for (const o of doc.overlays ?? []) {
5670
+ if (isStageClip(o) && !merged.some((m) => m.id === o.id)) merged.push(o);
5671
+ }
5672
+ doc.overlays = merged.length ? merged : void 0;
5673
+ if (skippedMarks.length)
5674
+ notes.push(
5675
+ `${skippedMarks.join(", ")} left out: the layout's image is its own brand's; add yours`
5676
+ );
5677
+ const lean = restLeanOf(from);
5678
+ if (lean) {
5679
+ if (!doc.tilt?.length) {
5680
+ doc.tilt = [
5681
+ {
5682
+ id: REST_TILT_ID,
5683
+ in: 0,
5684
+ out: Math.round(anchorSourceDuration(doc) * 1e3) / 1e3,
5685
+ rx: lean.rx,
5686
+ ry: lean.ry,
5687
+ source: "manual"
5688
+ }
5689
+ ];
5690
+ } else {
5691
+ notes.push("lean kept: this take has tilt spans of its own");
5692
+ }
5693
+ }
5694
+ const hold = from.segments.at(-1)?.hold;
5695
+ const last = doc.segments.at(-1);
5696
+ if (typeof hold === "number" && hold > 0 && last) last.hold = hold;
5697
+ return { doc, notes };
5698
+ }
5699
+ var shiftBy = (at, templateEnd, takeEnd) => at === "start" ? 0 : at === "end" ? takeEnd - templateEnd : at;
5700
+ function clipsFrom(doc, from) {
5701
+ return {
5702
+ overlays: (doc.overlays ?? []).filter((o) => o.from === from),
5703
+ objects: (doc.objects ?? []).filter((o) => o.from === from),
5704
+ audio: (doc.audio ?? []).filter((a) => a.from === from)
5705
+ };
5706
+ }
5707
+ function dropTemplate(doc, from) {
5708
+ const has = clipsFrom(doc, from);
5709
+ if (!has.overlays.length && !has.objects.length && !has.audio.length)
5710
+ return doc;
5711
+ const out = { ...doc };
5712
+ if (doc.overlays) out.overlays = doc.overlays.filter((o) => o.from !== from);
5713
+ if (doc.objects) out.objects = doc.objects.filter((o) => o.from !== from);
5714
+ out.audio = doc.audio.filter((a) => a.from !== from);
5715
+ return out;
5308
5716
  }
5717
+ function applyTemplate(template, take, opts) {
5718
+ const from = opts.from ?? "template";
5719
+ const notes = [];
5720
+ const base = dropTemplate(take, from);
5721
+ const doc = structuredClone(base);
5722
+ const templateEnd = totalDuration4(ratedSegments(template));
5723
+ const takeEnd = totalDuration4(ratedSegments(take));
5724
+ const shift = shiftBy(opts.at, templateEnd, takeEnd);
5725
+ const own = new Map((take.overlays ?? []).map((o) => [o.id, o]));
5726
+ const overlays = (doc.overlays ?? []).filter(
5727
+ (o) => !(template.overlays ?? []).some((t) => t.id === o.id)
5728
+ );
5729
+ const skipped = [];
5730
+ for (const clip of template.overlays ?? []) {
5731
+ const next = structuredClone(clip);
5732
+ next.start = Math.max(0, round33(clip.start + shift));
5733
+ next.from = from;
5734
+ const mine = own.get(clip.id);
5735
+ if (next.kind === "text") {
5736
+ const word = opts.words?.[clip.id];
5737
+ if (word !== void 0) next.text = word;
5738
+ else if (mine?.kind === "text") next.text = mine.text;
5739
+ } else if (opts.keys?.[clip.id]) {
5740
+ next.key = opts.keys[clip.id];
5741
+ } else if (mine && mine.kind !== "text") {
5742
+ next.key = mine.key;
5743
+ } else if (!/^(https?:|\/\/)/.test(next.key)) {
5744
+ skipped.push(clip.id);
5745
+ continue;
5746
+ }
5747
+ overlays.push(next);
5748
+ }
5749
+ if (skipped.length)
5750
+ notes.push(
5751
+ `${skipped.join(", ")}: the template's media key is its own; pass keys[id] or the take's clip`
5752
+ );
5753
+ if (overlays.length || doc.overlays) doc.overlays = overlays;
5754
+ const objects = (doc.objects ?? []).filter(
5755
+ (o) => !(template.objects ?? []).some((t) => t.id === o.id)
5756
+ );
5757
+ for (const clip of template.objects ?? []) {
5758
+ const next = structuredClone(clip);
5759
+ if (next.span)
5760
+ next.span.start = Math.max(0, round33(next.span.start + shift));
5761
+ next.from = from;
5762
+ objects.push(next);
5763
+ }
5764
+ if (objects.length || doc.objects) doc.objects = objects;
5765
+ const audio = doc.audio.filter(
5766
+ (a) => !template.audio.some((t) => t.id === a.id)
5767
+ );
5768
+ for (const clip of template.audio) {
5769
+ const next = structuredClone(clip);
5770
+ next.start = Math.max(0, round33(clip.start + shift));
5771
+ next.from = from;
5772
+ audio.push(next);
5773
+ }
5774
+ doc.audio = audio;
5775
+ if (template.frame.anim) {
5776
+ doc.frame = {
5777
+ ...doc.frame,
5778
+ anim: { ...doc.frame.anim ?? {}, ...template.frame.anim }
5779
+ };
5780
+ }
5781
+ if (opts.look) {
5782
+ const laid = copyLayout(template, doc, { keys: opts.keys });
5783
+ laid.doc.overlays = (laid.doc.overlays ?? []).map(
5784
+ (o) => isStageClip(o) ? { ...o, from } : o
5785
+ );
5786
+ notes.push(...laid.notes);
5787
+ return { doc: laid.doc, notes };
5788
+ }
5789
+ return { doc, notes };
5790
+ }
5791
+ var round33 = (v) => Math.round(v * 1e3) / 1e3;
5309
5792
 
5310
5793
  // src/digest/geometry.ts
5311
5794
  var CROP_MIN_FRAC = 0.25;
@@ -5382,7 +5865,6 @@ function cropBox(m, geo, meta) {
5382
5865
  }
5383
5866
 
5384
5867
  // src/digest/build.ts
5385
- import { totalDuration as totalDuration2 } from "@vosjs/timeline";
5386
5868
  var DIGEST_VERSION = 1;
5387
5869
  function buildDigest(input) {
5388
5870
  const { doc, meta } = { doc: input.doc, meta: input.doc.source.meta };
@@ -5450,7 +5932,7 @@ function buildDigest(input) {
5450
5932
  };
5451
5933
  }
5452
5934
  function outputDurationOf(doc) {
5453
- return totalDuration2(ratedSegments(doc));
5935
+ return docOutputDuration(doc);
5454
5936
  }
5455
5937
  function round8(v) {
5456
5938
  return Math.round(v * 1e3) / 1e3;
@@ -5548,7 +6030,7 @@ function round9(v) {
5548
6030
 
5549
6031
  // src/lower/lowerStudioDoc.ts
5550
6032
  import { applyTimelineEdits } from "@vosjs/shared/timelineEdits";
5551
- import { totalDuration as totalDuration3 } from "@vosjs/timeline";
6033
+ import { totalDuration as totalDuration5 } from "@vosjs/timeline";
5552
6034
  function lowerStudioDoc(doc, opts = {}) {
5553
6035
  return isRecordingDoc(doc) ? lowerToComposition(doc) : lowerProgramDoc(doc, opts);
5554
6036
  }
@@ -5579,7 +6061,7 @@ function lowerProgramDoc(doc, opts = {}) {
5579
6061
  ) : doc.program.config;
5580
6062
  const own = programDuration(doc);
5581
6063
  const rated = ratedSegments(doc);
5582
- const duration = own > 0 ? totalDuration3(rated) : 0;
6064
+ const duration = own > 0 ? totalDuration5(rated) : 0;
5583
6065
  const entryData = studioLayerData(doc, duration);
5584
6066
  const baseData = anchor.data && typeof anchor.data === "object" ? anchor.data : {};
5585
6067
  const retimed = !!doc.speed?.length && own > 0;
@@ -6177,99 +6659,15 @@ import {
6177
6659
  removeSegment,
6178
6660
  segmentRate as segmentRate3,
6179
6661
  sourceToTimeline as sourceToTimeline3,
6180
- splitBySpeed as splitBySpeed2,
6181
- totalDuration as totalDuration5,
6662
+ splitBySpeed as splitBySpeed3,
6663
+ totalDuration as totalDuration6,
6182
6664
  trimSegment
6183
6665
  } from "@vosjs/timeline";
6184
-
6185
- // src/audioBeds.ts
6186
- import { totalDuration as totalDuration4 } from "@vosjs/timeline";
6187
- var round32 = (v) => Math.round(v * 1e3) / 1e3;
6188
- function docOutputDuration(doc) {
6189
- return totalDuration4(ratedSegments(doc));
6190
- }
6191
- function musicBedClip(input) {
6192
- const track = round32(input.trackDuration);
6193
- const output = round32(input.outputDuration);
6194
- const clip = {
6195
- id: input.id,
6196
- key: input.key,
6197
- name: input.name,
6198
- start: 0,
6199
- in: 0,
6200
- out: track,
6201
- duration: track,
6202
- // Under speech, not over it; catalog tracks are loudness-normalized so
6203
- // one default means the same thing across the library.
6204
- gain: 0.5,
6205
- fadeIn: 0,
6206
- // A bed that just stops reads as a glitch; clamp so a very short cut
6207
- // still spends most of its time at full level.
6208
- fadeOut: output > 0 ? Math.min(1.5, round32(output / 4)) : 1.5,
6209
- duck: input.hasMic || void 0
6210
- };
6211
- if (output <= 0) return clip;
6212
- if (track >= output) {
6213
- clip.out = output;
6214
- } else {
6215
- clip.loop = true;
6216
- clip.loopLen = output;
6217
- }
6218
- return clip;
6219
- }
6220
- function isMusicBed(clip, outputDuration) {
6221
- if (clip.start > 0.05) return false;
6222
- if (clip.loop) return true;
6223
- const placed = clip.out - clip.in;
6224
- return Math.abs(placed - outputDuration) <= 1;
6225
- }
6226
- function refillAudioBeds(doc, prevDuration, nextDuration) {
6227
- const EPS2 = 0.05;
6228
- if (Math.abs(nextDuration - prevDuration) <= EPS2 || nextDuration <= 0) {
6229
- return false;
6230
- }
6231
- let changed = false;
6232
- for (const clip of doc.audio) {
6233
- if (clip.start >= nextDuration) continue;
6234
- const placed = clip.loop ? Math.max(clip.out - clip.in, clip.loopLen ?? clip.out - clip.in) : clip.out - clip.in;
6235
- if (Math.abs(clip.start + placed - prevDuration) > EPS2) continue;
6236
- const span = clip.out - clip.in;
6237
- const nextLen = round32(nextDuration - clip.start);
6238
- if (clip.loop) {
6239
- if (nextLen >= span) {
6240
- clip.loopLen = nextLen;
6241
- } else {
6242
- clip.loop = void 0;
6243
- clip.loopLen = void 0;
6244
- clip.out = round32(clip.in + nextLen);
6245
- }
6246
- } else {
6247
- if (clip.in + nextLen <= clip.duration) {
6248
- clip.out = round32(clip.in + nextLen);
6249
- } else {
6250
- clip.out = clip.duration;
6251
- clip.loop = true;
6252
- clip.loopLen = nextLen;
6253
- }
6254
- }
6255
- changed = true;
6256
- }
6257
- return changed;
6258
- }
6259
- function voiceKey(doc) {
6260
- if (!("source" in doc)) return null;
6261
- const src = doc.source;
6262
- if (src.micKey) return src.micKey;
6263
- if (src.meta.hasAudio && !src.meta.hasMic) return src.videoKey;
6264
- return null;
6265
- }
6266
-
6267
- // src/timeline/lanes.ts
6268
6666
  function effectiveSegments(doc) {
6269
6667
  if (isRecordingDoc(doc) && doc.segments.length) return doc.segments;
6270
6668
  return [{ in: 0, out: anchorSourceDuration(doc) }];
6271
6669
  }
6272
- var outputLen = (seg, speeds) => totalDuration5(splitBySpeed2([seg], speeds));
6670
+ var outputLen = (seg, speeds) => totalDuration6(splitBySpeed3([seg], speeds));
6273
6671
  var segmentStarts = (segments, speeds) => {
6274
6672
  const starts = [];
6275
6673
  let acc = 0;
@@ -6326,7 +6724,7 @@ var videoLane = {
6326
6724
  if (index < 0 || index >= segments.length) return null;
6327
6725
  const seg = segments[index];
6328
6726
  const starts = segmentStarts(segments, speeds);
6329
- const fullMap = splitBySpeed2([{ in: 0, out: sourceDuration }], speeds);
6727
+ const fullMap = splitBySpeed3([{ in: 0, out: sourceDuration }], speeds);
6330
6728
  const edgeSrc = g.edge === "start" ? seg.in : seg.out;
6331
6729
  const edgeOutNow = g.edge === "start" ? starts[index] : starts[index] + outputLen(seg, speeds);
6332
6730
  const anchorOut = sourceToTimeline3(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
@@ -6349,7 +6747,7 @@ var videoLane = {
6349
6747
  );
6350
6748
  if (index < 0) return null;
6351
6749
  const s = segments[index];
6352
- const sourceT = mapTime2(splitBySpeed2([s], speeds), g.t - starts[index]);
6750
+ const sourceT = mapTime2(splitBySpeed3([s], speeds), g.t - starts[index]);
6353
6751
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
6354
6752
  const next = [
6355
6753
  ...segments.slice(0, index),
@@ -6901,7 +7299,7 @@ var speedLane = {
6901
7299
  const sp = spans.find((s) => s.id === g.id);
6902
7300
  if (!sp) return null;
6903
7301
  const others = spans.filter((o) => o.id !== g.id);
6904
- const base = splitBySpeed2(effectiveSegments(doc), others);
7302
+ const base = splitBySpeed3(effectiveSegments(doc), others);
6905
7303
  if (g.type === "move") {
6906
7304
  const len = sp.out - sp.in;
6907
7305
  let newIn = clampToKept(
@@ -7427,6 +7825,9 @@ export {
7427
7825
  CAM_SPAN_MIN,
7428
7826
  CAPTURE_COVERAGE_MIN,
7429
7827
  CARD_EDGE_OVERDRAW,
7828
+ CARD_ENTER_KINDS,
7829
+ CARD_EXIT_KINDS,
7830
+ CARD_FADE_SECONDS,
7430
7831
  CARD_FOV,
7431
7832
  CARD_Z,
7432
7833
  CHANNEL_SPECS_HASH,
@@ -7465,6 +7866,8 @@ export {
7465
7866
  DIGEST_VERSION,
7466
7867
  DOC_SCHEMA_VERSION,
7467
7868
  DRAG_FIT_LEVEL,
7869
+ END_CARD_FROM,
7870
+ END_CARD_RECEDE,
7468
7871
  END_CARD_SECONDS,
7469
7872
  ENTRANCE_SECONDS,
7470
7873
  EXPORT_RESOLUTION_OPTIONS,
@@ -7475,7 +7878,11 @@ export {
7475
7878
  FRAME_BORDER_DEFAULT,
7476
7879
  FRAME_BORDER_WIDTH_DEFAULT,
7477
7880
  HOUSE_GRADIENT,
7881
+ IDLE_KINDS,
7882
+ LAYOUT_FRAME_FIELDS,
7478
7883
  LOOK_KINDS,
7884
+ MEDIA_ENTER_KINDS,
7885
+ MEDIA_EXIT_KINDS,
7479
7886
  MINIMAL_BAR_THEMES,
7480
7887
  MOTION_DELTA,
7481
7888
  MOTION_EASE,
@@ -7494,16 +7901,20 @@ export {
7494
7901
  PLAYBACK_ACTIVITY,
7495
7902
  PROGRAM_RETIME,
7496
7903
  REJECT_OVERLAP,
7904
+ REST_TILT_ID,
7497
7905
  SCENE_MOTION,
7498
7906
  SCENE_QUIET,
7499
7907
  SPEED_RATE_MAX,
7500
7908
  SPEED_RATE_MIN,
7501
7909
  SPEED_SPAN_MIN,
7910
+ STAGE_CLIP_PREFIX,
7502
7911
  STUDIO_ENTRY_ID,
7503
7912
  STYLE_FIELDS,
7504
7913
  TEXT3D_DEPTH_DEFAULT,
7505
7914
  TEXT3D_DEPTH_MAX,
7506
7915
  TEXT3D_DEPTH_MIN,
7916
+ TEXT_ENTER_KINDS,
7917
+ TEXT_EXIT_KINDS,
7507
7918
  TEXT_PRESETS,
7508
7919
  TILT_AUTO_DEAD_ZONE,
7509
7920
  TILT_AUTO_MIN,
@@ -7534,7 +7945,9 @@ export {
7534
7945
  ZOOM_STYLE_OPTIONS,
7535
7946
  anchorKindOf,
7536
7947
  anchorSourceDuration,
7948
+ animStep,
7537
7949
  applyLook,
7950
+ applyTemplate,
7538
7951
  aspectRatioValue,
7539
7952
  audioLane,
7540
7953
  backdropMedia,
@@ -7546,6 +7959,8 @@ export {
7546
7959
  camRectFromPose,
7547
7960
  camRestPose,
7548
7961
  camTrackFromDoc,
7962
+ cardEnter,
7963
+ cardExit,
7549
7964
  cardInset,
7550
7965
  cardPointToScreen,
7551
7966
  cardPoseTrack,
@@ -7557,9 +7972,11 @@ export {
7557
7972
  clampZoomLevel,
7558
7973
  clipEnvelope,
7559
7974
  clipLength,
7975
+ clipsFrom,
7560
7976
  computeCardLayout,
7561
7977
  computeMicRms,
7562
7978
  computePeaks,
7979
+ copyLayout,
7563
7980
  copyStyle,
7564
7981
  cropBox,
7565
7982
  cursorIdleFade,
@@ -7568,13 +7985,21 @@ export {
7568
7985
  destinationsForChannel,
7569
7986
  docCardLayout,
7570
7987
  docOutputDuration,
7988
+ docRestTime,
7571
7989
  docToCropSpace,
7572
7990
  docToFullSpace,
7991
+ dropTemplate,
7573
7992
  duckCurve,
7574
7993
  effectiveSegments,
7994
+ endCardClips,
7995
+ enterKey,
7996
+ enterOf,
7575
7997
  entranceSeconds,
7576
7998
  envelopeAt,
7577
7999
  envelopeValueAt,
8000
+ exitKey,
8001
+ exitOf,
8002
+ exitSeconds,
7578
8003
  expandEndCard,
7579
8004
  expectedFrameSize,
7580
8005
  exportSizeFor,
@@ -7585,12 +8010,17 @@ export {
7585
8010
  hexToRgbTriplet,
7586
8011
  houseLook,
7587
8012
  idleGaps,
8013
+ idleKey,
7588
8014
  isLookKind,
7589
8015
  isMusicBed,
7590
8016
  isPlayback,
7591
8017
  isProgramDoc,
7592
8018
  isRecordingDoc,
7593
8019
  isRejected,
8020
+ isStageClip,
8021
+ isUnitStep,
8022
+ lastLayerEnd,
8023
+ layoutOf,
7594
8024
  levelForFocusFraction,
7595
8025
  lookFromBrand,
7596
8026
  lookKindForGround,
@@ -7599,6 +8029,7 @@ export {
7599
8029
  lowerToComposition,
7600
8030
  micLane,
7601
8031
  migrateHostedDoc,
8032
+ migrateMotion,
7602
8033
  momentsFromDoc,
7603
8034
  motionTrack,
7604
8035
  musicBedClip,
@@ -7607,11 +8038,13 @@ export {
7607
8038
  objectMotionPoseAt,
7608
8039
  objectsLane,
7609
8040
  outputDurationOf,
8041
+ outputEnd,
7610
8042
  outputRangeToSource,
7611
8043
  overlapFraction,
7612
8044
  overlayFaceFor,
7613
8045
  overlayFontFaces,
7614
8046
  overlayFontString,
8047
+ overlayFxSpec,
7615
8048
  overlayHit,
7616
8049
  overlayLines,
7617
8050
  overlayMotionBase,
@@ -7640,6 +8073,7 @@ export {
7640
8073
  resolveOverlayStyle,
7641
8074
  resolveText3dAsset,
7642
8075
  resolveZoomStyle,
8076
+ restLeanOf,
7643
8077
  sceneChanges,
7644
8078
  scrollRuns,
7645
8079
  setSpeedInRange,