@vosjs/studio-core 0.16.0 → 0.18.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
@@ -98,6 +98,8 @@ function clipLength(clip) {
98
98
  return clip.loop ? Math.max(span, clip.loopLen ?? span) : span;
99
99
  }
100
100
  var MOTION_EASE = "power2.inOut";
101
+ var LAYER_CARD_SHADOW = 0.32;
102
+ var MEDIA_KEY_PREFIX = "media:";
101
103
  var OVERLAY_SCALE_MAX = 8;
102
104
  var OVERLAY_MEDIA_DEFAULT_WIDTH = 0.35;
103
105
  var OVERLAY_MEDIA_DEFAULT_RADIUS = 12;
@@ -454,9 +456,35 @@ var CARD_ENTER_KINDS = [
454
456
  "fade",
455
457
  "rise",
456
458
  "tilt-in",
457
- "pull-out"
459
+ "pull-out",
460
+ "slide"
458
461
  ];
459
462
  var CARD_EXIT_KINDS = ["none", "fade", "recede"];
463
+ var SEGMENT_ENTER_KINDS = [
464
+ "none",
465
+ "slide",
466
+ "fade",
467
+ "scale"
468
+ ];
469
+ var SEGMENT_EXIT_KINDS = [
470
+ "none",
471
+ "slide",
472
+ "fade",
473
+ "scale"
474
+ ];
475
+ var TRANSITION_SECONDS = 0.6;
476
+ var TRANSITION_SECONDS_MIN = 0.15;
477
+ var TRANSITION_SECONDS_MAX = 2;
478
+ function transitionSeconds(step) {
479
+ if (!step || step.kind === "none") return 0;
480
+ return Math.max(
481
+ TRANSITION_SECONDS_MIN,
482
+ Math.min(TRANSITION_SECONDS_MAX, step.seconds ?? TRANSITION_SECONDS)
483
+ );
484
+ }
485
+ function slideSide(step, which) {
486
+ return step?.side ?? (which === "enter" ? "right" : "left");
487
+ }
460
488
  var TEXT_ENTER_KINDS = [
461
489
  "none",
462
490
  "fade",
@@ -582,6 +610,15 @@ function cardFields(frame) {
582
610
  }
583
611
  return out;
584
612
  }
613
+ var mediaRef = (id) => MEDIA_KEY_PREFIX + id;
614
+ function mediaRefId(key) {
615
+ if (typeof key !== "string" || !key.startsWith(MEDIA_KEY_PREFIX)) return null;
616
+ return key.slice(MEDIA_KEY_PREFIX.length);
617
+ }
618
+ function layerMedia(doc, key) {
619
+ const id = mediaRefId(key);
620
+ return id === null ? null : mediaSource(doc, id);
621
+ }
585
622
  function nextMediaId(doc) {
586
623
  const taken = new Set((doc.media ?? []).map((m) => m.id));
587
624
  let n = 1;
@@ -786,7 +823,7 @@ function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
786
823
  const keyframes = [];
787
824
  const s = entranceSeconds(enter);
788
825
  if (s) {
789
- 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];
826
+ const from = enter?.kind === "rise" ? [0.96, 0.08, 0.7] : enter?.kind === "pull-out" || enter?.kind === "fade" || enter?.kind === "slide" ? enter.kind === "fade" ? [1, 0, 0] : [1, 0, 1] : [0.94, 0.05, 0.7];
790
827
  keyframes.push({ t: 0, value: from, ease: "none" });
791
828
  keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
792
829
  }
@@ -1687,8 +1724,8 @@ import {
1687
1724
  segmentRate as segmentRate2,
1688
1725
  sortKeyframes as sortKeyframes2,
1689
1726
  sourceToTimeline,
1690
- splitBySpeed as splitBySpeed2,
1691
- totalDuration as totalDuration2
1727
+ splitBySpeed as splitBySpeed3,
1728
+ totalDuration as totalDuration3
1692
1729
  } from "@vosjs/timeline";
1693
1730
  import { timelineRuntimeCode as timelineRuntimeCode2 } from "@vosjs/timeline/bundle";
1694
1731
 
@@ -2177,6 +2214,142 @@ function resolveText3dAsset(asset) {
2177
2214
  };
2178
2215
  }
2179
2216
 
2217
+ // src/lower/segmentStarts.ts
2218
+ import { splitBySpeed as splitBySpeed2, totalDuration as totalDuration2 } from "@vosjs/timeline";
2219
+ function effectiveSegments(doc) {
2220
+ if (isRecordingDoc(doc) && doc.segments.length) return doc.segments;
2221
+ return [{ in: 0, out: anchorSourceDuration(doc) }];
2222
+ }
2223
+ var contiguous = (segments, i) => i > 0 && sameMedia(segments[i - 1].media, segments[i].media) && Math.abs(segments[i - 1].out - segments[i].in) < 1e-9;
2224
+ var segmentPieces = (seg, speeds, freezes, leading) => {
2225
+ const own = freezes.filter(
2226
+ (f) => sameMedia(f.media, seg.media) && f.at <= seg.out + 1e-9 && (leading ? f.at >= seg.in - 1e-9 : f.at > seg.in + 1e-9)
2227
+ );
2228
+ return withFreezes(
2229
+ splitBySpeed2(
2230
+ [seg],
2231
+ speeds.filter((sp) => sameMedia(sp.media, seg.media))
2232
+ ),
2233
+ own
2234
+ );
2235
+ };
2236
+ var outputLen = (seg, speeds, freezes = [], leading = true) => totalDuration2(segmentPieces(seg, speeds, freezes, leading));
2237
+ var segmentStarts = (segments, speeds, freezes = []) => {
2238
+ const starts = [];
2239
+ let acc = 0;
2240
+ segments.forEach((s, i) => {
2241
+ starts.push(acc);
2242
+ acc += outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i));
2243
+ });
2244
+ return starts;
2245
+ };
2246
+ function segmentOutputExtents(doc) {
2247
+ const segments = effectiveSegments(doc);
2248
+ const speeds = doc.speed ?? [];
2249
+ const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
2250
+ const starts = segmentStarts(segments, speeds, freezes);
2251
+ return segments.map((s, i) => ({
2252
+ start: starts[i],
2253
+ end: starts[i] + outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
2254
+ }));
2255
+ }
2256
+
2257
+ // src/lower/transitions.ts
2258
+ var TRANSITION_TRAVEL = 1.04;
2259
+ var TRANSITION_SCALE_STEP = 0.08;
2260
+ var GHOST_Z_LIFT = 4e-3;
2261
+ var GHOST_RENDER_ORDER = 1.1;
2262
+ var LAST_FRAME_BACK = 2e-3;
2263
+ var round32 = (v) => Math.round(v * 1e3) / 1e3;
2264
+ function move(step, which, cap) {
2265
+ const secs = transitionSeconds(step);
2266
+ if (!secs || !step) return null;
2267
+ const kind = step.kind;
2268
+ if (kind !== "slide" && kind !== "fade" && kind !== "scale") return null;
2269
+ const d = Math.min(secs, cap);
2270
+ if (d < TRANSITION_SECONDS_MIN) return null;
2271
+ return { kind, side: slideSide(step, which), d: round32(d) };
2272
+ }
2273
+ function docTransitions(doc) {
2274
+ const out = [];
2275
+ const segments = doc.segments;
2276
+ const extents = segmentOutputExtents(doc);
2277
+ const open = enterOf(doc.frame.anim);
2278
+ if (open?.kind === "slide" && extents.length) {
2279
+ const first = extents[0];
2280
+ const m = move(open, "enter", Math.max(0, first.end - first.start) / 2);
2281
+ if (m) out.push({ t: 0, d: m.d, in: m });
2282
+ }
2283
+ for (let i = 0; i + 1 < segments.length; i++) {
2284
+ const a = segments[i];
2285
+ const b = segments[i + 1];
2286
+ const la = extents[i].end - extents[i].start;
2287
+ const lb = extents[i + 1].end - extents[i + 1].start;
2288
+ const cap = Math.min(la, lb) / 2;
2289
+ const exit = move(exitOf(a.anim), "exit", cap);
2290
+ const enter = move(enterOf(b.anim), "enter", cap);
2291
+ if (!exit && !enter) continue;
2292
+ const t = round32(extents[i + 1].start);
2293
+ const rec = {
2294
+ t,
2295
+ d: Math.max(exit?.d ?? 0, enter?.d ?? 0)
2296
+ };
2297
+ if (enter) rec.in = enter;
2298
+ if (exit)
2299
+ rec.out = {
2300
+ ...exit,
2301
+ media: a.media ?? "",
2302
+ at: round32(Math.max(a.in, a.out - LAST_FRAME_BACK))
2303
+ };
2304
+ out.push(rec);
2305
+ }
2306
+ return out;
2307
+ }
2308
+ var REST_LEAD = 0.1;
2309
+ function restSpansThroughTransitions(spans, rated, transitions, minLen) {
2310
+ const windows = transitions.filter((tr) => tr.t > 0 && tr.d > 0);
2311
+ if (!windows.length) return [...spans];
2312
+ const out = [];
2313
+ for (const span of spans) {
2314
+ const start = outputStartOf(rated, span.in, span.out, span.media);
2315
+ const win = start === null ? void 0 : windows.find((tr) => start >= tr.t - REST_LEAD && start < tr.t + tr.d);
2316
+ if (start === null || !win) {
2317
+ out.push(span);
2318
+ continue;
2319
+ }
2320
+ const after = win.t + win.d;
2321
+ if (mediaAtOutput(rated, after) !== (span.media ?? "")) continue;
2322
+ const head = sourceAt(rated, after);
2323
+ if (head === null || head > span.out - minLen) continue;
2324
+ out.push({ ...span, in: round32(head) });
2325
+ }
2326
+ return out;
2327
+ }
2328
+ function outputStartOf(rated, sIn, sOut, media) {
2329
+ let acc = 0;
2330
+ for (const p of rated) {
2331
+ const rate = p.rate ?? 1;
2332
+ const len = Math.max(0, p.out - p.in) / rate;
2333
+ if (sameMedia(p.media, media)) {
2334
+ const ovIn = Math.max(sIn, p.in);
2335
+ const ovOut = Math.min(sOut, p.out);
2336
+ if (ovOut > ovIn) return acc + (ovIn - p.in) / rate;
2337
+ }
2338
+ acc += len;
2339
+ }
2340
+ return null;
2341
+ }
2342
+ function sourceAt(rated, t) {
2343
+ let acc = 0;
2344
+ for (const p of rated) {
2345
+ const rate = p.rate ?? 1;
2346
+ const len = Math.max(0, p.out - p.in) / rate;
2347
+ if (t < acc + len) return p.in + (t - acc) * rate;
2348
+ acc += len;
2349
+ }
2350
+ return null;
2351
+ }
2352
+
2180
2353
  // src/lower/audioEnvelope.ts
