@vosjs/studio-core 0.17.0 → 0.18.1

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
@@ -456,9 +456,35 @@ var CARD_ENTER_KINDS = [
456
456
  "fade",
457
457
  "rise",
458
458
  "tilt-in",
459
- "pull-out"
459
+ "pull-out",
460
+ "slide"
460
461
  ];
461
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
+ }
462
488
  var TEXT_ENTER_KINDS = [
463
489
  "none",
464
490
  "fade",
@@ -797,7 +823,7 @@ function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
797
823
  const keyframes = [];
798
824
  const s = entranceSeconds(enter);
799
825
  if (s) {
800
- 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];
801
827
  keyframes.push({ t: 0, value: from, ease: "none" });
802
828
  keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
803
829
  }
@@ -1698,8 +1724,8 @@ import {
1698
1724
  segmentRate as segmentRate2,
1699
1725
  sortKeyframes as sortKeyframes2,
1700
1726
  sourceToTimeline,
1701
- splitBySpeed as splitBySpeed2,
1702
- totalDuration as totalDuration2
1727
+ splitBySpeed as splitBySpeed3,
1728
+ totalDuration as totalDuration3
1703
1729
  } from "@vosjs/timeline";
1704
1730
  import { timelineRuntimeCode as timelineRuntimeCode2 } from "@vosjs/timeline/bundle";
1705
1731
 
@@ -2188,6 +2214,142 @@ function resolveText3dAsset(asset) {
2188
2214
  };
2189
2215
  }
2190
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
+
2191
2353
  // src/lower/audioEnvelope.ts
