@vosjs/studio-core 0.35.3 → 0.36.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.d.ts CHANGED
@@ -3630,6 +3630,17 @@ declare function spanOutputExtent(segments: readonly (Segment & {
3630
3630
  start: number;
3631
3631
  end: number;
3632
3632
  } | null;
3633
+ /** What a span→track expansion is told about the time around it. */
3634
+ interface TrackOptions {
3635
+ /**
3636
+ * No span motion before this output time: the card's entrance owns it
3637
+ * (`prependEntrance` drops what the track put there). A span already
3638
+ * under way at it ramps in FROM it, at its full duration where the span
3639
+ * has the room, instead of losing its ramp and creeping from the
3640
+ * entrance's last pose to the span's end in one straight line.
3641
+ */
3642
+ notBefore?: number;
3643
+ }
3633
3644
  /**
3634
3645
  * Expand the doc's source-anchored zoom spans into a standard @vosjs/timeline
3635
3646
  * keyframe track in OUTPUT time (values are [level, cx, cy] vectors):
@@ -3647,7 +3658,7 @@ declare function spanOutputExtent(segments: readonly (Segment & {
3647
3658
  interface LoweredZoomSpan extends ZoomSpan {
3648
3659
  followEvents?: FollowEvent[];
3649
3660
  }
3650
- declare function zoomTrackFromDoc(zoom: LoweredZoomSpan[], segments: Segment[], style?: ZoomStyleParams): KeyframeTrack<number[]>;
3661
+ declare function zoomTrackFromDoc(zoom: LoweredZoomSpan[], segments: Segment[], style?: ZoomStyleParams, opts?: TrackOptions): KeyframeTrack<number[]>;
3651
3662
  /**
3652
3663
  * Expand the doc's source-anchored tilt spans into an OUTPUT-time keyframe
3653
3664
  * track of [rx, ry] DEGREES (ON_FRAME converts to radians at the mesh):
@@ -3670,7 +3681,7 @@ declare function tiltTrackFromDoc(tilt: TiltSpan[], segments: Segment[], motion?
3670
3681
  rampOut?: number;
3671
3682
  chainGap?: number;
3672
3683
  pan?: number;
3673
- }): KeyframeTrack<number[]>;
3684
+ }, opts?: TrackOptions): KeyframeTrack<number[]>;
3674
3685
  /**
3675
3686
  * The bubble's rest pose as [x, y, size] frame fractions, resolved through the
3676
3687
  * SAME oracle the picking layer uses (camBubbleRect) so the corner math can
package/dist/index.js CHANGED
@@ -6041,7 +6041,20 @@ function trackEmitter() {
6041
6041
  };
6042
6042
  return { keyframes, push: push2 };
6043
6043
  }
6044
- function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE]) {
6044
+ function connectedWindow(o) {
6045
+ const start = Math.max(o.arrived, Math.min(o.tOut, o.landBy - o.dur));
6046
+ const land = Math.max(
6047
+ start + o.floor,
6048
+ Math.min(start + o.dur, Math.max(o.latest, o.landBy))
6049
+ );
6050
+ return { start, land };
6051
+ }
6052
+ function withEntrance(head, build) {
6053
+ return prependEntrance(build({ notBefore: head.at(-1)?.t }), head);
6054
+ }
6055
+ var spillLimit = (tIn, tOut) => tIn + (tOut - tIn) / 2;
6056
+ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE], opts = {}) {
6057
+ const notBefore = opts.notBefore ?? -Infinity;
6045
6058
  const panEase = style.panEase;
6046
6059
  const mapped = [...zoom].sort((a, b) => a.in - b.in).flatMap((z) => {
6047
6060
  const ext = spanOutputExtent(segments, z.in, z.out, z.media);
@@ -6062,19 +6075,35 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
6062
6075
  let cur = entry;
6063
6076
  const m = transitionMult(z.transition);
6064
6077
  if (!chained) {
6078
+ if (tOut <= notBefore + RAMP_FLOOR) continue;
6065
6079
  const startWanted = tIn - (style.rampIn - style.rampInOverlap) * m;
6066
- const start = push2(startWanted, [1, z.cx, z.cy, 0, ...sc(z)], "none");
6080
+ const start = push2(
6081
+ Math.max(startWanted, notBefore),
6082
+ [1, z.cx, z.cy, 0, ...sc(z)],
6083
+ "none"
6084
+ );
6067
6085
  push2(
6068
6086
  rampLanding(start, startWanted, style.rampIn * m, tOut),
6069
6087
  entry,
6070
6088
  spanEase(z.ease, style.ease)
6071
6089
  );
6072
6090
  }
6091
+ const arrived = keyframes.at(-1)?.t ?? tIn;
6092
+ const next = mapped.at(i + 1);
6093
+ const pan = next && next.tIn - tOut <= style.chainGap ? connectedWindow({
6094
+ arrived,
6095
+ tOut,
6096
+ landBy: next.tIn + style.rampInOverlap * transitionMult(next.z.transition),
6097
+ latest: spillLimit(next.tIn, next.tOut),
6098
+ dur: style.pan * transitionMult(next.z.transition),
6099
+ floor: RAMP_FLOOR
6100
+ }) : null;
6101
+ const holdEnd = pan ? pan.start : tOut;
6073
6102
  let inPath = false;
6074
6103
  for (const e of z.followEvents ?? []) {
6075
6104
  const eOut = sourceToTimeline(segments, e.t);
6076
- if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
6077
- if (eOut - tIn < RAMP_FLOOR || tOut - eOut < RAMP_FLOOR) continue;
6105
+ if (eOut === null || eOut <= tIn || eOut >= holdEnd) continue;
6106
+ if (eOut - tIn < RAMP_FLOOR || holdEnd - eOut < RAMP_FLOOR) continue;
6078
6107
  const next2 = [cur[0], e.cx, e.cy, 1, ...cur.slice(4)];
6079
6108
  if (e.path) {
6080
6109
  if (!inPath) {
@@ -6087,14 +6116,14 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
6087
6116
  } else {
6088
6117
  inPath = false;
6089
6118
  push2(eOut, cur, "none");
6090
- push2(Math.min(eOut + style.followRecenter, tOut), next2, panEase);
6119
+ push2(Math.min(eOut + style.followRecenter, holdEnd), next2, panEase);
6091
6120
  }
6092
6121
  cur = next2;
6093
6122
  }
6094
6123
  const drift = Math.max(0, style.holdDrift ?? 0);
6095
6124
  const landed = keyframes.at(-1)?.t;
6096
- if (drift > 0 && z.focusMode !== "auto" && !(z.followEvents ?? []).length && landed !== void 0 && tOut > landed) {
6097
- const grow = Math.min(HOLD_DRIFT_MAX, drift * (tOut - landed));
6125
+ if (drift > 0 && z.focusMode !== "auto" && !(z.followEvents ?? []).length && landed !== void 0 && holdEnd > landed) {
6126
+ const grow = Math.min(HOLD_DRIFT_MAX, drift * (holdEnd - landed));
6098
6127
  cur = [
6099
6128
  clampZoomLevel(cur[0] * (1 + grow)),
6100
6129
  cur[1],
@@ -6102,13 +6131,11 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
6102
6131
  cur[3],
6103
6132
  ...cur.slice(4)
6104
6133
  ];
6105
- push2(tOut, cur, "sine.inOut");
6134
+ push2(holdEnd, cur, "sine.inOut");
6106
6135
  } else {
6107
- push2(tOut, cur, "none");
6136
+ push2(holdEnd, cur, "none");
6108
6137
  }
6109
- const next = mapped.at(i + 1);
6110
- if (next && next.tIn - tOut <= style.chainGap) {
6111
- const mNext = transitionMult(next.z.transition);
6138
+ if (next && pan) {
6112
6139
  const nextValue = [
6113
6140
  clampZoomLevel(next.z.level),
6114
6141
  next.z.cx,
@@ -6116,17 +6143,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
6116
6143
  1,
6117
6144
  ...sc(next.z)
6118
6145
  ];
6119
- push2(
6120
- Math.max(
6121
- tOut + RAMP_FLOOR,
6122
- Math.min(
6123
- tOut + style.pan * mNext,
6124
- next.tIn + style.rampInOverlap * mNext
6125
- )
6126
- ),
6127
- nextValue,
6128
- panEase
6129
- );
6146
+ push2(pan.land, nextValue, panEase);
6130
6147
  chained = true;
6131
6148
  } else {
6132
6149
  push2(
@@ -6142,7 +6159,8 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
6142
6159
  function sameVec(a, b) {
6143
6160
  return a.length === b.length && a.every((v, i) => Math.abs(v - b[i]) < 1e-6);
6144
6161
  }
6145
- function tiltTrackFromDoc(tilt, segments, motion = {}) {
6162
+ function tiltTrackFromDoc(tilt, segments, motion = {}, opts = {}) {
6163
+ const notBefore = opts.notBefore ?? -Infinity;
6146
6164
  const rampInDur = motion.rampIn ?? TILT_RAMP_IN;
6147
6165
  const rampOutDur = motion.rampOut ?? TILT_RAMP_OUT;
6148
6166
  const chainGap = motion.chainGap ?? TILT_CHAIN_GAP;
@@ -6160,24 +6178,30 @@ function tiltTrackFromDoc(tilt, segments, motion = {}) {
6160
6178
  const pose = [clampTiltDeg(z.rx), clampTiltDeg(z.ry)];
6161
6179
  const m = transitionMult(z.transition);
6162
6180
  if (!chained) {
6181
+ if (tOut <= notBefore + RAMP_FLOOR) continue;
6163
6182
  const startWanted = tIn - rampInDur * m;
6164
- const start = push2(startWanted, rest, "none");
6183
+ const start = push2(Math.max(startWanted, notBefore), rest, "none");
6165
6184
  push2(
6166
6185
  rampLanding(start, startWanted, rampInDur * m, tOut),
6167
6186
  pose,
6168
6187
  spanEase(z.ease, TILT_EASE)
6169
6188
  );
6170
6189
  }
6171
- push2(tOut, pose, "none");
6190
+ const arrived = keyframes.at(-1)?.t ?? tIn;
6172
6191
  const next = mapped.at(i + 1);
6173
- if (next && next.tIn - tOut <= chainGap) {
6174
- const nextPose = [clampTiltDeg(next.z.rx), clampTiltDeg(next.z.ry)];
6192
+ const swing = next && next.tIn - tOut <= chainGap ? connectedWindow({
6193
+ arrived,
6194
+ tOut,
6195
+ landBy: next.tIn,
6196
+ latest: spillLimit(next.tIn, next.tOut),
6197
+ dur: panDur * transitionMult(next.z.transition),
6198
+ floor: RAMP_FLOOR
6199
+ }) : null;
6200
+ push2(swing ? swing.start : tOut, pose, "none");
6201
+ if (next && swing) {
6175
6202
  push2(
6176
- Math.max(
6177
- tOut + RAMP_FLOOR,
6178
- Math.min(tOut + panDur * transitionMult(next.z.transition), next.tIn)
6179
- ),
6180
- nextPose,
6203
+ swing.land,
6204
+ [clampTiltDeg(next.z.rx), clampTiltDeg(next.z.ry)],
6181
6205
  panEase
6182
6206
  );
6183
6207
  chained = true;
@@ -6214,14 +6238,19 @@ function camTrackFromDoc(cam, spans, segments, W, H = 1080) {
6214
6238
  const start = push2(tIn - CAM_RAMP_IN * m, rest, "none");
6215
6239
  push2(start + CAM_RAMP_IN * m, pose, spanEase(z.ease, CAM_EASE));
6216
6240
  }
6217
- push2(tOut, pose, "none");
6241
+ const arrived = keyframes.at(-1)?.t ?? tIn;
6218
6242
  const next = mapped.at(i + 1);
6219
- if (next && next.tIn - tOut <= CAM_CHAIN_GAP) {
6220
- push2(
6221
- Math.min(tOut + CAM_PAN * transitionMult(next.z.transition), next.tIn),
6222
- poseOf(next.z),
6223
- panEase
6224
- );
6243
+ const morph = next && next.tIn - tOut <= CAM_CHAIN_GAP ? connectedWindow({
6244
+ arrived,
6245
+ tOut,
6246
+ landBy: next.tIn,
6247
+ latest: spillLimit(next.tIn, next.tOut),
6248
+ dur: CAM_PAN * transitionMult(next.z.transition),
6249
+ floor: 0
6250
+ }) : null;
6251
+ push2(morph ? morph.start : tOut, pose, "none");
6252
+ if (next && morph) {
6253
+ push2(morph.land, poseOf(next.z), panEase);
6225
6254
  chained = true;
6226
6255
  } else {
6227
6256
  push2(tOut + CAM_RAMP_OUT * m, rest, spanEase(z.ease, CAM_EASE));
@@ -6367,6 +6396,21 @@ var FRAME_STEP_SRC = `
6367
6396
  }
6368
6397
  function stepBg(bgEl, bgT) {
6369
6398
  if (!bgEl.paused) bgEl.pause()
6399
+ // Capture pages: the loop's frame by PTS from its own provider, like the
6400
+ // footage. The loop clock only moves forward between wraps, so the walk
6401
+ // stays sequential and a wrap is one re-seek to the keyframe at 0.
6402
+ var bwc = bgEl.__voilaWc
6403
+ if (bwc) {
6404
+ var bwT = Math.min(bgT, bwc.duration || bgT)
6405
+ if (bwc.req !== bwT) {
6406
+ var bwp = bwc.seek(bwT)
6407
+ if (ns.pendingDecodes) {
6408
+ ns.pendingDecodes.add(bwp)
6409
+ bwp.finally(function () { ns.pendingDecodes.delete(bwp) })
6410
+ }
6411
+ }
6412
+ return
6413
+ }
6370
6414
  var bgTarget = Math.min(bgT, bgEl.duration || bgT)
6371
6415
  if (bgEl.readyState >= 1 && !bgEl.seeking && Math.abs(bgEl.currentTime - bgTarget) > 0.02) {
6372
6416
  if (ns.pendingDecodes) {
@@ -6450,6 +6494,7 @@ var SETUP = `async (ctx) => {
6450
6494
  // page has a memory budget) and FAIL-OPEN: any fetch trouble degrades to
6451
6495
  // the plain network element, never a dead LOAD.
6452
6496
  const BLOB_FETCH_MAX = 400 * 1024 * 1024
6497
+ const BG_BLOB_MAX = 64 * 1024 * 1024
6453
6498
  const toBlobUrl = async (src) => {
6454
6499
  const resp = await fetch(src)
6455
6500
  if (!resp.ok) throw new Error('[voila] blob fetch HTTP ' + resp.status)
@@ -6650,9 +6695,48 @@ var SETUP = `async (ctx) => {
6650
6695
  if (bgm && bgm.key) {
6651
6696
  try {
6652
6697
  if (bgm.kind === 'image') await loadImage(bgm.key)
6653
- else (await load(bgm.key, true)).loop = true
6698
+ else {
6699
+ const bgV = await load(bgm.key, true)
6700
+ bgV.loop = true
6701
+ // Capture pages: the loop gets its own provider. An element seek
6702
+ // decodes from the previous keyframe on EVERY frame (a baked loop
6703
+ // has one every few seconds), and the capture waits for it; the
6704
+ // sequential walk decodes each packet once. The whole loop is read
6705
+ // into a Blob first when it is small enough (one request instead
6706
+ // of ranged reads against a CDN). FAIL-OPEN to the element at
6707
+ // every step, like the footage.
6708
+ if (ctx.data.videoDecodeMode === 'webcodecs' && !bgV.__voilaWc) {
6709
+ try {
6710
+ if (!(ns.videoBlobs && ns.videoBlobs.get(bgm.key)) && bgm.key.indexOf('blob:') !== 0 && bgm.key.indexOf('data:') !== 0) {
6711
+ try {
6712
+ const bgResp = await fetch(bgm.key)
6713
+ const bgLen = Number(bgResp.headers.get('content-length') || 0)
6714
+ if (bgResp.ok && bgLen <= BG_BLOB_MAX) {
6715
+ ;(ns.videoBlobs || (ns.videoBlobs = new Map())).set(bgm.key, await bgResp.blob())
6716
+ } else {
6717
+ try { if (bgResp.body) await bgResp.body.cancel() } catch (e) { void e }
6718
+ }
6719
+ } catch (e) { void e }
6720
+ }
6721
+ const bgWc = await makeWcProvider(bgm.key)
6722
+ if (bgWc && (!bgV.videoWidth || (bgWc.width === bgV.videoWidth && bgWc.height === bgV.videoHeight))) bgV.__voilaWc = bgWc
6723
+ } catch (e) {
6724
+ console.warn('[voila] webcodecs provider unavailable for the background, seeks stay html5', e)
6725
+ }
6726
+ }
6727
+ }
6654
6728
  } catch (e) { console.warn('[voila] background media failed to load', e) }
6655
6729
  }
6730
+ // Which path decodes what, for a host's telemetry: 'provider' is the
6731
+ // sequential WebCodecs walk, 'element' an HTMLVideoElement seek per frame.
6732
+ ns.voilaDecode = {
6733
+ recording: ctx.data.isImage ? 'none' : video.__voilaWc ? 'provider' : 'element',
6734
+ backdrop: (function () {
6735
+ if (!bgm || !bgm.key || bgm.kind === 'image') return 'none'
6736
+ const bgHit = cache.get(bgm.key)
6737
+ return bgHit && bgHit.__voilaWc ? 'provider' : 'element'
6738
+ })(),
6739
+ }
6656
6740
  // Frame prep: a capture harness calls every ns.framePrep hook right after
6657
6741
  // seeking the timeline and before the first paint, then awaits
6658
6742
  // waitForVideosReady. The footage for the frame is requested HERE, so the
@@ -7281,7 +7365,14 @@ var ON_FRAME = `(ctx, content, dt) => {
7281
7365
  // Blur: softens the media behind the card (design px \xD7 s).
7282
7366
  var bgBlur = bgm.blur || 0
7283
7367
  if (bgBlur > 0 && bgC.filter !== undefined) bgC.filter = 'blur(' + bgBlur * s + 'px)'
7284
- try { bgC.drawImage(bgEl, (W - bgDw) / 2 + bgOx, (H - bgDh) / 2 + bgOy, bgDw, bgDh) } catch (e) {}
7368
+ // The loop's provider draws its current sample (paused capture
7369
+ // pages); false \u21D2 the element (playback, or before the first ask).
7370
+ try {
7371
+ var bgWcp = !bgIsImg && !playing ? bgEl.__voilaWc : null
7372
+ if (!(bgWcp && bgWcp.draw(bgC, null, (W - bgDw) / 2 + bgOx, (H - bgDh) / 2 + bgOy, bgDw, bgDh))) {
7373
+ bgC.drawImage(bgEl, (W - bgDw) / 2 + bgOx, (H - bgDh) / 2 + bgOy, bgDw, bgDh)
7374
+ }
7375
+ } catch (e) {}
7285
7376
  if (bgBlur > 0 && bgC.filter !== undefined) bgC.filter = 'none'
7286
7377
  var bgDim = bgm.dim || 0
7287
7378
  if (bgDim > 0) {
@@ -8369,9 +8460,9 @@ function lowerToComposition(input) {
8369
8460
  camera,
8370
8461
  zoomStyle
8371
8462
  );
8372
- const zoomTrack = prependEntrance(
8373
- zoomTrackFromDoc(zoomSpans, rated, zoomStyle),
8374
- entranceZoomKeyframes(cardEnter(doc.frame))
8463
+ const zoomTrack = withEntrance(
8464
+ entranceZoomKeyframes(cardEnter(doc.frame)),
8465
+ (opts) => zoomTrackFromDoc(zoomSpans, rated, zoomStyle, opts)
8375
8466
  );
8376
8467
  const pins = resolvePins(doc, layout, camera, zoomTrack);
8377
8468
  const pinsData = pinsDataOf(doc, pins);
@@ -8470,8 +8561,8 @@ function lowerToComposition(input) {
8470
8561
  transitions,
8471
8562
  TILT_SPAN_MIN
8472
8563
  ) : [];
8473
- const spans = tilts.length ? tiltTrackFromDoc(tilts, rated, zoomStyle.tilt) : void 0;
8474
- const track = prependEntrance(spans, head);
8564
+ const spans = tilts.length ? (opts) => tiltTrackFromDoc(tilts, rated, zoomStyle.tilt, opts) : () => void 0;
8565
+ const track = withEntrance(head, spans);
8475
8566
  return track ? { tiltTrack: track } : {};
8476
8567
  })(),
8477
8568
  // The card's pose through its enter and its exit: [scale, dy, opacity]
@@ -8567,9 +8658,9 @@ function docZoomTrack(input) {
8567
8658
  camera,
8568
8659
  zoomStyle
8569
8660
  );
8570
- return prependEntrance(
8571
- zoomTrackFromDoc(spans, rated, zoomStyle),
8572
- entranceZoomKeyframes(cardEnter(doc.frame))
8661
+ return withEntrance(
8662
+ entranceZoomKeyframes(cardEnter(doc.frame)),
8663
+ (opts) => zoomTrackFromDoc(spans, rated, zoomStyle, opts)
8573
8664
  );
8574
8665
  }
8575
8666
  function resolvePins(doc, layout, camera, zoomTrack) {