2181
2354
  function clipEnvelope(clip) {
2182
2355
  const span = clipLength(clip);
@@ -2698,6 +2871,8 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
2698
2871
  // over its whole life, so it must repaint every visible frame \u2014 the
2699
2872
  // signature alone would freeze it between edits (the olVisSig trap).
2700
2873
  if (ol0.track) olAnim = true
2874
+ // A layer CARD is painted on its own plane every frame it is visible.
2875
+ if (ol0.card) olAnim = true
2701
2876
  // Video overlays advance every frame; images redraw until decoded.
2702
2877
  if (ol0.kind === 'video') olAnim = true
2703
2878
  else if (ol0.kind === 'image') {
@@ -2707,6 +2882,76 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
2707
2882
  }
2708
2883
  var ovSig = W + 'x' + H + '|' + olVisSig
2709
2884
  var ovDirty = ov.sig !== ovSig || ov.active || olAnim
2885
+ // The layer cards visible this frame, with their pose, for the card
2886
+ // planes below (the overlay loop collects them instead of painting).
2887
+ var lcList = []
2888
+ // Acquire a media overlay's element through the shared cache and sync it
2889
+ // to CLIP-LOCAL time (pure f(t); muted always \u2014 soundtracks belong to
2890
+ // doc.audio). Null until it has a frame to draw. One seam for the flat
2891
+ // picture and the layer card.
2892
+ function olAcquire(ol, olT) {
2893
+ var olEl = ns.videoCache ? ns.videoCache.get(ol.key) : null
2894
+ if (!olEl && ns.videoCache) {
2895
+ if (ol.kind === 'image') {
2896
+ olEl = new Image()
2897
+ olEl.crossOrigin = 'anonymous'
2898
+ olEl.src = ol.key
2899
+ } else {
2900
+ olEl = document.createElement('video')
2901
+ olEl.crossOrigin = 'anonymous'
2902
+ olEl.muted = true
2903
+ olEl.playsInline = true
2904
+ olEl.preload = 'auto'
2905
+ olEl.src = ol.key
2906
+ olEl.load()
2907
+ }
2908
+ ns.videoCache.set(ol.key, olEl)
2909
+ }
2910
+ if (!olEl) return null
2911
+ var olIsImg = ol.kind === 'image'
2912
+ if (!olIsImg && olEl.play) {
2913
+ var olDur = olEl.duration || 0
2914
+ var olMT = olT
2915
+ if (ol.loop && olDur > 0) olMT = olT % olDur
2916
+ else if (olDur > 0) olMT = Math.min(olT, olDur - 0.001)
2917
+ try {
2918
+ if (playing) {
2919
+ if (olEl.playbackRate !== 1) olEl.playbackRate = 1
2920
+ var olDrift = Math.abs(olEl.currentTime - olMT)
2921
+ if (olDur > 0 && !olEl.seeking && olDrift > 0.3 && (!ol.loop || olDur - olDrift > 0.3)) olEl.currentTime = olMT
2922
+ if (!ol.loop && olDur > 0 && olT >= olDur) { if (!olEl.paused) olEl.pause() }
2923
+ else if (olEl.paused) { var olP = olEl.play(); if (olP && olP.catch) olP.catch(function () {}) }
2924
+ } else {
2925
+ if (!olEl.paused) olEl.pause()
2926
+ var olTarget = Math.min(olMT, olEl.duration || olMT)
2927
+ // Coalesce scrub seeks (the backgroundMedia pattern): re-assigning
2928
+ // currentTime aborts the in-flight seek, so a per-frame scrub keeps
2929
+ // a remote source seeking forever. Defer until 'seeked' lands.
2930
+ if (olEl.readyState >= 1 && !olEl.seeking && Math.abs(olEl.currentTime - olTarget) > 0.02) {
2931
+ if (ns.pendingDecodes) {
2932
+ var olDp = new Promise(function (resolve) {
2933
+ var olDone = function () { olEl.removeEventListener('seeked', olDone); resolve() }
2934
+ olEl.addEventListener('seeked', olDone)
2935
+ setTimeout(olDone, 250)
2936
+ })
2937
+ ns.pendingDecodes.add(olDp)
2938
+ olDp.finally(function () { ns.pendingDecodes.delete(olDp) })
2939
+ }
2940
+ olEl.currentTime = olTarget
2941
+ }
2942
+ }
2943
+ } catch (e) {}
2944
+ }
2945
+ // Video readiness is STICKY through seeks (the cam-bubble pattern):
2946
+ // readyState dips below HAVE_CURRENT_DATA while a scrub seek is in
2947
+ // flight, and this layer repaints every frame a video clip is visible \u2014
2948
+ // gating each frame on it would blink the clip out for the whole drag.
2949
+ // After the first decoded frame, keep drawing: Chrome paints the
2950
+ // element's retained frame mid-seek.
2951
+ if (!olIsImg && olEl.readyState >= 2) olEl.__vosHasFrame = true
2952
+ var olReady = olIsImg ? !!(olEl.complete && olEl.naturalWidth) : !!(olEl.readyState >= 2 || olEl.__vosHasFrame)
2953
+ return olReady ? olEl : null
2954
+ }
2710
2955
  if (ovDirty) {
2711
2956
  ovC.clearRect(0, 0, W, H)
2712
2957
  // --- text overlays (compositor v2): OUTPUT-anchored clips, styles resolved
@@ -2764,71 +3009,19 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
2764
3009
  if (olPose) olA *= Math.max(0, Math.min(1, olPose[4]))
2765
3010
  if (olA <= 0.004) continue
2766
3011
  if (ol.kind === 'image' || ol.kind === 'video') {
3012
+ // A layer CARD is painted by the card painter on its own plane, below:
3013
+ // collect it with its pose and skip the flat picture.
3014
+ if (ol.card) {
3015
+ lcList.push({ ol: ol, t: olT, a: olA * (ol.opacity == null ? 1 : ol.opacity), x: olMX, y: olMY, s: olMS, r: olMR, yof: olYof })
3016
+ continue
3017
+ }
2767
3018
  // Media overlay: lazy-acquire through the shared cache (SET_DATA-added
2768
3019
  // clips load without a LOAD \u2014 the backgroundMedia pattern), sync video
2769
3020
  // to CLIP-LOCAL time (pure f(t)), draw a rounded media card centered on
2770
3021
  // the fraction anchor. Muted always \u2014 soundtracks belong to doc.audio.
2771
- var olEl = ns.videoCache ? ns.videoCache.get(ol.key) : null
2772
- if (!olEl && ns.videoCache) {
2773
- if (ol.kind === 'image') {
2774
- olEl = new Image()
2775
- olEl.crossOrigin = 'anonymous'
2776
- olEl.src = ol.key
2777
- } else {
2778
- olEl = document.createElement('video')
2779
- olEl.crossOrigin = 'anonymous'
2780
- olEl.muted = true
2781
- olEl.playsInline = true
2782
- olEl.preload = 'auto'
2783
- olEl.src = ol.key
2784
- olEl.load()
2785
- }
2786
- ns.videoCache.set(ol.key, olEl)
2787
- }
3022
+ var olEl = olAcquire(ol, olT)
2788
3023
  if (!olEl) continue
2789
3024
  var olIsImg = ol.kind === 'image'
2790
- if (!olIsImg && olEl.play) {
2791
- var olDur = olEl.duration || 0
2792
- var olMT = olT
2793
- if (ol.loop && olDur > 0) olMT = olT % olDur
2794
- else if (olDur > 0) olMT = Math.min(olT, olDur - 0.001)
2795
- try {
2796
- if (playing) {
2797
- if (olEl.playbackRate !== 1) olEl.playbackRate = 1
2798
- var olDrift = Math.abs(olEl.currentTime - olMT)
2799
- if (olDur > 0 && !olEl.seeking && olDrift > 0.3 && (!ol.loop || olDur - olDrift > 0.3)) olEl.currentTime = olMT
2800
- if (!ol.loop && olDur > 0 && olT >= olDur) { if (!olEl.paused) olEl.pause() }
2801
- else if (olEl.paused) { var olP = olEl.play(); if (olP && olP.catch) olP.catch(function () {}) }
2802
- } else {
2803
- if (!olEl.paused) olEl.pause()
2804
- var olTarget = Math.min(olMT, olEl.duration || olMT)
2805
- // Coalesce scrub seeks (the backgroundMedia pattern): re-assigning
2806
- // currentTime aborts the in-flight seek, so a per-frame scrub keeps
2807
- // a remote source seeking forever. Defer until 'seeked' lands.
2808
- if (olEl.readyState >= 1 && !olEl.seeking && Math.abs(olEl.currentTime - olTarget) > 0.02) {
2809
- if (ns.pendingDecodes) {
2810
- var olDp = new Promise(function (resolve) {
2811
- var olDone = function () { olEl.removeEventListener('seeked', olDone); resolve() }
2812
- olEl.addEventListener('seeked', olDone)
2813
- setTimeout(olDone, 250)
2814
- })
2815
- ns.pendingDecodes.add(olDp)
2816
- olDp.finally(function () { ns.pendingDecodes.delete(olDp) })
2817
- }
2818
- olEl.currentTime = olTarget
2819
- }
2820
- }
2821
- } catch (e) {}
2822
- }
2823
- // Video readiness is STICKY through seeks (the cam-bubble pattern):
2824
- // readyState dips below HAVE_CURRENT_DATA while a scrub seek is in
2825
- // flight, and this layer repaints every frame a video clip is visible \u2014
2826
- // gating each frame on it would blink the clip out for the whole drag.
2827
- // After the first decoded frame, keep drawing: Chrome paints the
2828
- // element's retained frame mid-seek.
2829
- if (!olIsImg && olEl.readyState >= 2) olEl.__vosHasFrame = true
2830
- var olReady = olIsImg ? !!(olEl.complete && olEl.naturalWidth) : !!(olEl.readyState >= 2 || olEl.__vosHasFrame)
2831
- if (!olReady) continue
2832
3025
  var olNW = (olIsImg ? olEl.naturalWidth : olEl.videoWidth) || 16
2833
3026
  var olNH = (olIsImg ? olEl.naturalHeight : olEl.videoHeight) || 9
2834
3027
  var olDW = ol.w * W * olMS
@@ -3340,6 +3533,194 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
3340
3533
  }
3341
3534
  obM.scale.setScalar(obPS * obRefH * (obE.norm || 1))
3342
3535
  }
3536
+ // --- layer cards (many cards): a media overlay that names a frame is
3537
+ // drawn by the card painter on its OWN plane in the prop group
3538
+ // (renderOrder 1.25: above the primary card, below the props): the
3539
+ // layered shadow (the primary's table and strengths), the media clipped
3540
+ // to its corners, the browser bar after it, the border, and for a
3541
+ // document media its click rings and cursor dot inside the card, at
3542
+ // clip-local time. Chrome scales WITH the card (a smaller window has
3543
+ // smaller chrome), the way the primary's scales with cf. The plane is
3544
+ // sized to the canvas at the card depth, placed at the clip's anchor,
3545
+ // leaned by the card's own rx/ry (the tilt convention) and turned by the
3546
+ // clip's rotation. Locals lc*.
3547
+ var lcC = obC.cards || (obC.cards = new Map())
3548
+ var lcSeen = {}
3549
+ var lcK = obRefH / H
3550
+ for (var lci = 0; lci < lcList.length; lci++) {
3551
+ var lcI = lcList[lci], lcO = lcI.ol, lcCard = lcO.card
3552
+ var lcEl = olAcquire(lcO, lcI.t)
3553
+ if (!lcEl) continue
3554
+ lcSeen[lcO.id] = true
3555
+ var lcE = lcC.get(lcO.id)
3556
+ if (!lcE) {
3557
+ var lcCv = document.createElement('canvas')
3558
+ var lcTx = new THREE3.CanvasTexture(lcCv)
3559
+ if (THREE3.SRGBColorSpace) lcTx.colorSpace = THREE3.SRGBColorSpace
3560
+ if (THREE3.LinearFilter) { lcTx.minFilter = THREE3.LinearFilter; lcTx.magFilter = THREE3.LinearFilter }
3561
+ lcTx.generateMipmaps = false
3562
+ var lcM = new THREE3.Mesh(new THREE3.PlaneGeometry(1, 1), new THREE3.MeshBasicMaterial({ map: lcTx, transparent: true, depthTest: false, depthWrite: false }))
3563
+ lcM.renderOrder = 1.25
3564
+ lcM.frustumCulled = false
3565
+ obC.group.add(lcM)
3566
+ lcE = { mesh: lcM, canvas: lcCv, c2d: lcCv.getContext('2d'), texture: lcTx }
3567
+ lcC.set(lcO.id, lcE)
3568
+ }
3569
+ var lcIsImg = lcO.kind === 'image'
3570
+ var lcCrop = lcO.crop || null
3571
+ var lcNW = lcCrop ? lcCrop.w : ((lcIsImg ? lcEl.naturalWidth : lcEl.videoWidth) || 16)
3572
+ var lcNH = lcCrop ? lcCrop.h : ((lcIsImg ? lcEl.naturalHeight : lcEl.videoHeight) || 9)
3573
+ var lcW = Math.max(2, lcO.w * W * lcI.s)
3574
+ var lcS = s * (lcW / W)
3575
+ var lcBar = lcCard.bar || {}
3576
+ var lcBarH = lcBar.kind && lcBar.kind !== 'none' ? (lcBar.height || 44) * lcS : 0
3577
+ var lcMH = lcW * (lcNH / lcNW)
3578
+ var lcH = lcMH + lcBarH
3579
+ var lcRad = Math.min((lcO.radius || 0) * lcS, lcH / 2)
3580
+ var lcMg = Math.ceil(170 * lcS)
3581
+ var lcCW = Math.ceil(lcW + 2 * lcMg), lcCH = Math.ceil(lcH + 2 * lcMg)
3582
+ var lcCv2 = lcE.canvas, lcX = lcE.c2d
3583
+ if (lcCv2.width !== lcCW || lcCv2.height !== lcCH) { lcCv2.width = lcCW; lcCv2.height = lcCH }
3584
+ lcX.clearRect(0, 0, lcCW, lcCH)
3585
+ var lcX0 = lcMg, lcY0 = lcMg
3586
+ var lcSh = lcCard.shadow == null ? ${LAYER_CARD_SHADOW} : lcCard.shadow
3587
+ var lcRgb = '0,0,0'
3588
+ var lcHex = lcCard.shadowColor
3589
+ if (typeof lcHex === 'string' && /^#[0-9a-fA-F]{6}$/.test(lcHex)) lcRgb = parseInt(lcHex.slice(1, 3), 16) + ',' + parseInt(lcHex.slice(3, 5), 16) + ',' + parseInt(lcHex.slice(5, 7), 16)
3590
+ // the shadows are cast by a body drawn off the canvas and brought back
3591
+ // by shadowOffsetX (the primary's seam rule)
3592
+ var lcD = lcCW * 2 + lcCH * 2
3593
+ if (lcSh > 0) {
3594
+ var lcL = [[2, 1, 0.1], [12, 4, 0.1], [40, 16, 0.14], [120, 44, 0.3]]
3595
+ for (var lcj = 0; lcj < lcL.length; lcj++) {
3596
+ lcX.save()
3597
+ lcX.shadowColor = 'rgba(' + lcRgb + ',' + +(lcSh * lcL[lcj][2]).toFixed(3) + ')'
3598
+ lcX.shadowBlur = lcL[lcj][0] * lcS; lcX.shadowOffsetX = lcD; lcX.shadowOffsetY = lcL[lcj][1] * lcS
3599
+ lcX.fillStyle = '#000'
3600
+ rr(lcX0 - lcD, lcY0, lcW, lcH, lcRad, lcX); lcX.fill()
3601
+ lcX.restore()
3602
+ }
3603
+ }
3604
+ if (lcCard.shadowContact > 0) {
3605
+ lcX.save()
3606
+ lcX.shadowColor = 'rgba(' + lcRgb + ',' + lcCard.shadowContact + ')'
3607
+ lcX.shadowBlur = 10 * lcS; lcX.shadowOffsetX = lcD; lcX.shadowOffsetY = 3 * lcS
3608
+ lcX.fillStyle = '#000'
3609
+ rr(lcX0 - lcD, lcY0, lcW, lcH, lcRad, lcX); lcX.fill()
3610
+ lcX.restore()
3611
+ }
3612
+ lcX.save()
3613
+ rr(lcX0, lcY0, lcW, lcH, lcRad, lcX); lcX.clip()
3614
+ var lcMY = lcY0 + lcBarH
3615
+ try {
3616
+ if (lcCrop) lcX.drawImage(lcEl, lcCrop.x, lcCrop.y, lcCrop.w, lcCrop.h, lcX0, lcMY, lcW, lcMH)
3617
+ else lcX.drawImage(lcEl, lcX0, lcMY, lcW, lcMH)
3618
+ } catch (e) {}
3619
+ if (lcBarH > 0) {
3620
+ var lcDark = lcBar.kind.indexOf('dark') >= 0, lcMin = lcBar.kind === 'minimal'
3621
+ var lcThm = (lcMin && lcBar.theme) || null
3622
+ lcX.fillStyle = lcMin ? (lcThm ? lcThm.bar : '#141417') : lcDark ? '#2a2a2e' : '#e9e9eb'
3623
+ lcX.fillRect(lcX0, lcY0, lcW, lcBarH)
3624
+ lcX.fillStyle = lcDark || (lcMin && !(lcThm && lcThm.light)) ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'
3625
+ lcX.fillRect(lcX0, lcY0 + lcBarH - lcS, lcW, lcS)
3626
+ var lcMid = lcY0 + lcBarH / 2
3627
+ if (lcBar.showControls !== false && !lcMin) {
3628
+ if (lcBar.kind.indexOf('mac') === 0) {
3629
+ var lcLights = ['#ff5f57', '#febc2e', '#28c840']
3630
+ for (var lcl = 0; lcl < 3; lcl++) { lcX.fillStyle = lcLights[lcl]; lcX.beginPath(); lcX.arc(lcX0 + (20 + lcl * 20) * lcS, lcMid, 6 * lcS, 0, Math.PI * 2); lcX.fill() }
3631
+ } else {
3632
+ lcX.strokeStyle = lcDark ? 'rgba(255,255,255,0.75)' : 'rgba(0,0,0,0.6)'
3633
+ lcX.lineWidth = 1.5 * lcS
3634
+ var lcG = 4.5 * lcS, lcGx = lcX0 + lcW - 22 * lcS
3635
+ lcX.beginPath(); lcX.moveTo(lcGx - lcG, lcMid - lcG); lcX.lineTo(lcGx + lcG, lcMid + lcG); lcX.moveTo(lcGx + lcG, lcMid - lcG); lcX.lineTo(lcGx - lcG, lcMid + lcG); lcX.stroke()
3636
+ lcX.strokeRect(lcGx - 28 * lcS - lcG, lcMid - lcG, lcG * 2, lcG * 2)
3637
+ lcX.beginPath(); lcX.moveTo(lcGx - 56 * lcS - lcG, lcMid); lcX.lineTo(lcGx - 56 * lcS + lcG, lcMid); lcX.stroke()
3638
+ }
3639
+ }
3640
+ if (lcBar.showUrl !== false && lcBar.url) {
3641
+ var lcPW = Math.min(lcW * 0.5, Math.max(200 * lcS, lcW * 0.34)), lcPH = lcBarH - 16 * lcS
3642
+ var lcPx = lcX0 + (lcW - lcPW) / 2, lcPy = lcY0 + 8 * lcS
3643
+ lcX.fillStyle = lcMin ? (lcThm ? lcThm.pill : '#26262b') : lcDark ? '#1d1d20' : '#ffffff'
3644
+ rr(lcPx, lcPy, lcPW, lcPH, lcPH / 2, lcX); lcX.fill()
3645
+ lcX.fillStyle = lcMin ? (lcThm ? lcThm.text : '#9a9aa1') : lcDark ? '#a1a1a6' : '#5f5f64'
3646
+ lcX.font = 13 * lcS + 'px -apple-system, system-ui, sans-serif'
3647
+ lcX.textAlign = 'center'; lcX.textBaseline = 'middle'
3648
+ var lcLabel = String(lcBar.url), lcMaxW = lcPW - 28 * lcS
3649
+ if (lcX.measureText(lcLabel).width > lcMaxW) {
3650
+ while (lcLabel.length > 1 && lcX.measureText(lcLabel + '\u2026').width > lcMaxW) lcLabel = lcLabel.slice(0, -1)
3651
+ lcLabel += '\u2026'
3652
+ }
3653
+ lcX.fillText(lcLabel, lcPx + lcPW / 2, lcPy + lcPH / 2)
3654
+ lcX.textAlign = 'start'; lcX.textBaseline = 'alphabetic'
3655
+ }
3656
+ }
3657
+ // the media's facts: click rings and the cursor dot, video-anchored,
3658
+ // at clip-local time
3659
+ var lcCu = lcO.cur
3660
+ if (lcCu && lcCu.pts && lcCu.pts.length) {
3661
+ var lcSp = lcCu.space || { w: lcNW, h: lcNH }
3662
+ var lcSize = (lcCu.size || 24) * lcS * 0.5
3663
+ var lcT = lcI.t
3664
+ var lcCks = lcCu.clicks || []
3665
+ for (var lcc = 0; lcc < lcCks.length; lcc++) {
3666
+ var lcCk = lcCks[lcc]
3667
+ var lcAge = lcT - lcCk.t
3668
+ if (lcAge < -0.08 || lcAge > 0.6) continue
3669
+ var lcU = Math.max(0, Math.min(1, (lcAge + 0.08) / 0.68))
3670
+ var lcFade = 1 - Math.pow(1 - lcU, 3)
3671
+ lcX.save()
3672
+ lcX.strokeStyle = 'rgba(255,255,255,' + (0.9 * (1 - lcU)) + ')'
3673
+ lcX.lineWidth = Math.max(1, 3 * lcS * (1 - lcU * 0.6))
3674
+ lcX.beginPath(); lcX.arc(lcX0 + (lcCk.x / (lcSp.w || lcNW)) * lcW, lcMY + (lcCk.y / (lcSp.h || lcNH)) * lcMH, lcSize * 0.8 + lcFade * 44 * lcS, 0, Math.PI * 2); lcX.stroke()
3675
+ lcX.restore()
3676
+ }
3677
+ var lcPts = lcCu.pts, lcPX = lcPts[0].x, lcPY = lcPts[0].y
3678
+ for (var lcp = 0; lcp < lcPts.length; lcp++) { if (lcPts[lcp].t <= lcT) { lcPX = lcPts[lcp].x; lcPY = lcPts[lcp].y } }
3679
+ lcX.save()
3680
+ lcX.fillStyle = 'rgba(255,255,255,0.95)'; lcX.strokeStyle = 'rgba(0,0,0,0.4)'; lcX.lineWidth = 2 * lcS
3681
+ lcX.beginPath(); lcX.arc(lcX0 + (lcPX / (lcSp.w || lcNW)) * lcW, lcMY + (lcPY / (lcSp.h || lcNH)) * lcMH, lcSize, 0, Math.PI * 2); lcX.fill(); lcX.stroke()
3682
+ lcX.restore()
3683
+ }
3684
+ lcX.restore()
3685
+ if (lcO.border && lcO.border.width > 0) {
3686
+ var lcBW = lcO.border.width * lcS
3687
+ lcX.save(); lcX.strokeStyle = lcO.border.color || '#fff'; lcX.lineWidth = lcBW
3688
+ rr(lcX0 - lcBW / 2, lcY0 - lcBW / 2, lcW + lcBW, lcH + lcBW, lcRad > 0 ? lcRad + lcBW / 2 : 0, lcX); lcX.stroke()
3689
+ lcX.restore()
3690
+ }
3691
+ lcE.texture.needsUpdate = true
3692
+ var lcM2 = lcE.mesh
3693
+ lcM2.visible = true
3694
+ if (lcM2.material) lcM2.material.opacity = lcI.a
3695
+ var lcLean = lcCard.lean || [0, 0]
3696
+ var lcRx = (lcLean[0] || 0) * Math.PI / 180, lcRy = (lcLean[1] || 0) * Math.PI / 180, lcRz = -(lcI.r || 0) * Math.PI / 180
3697
+ var lcPlW = lcCW * lcK, lcPlH = lcCH * lcK
3698
+ var lcWX = (lcI.x - 0.5) * obRefH * obAspect
3699
+ var lcWY = -(lcI.y - 0.5) * obRefH - lcI.yof * lcK
3700
+ if (obOrtho) {
3701
+ lcM2.position.set((obOCX + (lcI.x - 0.5) * obOW) / obAX, obOCY - (lcI.y - 0.5) * obOH, -obOD)
3702
+ obB.e.set(lcRx, lcRy, lcRz); lcM2.quaternion.setFromEuler(obB.e)
3703
+ } else if (obB) {
3704
+ lcM2.position.copy(obCam.position).addScaledVector(obB.f, Math.abs(${CARD_Z}) - 0.01).addScaledVector(obB.r, lcWX).addScaledVector(obB.u, lcWY)
3705
+ obB.e.set(lcRx, lcRy, lcRz); lcM2.quaternion.copy(obCam.quaternion).multiply(obB.q.setFromEuler(obB.e))
3706
+ } else {
3707
+ lcM2.position.set(lcWX, lcWY, ${CARD_Z} + 0.01)
3708
+ lcM2.rotation.set(lcRx, lcRy, lcRz)
3709
+ }
3710
+ lcM2.scale.set(lcPlW, lcPlH, 1)
3711
+ }
3712
+ // A card off screen rests; one gone from the data is disposed.
3713
+ lcC.forEach(function (lcE2, lcId) {
3714
+ if (lcSeen[lcId]) return
3715
+ var lcStill = false
3716
+ for (var lcq = 0; lcq < ols.length; lcq++) if (ols[lcq].id === lcId && ols[lcq].card) lcStill = true
3717
+ if (lcStill) { lcE2.mesh.visible = false; return }
3718
+ obC.group.remove(lcE2.mesh)
3719
+ if (lcE2.mesh.geometry && lcE2.mesh.geometry.dispose) lcE2.mesh.geometry.dispose()
3720
+ if (lcE2.mesh.material && lcE2.mesh.material.dispose) lcE2.mesh.material.dispose()
3721
+ if (lcE2.texture && lcE2.texture.dispose) lcE2.texture.dispose()
3722
+ lcC.delete(lcId)
3723
+ })
3343
3724
  // Dispose props no longer in the data (live removal).
3344
3725
  obC.pool.forEach(function (obE2, obId) {
3345
3726
  if (!obSeen[obId]) {
@@ -3479,7 +3860,7 @@ function ratedSegments(doc) {
3479
3860
  const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
3480
3861
  const speeds = doc.speed ?? [];
3481
3862
  const pieces = segs.flatMap(
3482
- (seg) => splitBySpeed2(
3863
+ (seg) => splitBySpeed3(
3483
3864
  [seg],
3484
3865
  speeds.filter(
3485
3866
  (sp) => sameMedia(sp.media, seg.media)
@@ -3489,7 +3870,7 @@ function ratedSegments(doc) {
3489
3870
  return withFreezes(pieces, freezes);
3490
3871
  }
3491
3872
  function durationSec(doc, rated) {
3492
- const trimmed = totalDuration2(rated);
3873
+ const trimmed = totalDuration3(rated);
3493
3874
  return trimmed > 0 ? trimmed : doc.source.meta.durationMs / 1e3;
3494
3875
  }
3495
3876
  function spanOutputExtent(segments, sIn, sOut, media) {
@@ -3817,6 +4198,52 @@ var FRAME_STEP_SRC = `
3817
4198
  }
3818
4199
  bgEl.currentTime = bgTarget
3819
4200
  }
4201
+ }
4202
+ // A GHOST element: a second element on the same bytes, held on ONE frame
4203
+ // (a transition's outgoing snapshot), so one recording can be both cards
4204
+ // at a boundary. Cached by source. Its readiness rides pendingDecodes,
4205
+ // so an export's first paint inside a window waits for it, and a frame
4206
+ // asked before it could seek is sought the moment it can.
4207
+ function ghostEl(el) {
4208
+ var gc = ns.ghostCache || (ns.ghostCache = new Map())
4209
+ var src = el.currentSrc || el.src
4210
+ var g = gc.get(src)
4211
+ if (g) return g
4212
+ g = document.createElement('video')
4213
+ g.crossOrigin = 'anonymous'
4214
+ g.muted = true
4215
+ g.playsInline = true
4216
+ g.preload = 'auto'
4217
+ var gp = new Promise(function (resolve) {
4218
+ var fired = false
4219
+ var done = function () {
4220
+ if (fired) return
4221
+ fired = true
4222
+ g.removeEventListener('loadeddata', done)
4223
+ if (g.__voilaWant != null && g.readyState >= 1) {
4224
+ var want = g.__voilaWant
4225
+ g.__voilaWant = null
4226
+ var seeked = function () { g.removeEventListener('seeked', seeked); resolve() }
4227
+ g.addEventListener('seeked', seeked)
4228
+ setTimeout(seeked, 250)
4229
+ g.currentTime = Math.min(want, g.duration || want)
4230
+ } else resolve()
4231
+ }
4232
+ g.addEventListener('loadeddata', done)
4233
+ setTimeout(done, 2000)
4234
+ })
4235
+ if (ns.pendingDecodes) {
4236
+ ns.pendingDecodes.add(gp)
4237
+ gp.finally(function () { ns.pendingDecodes.delete(gp) })
4238
+ }
4239
+ g.src = src
4240
+ g.load()
4241
+ gc.set(src, g)
4242
+ return g
4243
+ }
4244
+ function stepGhost(g, at) {
4245
+ if (g.readyState >= 1) { g.__voilaWant = null; stepPaused(g, at); return }
4246
+ g.__voilaWant = at
3820
4247
  }`;
3821
4248
  var SETUP = `async (ctx) => {
3822
4249
  ;${timelineRuntimeCode2}
@@ -4083,6 +4510,15 @@ var SETUP = `async (ctx) => {
4083
4510
  if (cam) stepPaused(cam, srcT)
4084
4511
  if (mic) stepPaused(mic, srcT)
4085
4512
  }
4513
+ // A boundary's outgoing snapshot: the ghost element on the clip's last
4514
+ // frame, stepped like the footage, and warmed a second ahead.
4515
+ const trs = d.transitions || []
4516
+ for (let i = 0; i < trs.length; i++) {
4517
+ const tr = trs[i]
4518
+ if (!tr.out || t < tr.t - 1 || t >= tr.t + tr.d) continue
4519
+ const live = tr.out.media ? mediaEls[tr.out.media] : video
4520
+ if (live && live.play) stepGhost(ghostEl(live), tr.out.at)
4521
+ }
4086
4522
  const bg2 = d.frame && d.frame.backgroundMedia
4087
4523
  if (bg2 && bg2.key && bg2.kind !== 'image') {
4088
4524
  const bgEl2 = cache.get(bg2.key)
@@ -4360,6 +4796,87 @@ var ON_FRAME = `(ctx, content, dt) => {
4360
4796
  if (video.play) syncVid(video) // stills (HTMLImageElement) have nothing to sync
4361
4797
  if (r.cam && !amId) syncVid(r.cam)
4362
4798
  if (r.mic && !amId) syncVid(r.mic)
4799
+
4800
+ // --- transitions at a boundary (data.transitions, output seconds): the
4801
+ // record whose window holds t, else the next one within a second (its
4802
+ // snapshot warms ahead, so playback never shows a blank ghost). Locals
4803
+ // are tr-prefixed (one var scope).
4804
+ var trs = d.transitions || [], trA = null, trW = null
4805
+ for (var trI = 0; trI < trs.length; trI++) {
4806
+ var trR = trs[trI]
4807
+ if (t >= trR.t && t < trR.t + trR.d) { trA = trR; break }
4808
+ if (trR.out && trR.t > t && trR.t <= t + 1) { trW = trR; break }
4809
+ }
4810
+ function trEase(p) { p = p < 0 ? 0 : p > 1 ? 1 : p; var q = 1 - p; return 1 - q * q * q }
4811
+ function trSide(side) { return side === 'left' ? [-1, 0] : side === 'right' ? [1, 0] : side === 'up' ? [0, 1] : [0, -1] }
4812
+ // A record's outgoing media: its live element, and the GHOST held on the
4813
+ // clip's last frame (a second element on the same bytes, so one
4814
+ // recording can be both cards: a page change inside a single take). A
4815
+ // still needs no ghost.
4816
+ function trLiveEl(o) { return o.media ? (r.media ? r.media[o.media] : null) : r.video }
4817
+ function trGhostEl(o) {
4818
+ var el = trLiveEl(o)
4819
+ if (!el) return null
4820
+ if (!el.play) return el
4821
+ var g = ghostEl(el)
4822
+ stepGhost(g, o.at)
4823
+ return g
4824
+ }
4825
+ if (trW) trGhostEl(trW.out)
4826
+ var trIn = trA && trA.in ? trA.in : null, trOut = trA && trA.out ? trA.out : null
4827
+ var trGEl = trOut ? trGhostEl(trOut) : null
4828
+ if (!trGEl) trOut = null
4829
+ // The live card's enter and the ghost's exit as plane-fraction offsets
4830
+ // (x in plane widths, y in plane heights; +y up), a scale and an opacity.
4831
+ var trOx = 0, trOy = 0, trSc = 1, trOp = 1
4832
+ if (trIn) {
4833
+ var trP = 1 - trEase((t - trA.t) / trIn.d)
4834
+ if (trIn.kind === 'slide') { var trV = trSide(trIn.side); trOx = trV[0] * trP * ${TRANSITION_TRAVEL}; trOy = trV[1] * trP * ${TRANSITION_TRAVEL} }
4835
+ else if (trIn.kind === 'fade') trOp = 1 - trP
4836
+ else { trSc = 1 - ${TRANSITION_SCALE_STEP} * trP; trOp = 1 - trP }
4837
+ }
4838
+ var trGox = 0, trGoy = 0, trGsc = 1, trGop = 1
4839
+ if (trOut) {
4840
+ var trQ = trEase((t - trA.t) / trOut.d)
4841
+ if (trOut.kind === 'slide') { var trV2 = trSide(trOut.side); trGox = trV2[0] * trQ * ${TRANSITION_TRAVEL}; trGoy = trV2[1] * trQ * ${TRANSITION_TRAVEL} }
4842
+ else if (trOut.kind === 'fade') trGop = 1 - trQ
4843
+ else { trGsc = 1 - ${TRANSITION_SCALE_STEP} * trQ; trGop = 1 - trQ }
4844
+ }
4845
+ var trGM = null
4846
+ if (trOut && trOut.media && d.media) for (var trJ = 0; trJ < d.media.length; trJ++) if (d.media[trJ].id === trOut.media) trGM = d.media[trJ]
4847
+ var trGFr = trOut && trOut.media && trGM && trGM.frame ? trGM.frame : frame
4848
+ // The ghost layer: made on first need (most documents never cross a
4849
+ // boundary), sized with the card layer, above it and below the stack's
4850
+ // card planes. The flat stub paints the ghost on the card's own canvas.
4851
+ var gh = null
4852
+ if (trOut) {
4853
+ if (!r.ghost && ctx.THREE && ctx.scene && card.mesh) {
4854
+ var gT = ctx.THREE
4855
+ var gCv = document.createElement('canvas')
4856
+ gCv.width = cW; gCv.height = cH
4857
+ var gTex = new gT.CanvasTexture(gCv)
4858
+ gTex.colorSpace = gT.SRGBColorSpace
4859
+ gTex.minFilter = gT.LinearFilter; gTex.magFilter = gT.LinearFilter; gTex.generateMipmaps = false
4860
+ var gPh = 2 * ${Math.abs(CARD_Z)} * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
4861
+ var gMesh = new gT.Mesh(new gT.PlaneGeometry(gPh * (W / H) * (cW / W), gPh * (cH / H)), new gT.MeshBasicMaterial({ map: gTex, transparent: true, depthTest: false, depthWrite: false }))
4862
+ gMesh.position.set(0, 0, ${CARD_Z + GHOST_Z_LIFT})
4863
+ gMesh.frustumCulled = false
4864
+ gMesh.renderOrder = ${GHOST_RENDER_ORDER}
4865
+ ctx.scene.add(gMesh)
4866
+ r.ghost = { canvas: gCv, c2d: gCv.getContext('2d'), texture: gTex, mesh: gMesh, w: cW, h: cH }
4867
+ }
4868
+ gh = r.ghost || { c2d: c, canvas: cv }
4869
+ if (r.ghost && (r.ghost.w !== cW || r.ghost.h !== cH)) {
4870
+ r.ghost.canvas.width = cW; r.ghost.canvas.height = cH; r.ghost.w = cW; r.ghost.h = cH
4871
+ if (r.ghost.texture.dispose) r.ghost.texture.dispose()
4872
+ if (r.ghost.mesh.geometry && r.ghost.mesh.geometry.dispose) r.ghost.mesh.geometry.dispose()
4873
+ var gPh2 = 2 * ${Math.abs(CARD_Z)} * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
4874
+ r.ghost.mesh.geometry = new ctx.THREE.PlaneGeometry(gPh2 * (W / H) * (cW / W), gPh2 * (cH / H))
4875
+ }
4876
+ }
4877
+ // The two cursor points a boundary lerps between (frame space), set by
4878
+ // the passes below: the ghost's last, the live card's current.
4879
+ var trQa = null, trQb = null, trQs = 0, trQw = 0
4363
4880
  // Gain routing (live via SET_DATA). With a mic sidecar (AT split) the
4364
4881
  // recording <video> carries SYSTEM audio \u2014 its volume is the system fader \u2014
4365
4882
  // and the sidecar element is the voice (micGain). Legacy takes have one
@@ -4556,6 +5073,19 @@ var ON_FRAME = `(ctx, content, dt) => {
4556
5073
  if (bg.texture) bg.texture.needsUpdate = true
4557
5074
  }
4558
5075
 
5076
+ // The card paints in PASSES: the ghost first (the outgoing clip's last
5077
+ // frame on its own canvas, while a boundary is crossing), then the live
5078
+ // card. A pass swaps the canvas, the element, the media, the source
5079
+ // moment and the card fields it reads; the ghost reads the camera as it
5080
+ // stood at the boundary and paints no clicks and no dot. The live pass
5081
+ // runs last, so everything after the loop reads the live card.
5082
+ var lvVideo = video, lvActM = actM, lvAmId = amId, lvSrcT = srcT, lvCfr = cfr, lvC = c, lvCv = cv
5083
+ var trPasses = trOut ? [true, false] : [false]
5084
+ for (var tpI = 0; tpI < trPasses.length; tpI++) {
5085
+ var tpG = trPasses[tpI]
5086
+ var tpT = t
5087
+ if (tpG) { c = gh.c2d; cv = gh.canvas; video = trGEl; actM = trGM; amId = trOut.media; srcT = trOut.at; cfr = trGFr; tpT = trA.t - 0.001 }
5088
+ else { c = lvC; cv = lvCv; video = lvVideo; actM = lvActM; amId = lvAmId; srcT = lvSrcT; cfr = lvCfr }
4559
5089
  // The CARD layer canvas starts transparent each frame \u2014 the padding around
4560
5090
  // the contain-fit card shows the background layer through the plane's alpha.
4561
5091
  // Under overscan the canvas is larger than the frame and every frame-space
@@ -4612,7 +5142,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4612
5142
  var lvl = 1, zx = 0.5, zy = 0.5
4613
5143
  var zt = d.zoomTrack
4614
5144
  if (zt && zt.keyframes && zt.keyframes.length) {
4615
- var z = TL.sample(zt, t, TL.lerpArray)
5145
+ var z = TL.sample(zt, tpT, TL.lerpArray)
4616
5146
  lvl = z[0]; zx = z[1]; zy = z[2]
4617
5147
  }
4618
5148
  var barH = bar.kind && bar.kind !== 'none' ? (bar.height || 44) * s2 : 0
@@ -4815,7 +5345,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4815
5345
  var cks = d.clicks || []
4816
5346
  var ckF = d.clickFx || {}
4817
5347
  var ckPress = 1
4818
- if (cks.length) {
5348
+ if (cks.length && !tpG) {
4819
5349
  var ckK = ckF.k || 1
4820
5350
  var ckPre = ${CLICK_FX_PRE}
4821
5351
  var ckRD = ${CLICK_RIPPLE_DUR} * (ckF.dur || 1)
@@ -4974,7 +5504,15 @@ var ON_FRAME = `(ctx, content, dt) => {
4974
5504
  }
4975
5505
  }
4976
5506
  }
4977
- if (cuA > 0.01) {
5507
+ // Across a boundary the dot is ONE, drawn in frame space on the
5508
+ // overlay layer (the lerp after the passes), never on a card that is
5509
+ // moving: each pass leaves its point, mapped out of the zoom.
5510
+ if (trOut) {
5511
+ var tqX = ax, tqY = ay
5512
+ if (lvl > 1.001 && !d.zoomSuppressed) { tqX = fx + (ax - fx) * lvl; tqY = fy + (ay - fy) * lvl }
5513
+ if (tpG) trQa = [tqX, tqY]
5514
+ else { trQb = [tqX, tqY]; trQs = curSize * ckPress; trQw = s2 }
5515
+ } else if (cuA > 0.01) {
4978
5516
  c.save()
4979
5517
  c.fillStyle = 'rgba(255,255,255,' + (0.95 * cuA) + ')'
4980
5518
  c.strokeStyle = 'rgba(0,0,0,' + (0.4 * cuA) + ')'
@@ -4985,6 +5523,8 @@ var ON_FRAME = `(ctx, content, dt) => {
4985
5523
  }
4986
5524
  if (fitCover) c.restore()
4987
5525
  c.restore()
5526
+ if (tpG && gh.texture) gh.texture.needsUpdate = true
5527
+ }
4988
5528
 
4989
5529
  // --- OVERLAY layer (screen-space plane, never tilts): the cam bubble, the
4990
5530
  // recording's own footage. Text/image/video overlay CLIPS are the studio
@@ -5002,7 +5542,7 @@ var ON_FRAME = `(ctx, content, dt) => {
5002
5542
  if (camV && camV.readyState >= 2) r.camHasFrame = true
5003
5543
  var camActiveNow = !!(camV && camOn && camS.visible !== false && r.camHasFrame)
5004
5544
  var ovSig = W + 'x' + H
5005
- var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active
5545
+ var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut
5006
5546
  if (ovDirty) {
5007
5547
  ovC.clearRect(0, 0, W, H)
5008
5548
  // webcam bubble \u2014 pinned to the frame corner regardless of card tilt/zoom.
@@ -5052,8 +5592,26 @@ var ON_FRAME = `(ctx, content, dt) => {
5052
5592
  ovC.restore()
5053
5593
  }
5054
5594
  }
5595
+ // The boundary dot: the outgoing card's last cursor point and the
5596
+ // incoming's current one, each carried by its card's move, one dot on
5597
+ // the eased lerp between them (the idle fade never fires across a page
5598
+ // change). It lands on the incoming's own dot as the window closes.
5599
+ if (trOut && (trQa || trQb) && !(d.cursorStyle && d.cursorStyle.visible === false)) {
5600
+ var tqA = trQa || trQb, tqB = trQb || trQa
5601
+ var tqP = trEase((t - trA.t) / trA.d)
5602
+ var tqAx = (tqA[0] - W / 2) * trGsc + W / 2 + trGox * W, tqAy = (tqA[1] - H / 2) * trGsc + H / 2 - trGoy * H
5603
+ var tqBx = (tqB[0] - W / 2) * trSc + W / 2 + trOx * W, tqBy = (tqB[1] - H / 2) * trSc + H / 2 - trOy * H
5604
+ var tqX2 = tqAx + (tqBx - tqAx) * tqP, tqY2 = tqAy + (tqBy - tqAy) * tqP
5605
+ var tqR = trQs || (((d.cursorStyle && d.cursorStyle.size) || 24) * s * 0.5)
5606
+ ovC.save()
5607
+ ovC.fillStyle = 'rgba(255,255,255,0.95)'
5608
+ ovC.strokeStyle = 'rgba(0,0,0,0.4)'
5609
+ ovC.lineWidth = 2 * (trQw || s)
5610
+ ovC.beginPath(); ovC.arc(tqX2, tqY2, tqR, 0, Math.PI * 2); ovC.fill(); ovC.stroke()
5611
+ ovC.restore()
5612
+ }
5055
5613
  ov.sig = ovSig
5056
- ov.active = camActiveNow
5614
+ ov.active = camActiveNow || !!trOut
5057
5615
  if (ov.texture) ov.texture.needsUpdate = true
5058
5616
  }
5059
5617
 
@@ -5082,15 +5640,22 @@ var ON_FRAME = `(ctx, content, dt) => {
5082
5640
  // which the overscan grows).
5083
5641
  var cpk = d.cardPoseTrack
5084
5642
  var cpH = 2 * ${Math.abs(CARD_Z)} * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
5085
- if (cpk && cpk.keyframes && cpk.keyframes.length && card.mesh.scale && card.mesh.position) {
5086
- var cpv = TL.sample(cpk, t, TL.lerpArray)
5087
- card.mesh.scale.x = cpv[0]; card.mesh.scale.y = cpv[0]
5088
- card.mesh.position.y = cpv[1] * cpH
5089
- if (card.mesh.material) card.mesh.material.opacity = cpv[2]
5643
+ var cpv = null
5644
+ if (cpk && cpk.keyframes && cpk.keyframes.length) cpv = TL.sample(cpk, t, TL.lerpArray)
5645
+ if ((cpv || trIn) && card.mesh.scale && card.mesh.position) {
5646
+ // The pose track, then a boundary's enter on top: a slide is an
5647
+ // offset in plane widths and heights, a fade an opacity, a scale both.
5648
+ var cpS = (cpv ? cpv[0] : 1) * trSc
5649
+ var cpO = (cpv ? cpv[2] : 1) * trOp
5650
+ card.mesh.scale.x = cpS; card.mesh.scale.y = cpS
5651
+ card.mesh.position.x = trOx * cpH * (W / H)
5652
+ card.mesh.position.y = ((cpv ? cpv[1] : 0) + trOy) * cpH
5653
+ if (card.mesh.material) card.mesh.material.opacity = cpO
5090
5654
  // Gone past its clip: the mesh leaves the scene, not just its paint.
5091
- card.mesh.visible = cpv[2] > 0.001
5092
- } else if (card.mesh.scale && card.mesh.position && ((card.mesh.scale.x !== undefined && card.mesh.scale.x !== 1) || (card.mesh.position.y !== undefined && card.mesh.position.y !== 0) || card.mesh.visible === false)) {
5655
+ card.mesh.visible = cpO > 0.001
5656
+ } else if (card.mesh.scale && card.mesh.position && ((card.mesh.scale.x !== undefined && card.mesh.scale.x !== 1) || (card.mesh.position.y !== undefined && card.mesh.position.y !== 0) || (card.mesh.position.x !== undefined && card.mesh.position.x !== 0) || card.mesh.visible === false)) {
5093
5657
  card.mesh.scale.x = 1; card.mesh.scale.y = 1
5658
+ card.mesh.position.x = 0
5094
5659
  card.mesh.position.y = 0
5095
5660
  if (card.mesh.material) card.mesh.material.opacity = 1
5096
5661
  card.mesh.visible = true
@@ -5107,6 +5672,29 @@ var ON_FRAME = `(ctx, content, dt) => {
5107
5672
  }
5108
5673
  }
5109
5674
 
5675
+ // The ghost plane: posed as the card stood at the boundary (its lean, its
5676
+ // pose), then moved by its exit; hidden while no boundary is crossing.
5677
+ if (r.ghost && r.ghost.mesh) {
5678
+ var gm = r.ghost.mesh
5679
+ if (trOut && gh === r.ghost) {
5680
+ var gtT = trA.t - 0.001
5681
+ var grx = 0, gry = 0
5682
+ var gtk = d.tiltTrack
5683
+ if (gtk && gtk.keyframes && gtk.keyframes.length && !d.tiltSuppressed) { var gtv = TL.sample(gtk, gtT, TL.lerpArray); grx = gtv[0] * Math.PI / 180; gry = gtv[1] * Math.PI / 180 }
5684
+ gm.rotation.x = grx; gm.rotation.y = gry
5685
+ var gpv = [1, 0, 1]
5686
+ var gpk = d.cardPoseTrack
5687
+ if (gpk && gpk.keyframes && gpk.keyframes.length) gpv = TL.sample(gpk, gtT, TL.lerpArray)
5688
+ var gpH = 2 * ${Math.abs(CARD_Z)} * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
5689
+ var gS = gpv[0] * trGsc, gO = gpv[2] * trGop
5690
+ gm.scale.x = gS; gm.scale.y = gS
5691
+ gm.position.x = trGox * gpH * (W / H)
5692
+ gm.position.y = (gpv[1] + trGoy) * gpH
5693
+ if (gm.material) gm.material.opacity = gO
5694
+ gm.visible = gO > 0.001
5695
+ } else if (gm.visible !== false) gm.visible = false
5696
+ }
5697
+
5110
5698
  // The card layer redraws every frame (dynamic content); bg/overlay uploads are
5111
5699
  // gated inside their blocks (dirty-tracking above).
5112
5700
  if (card.texture) card.texture.needsUpdate = true
@@ -5118,6 +5706,7 @@ var ON_FRAME = `(ctx, content, dt) => {
5118
5706
  }
5119
5707
  }`;
5120
5708
  function studioLayerData(layers, duration) {
5709
+ const rec = "source" in layers ? layers : null;
5121
5710
  return {
5122
5711
  // Music/SFX clips with their gain envelopes baked (shared truth for the
5123
5712
  // preview scheduler and the export's offline mix). `duckEnv` (the mic-derived
@@ -5205,9 +5794,60 @@ function studioLayerData(layers, duration) {
5205
5794
  })()
5206
5795
  };
5207
5796
  if (o.kind !== "text") {
5797
+ const lm = rec ? layerMedia(rec, o.key) : null;
5798
+ const lmImage = lm ? lm.sourceKind === "image" : o.kind === "image";
5799
+ const lmCard = o.frame ? {
5800
+ bar: {
5801
+ kind: o.frame.browserBar?.kind ?? "none",
5802
+ url: o.frame.browserBar?.url ?? (lm ? pageDisplayUrl(lm.meta.pageUrl) : ""),
5803
+ showUrl: o.frame.browserBar?.showUrl ?? true,
5804
+ showControls: o.frame.browserBar?.showControls ?? true,
5805
+ height: o.frame.browserBar?.height ?? 44,
5806
+ ...o.frame.browserBar?.theme ? { theme: o.frame.browserBar.theme } : {}
5807
+ },
5808
+ lean: [
5809
+ round6(o.frame.lean?.rx ?? 0),
5810
+ round6(o.frame.lean?.ry ?? 0)
5811
+ ],
5812
+ shadow: o.frame.shadow ?? LAYER_CARD_SHADOW,
5813
+ shadowContact: o.frame.shadowContact ?? 0,
5814
+ ...o.frame.shadowColor ? { shadowColor: o.frame.shadowColor } : {}
5815
+ } : null;
5816
+ const lmFacts = lm && o.frame && o.frame.cursor !== false && lm.cursor.length ? (() => {
5817
+ const lfx = rec.cursor.clickFx;
5818
+ const sm = smoothCursor(lm.cursor, {
5819
+ factor: rec.cursor.smoothing,
5820
+ clickSnap: lfx.style !== "none" || lfx.press
5821
+ });
5822
+ const clicks = [];
5823
+ for (const ev of lm.cursor) {
5824
+ if (ev.type === "down")
5825
+ clicks.push({
5826
+ t: round6(ev.t / 1e3),
5827
+ x: round6(ev.x),
5828
+ y: round6(ev.y)
5829
+ });
5830
+ }
5831
+ return {
5832
+ cur: {
5833
+ pts: sm.map((p) => ({
5834
+ t: round6(p.t),
5835
+ x: round6(p.x),
5836
+ y: round6(p.y)
5837
+ })),
5838
+ space: { w: lm.meta.width, h: lm.meta.height },
5839
+ size: rec.cursor.size,
5840
+ clicks
5841
+ }
5842
+ };
5843
+ })() : {};
5208
5844
  return {
5209
5845
  ...base,
5210
- key: o.key,
5846
+ ...lm ? { kind: lmImage ? "image" : "video" } : {},
5847
+ key: lm ? lm.videoKey : o.key,
5848
+ ...lm && lm.crop ? { crop: lm.crop } : {},
5849
+ ...lmCard ? { card: lmCard } : {},
5850
+ ...lmFacts,
5211
5851
  w: round6(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
5212
5852
  radius: o.radius ?? OVERLAY_MEDIA_DEFAULT_RADIUS,
5213
5853
  opacity: o.opacity ?? 1,
@@ -5279,6 +5919,7 @@ function studioLayerData(layers, duration) {
5279
5919
  function lowerToComposition(input) {
5280
5920
  const doc = migrateMotion(input);
5281
5921
  const rated = ratedSegments(doc);
5922
+ const transitions = docTransitions(doc);
5282
5923
  const footageEnd = durationSec(doc, rated);
5283
5924
  const duration = outputEnd(doc, footageEnd);
5284
5925
  const fx = doc.cursor.clickFx;
@@ -5293,7 +5934,12 @@ function lowerToComposition(input) {
5293
5934
  const layout = docCardLayout(doc);
5294
5935
  const meta = doc.source.meta;
5295
5936
  const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
5296
- const zoomSpans = doc.zoom.map((z) => {
5937
+ const zoomSpans = restSpansThroughTransitions(
5938
+ doc.zoom,
5939
+ rated,
5940
+ transitions,
5941
+ ZOOM_SPAN_MIN
5942
+ ).map((z) => {
5297
5943
  if (z.focusMode === "auto") {
5298
5944
  const f = followFocusEvents(
5299
5945
  z,
@@ -5380,6 +6026,9 @@ function lowerToComposition(input) {
5380
6026
  })
5381
6027
  } : {},
5382
6028
  ...cursorFade.length ? { cursorFade } : {},
6029
+ // The boundaries that move: ON_FRAME reads them by output time and
6030
+ // needs no document. Absent when none moves.
6031
+ ...transitions.length ? { transitions } : {},
5383
6032
  // Click effects: OUTPUT-anchored click records + resolved styling (named
5384
6033
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
5385
6034
  ...clickFxData(doc, rated),
@@ -5397,7 +6046,13 @@ function lowerToComposition(input) {
5397
6046
  // producer of this track, never a second pose.
5398
6047
  ...(() => {
5399
6048
  const head = entranceTiltKeyframes(cardEnter(doc.frame));
5400
- const spans = doc.tilt && doc.tilt.length ? tiltTrackFromDoc(doc.tilt, rated, zoomStyle.tilt) : void 0;
6049
+ const tilts = doc.tilt ? restSpansThroughTransitions(
6050
+ doc.tilt,
6051
+ rated,
6052
+ transitions,
6053
+ TILT_SPAN_MIN
6054
+ ) : [];
6055
+ const spans = tilts.length ? tiltTrackFromDoc(tilts, rated, zoomStyle.tilt) : void 0;
5401
6056
  const track = prependEntrance(spans, head);
5402
6057
  return track ? { tiltTrack: track } : {};
5403
6058
  })(),
@@ -5874,18 +6529,18 @@ function zoomCoversRect(span, rect, layout, tol = 0.02) {
5874
6529
  }
5875
6530
 
5876
6531
  // src/digest/style.ts
5877
- import { totalDuration as totalDuration4 } from "@vosjs/timeline";
6532
+ import { totalDuration as totalDuration5 } from "@vosjs/timeline";
5878
6533
 
5879
6534
  // src/audioBeds.ts
5880
- import { totalDuration as totalDuration3 } from "@vosjs/timeline";
5881
- var round32 = (v) => Math.round(v * 1e3) / 1e3;
6535
+ import { totalDuration as totalDuration4 } from "@vosjs/timeline";
6536
+ var round33 = (v) => Math.round(v * 1e3) / 1e3;
5882
6537
  function docOutputDuration(doc) {
5883
- const footage = totalDuration3(ratedSegments(doc));
6538
+ const footage = totalDuration4(ratedSegments(doc));
5884
6539
  return isRecordingDoc(doc) ? outputEnd(migrateMotion(doc), footage) : footage;
5885
6540
  }
5886
6541
  function musicBedClip(input) {
5887
- const track = round32(input.trackDuration);
5888
- const output = round32(input.outputDuration);
6542
+ const track = round33(input.trackDuration);
6543
+ const output = round33(input.outputDuration);
5889
6544
  const clip = {
5890
6545
  id: input.id,
5891
6546
  key: input.key,
@@ -5900,7 +6555,7 @@ function musicBedClip(input) {
5900
6555
  fadeIn: 0,
5901
6556
  // A bed that just stops reads as a glitch; clamp so a very short cut
5902
6557
  // still spends most of its time at full level.
5903
- fadeOut: output > 0 ? Math.min(1.5, round32(output / 4)) : 1.5,
6558
+ fadeOut: output > 0 ? Math.min(1.5, round33(output / 4)) : 1.5,
5904
6559
  duck: input.hasMic || void 0
5905
6560
  };
5906
6561
  if (output <= 0) return clip;
@@ -5929,18 +6584,18 @@ function refillAudioBeds(doc, prevDuration, nextDuration) {
5929
6584
  const placed = clip.loop ? Math.max(clip.out - clip.in, clip.loopLen ?? clip.out - clip.in) : clip.out - clip.in;
5930
6585
  if (Math.abs(clip.start + placed - prevDuration) > EPS2) continue;
5931
6586
  const span = clip.out - clip.in;
5932
- const nextLen = round32(nextDuration - clip.start);
6587
+ const nextLen = round33(nextDuration - clip.start);
5933
6588
  if (clip.loop) {
5934
6589
  if (nextLen >= span) {
5935
6590
  clip.loopLen = nextLen;
5936
6591
  } else {
5937
6592
  clip.loop = void 0;
5938
6593
  clip.loopLen = void 0;
5939
- clip.out = round32(clip.in + nextLen);
6594
+ clip.out = round33(clip.in + nextLen);
5940
6595
  }
5941
6596
  } else {
5942
6597
  if (clip.in + nextLen <= clip.duration) {
5943
- clip.out = round32(clip.in + nextLen);
6598
+ clip.out = round33(clip.in + nextLen);
5944
6599
  } else {
5945
6600
  clip.out = clip.duration;
5946
6601
  clip.loop = true;
@@ -6161,8 +6816,8 @@ function applyTemplate(template, take, opts) {
6161
6816
  }
6162
6817
  ].sort((a, b) => a.at - b.at);
6163
6818
  }
6164
- const templateEnd = totalDuration4(ratedSegments(template));
6165
- const takeEnd = totalDuration4(ratedSegments(doc));
6819
+ const templateEnd = totalDuration5(ratedSegments(template));
6820
+ const takeEnd = totalDuration5(ratedSegments(doc));
6166
6821
  const shift = shiftBy(opts.at, templateEnd, takeEnd);
6167
6822
  const own = new Map((take.overlays ?? []).map((o) => [o.id, o]));
6168
6823
  const overlays = (doc.overlays ?? []).filter(
@@ -6171,7 +6826,7 @@ function applyTemplate(template, take, opts) {
6171
6826
  const skipped = [];
6172
6827
  for (const clip of template.overlays ?? []) {
6173
6828
  const next = structuredClone(clip);
6174
- next.start = Math.max(0, round33(clip.start + shift));
6829
+ next.start = Math.max(0, round34(clip.start + shift));
6175
6830
  next.from = from;
6176
6831
  const mine = own.get(clip.id);
6177
6832
  if (next.kind === "text") {
@@ -6199,7 +6854,7 @@ function applyTemplate(template, take, opts) {
6199
6854
  for (const clip of template.objects ?? []) {
6200
6855
  const next = structuredClone(clip);
6201
6856
  if (next.span)
6202
- next.span.start = Math.max(0, round33(next.span.start + shift));
6857
+ next.span.start = Math.max(0, round34(next.span.start + shift));
6203
6858
  next.from = from;
6204
6859
  objects.push(next);
6205
6860
  }
@@ -6209,7 +6864,7 @@ function applyTemplate(template, take, opts) {
6209
6864
  );
6210
6865
  for (const clip of template.audio) {
6211
6866
  const next = structuredClone(clip);
6212
- next.start = Math.max(0, round33(clip.start + shift));
6867
+ next.start = Math.max(0, round34(clip.start + shift));
6213
6868
  next.from = from;
6214
6869
  audio.push(next);
6215
6870
  }
@@ -6230,7 +6885,7 @@ function applyTemplate(template, take, opts) {
6230
6885
  }
6231
6886
  return { doc, notes };
6232
6887
  }
6233
- var round33 = (v) => Math.round(v * 1e3) / 1e3;
6888
+ var round34 = (v) => Math.round(v * 1e3) / 1e3;
6234
6889
 
6235
6890
  // src/digest/geometry.ts
6236
6891
  var CROP_MIN_FRAC = 0.25;
@@ -6472,7 +7127,7 @@ function round10(v) {
6472
7127
 
6473
7128
  // src/lower/lowerStudioDoc.ts
6474
7129
  import { applyTimelineEdits } from "@vosjs/shared/timelineEdits";
6475
- import { totalDuration as totalDuration5 } from "@vosjs/timeline";
7130
+ import { totalDuration as totalDuration6 } from "@vosjs/timeline";
6476
7131
  function lowerStudioDoc(doc, opts = {}) {
6477
7132
  return isRecordingDoc(doc) ? lowerToComposition(doc) : lowerProgramDoc(doc, opts);
6478
7133
  }
@@ -6503,7 +7158,7 @@ function lowerProgramDoc(doc, opts = {}) {
6503
7158
  ) : doc.program.config;
6504
7159
  const own = programDuration(doc);
6505
7160
  const rated = ratedSegments(doc);
6506
- const duration = own > 0 ? totalDuration5(rated) : 0;
7161
+ const duration = own > 0 ? totalDuration6(rated) : 0;
6507
7162
  const entryData = studioLayerData(doc, duration);
6508
7163
  const baseData = anchor.data && typeof anchor.data === "object" ? anchor.data : {};
6509
7164
  const retimed = !!doc.speed?.length && own > 0;
@@ -7101,31 +7756,10 @@ import {
7101
7756
  removeSegment,
7102
7757
  segmentRate as segmentRate3,
7103
7758
  sourceToTimeline as sourceToTimeline2,
7104
- splitBySpeed as splitBySpeed3,
7105
- totalDuration as totalDuration6,
7759
+ splitBySpeed as splitBySpeed4,
7760
+ totalDuration as totalDuration7,
7106
7761
  trimSegment
7107
7762
  } from "@vosjs/timeline";
7108
- function effectiveSegments(doc) {
7109
- if (isRecordingDoc(doc) && doc.segments.length) return doc.segments;
7110
- return [{ in: 0, out: anchorSourceDuration(doc) }];
7111
- }
7112
- var segmentPieces = (seg, speeds, freezes, leading) => {
7113
- const own = freezes.filter(
7114
- (f) => f.at <= seg.out + 1e-9 && (leading ? f.at >= seg.in - 1e-9 : f.at > seg.in + 1e-9)
7115
- );
7116
- return withFreezes(splitBySpeed3([seg], speeds), own);
7117
- };
7118
- var outputLen = (seg, speeds, freezes = [], leading = true) => totalDuration6(segmentPieces(seg, speeds, freezes, leading));
7119
- var segmentStarts = (segments, speeds, freezes = []) => {
7120
- const starts = [];
7121
- let acc = 0;
7122
- segments.forEach((s, i) => {
7123
- starts.push(acc);
7124
- acc += outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i));
7125
- });
7126
- return starts;
7127
- };
7128
- var contiguous = (segments, i) => i > 0 && Math.abs(segments[i - 1].out - segments[i].in) < 1e-9;
7129
7763
  var videoLane = {
7130
7764
  id: "video",
7131
7765
  label: "Video",
@@ -7177,7 +7811,7 @@ var videoLane = {
7177
7811
  if (index < 0 || index >= segments.length) return null;
7178
7812
  const seg = segments[index];
7179
7813
  const starts = segmentStarts(segments, speeds, freezes);
7180
- const fullMap = splitBySpeed3([{ in: 0, out: sourceDuration }], speeds);
7814
+ const fullMap = splitBySpeed4([{ in: 0, out: sourceDuration }], speeds);
7181
7815
  const edgeSrc = g.edge === "start" ? seg.in : seg.out;
7182
7816
  const edgeOutNow = g.edge === "start" ? starts[index] : starts[index] + outputLen(
7183
7817
  seg,
@@ -7213,7 +7847,7 @@ var videoLane = {
7213
7847
  const leading = index === 0 || !contiguous(segments, index);
7214
7848
  const local = g.t - starts[index];
7215
7849
  for (const m of freezeOutputExtents(
7216
- splitBySpeed3([s], speeds),
7850
+ splitBySpeed4([s], speeds),
7217
7851
  freezes.filter(
7218
7852
  (f) => f.at <= s.out + 1e-9 && (leading ? f.at >= s.in - 1e-9 : f.at > s.in + 1e-9)
7219
7853
  )
@@ -7280,7 +7914,7 @@ var freezeLane = {
7280
7914
  if (!freezes.length) return [];
7281
7915
  const segs = effectiveSegments(doc);
7282
7916
  const marks = freezeOutputExtents(
7283
- splitBySpeed3(segs, doc.speed ?? []),
7917
+ splitBySpeed4(segs, doc.speed ?? []),
7284
7918
  freezes
7285
7919
  );
7286
7920
  return freezes.flatMap((f) => {
@@ -7306,12 +7940,12 @@ var freezeLane = {
7306
7940
  if (g.type === "create") {
7307
7941
  const rated = ratedSegments(doc);
7308
7942
  for (const m of freezeOutputExtents(
7309
- splitBySpeed3(segs, speeds),
7943
+ splitBySpeed4(segs, speeds),
7310
7944
  freezes
7311
7945
  ).values()) {
7312
7946
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
7313
7947
  }
7314
- if (g.t > totalDuration6(rated) + 1e-9) return null;
7948
+ if (g.t > totalDuration7(rated) + 1e-9) return null;
7315
7949
  const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
7316
7950
  if (!kept(srcT) || !clear(srcT)) return null;
7317
7951
  const id = nextFreezeId(doc);
@@ -7347,11 +7981,11 @@ var freezeLane = {
7347
7981
  return mine;
7348
7982
  };
7349
7983
  const base = withFreezes(
7350
- splitBySpeed3(segs, speeds),
7984
+ splitBySpeed4(segs, speeds),
7351
7985
  freezes.filter((x) => x.id !== f.id)
7352
7986
  );
7353
7987
  if (g.type === "move" || g.type === "resize" && g.edge === "start") {
7354
- const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
7988
+ const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
7355
7989
  const mine = marks.get(f.id);
7356
7990
  const endOut = mine ? mine.t + mine.duration : null;
7357
7991
  const srcT = round11(
@@ -7364,7 +7998,7 @@ var freezeLane = {
7364
7998
  x.at = srcT;
7365
7999
  if (g.type === "resize" && endOut !== null) {
7366
8000
  const after = freezeOutputExtents(
7367
- splitBySpeed3(effectiveSegments(d), d.speed ?? []),
8001
+ splitBySpeed4(effectiveSegments(d), d.speed ?? []),
7368
8002
  docFreezes({
7369
8003
  ...d,
7370
8004
  freeze: (d.freeze ?? []).map(
@@ -7378,7 +8012,7 @@ var freezeLane = {
7378
8012
  };
7379
8013
  }
7380
8014
  if (g.type === "resize") {
7381
- const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
8015
+ const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
7382
8016
  const mine = marks.get(f.id);
7383
8017
  if (!mine) return null;
7384
8018
  const seconds = clampFreezeSeconds(g.t - mine.t);
@@ -7939,7 +8573,7 @@ var speedLane = {
7939
8573
  const sp = spans.find((s) => s.id === g.id);
7940
8574
  if (!sp) return null;
7941
8575
  const others = spans.filter((o) => o.id !== g.id);
7942
- const base = splitBySpeed3(effectiveSegments(doc), others);
8576
+ const base = splitBySpeed4(effectiveSegments(doc), others);
7943
8577
  if (g.type === "move") {
7944
8578
  const len = sp.out - sp.in;
7945
8579
  let newIn = clampToKept(
@@ -8522,11 +9156,13 @@ export {
8522
9156
  FREEZE_SECONDS_MIN,
8523
9157
  HOUSE_GRADIENT,
8524
9158
  IDLE_KINDS,
9159
+ LAYER_CARD_SHADOW,
8525
9160
  LAYOUT_FRAME_FIELDS,
8526
9161
  LOOK_KINDS,
8527
9162
  MEDIA_ENTER_KINDS,
8528
9163
  MEDIA_EXIT_KINDS,
8529
9164
  MEDIA_FRAME_KEYS,
9165
+ MEDIA_KEY_PREFIX,
8530
9166
  MINIMAL_BAR_THEMES,
8531
9167
  MOTION_DELTA,
8532
9168
  MOTION_EASE,
@@ -8549,6 +9185,8 @@ export {
8549
9185
  REST_TILT_ID,
8550
9186
  SCENE_MOTION,
8551
9187
  SCENE_QUIET,
9188
+ SEGMENT_ENTER_KINDS,
9189
+ SEGMENT_EXIT_KINDS,
8552
9190
  SPEED_RATE_MAX,
8553
9191
  SPEED_RATE_MIN,
8554
9192
  SPEED_SPAN_MIN,
@@ -8574,6 +9212,9 @@ export {
8574
9212
  TILT_RAMP_OUT,
8575
9213
  TILT_SPAN_MIN,
8576
9214
  TILT_UI_DEG_MAX,
9215
+ TRANSITION_SECONDS,
9216
+ TRANSITION_SECONDS_MAX,
9217
+ TRANSITION_SECONDS_MIN,
8577
9218
  TRANSITION_SPEED_MULT,
8578
9219
  WINDOW_FOCUS_MIN,
8579
9220
  ZOOM_CHAIN_GAP,
@@ -8639,6 +9280,7 @@ export {
8639
9280
  docStillTime,
8640
9281
  docToCropSpace,
8641
9282
  docToFullSpace,
9283
+ docTransitions,
8642
9284
  dropTemplate,
8643
9285
  duckCurve,
8644
9286
  effectiveSegments,
@@ -8673,6 +9315,7 @@ export {
8673
9315
  isStageClip,
8674
9316
  isUnitStep,
8675
9317
  lastLayerEnd,
9318
+ layerMedia,
8676
9319
  layoutOf,
8677
9320
  levelForFocusFraction,
8678
9321
  lookFromBrand,
@@ -8684,6 +9327,8 @@ export {
8684
9327
  mediaDuration,
8685
9328
  mediaFrame,
8686
9329
  mediaKey,
9330
+ mediaRef,
9331
+ mediaRefId,
8687
9332
  mediaSource,
8688
9333
  micLane,
8689
9334
  migrateHostedDoc,
@@ -8735,10 +9380,15 @@ export {
8735
9380
  resolveText3dAsset,
8736
9381
  resolveZoomStyle,
8737
9382
  restLeanOf,
9383
+ restSpansThroughTransitions,
8738
9384
  sameMedia,
8739
9385
  sceneChanges,
8740
9386
  scrollRuns,
9387
+ segmentOutputExtents,
9388
+ outputLen as segmentOutputLen,
9389
+ segmentStarts,
8741
9390
  setSpeedInRange,
9391
+ slideSide,
8742
9392
  smoothCursor,
8743
9393
  spanOutputExtent,
8744
9394
  speedLane,
@@ -8748,6 +9398,7 @@ export {
8748
9398
  tiltLane,
8749
9399
  tiltTrackFromDoc,
8750
9400
  transitionMult,
9401
+ transitionSeconds,
8751
9402
  videoLane,
8752
9403
  voiceKey,
8753
9404
  withBackdrop,