2192
2354
  function clipEnvelope(clip) {
2193
2355
  const span = clipLength(clip);
@@ -3698,7 +3860,7 @@ function ratedSegments(doc) {
3698
3860
  const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
3699
3861
  const speeds = doc.speed ?? [];
3700
3862
  const pieces = segs.flatMap(
3701
- (seg) => splitBySpeed2(
3863
+ (seg) => splitBySpeed3(
3702
3864
  [seg],
3703
3865
  speeds.filter(
3704
3866
  (sp) => sameMedia(sp.media, seg.media)
@@ -3708,7 +3870,7 @@ function ratedSegments(doc) {
3708
3870
  return withFreezes(pieces, freezes);
3709
3871
  }
3710
3872
  function durationSec(doc, rated) {
3711
- const trimmed = totalDuration2(rated);
3873
+ const trimmed = totalDuration3(rated);
3712
3874
  return trimmed > 0 ? trimmed : doc.source.meta.durationMs / 1e3;
3713
3875
  }
3714
3876
  function spanOutputExtent(segments, sIn, sOut, media) {
@@ -4036,6 +4198,52 @@ var FRAME_STEP_SRC = `
4036
4198
  }
4037
4199
  bgEl.currentTime = bgTarget
4038
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
4039
4247
  }`;
4040
4248
  var SETUP = `async (ctx) => {
4041
4249
  ;${timelineRuntimeCode2}
@@ -4302,6 +4510,15 @@ var SETUP = `async (ctx) => {
4302
4510
  if (cam) stepPaused(cam, srcT)
4303
4511
  if (mic) stepPaused(mic, srcT)
4304
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
+ }
4305
4522
  const bg2 = d.frame && d.frame.backgroundMedia
4306
4523
  if (bg2 && bg2.key && bg2.kind !== 'image') {
4307
4524
  const bgEl2 = cache.get(bg2.key)
@@ -4579,6 +4796,87 @@ var ON_FRAME = `(ctx, content, dt) => {
4579
4796
  if (video.play) syncVid(video) // stills (HTMLImageElement) have nothing to sync
4580
4797
  if (r.cam && !amId) syncVid(r.cam)
4581
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
4582
4880
  // Gain routing (live via SET_DATA). With a mic sidecar (AT split) the
4583
4881
  // recording <video> carries SYSTEM audio \u2014 its volume is the system fader \u2014
4584
4882
  // and the sidecar element is the voice (micGain). Legacy takes have one
@@ -4775,6 +5073,19 @@ var ON_FRAME = `(ctx, content, dt) => {
4775
5073
  if (bg.texture) bg.texture.needsUpdate = true
4776
5074
  }
4777
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 }
4778
5089
  // The CARD layer canvas starts transparent each frame \u2014 the padding around
4779
5090
  // the contain-fit card shows the background layer through the plane's alpha.
4780
5091
  // Under overscan the canvas is larger than the frame and every frame-space
@@ -4831,7 +5142,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4831
5142
  var lvl = 1, zx = 0.5, zy = 0.5
4832
5143
  var zt = d.zoomTrack
4833
5144
  if (zt && zt.keyframes && zt.keyframes.length) {
4834
- var z = TL.sample(zt, t, TL.lerpArray)
5145
+ var z = TL.sample(zt, tpT, TL.lerpArray)
4835
5146
  lvl = z[0]; zx = z[1]; zy = z[2]
4836
5147
  }
4837
5148
  var barH = bar.kind && bar.kind !== 'none' ? (bar.height || 44) * s2 : 0
@@ -5034,7 +5345,7 @@ var ON_FRAME = `(ctx, content, dt) => {
5034
5345
  var cks = d.clicks || []
5035
5346
  var ckF = d.clickFx || {}
5036
5347
  var ckPress = 1
5037
- if (cks.length) {
5348
+ if (cks.length && !tpG) {
5038
5349
  var ckK = ckF.k || 1
5039
5350
  var ckPre = ${CLICK_FX_PRE}
5040
5351
  var ckRD = ${CLICK_RIPPLE_DUR} * (ckF.dur || 1)
@@ -5193,7 +5504,15 @@ var ON_FRAME = `(ctx, content, dt) => {
5193
5504
  }
5194
5505
  }
5195
5506
  }
5196
- 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) {
5197
5516
  c.save()
5198
5517
  c.fillStyle = 'rgba(255,255,255,' + (0.95 * cuA) + ')'
5199
5518
  c.strokeStyle = 'rgba(0,0,0,' + (0.4 * cuA) + ')'
@@ -5204,6 +5523,8 @@ var ON_FRAME = `(ctx, content, dt) => {
5204
5523
  }
5205
5524
  if (fitCover) c.restore()
5206
5525
  c.restore()
5526
+ if (tpG && gh.texture) gh.texture.needsUpdate = true
5527
+ }
5207
5528
 
5208
5529
  // --- OVERLAY layer (screen-space plane, never tilts): the cam bubble, the
5209
5530
  // recording's own footage. Text/image/video overlay CLIPS are the studio
@@ -5221,7 +5542,7 @@ var ON_FRAME = `(ctx, content, dt) => {
5221
5542
  if (camV && camV.readyState >= 2) r.camHasFrame = true
5222
5543
  var camActiveNow = !!(camV && camOn && camS.visible !== false && r.camHasFrame)
5223
5544
  var ovSig = W + 'x' + H
5224
- var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active
5545
+ var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut
5225
5546
  if (ovDirty) {
5226
5547
  ovC.clearRect(0, 0, W, H)
5227
5548
  // webcam bubble \u2014 pinned to the frame corner regardless of card tilt/zoom.
@@ -5271,8 +5592,26 @@ var ON_FRAME = `(ctx, content, dt) => {
5271
5592
  ovC.restore()
5272
5593
  }
5273
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
+ }
5274
5613
  ov.sig = ovSig
5275
- ov.active = camActiveNow
5614
+ ov.active = camActiveNow || !!trOut
5276
5615
  if (ov.texture) ov.texture.needsUpdate = true
5277
5616
  }
5278
5617
 
@@ -5301,15 +5640,22 @@ var ON_FRAME = `(ctx, content, dt) => {
5301
5640
  // which the overscan grows).
5302
5641
  var cpk = d.cardPoseTrack
5303
5642
  var cpH = 2 * ${Math.abs(CARD_Z)} * Math.tan(${CARD_FOV} * Math.PI / 180 / 2)
5304
- if (cpk && cpk.keyframes && cpk.keyframes.length && card.mesh.scale && card.mesh.position) {
5305
- var cpv = TL.sample(cpk, t, TL.lerpArray)
5306
- card.mesh.scale.x = cpv[0]; card.mesh.scale.y = cpv[0]
5307
- card.mesh.position.y = cpv[1] * cpH
5308
- 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
5309
5654
  // Gone past its clip: the mesh leaves the scene, not just its paint.
5310
- card.mesh.visible = cpv[2] > 0.001
5311
- } 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)) {
5312
5657
  card.mesh.scale.x = 1; card.mesh.scale.y = 1
5658
+ card.mesh.position.x = 0
5313
5659
  card.mesh.position.y = 0
5314
5660
  if (card.mesh.material) card.mesh.material.opacity = 1
5315
5661
  card.mesh.visible = true
@@ -5326,6 +5672,29 @@ var ON_FRAME = `(ctx, content, dt) => {
5326
5672
  }
5327
5673
  }
5328
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
+
5329
5698
  // The card layer redraws every frame (dynamic content); bg/overlay uploads are
5330
5699
  // gated inside their blocks (dirty-tracking above).
5331
5700
  if (card.texture) card.texture.needsUpdate = true
@@ -5550,6 +5919,7 @@ function studioLayerData(layers, duration) {
5550
5919
  function lowerToComposition(input) {
5551
5920
  const doc = migrateMotion(input);
5552
5921
  const rated = ratedSegments(doc);
5922
+ const transitions = docTransitions(doc);
5553
5923
  const footageEnd = durationSec(doc, rated);
5554
5924
  const duration = outputEnd(doc, footageEnd);
5555
5925
  const fx = doc.cursor.clickFx;
@@ -5564,7 +5934,12 @@ function lowerToComposition(input) {
5564
5934
  const layout = docCardLayout(doc);
5565
5935
  const meta = doc.source.meta;
5566
5936
  const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
5567
- const zoomSpans = doc.zoom.map((z) => {
5937
+ const zoomSpans = restSpansThroughTransitions(
5938
+ doc.zoom,
5939
+ rated,
5940
+ transitions,
5941
+ ZOOM_SPAN_MIN
5942
+ ).map((z) => {
5568
5943
  if (z.focusMode === "auto") {
5569
5944
  const f = followFocusEvents(
5570
5945
  z,
@@ -5651,6 +6026,9 @@ function lowerToComposition(input) {
5651
6026
  })
5652
6027
  } : {},
5653
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 } : {},
5654
6032
  // Click effects: OUTPUT-anchored click records + resolved styling (named
5655
6033
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
5656
6034
  ...clickFxData(doc, rated),
@@ -5668,7 +6046,13 @@ function lowerToComposition(input) {
5668
6046
  // producer of this track, never a second pose.
5669
6047
  ...(() => {
5670
6048
  const head = entranceTiltKeyframes(cardEnter(doc.frame));
5671
- 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;
5672
6056
  const track = prependEntrance(spans, head);
5673
6057
  return track ? { tiltTrack: track } : {};
5674
6058
  })(),
@@ -6145,18 +6529,18 @@ function zoomCoversRect(span, rect, layout, tol = 0.02) {
6145
6529
  }
6146
6530
 
6147
6531
  // src/digest/style.ts
6148
- import { totalDuration as totalDuration4 } from "@vosjs/timeline";
6532
+ import { totalDuration as totalDuration5 } from "@vosjs/timeline";
6149
6533
 
6150
6534
  // src/audioBeds.ts
6151
- import { totalDuration as totalDuration3 } from "@vosjs/timeline";
6152
- 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;
6153
6537
  function docOutputDuration(doc) {
6154
- const footage = totalDuration3(ratedSegments(doc));
6538
+ const footage = totalDuration4(ratedSegments(doc));
6155
6539
  return isRecordingDoc(doc) ? outputEnd(migrateMotion(doc), footage) : footage;
6156
6540
  }
6157
6541
  function musicBedClip(input) {
6158
- const track = round32(input.trackDuration);
6159
- const output = round32(input.outputDuration);
6542
+ const track = round33(input.trackDuration);
6543
+ const output = round33(input.outputDuration);
6160
6544
  const clip = {
6161
6545
  id: input.id,
6162
6546
  key: input.key,
@@ -6171,7 +6555,7 @@ function musicBedClip(input) {
6171
6555
  fadeIn: 0,
6172
6556
  // A bed that just stops reads as a glitch; clamp so a very short cut
6173
6557
  // still spends most of its time at full level.
6174
- 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,
6175
6559
  duck: input.hasMic || void 0
6176
6560
  };
6177
6561
  if (output <= 0) return clip;
@@ -6200,18 +6584,18 @@ function refillAudioBeds(doc, prevDuration, nextDuration) {
6200
6584
  const placed = clip.loop ? Math.max(clip.out - clip.in, clip.loopLen ?? clip.out - clip.in) : clip.out - clip.in;
6201
6585
  if (Math.abs(clip.start + placed - prevDuration) > EPS2) continue;
6202
6586
  const span = clip.out - clip.in;
6203
- const nextLen = round32(nextDuration - clip.start);
6587
+ const nextLen = round33(nextDuration - clip.start);
6204
6588
  if (clip.loop) {
6205
6589
  if (nextLen >= span) {
6206
6590
  clip.loopLen = nextLen;
6207
6591
  } else {
6208
6592
  clip.loop = void 0;
6209
6593
  clip.loopLen = void 0;
6210
- clip.out = round32(clip.in + nextLen);
6594
+ clip.out = round33(clip.in + nextLen);
6211
6595
  }
6212
6596
  } else {
6213
6597
  if (clip.in + nextLen <= clip.duration) {
6214
- clip.out = round32(clip.in + nextLen);
6598
+ clip.out = round33(clip.in + nextLen);
6215
6599
  } else {
6216
6600
  clip.out = clip.duration;
6217
6601
  clip.loop = true;
@@ -6432,8 +6816,8 @@ function applyTemplate(template, take, opts) {
6432
6816
  }
6433
6817
  ].sort((a, b) => a.at - b.at);
6434
6818
  }
6435
- const templateEnd = totalDuration4(ratedSegments(template));
6436
- const takeEnd = totalDuration4(ratedSegments(doc));
6819
+ const templateEnd = totalDuration5(ratedSegments(template));
6820
+ const takeEnd = totalDuration5(ratedSegments(doc));
6437
6821
  const shift = shiftBy(opts.at, templateEnd, takeEnd);
6438
6822
  const own = new Map((take.overlays ?? []).map((o) => [o.id, o]));
6439
6823
  const overlays = (doc.overlays ?? []).filter(
@@ -6442,7 +6826,7 @@ function applyTemplate(template, take, opts) {
6442
6826
  const skipped = [];
6443
6827
  for (const clip of template.overlays ?? []) {
6444
6828
  const next = structuredClone(clip);
6445
- next.start = Math.max(0, round33(clip.start + shift));
6829
+ next.start = Math.max(0, round34(clip.start + shift));
6446
6830
  next.from = from;
6447
6831
  const mine = own.get(clip.id);
6448
6832
  if (next.kind === "text") {
@@ -6470,7 +6854,7 @@ function applyTemplate(template, take, opts) {
6470
6854
  for (const clip of template.objects ?? []) {
6471
6855
  const next = structuredClone(clip);
6472
6856
  if (next.span)
6473
- next.span.start = Math.max(0, round33(next.span.start + shift));
6857
+ next.span.start = Math.max(0, round34(next.span.start + shift));
6474
6858
  next.from = from;
6475
6859
  objects.push(next);
6476
6860
  }
@@ -6480,7 +6864,7 @@ function applyTemplate(template, take, opts) {
6480
6864
  );
6481
6865
  for (const clip of template.audio) {
6482
6866
  const next = structuredClone(clip);
6483
- next.start = Math.max(0, round33(clip.start + shift));
6867
+ next.start = Math.max(0, round34(clip.start + shift));
6484
6868
  next.from = from;
6485
6869
  audio.push(next);
6486
6870
  }
@@ -6501,7 +6885,7 @@ function applyTemplate(template, take, opts) {
6501
6885
  }
6502
6886
  return { doc, notes };
6503
6887
  }
6504
- var round33 = (v) => Math.round(v * 1e3) / 1e3;
6888
+ var round34 = (v) => Math.round(v * 1e3) / 1e3;
6505
6889
 
6506
6890
  // src/digest/geometry.ts
6507
6891
  var CROP_MIN_FRAC = 0.25;
@@ -6743,7 +7127,7 @@ function round10(v) {
6743
7127
 
6744
7128
  // src/lower/lowerStudioDoc.ts
6745
7129
  import { applyTimelineEdits } from "@vosjs/shared/timelineEdits";
6746
- import { totalDuration as totalDuration5 } from "@vosjs/timeline";
7130
+ import { totalDuration as totalDuration6 } from "@vosjs/timeline";
6747
7131
  function lowerStudioDoc(doc, opts = {}) {
6748
7132
  return isRecordingDoc(doc) ? lowerToComposition(doc) : lowerProgramDoc(doc, opts);
6749
7133
  }
@@ -6774,7 +7158,7 @@ function lowerProgramDoc(doc, opts = {}) {
6774
7158
  ) : doc.program.config;
6775
7159
  const own = programDuration(doc);
6776
7160
  const rated = ratedSegments(doc);
6777
- const duration = own > 0 ? totalDuration5(rated) : 0;
7161
+ const duration = own > 0 ? totalDuration6(rated) : 0;
6778
7162
  const entryData = studioLayerData(doc, duration);
6779
7163
  const baseData = anchor.data && typeof anchor.data === "object" ? anchor.data : {};
6780
7164
  const retimed = !!doc.speed?.length && own > 0;
@@ -7372,31 +7756,10 @@ import {
7372
7756
  removeSegment,
7373
7757
  segmentRate as segmentRate3,
7374
7758
  sourceToTimeline as sourceToTimeline2,
7375
- splitBySpeed as splitBySpeed3,
7376
- totalDuration as totalDuration6,
7759
+ splitBySpeed as splitBySpeed4,
7760
+ totalDuration as totalDuration7,
7377
7761
  trimSegment
7378
7762
  } from "@vosjs/timeline";
7379
- function effectiveSegments(doc) {
7380
- if (isRecordingDoc(doc) && doc.segments.length) return doc.segments;
7381
- return [{ in: 0, out: anchorSourceDuration(doc) }];
7382
- }
7383
- var segmentPieces = (seg, speeds, freezes, leading) => {
7384
- const own = freezes.filter(
7385
- (f) => f.at <= seg.out + 1e-9 && (leading ? f.at >= seg.in - 1e-9 : f.at > seg.in + 1e-9)
7386
- );
7387
- return withFreezes(splitBySpeed3([seg], speeds), own);
7388
- };
7389
- var outputLen = (seg, speeds, freezes = [], leading = true) => totalDuration6(segmentPieces(seg, speeds, freezes, leading));
7390
- var segmentStarts = (segments, speeds, freezes = []) => {
7391
- const starts = [];
7392
- let acc = 0;
7393
- segments.forEach((s, i) => {
7394
- starts.push(acc);
7395
- acc += outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i));
7396
- });
7397
- return starts;
7398
- };
7399
- var contiguous = (segments, i) => i > 0 && Math.abs(segments[i - 1].out - segments[i].in) < 1e-9;
7400
7763
  var videoLane = {
7401
7764
  id: "video",
7402
7765
  label: "Video",
@@ -7448,7 +7811,7 @@ var videoLane = {
7448
7811
  if (index < 0 || index >= segments.length) return null;
7449
7812
  const seg = segments[index];
7450
7813
  const starts = segmentStarts(segments, speeds, freezes);
7451
- const fullMap = splitBySpeed3([{ in: 0, out: sourceDuration }], speeds);
7814
+ const fullMap = splitBySpeed4([{ in: 0, out: sourceDuration }], speeds);
7452
7815
  const edgeSrc = g.edge === "start" ? seg.in : seg.out;
7453
7816
  const edgeOutNow = g.edge === "start" ? starts[index] : starts[index] + outputLen(
7454
7817
  seg,
@@ -7484,7 +7847,7 @@ var videoLane = {
7484
7847
  const leading = index === 0 || !contiguous(segments, index);
7485
7848
  const local = g.t - starts[index];
7486
7849
  for (const m of freezeOutputExtents(
7487
- splitBySpeed3([s], speeds),
7850
+ splitBySpeed4([s], speeds),
7488
7851
  freezes.filter(
7489
7852
  (f) => f.at <= s.out + 1e-9 && (leading ? f.at >= s.in - 1e-9 : f.at > s.in + 1e-9)
7490
7853
  )
@@ -7551,7 +7914,7 @@ var freezeLane = {
7551
7914
  if (!freezes.length) return [];
7552
7915
  const segs = effectiveSegments(doc);
7553
7916
  const marks = freezeOutputExtents(
7554
- splitBySpeed3(segs, doc.speed ?? []),
7917
+ splitBySpeed4(segs, doc.speed ?? []),
7555
7918
  freezes
7556
7919
  );
7557
7920
  return freezes.flatMap((f) => {
@@ -7577,12 +7940,12 @@ var freezeLane = {
7577
7940
  if (g.type === "create") {
7578
7941
  const rated = ratedSegments(doc);
7579
7942
  for (const m of freezeOutputExtents(
7580
- splitBySpeed3(segs, speeds),
7943
+ splitBySpeed4(segs, speeds),
7581
7944
  freezes
7582
7945
  ).values()) {
7583
7946
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
7584
7947
  }
7585
- if (g.t > totalDuration6(rated) + 1e-9) return null;
7948
+ if (g.t > totalDuration7(rated) + 1e-9) return null;
7586
7949
  const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
7587
7950
  if (!kept(srcT) || !clear(srcT)) return null;
7588
7951
  const id = nextFreezeId(doc);
@@ -7618,11 +7981,11 @@ var freezeLane = {
7618
7981
  return mine;
7619
7982
  };
7620
7983
  const base = withFreezes(
7621
- splitBySpeed3(segs, speeds),
7984
+ splitBySpeed4(segs, speeds),
7622
7985
  freezes.filter((x) => x.id !== f.id)
7623
7986
  );
7624
7987
  if (g.type === "move" || g.type === "resize" && g.edge === "start") {
7625
- const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
7988
+ const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
7626
7989
  const mine = marks.get(f.id);
7627
7990
  const endOut = mine ? mine.t + mine.duration : null;
7628
7991
  const srcT = round11(
@@ -7635,7 +7998,7 @@ var freezeLane = {
7635
7998
  x.at = srcT;
7636
7999
  if (g.type === "resize" && endOut !== null) {
7637
8000
  const after = freezeOutputExtents(
7638
- splitBySpeed3(effectiveSegments(d), d.speed ?? []),
8001
+ splitBySpeed4(effectiveSegments(d), d.speed ?? []),
7639
8002
  docFreezes({
7640
8003
  ...d,
7641
8004
  freeze: (d.freeze ?? []).map(
@@ -7649,7 +8012,7 @@ var freezeLane = {
7649
8012
  };
7650
8013
  }
7651
8014
  if (g.type === "resize") {
7652
- const marks = freezeOutputExtents(splitBySpeed3(segs, speeds), freezes);
8015
+ const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
7653
8016
  const mine = marks.get(f.id);
7654
8017
  if (!mine) return null;
7655
8018
  const seconds = clampFreezeSeconds(g.t - mine.t);
@@ -8210,7 +8573,7 @@ var speedLane = {
8210
8573
  const sp = spans.find((s) => s.id === g.id);
8211
8574
  if (!sp) return null;
8212
8575
  const others = spans.filter((o) => o.id !== g.id);
8213
- const base = splitBySpeed3(effectiveSegments(doc), others);
8576
+ const base = splitBySpeed4(effectiveSegments(doc), others);
8214
8577
  if (g.type === "move") {
8215
8578
  const len = sp.out - sp.in;
8216
8579
  let newIn = clampToKept(
@@ -8822,6 +9185,8 @@ export {
8822
9185
  REST_TILT_ID,
8823
9186
  SCENE_MOTION,
8824
9187
  SCENE_QUIET,
9188
+ SEGMENT_ENTER_KINDS,
9189
+ SEGMENT_EXIT_KINDS,
8825
9190
  SPEED_RATE_MAX,
8826
9191
  SPEED_RATE_MIN,
8827
9192
  SPEED_SPAN_MIN,
@@ -8847,6 +9212,9 @@ export {
8847
9212
  TILT_RAMP_OUT,
8848
9213
  TILT_SPAN_MIN,
8849
9214
  TILT_UI_DEG_MAX,
9215
+ TRANSITION_SECONDS,
9216
+ TRANSITION_SECONDS_MAX,
9217
+ TRANSITION_SECONDS_MIN,
8850
9218
  TRANSITION_SPEED_MULT,
8851
9219
  WINDOW_FOCUS_MIN,
8852
9220
  ZOOM_CHAIN_GAP,
@@ -8912,6 +9280,7 @@ export {
8912
9280
  docStillTime,
8913
9281
  docToCropSpace,
8914
9282
  docToFullSpace,
9283
+ docTransitions,
8915
9284
  dropTemplate,
8916
9285
  duckCurve,
8917
9286
  effectiveSegments,
@@ -9011,10 +9380,15 @@ export {
9011
9380
  resolveText3dAsset,
9012
9381
  resolveZoomStyle,
9013
9382
  restLeanOf,
9383
+ restSpansThroughTransitions,
9014
9384
  sameMedia,
9015
9385
  sceneChanges,
9016
9386
  scrollRuns,
9387
+ segmentOutputExtents,
9388
+ outputLen as segmentOutputLen,
9389
+ segmentStarts,
9017
9390
  setSpeedInRange,
9391
+ slideSide,
9018
9392
  smoothCursor,
9019
9393
  spanOutputExtent,
9020
9394
  speedLane,
@@ -9024,6 +9398,7 @@ export {
9024
9398
  tiltLane,
9025
9399
  tiltTrackFromDoc,
9026
9400
  transitionMult,
9401
+ transitionSeconds,
9027
9402
  videoLane,
9028
9403
  voiceKey,
9029
9404
  withBackdrop,