@vosjs/studio-core 0.10.0 → 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
@@ -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,164 +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
- import { splitBySpeed, totalDuration } from "@vosjs/timeline";
2778
- function docRestTime(doc) {
2779
- if (doc.endCard) return null;
2780
- const last = doc.segments.at(-1);
2781
- const hold = last?.hold;
2782
- if (typeof hold !== "number" || !(hold > 0)) return null;
2783
- const segs = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
2784
- const total = totalDuration(
2785
- withHolds(segs, splitBySpeed(segs, doc.speed ?? []))
2786
- );
2787
- const rest = total - hold;
2788
- if (!(rest >= 0)) return null;
2789
- return Math.round(rest * 1e3) / 1e3;
2790
- }
2791
- var ENTRANCE_SECONDS = 1.2;
2792
- var END_CARD_SECONDS = 2.5;
2793
- var END_CARD_RECEDE = 0.7;
2794
- var TILT_IN_POSE = [-9, 14];
2795
- var PULL_OUT_LEVEL = 1.32;
2796
- var HOLD_SOURCE_SPAN = 2e-3;
2797
- function entranceSeconds(e) {
2798
- if (!e || e.kind === "none") return 0;
2799
- return Math.max(0.2, Math.min(3, e.seconds ?? ENTRANCE_SECONDS));
2800
- }
2801
- function withHolds(docSegments, rated) {
2802
- const out = [];
2803
- const pending = docSegments.filter((s) => typeof s.hold === "number" && s.hold > 0).map((s) => ({ out: s.out, hold: s.hold, used: false }));
2804
- for (let i = 0; i < rated.length; i++) {
2805
- const piece = rated[i];
2806
- out.push(piece);
2807
- const next = rated[i + 1];
2808
- for (const p of pending) {
2809
- if (p.used) continue;
2810
- const endsHere = Math.abs(piece.out - p.out) < 1e-9;
2811
- const nextContinues = next && Math.abs(next.in - p.out) < 1e-9 && next.out > p.out;
2812
- if (endsHere && !nextContinues) {
2813
- out.push({
2814
- in: p.out - HOLD_SOURCE_SPAN,
2815
- out: p.out,
2816
- rate: HOLD_SOURCE_SPAN / p.hold
2817
- });
2818
- p.used = true;
2819
- }
2820
- }
2821
- }
2822
- return out;
2823
- }
2824
- function entranceTiltKeyframes(e) {
2825
- const s = entranceSeconds(e);
2826
- if (!s || e?.kind !== "tilt-in") return [];
2827
- return [
2828
- { t: 0, value: [...TILT_IN_POSE], ease: "none" },
2829
- { t: s, value: [0, 0], ease: "power3.out" }
2830
- ];
2831
- }
2832
- function entranceZoomKeyframes(e) {
2833
- const s = entranceSeconds(e);
2834
- if (!s) return [];
2835
- if (e?.kind === "pull-out")
2836
- return [
2837
- { t: 0, value: [PULL_OUT_LEVEL, 0.5, 0.42], ease: "none" },
2838
- { t: s, value: [1, 0.5, 0.5], ease: "power3.out" }
2839
- ];
2840
- return [
2841
- { t: 0, value: [1, 0.5, 0.5], ease: "none" },
2842
- { t: s, value: [1, 0.5, 0.5], ease: "none" }
2843
- ];
2844
- }
2845
- function prependEntrance(track, head) {
2846
- if (!head.length) return track;
2847
- const end = head[head.length - 1].t;
2848
- const rest = (track?.keyframes ?? []).filter((k) => k.t > end + 0.1);
2849
- return { keyframes: sortKeyframes([...head, ...rest]) };
2850
- }
2851
- function cardPoseTrack(e, endStart, endSeconds) {
2852
- const keyframes = [];
2853
- const s = entranceSeconds(e);
2854
- if (s) {
2855
- const from = e?.kind === "rise" ? [0.96, 0.08, 0.7] : e?.kind === "pull-out" ? [1, 0, 1] : [0.94, 0.05, 0.7];
2856
- keyframes.push({ t: 0, value: from, ease: "none" });
2857
- keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
2858
- }
2859
- if (endStart !== null && endSeconds > 0) {
2860
- const t0 = Math.max(endStart, s);
2861
- keyframes.push({ t: t0, value: [1, 0, 1], ease: "none" });
2862
- keyframes.push({
2863
- t: t0 + Math.min(END_CARD_RECEDE, endSeconds * 0.4),
2864
- value: [0.9, -0.02, 0.22],
2865
- ease: "power2.out"
2866
- });
2867
- }
2868
- if (!keyframes.length) return void 0;
2869
- return { keyframes: sortKeyframes(keyframes) };
2870
- }
2871
- function expandEndCard(doc, outputDuration) {
2872
- const card = doc.endCard;
2873
- if (!card || !doc.segments.length) return { doc, endStart: null, seconds: 0 };
2874
- const seconds = Math.max(1, Math.min(8, card.seconds ?? END_CARD_SECONDS));
2875
- const segments = doc.segments.map(
2876
- (s, i) => i === doc.segments.length - 1 ? { ...s, hold: (s.hold ?? 0) + seconds } : s
2877
- );
2878
- const endStart = outputDuration;
2879
- const clips = [];
2880
- const text = (id, body, preset, y, delay) => ({
2881
- id,
2882
- kind: "text",
2883
- text: body,
2884
- preset,
2885
- start: endStart + delay,
2886
- duration: Math.max(0.3, seconds - delay),
2887
- transform: { x: 0.5, y, scale: 1, rotation: 0 },
2888
- enter: "rise",
2889
- exit: "none",
2890
- align: "center",
2891
- ...card.ink ? { color: card.ink } : {},
2892
- shadow: 0
2893
- });
2894
- if (card.headline?.trim())
2895
- clips.push(text("endcard-title", card.headline.trim(), "title", 0.44, 0.35));
2896
- if (card.sub?.trim())
2897
- clips.push(text("endcard-sub", card.sub.trim(), "caption", 0.57, 0.5));
2898
- const mark = card.mark;
2899
- const wide = !!mark && mark.aspect > 2.2;
2900
- if (mark && mark.key) {
2901
- const size = resolveExportSize(doc);
2902
- const designW = 1080 * size.width / Math.max(1, size.height);
2903
- const hPx = wide ? 44 : 30;
2904
- const clip = {
2905
- id: "endcard-markimg",
2906
- kind: "image",
2907
- key: mark.key,
2908
- width: Math.min(0.9, hPx * Math.max(0.2, mark.aspect) / designW),
2909
- radius: 0,
2910
- shadow: "none",
2911
- start: endStart + 0.65,
2912
- duration: Math.max(0.3, seconds - 0.65),
2913
- transform: { x: 0.5, y: wide ? 0.88 : 0.835, scale: 1, rotation: 0 },
2914
- enter: "rise",
2915
- exit: "none"
2916
- };
2917
- clips.push(clip);
2918
- }
2919
- if (card.wordmark?.trim() && !wide)
2920
- clips.push(
2921
- text(
2922
- "endcard-mark",
2923
- card.wordmark.trim(),
2924
- "label",
2925
- mark && mark.key ? 0.895 : 0.88,
2926
- 0.65
2927
- )
2928
- );
2929
- const overlays = [...doc.overlays ?? [], ...clips];
2930
- return { doc: { ...doc, segments, overlays }, endStart, seconds };
2931
- }
2932
-
2933
3121
  // src/lower/extractClicks.ts
2934
3122
  import { segmentRate, sourceToTimeline } from "@vosjs/timeline";
2935
3123
  var CLICK_FX_PRE = 0.06;
@@ -4622,7 +4810,7 @@ function studioLayerData(layers, duration) {
4622
4810
  ry: round5(o.transform3d.ry),
4623
4811
  rz: round5(o.transform3d.rz),
4624
4812
  scale: round5(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
4625
- anim: o.animation ?? null,
4813
+ anim: idleKey(o),
4626
4814
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
4627
4815
  // sampled at t − span.start. Omitted when absent — parity.
4628
4816
  ...(() => {
@@ -4654,8 +4842,8 @@ function studioLayerData(layers, duration) {
4654
4842
  y: round5(o.transform.y),
4655
4843
  scale: round5(o.transform.scale || 1),
4656
4844
  rot: round5(o.transform.rotation || 0),
4657
- enter: o.enter ?? "rise",
4658
- exit: o.exit ?? "fade",
4845
+ enter: enterKey(o),
4846
+ exit: exitKey(o),
4659
4847
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
4660
4848
  // track, sampled in ON_FRAME at t − start. Omitted when the clip
4661
4849
  // has no motion — data byte parity.
@@ -4743,11 +4931,10 @@ function studioLayerData(layers, duration) {
4743
4931
  };
4744
4932
  }
4745
4933
  function lowerToComposition(input) {
4746
- const before = durationSec(input, ratedSegments(input));
4747
- const expanded = expandEndCard(input, before);
4748
- const doc = expanded.doc;
4934
+ const doc = migrateMotion(input);
4749
4935
  const rated = ratedSegments(doc);
4750
- const duration = durationSec(doc, rated);
4936
+ const footageEnd = durationSec(doc, rated);
4937
+ const duration = outputEnd(doc, footageEnd);
4751
4938
  const fx = doc.cursor.clickFx;
4752
4939
  const smoothed = smoothCursor(doc.source.cursor, {
4753
4940
  factor: doc.cursor.smoothing,
@@ -4821,7 +5008,7 @@ function lowerToComposition(input) {
4821
5008
  // A pull-out entrance writes the track's head.
4822
5009
  zoomTrack: prependEntrance(
4823
5010
  zoomTrackFromDoc(zoomSpans, rated, zoomStyle),
4824
- entranceZoomKeyframes(doc.frame.entrance)
5011
+ entranceZoomKeyframes(cardEnter(doc.frame))
4825
5012
  ),
4826
5013
  // Tilt spans: OUTPUT-time [rx, ry] degree track. The rest pose is
4827
5014
  // FLAT — there is no static card tilt any more — and the motion constants
@@ -4830,18 +5017,19 @@ function lowerToComposition(input) {
4830
5017
  // spans and no tilt-in entrance — byte parity. The entrance is a
4831
5018
  // producer of this track, never a second pose.
4832
5019
  ...(() => {
4833
- const head = entranceTiltKeyframes(doc.frame.entrance);
5020
+ const head = entranceTiltKeyframes(cardEnter(doc.frame));
4834
5021
  const spans = doc.tilt && doc.tilt.length ? tiltTrackFromDoc(doc.tilt, rated, zoomStyle.tilt) : void 0;
4835
5022
  const track = prependEntrance(spans, head);
4836
5023
  return track ? { tiltTrack: track } : {};
4837
5024
  })(),
4838
- // The card's pose through the entrance and the end card: [scale, dy,
4839
- // 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.
4840
5027
  ...(() => {
4841
5028
  const track = cardPoseTrack(
4842
- doc.frame.entrance,
4843
- expanded.endStart,
4844
- expanded.seconds
5029
+ cardEnter(doc.frame),
5030
+ cardExit(doc.frame),
5031
+ footageEnd,
5032
+ duration
4845
5033
  );
4846
5034
  return track ? { cardPoseTrack: track } : {};
4847
5035
  })()
@@ -5292,11 +5480,15 @@ function zoomCoversRect(span, rect, layout, tol = 0.02) {
5292
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;
5293
5481
  }
5294
5482
 
5483
+ // src/digest/style.ts
5484
+ import { totalDuration as totalDuration4 } from "@vosjs/timeline";
5485
+
5295
5486
  // src/audioBeds.ts
5296
5487
  import { totalDuration as totalDuration3 } from "@vosjs/timeline";
5297
5488
  var round32 = (v) => Math.round(v * 1e3) / 1e3;
5298
5489
  function docOutputDuration(doc) {
5299
- return totalDuration3(ratedSegments(doc));
5490
+ const footage = totalDuration3(ratedSegments(doc));
5491
+ return isRecordingDoc(doc) ? outputEnd(migrateMotion(doc), footage) : footage;
5300
5492
  }
5301
5493
  function musicBedClip(input) {
5302
5494
  const track = round32(input.trackDuration);
@@ -5418,7 +5610,7 @@ var LAYOUT_FRAME_FIELDS = [
5418
5610
  "focus",
5419
5611
  "focusFollow",
5420
5612
  "parallax",
5421
- "entrance"
5613
+ "anim"
5422
5614
  ];
5423
5615
  var STAGE_CLIP_PREFIX = "stage-";
5424
5616
  var REST_TILT_ID = "rest";
@@ -5504,6 +5696,99 @@ function copyLayout(from, to, opts = {}) {
5504
5696
  if (typeof hold === "number" && hold > 0 && last) last.hold = hold;
5505
5697
  return { doc, notes };
5506
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;
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;
5507
5792
 
5508
5793
  // src/digest/geometry.ts
5509
5794
  var CROP_MIN_FRAC = 0.25;
@@ -5580,7 +5865,6 @@ function cropBox(m, geo, meta) {
5580
5865
  }
5581
5866
 
5582
5867
  // src/digest/build.ts
5583
- import { totalDuration as totalDuration4 } from "@vosjs/timeline";
5584
5868
  var DIGEST_VERSION = 1;
5585
5869
  function buildDigest(input) {
5586
5870
  const { doc, meta } = { doc: input.doc, meta: input.doc.source.meta };
@@ -5648,7 +5932,7 @@ function buildDigest(input) {
5648
5932
  };
5649
5933
  }
5650
5934
  function outputDurationOf(doc) {
5651
- return totalDuration4(ratedSegments(doc));
5935
+ return docOutputDuration(doc);
5652
5936
  }
5653
5937
  function round8(v) {
5654
5938
  return Math.round(v * 1e3) / 1e3;
@@ -7541,6 +7825,9 @@ export {
7541
7825
  CAM_SPAN_MIN,
7542
7826
  CAPTURE_COVERAGE_MIN,
7543
7827
  CARD_EDGE_OVERDRAW,
7828
+ CARD_ENTER_KINDS,
7829
+ CARD_EXIT_KINDS,
7830
+ CARD_FADE_SECONDS,
7544
7831
  CARD_FOV,
7545
7832
  CARD_Z,
7546
7833
  CHANNEL_SPECS_HASH,
@@ -7579,6 +7866,8 @@ export {
7579
7866
  DIGEST_VERSION,
7580
7867
  DOC_SCHEMA_VERSION,
7581
7868
  DRAG_FIT_LEVEL,
7869
+ END_CARD_FROM,
7870
+ END_CARD_RECEDE,
7582
7871
  END_CARD_SECONDS,
7583
7872
  ENTRANCE_SECONDS,
7584
7873
  EXPORT_RESOLUTION_OPTIONS,
@@ -7589,8 +7878,11 @@ export {
7589
7878
  FRAME_BORDER_DEFAULT,
7590
7879
  FRAME_BORDER_WIDTH_DEFAULT,
7591
7880
  HOUSE_GRADIENT,
7881
+ IDLE_KINDS,
7592
7882
  LAYOUT_FRAME_FIELDS,
7593
7883
  LOOK_KINDS,
7884
+ MEDIA_ENTER_KINDS,
7885
+ MEDIA_EXIT_KINDS,
7594
7886
  MINIMAL_BAR_THEMES,
7595
7887
  MOTION_DELTA,
7596
7888
  MOTION_EASE,
@@ -7621,6 +7913,8 @@ export {
7621
7913
  TEXT3D_DEPTH_DEFAULT,
7622
7914
  TEXT3D_DEPTH_MAX,
7623
7915
  TEXT3D_DEPTH_MIN,
7916
+ TEXT_ENTER_KINDS,
7917
+ TEXT_EXIT_KINDS,
7624
7918
  TEXT_PRESETS,
7625
7919
  TILT_AUTO_DEAD_ZONE,
7626
7920
  TILT_AUTO_MIN,
@@ -7651,7 +7945,9 @@ export {
7651
7945
  ZOOM_STYLE_OPTIONS,
7652
7946
  anchorKindOf,
7653
7947
  anchorSourceDuration,
7948
+ animStep,
7654
7949
  applyLook,
7950
+ applyTemplate,
7655
7951
  aspectRatioValue,
7656
7952
  audioLane,
7657
7953
  backdropMedia,
@@ -7663,6 +7959,8 @@ export {
7663
7959
  camRectFromPose,
7664
7960
  camRestPose,
7665
7961
  camTrackFromDoc,
7962
+ cardEnter,
7963
+ cardExit,
7666
7964
  cardInset,
7667
7965
  cardPointToScreen,
7668
7966
  cardPoseTrack,
@@ -7674,6 +7972,7 @@ export {
7674
7972
  clampZoomLevel,
7675
7973
  clipEnvelope,
7676
7974
  clipLength,
7975
+ clipsFrom,
7677
7976
  computeCardLayout,
7678
7977
  computeMicRms,
7679
7978
  computePeaks,
@@ -7689,11 +7988,18 @@ export {
7689
7988
  docRestTime,
7690
7989
  docToCropSpace,
7691
7990
  docToFullSpace,
7991
+ dropTemplate,
7692
7992
  duckCurve,
7693
7993
  effectiveSegments,
7994
+ endCardClips,
7995
+ enterKey,
7996
+ enterOf,
7694
7997
  entranceSeconds,
7695
7998
  envelopeAt,
7696
7999
  envelopeValueAt,
8000
+ exitKey,
8001
+ exitOf,
8002
+ exitSeconds,
7697
8003
  expandEndCard,
7698
8004
  expectedFrameSize,
7699
8005
  exportSizeFor,
@@ -7704,6 +8010,7 @@ export {
7704
8010
  hexToRgbTriplet,
7705
8011
  houseLook,
7706
8012
  idleGaps,
8013
+ idleKey,
7707
8014
  isLookKind,
7708
8015
  isMusicBed,
7709
8016
  isPlayback,
@@ -7711,6 +8018,8 @@ export {
7711
8018
  isRecordingDoc,
7712
8019
  isRejected,
7713
8020
  isStageClip,
8021
+ isUnitStep,
8022
+ lastLayerEnd,
7714
8023
  layoutOf,
7715
8024
  levelForFocusFraction,
7716
8025
  lookFromBrand,
@@ -7720,6 +8029,7 @@ export {
7720
8029
  lowerToComposition,
7721
8030
  micLane,
7722
8031
  migrateHostedDoc,
8032
+ migrateMotion,
7723
8033
  momentsFromDoc,
7724
8034
  motionTrack,
7725
8035
  musicBedClip,
@@ -7728,11 +8038,13 @@ export {
7728
8038
  objectMotionPoseAt,
7729
8039
  objectsLane,
7730
8040
  outputDurationOf,
8041
+ outputEnd,
7731
8042
  outputRangeToSource,
7732
8043
  overlapFraction,
7733
8044
  overlayFaceFor,
7734
8045
  overlayFontFaces,
7735
8046
  overlayFontString,
8047
+ overlayFxSpec,
7736
8048
  overlayHit,
7737
8049
  overlayLines,
7738
8050
  overlayMotionBase,