@vosjs/studio-core 0.21.0 → 0.22.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
@@ -132,6 +132,9 @@ var DEFAULT_CURSOR_STYLE = {
132
132
  visible: true,
133
133
  smoothing: 0.15,
134
134
  size: 24,
135
+ // The dot every earlier take was cut with; a NEW take opens on the
136
+ // arrow (projectFromArtifact sets it), so a document that stores
137
+ // 'default' keeps its dot.
135
138
  style: "default",
136
139
  hideWhenIdle: true,
137
140
  clickFx: DEFAULT_CLICK_FX
@@ -157,6 +160,10 @@ var BASE_FRAME_STYLE = {
157
160
  radius: 12,
158
161
  shadow: 0.4,
159
162
  border: 0,
163
+ // The stage camera: a new take's zoom centres its target and shows the
164
+ // ground past the card's edge. A document without the field keeps the
165
+ // clamped magnifier it was cut with (see FrameStyle.camera).
166
+ camera: "stage",
160
167
  // 'native' = the recording's own aspect ratio (meta.width/height). See ASPECT_RATIOS.
161
168
  aspectRatio: "native",
162
169
  browserBar: DEFAULT_BROWSER_BAR
@@ -431,7 +438,9 @@ function projectFromArtifact(artifact, videoUrl, opts = {}) {
431
438
  zoom: [],
432
439
  // planner runs once the doc is loaded (editor) or planned (CLI)
433
440
  audio: [],
434
- cursor: { ...DEFAULT_CURSOR_STYLE },
441
+ // A new take wears the OS pointer; DEFAULT_CURSOR_STYLE keeps the dot
442
+ // so a document that stores it renders as it was cut.
443
+ cursor: { ...DEFAULT_CURSOR_STYLE, style: "arrow" },
435
444
  cam: { ...DEFAULT_CAM_STYLE },
436
445
  frame: {
437
446
  ...baseFrame,
@@ -445,7 +454,9 @@ function projectFromArtifact(artifact, videoUrl, opts = {}) {
445
454
  url: pageDisplayUrl(artifact.meta.pageUrl)
446
455
  }
447
456
  },
448
- export: { resolution: "1080p", fps: 30, format: "mp4" }
457
+ // 60 fps: the footage duplicates frames, the camera does not, and a
458
+ // 0.5 s glide at 30 fps is fifteen visible steps.
459
+ export: { resolution: "1080p", fps: 60, format: "mp4" }
449
460
  };
450
461
  return { doc, videoUrl };
451
462
  }
@@ -1132,6 +1143,12 @@ function clamp(v, lo, hi) {
1132
1143
  // src/zoomStyle.ts
1133
1144
  var ZOOM_STYLES = {
1134
1145
  // Cursorful strategy — one steady zoom per activity session, travel by pans.
1146
+ // The camera's timing was measured against a Cursorful take of the same
1147
+ // steps: a zoom-in of ~0.55 s on a heavy ease-out (most of the move in
1148
+ // the first quarter second, a long settle), a zoom-out of ~0.45 s, and
1149
+ // holds of four to five seconds per beat. The old 1.1 s ramp started
1150
+ // 0.75 s BEFORE the click and landed 0.35 s after it, so the frame was
1151
+ // moving more of the time than it was resting.
1135
1152
  glide: {
1136
1153
  autoZoom: true,
1137
1154
  clusterGap: 3,
@@ -1139,22 +1156,27 @@ var ZOOM_STYLES = {
1139
1156
  targetFill: 0.42,
1140
1157
  minLevel: 1.3,
1141
1158
  maxLevel: 1.8,
1142
- lead: 0.5,
1143
- hold: 0.6,
1159
+ lead: 0.35,
1160
+ hold: 1.2,
1144
1161
  followByDefault: true,
1145
1162
  typingZoom: true,
1146
1163
  typingGap: 2.5,
1147
1164
  typingHold: 1.1,
1148
1165
  typingMinLevel: 1.4,
1149
- rampIn: 1.1,
1150
- rampInOverlap: 0.35,
1151
- rampOut: 1,
1152
- chainGap: 2.5,
1153
- pan: 1,
1154
- ease: "css-bezier(0.26, 0, 0.16, 1)",
1155
- panEase: "css-bezier(0.3, 0, 0.2, 1)",
1166
+ rampIn: 0.55,
1167
+ rampInOverlap: 0.2,
1168
+ rampOut: 0.5,
1169
+ chainGap: 3,
1170
+ pan: 0.55,
1171
+ // The reference's corner trace at 60 fps: a quarter of the move by a
1172
+ // fifth of the time, two thirds by half, the rest a settle. That is
1173
+ // CSS `ease` (0.25, 0.1, 0.25, 1), a small lead-in and a long tail;
1174
+ // (0.16, 1, 0.3, 1) put half the move in the first sixty milliseconds
1175
+ // and read as a twitch.
1176
+ ease: "css-bezier(0.25, 0.1, 0.25, 1)",
1177
+ panEase: "css-bezier(0.3, 0.05, 0.25, 1)",
1156
1178
  followSafeRatio: 0.45,
1157
- followRecenter: 0.8,
1179
+ followRecenter: 0.55,
1158
1180
  followLookahead: 0.4,
1159
1181
  tilt: { intensity: "off" }
1160
1182
  },
@@ -1865,10 +1887,29 @@ function camBubbleRect(cam, W, H = 1080) {
1865
1887
  radius: cam.shape === "rounded" ? (cam.radius ?? 18) * s : size / 2
1866
1888
  };
1867
1889
  }
1868
- function focusBounds(level, layout) {
1890
+ function focusBounds(level, layout, camera = "card") {
1869
1891
  if (level <= 1.001) {
1870
1892
  return { minX: 0.5, maxX: 0.5, minY: 0.5, maxY: 0.5 };
1871
1893
  }
1894
+ if (camera === "stage") {
1895
+ const x2 = stageAxisBounds(
1896
+ layout.dx,
1897
+ layout.dw,
1898
+ layout.cardX,
1899
+ layout.cardW,
1900
+ layout.W,
1901
+ level
1902
+ );
1903
+ const y2 = stageAxisBounds(
1904
+ layout.dy,
1905
+ layout.dh,
1906
+ layout.cardY,
1907
+ layout.cardH,
1908
+ layout.H,
1909
+ level
1910
+ );
1911
+ return { minX: x2.min, maxX: x2.max, minY: y2.min, maxY: y2.max };
1912
+ }
1872
1913
  const x = axisBounds(
1873
1914
  layout.dx,
1874
1915
  layout.dw,
@@ -1898,8 +1939,19 @@ function axisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level)
1898
1939
  }
1899
1940
  return { min: clamp013(lo), max: clamp013(hi) };
1900
1941
  }
1901
- function clampFocus(cx, cy, level, layout) {
1902
- const b = focusBounds(level, layout);
1942
+ function stageAxisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level) {
1943
+ const b = CAMERA_COVER_MIN / 2 * viewport;
1944
+ let lo = (coverOff + b / level - anchorOff) / anchorSize;
1945
+ let hi = (coverOff + coverSize - b / level - anchorOff) / anchorSize;
1946
+ if (lo > hi) {
1947
+ const mid = (lo + hi) / 2;
1948
+ lo = mid;
1949
+ hi = mid;
1950
+ }
1951
+ return { min: clamp013(lo), max: clamp013(hi) };
1952
+ }
1953
+ function clampFocus(cx, cy, level, layout, camera = "card") {
1954
+ const b = focusBounds(level, layout, camera);
1903
1955
  return {
1904
1956
  cx: Math.min(b.maxX, Math.max(b.minX, cx)),
1905
1957
  cy: Math.min(b.maxY, Math.max(b.minY, cy))
@@ -1908,6 +1960,45 @@ function clampFocus(cx, cy, level, layout) {
1908
1960
  function clamp013(v) {
1909
1961
  return Math.max(0, Math.min(1, v));
1910
1962
  }
1963
+ function cameraModel(frame) {
1964
+ return frame?.camera === "stage" ? "stage" : "card";
1965
+ }
1966
+ var CAMERA_CENTRE_RAMP = 0.3;
1967
+ var CAMERA_COVER_MIN = 0.8;
1968
+ function cameraCentring(level) {
1969
+ const u = clamp013((level - 1) / CAMERA_CENTRE_RAMP);
1970
+ return u * u * (3 - 2 * u);
1971
+ }
1972
+ function zoomView(level, cx, cy, layout, camera = "card", centring) {
1973
+ const L = Math.max(1, level);
1974
+ const fx = layout.dx + cx * layout.dw;
1975
+ const fy = layout.dy + cy * layout.dh;
1976
+ const ox = layout.W / 2;
1977
+ const oy = layout.H / 2;
1978
+ const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
1979
+ const wcx = fx + (ox - fx) / L * (1 - t);
1980
+ const wcy = fy + (oy - fy) / L * (1 - t);
1981
+ return { level: L, fx, fy, wcx, wcy, ox, oy };
1982
+ }
1983
+ function zoomViewport(level, cx, cy, layout, camera = "card", centring) {
1984
+ const v = zoomView(level, cx, cy, layout, camera, centring);
1985
+ const size = 1 / v.level;
1986
+ return {
1987
+ x: (v.wcx - layout.W / (2 * v.level)) / layout.W,
1988
+ y: (v.wcy - layout.H / (2 * v.level)) / layout.H,
1989
+ w: size,
1990
+ h: size
1991
+ };
1992
+ }
1993
+ function focusForViewportCentre(cX, cY, level, layout, camera = "card", centring) {
1994
+ const L = Math.max(1, level);
1995
+ const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
1996
+ const a = 1 - (1 - t) / L;
1997
+ if (a <= 1e-6) return { cx: 0.5, cy: 0.5 };
1998
+ const fx = (cX * layout.W - layout.W / 2 * (1 - t) / L) / a;
1999
+ const fy = (cY * layout.H - layout.H / 2 * (1 - t) / L) / a;
2000
+ return { cx: (fx - layout.dx) / layout.dw, cy: (fy - layout.dy) / layout.dh };
2001
+ }
1911
2002
  function levelForFocusFraction(frac) {
1912
2003
  if (!(frac > 0)) return clampZoomLevel(Number.POSITIVE_INFINITY);
1913
2004
  return clampZoomLevel(Math.max(1.1, 1 / frac));
@@ -2812,6 +2903,7 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
2812
2903
  const safeRatio = options.safeRatio ?? FOLLOW_SAFE_RATIO;
2813
2904
  const recenter = options.recenter ?? FOLLOW_RECENTER;
2814
2905
  const lookahead = options.lookahead ?? 0;
2906
+ const camera = options.camera ?? "card";
2815
2907
  const level = clampZoomLevel(span.level);
2816
2908
  if (!cursor.length || !space.w || !space.h || level <= 1.001) {
2817
2909
  return { entry: null, events: [] };
@@ -2822,12 +2914,7 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
2822
2914
  ny: clamp014(e.y / space.h)
2823
2915
  }));
2824
2916
  if (!pts.length) return { entry: null, events: [] };
2825
- let entryPt = pts[0];
2826
- for (const p of pts) {
2827
- if (p.t > span.in) break;
2828
- entryPt = p;
2829
- }
2830
- const entry = clampFocus(entryPt.nx, entryPt.ny, level, layout);
2917
+ const entry = clampFocus(span.cx, span.cy, level, layout, camera);
2831
2918
  const thrX = safeRatio * layout.W / (2 * level * layout.dw);
2832
2919
  const thrY = safeRatio * layout.H / (2 * level * layout.dh);
2833
2920
  const events = [];
@@ -2840,7 +2927,7 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
2840
2927
  if (p.t < nextAllowed) continue;
2841
2928
  if (Math.abs(p.nx - cx) > thrX || Math.abs(p.ny - cy) > thrY) {
2842
2929
  const target = lookahead > 0 ? sampleAt2(pts, Math.min(p.t + lookahead, span.out)) : p;
2843
- const f = clampFocus(target.nx, target.ny, level, layout);
2930
+ const f = clampFocus(target.nx, target.ny, level, layout, camera);
2844
2931
  if (Math.abs(f.cx - cx) < 1e-3 && Math.abs(f.cy - cy) < 1e-3) continue;
2845
2932
  events.push({ t: round2(p.t), cx: round2(f.cx), cy: round2(f.cy) });
2846
2933
  cx = f.cx;
@@ -4573,13 +4660,13 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
4573
4660
  let chained = false;
4574
4661
  for (let i = 0; i < mapped.length; i++) {
4575
4662
  const { z, tIn, tOut } = mapped[i];
4576
- const entry = [clampZoomLevel(z.level), z.cx, z.cy];
4663
+ const entry = [clampZoomLevel(z.level), z.cx, z.cy, 1];
4577
4664
  let cur = entry;
4578
4665
  const m = transitionMult(z.transition);
4579
4666
  if (!chained) {
4580
4667
  const start = push2(
4581
4668
  tIn - (style.rampIn - style.rampInOverlap) * m,
4582
- [1, z.cx, z.cy],
4669
+ [1, z.cx, z.cy, 0],
4583
4670
  "none"
4584
4671
  );
4585
4672
  push2(start + style.rampIn * m, entry, spanEase(z.ease, style.ease));
@@ -4587,7 +4674,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
4587
4674
  for (const e of z.followEvents ?? []) {
4588
4675
  const eOut = sourceToTimeline(segments, e.t);
4589
4676
  if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
4590
- const next2 = [cur[0], e.cx, e.cy];
4677
+ const next2 = [cur[0], e.cx, e.cy, 1];
4591
4678
  push2(eOut, cur, "none");
4592
4679
  push2(Math.min(eOut + style.followRecenter, tOut), next2, panEase);
4593
4680
  cur = next2;
@@ -4596,7 +4683,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
4596
4683
  const next = mapped.at(i + 1);
4597
4684
  if (next && next.tIn - tOut <= style.chainGap) {
4598
4685
  const mNext = transitionMult(next.z.transition);
4599
- const nextValue = [clampZoomLevel(next.z.level), next.z.cx, next.z.cy];
4686
+ const nextValue = [clampZoomLevel(next.z.level), next.z.cx, next.z.cy, 1];
4600
4687
  push2(
4601
4688
  Math.min(
4602
4689
  tOut + style.pan * mNext,
@@ -4609,7 +4696,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
4609
4696
  } else {
4610
4697
  push2(
4611
4698
  tOut + style.rampOut * m,
4612
- [1, cur[1], cur[2]],
4699
+ [1, cur[1], cur[2], 0],
4613
4700
  spanEase(z.ease, style.ease)
4614
4701
  );
4615
4702
  chained = false;
@@ -5791,11 +5878,15 @@ var ON_FRAME = `(ctx, content, dt) => {
5791
5878
  // current zoom \u2014 a standard keyframe track in OUTPUT time (hold + arrival pairs
5792
5879
  // expanded by the lowering), sampled with the shared deterministic interpolator.
5793
5880
  // Sampled here, before the rect math, so a cover crop can follow it.
5794
- var lvl = 1, zx = 0.5, zy = 0.5
5881
+ var lvl = 1, zx = 0.5, zy = 0.5, zc = -1
5795
5882
  var zt = d.zoomTrack
5796
5883
  if (zt && zt.keyframes && zt.keyframes.length) {
5797
5884
  var z = TL.sample(zt, tpT, TL.lerpArray)
5798
5885
  lvl = z[0]; zx = z[1]; zy = z[2]
5886
+ // The stage camera's centring rides the track (0 rest, 1 apex, eased
5887
+ // with the level); a three-component track (a document lowered before
5888
+ // the component existed) falls back to the level band below.
5889
+ if (z.length > 3) zc = z[3]
5799
5890
  }
5800
5891
  var barH = bar.kind && bar.kind !== 'none' ? (bar.height || 44) * s2 : 0
5801
5892
  var availW = Math.max(1, W - ipL - ipR), availH = Math.max(1, H - ipT - ipB - barH)
@@ -5844,9 +5935,31 @@ var ON_FRAME = `(ctx, content, dt) => {
5844
5935
  // ceZs = the zoom transform's device scale, for the two values below that
5845
5936
  // are sized in DEVICE pixels inside a scaled user space.
5846
5937
  var ceZs = 1
5938
+ // The camera model (cfr.camera; MIRRORS layout.ts zoomView \u2014 change them
5939
+ // together). The magnifier scales the card about the focus, which stays
5940
+ // where it is on screen; the stage camera also slides the card so the
5941
+ // focus lands at the frame's centre, blended in over CAMERA_CENTRE_RAMP
5942
+ // of level (a smoothstep, 0 at level 1) so a ramp through the band slides
5943
+ // without a kink, and past the card's edge the ground shows. wcx/wcy is
5944
+ // the content point at the frame's centre under either model; the two
5945
+ // branches are the same map, the magnifier's kept in its old float order
5946
+ // so every existing document renders byte-identically.
5947
+ var camStage = cfr.camera === 'stage'
5948
+ var fx = dx + zx * dw, fy = dy + zy * dh
5949
+ var wcx = fx, wcy = fy
5847
5950
  if (lvl > 1.001 && !d.zoomSuppressed) {
5848
- var fx = dx + zx * dw, fy = dy + zy * dh
5849
- c.translate(fx, fy); c.scale(lvl, lvl); c.translate(-fx, -fy)
5951
+ if (camStage) {
5952
+ var ctt = zc
5953
+ if (ctt < 0) {
5954
+ var ctu = Math.max(0, Math.min(1, (lvl - 1) / ${CAMERA_CENTRE_RAMP}))
5955
+ ctt = ctu * ctu * (3 - 2 * ctu)
5956
+ }
5957
+ wcx = fx + ((W / 2 - fx) / lvl) * (1 - ctt)
5958
+ wcy = fy + ((H / 2 - fy) / lvl) * (1 - ctt)
5959
+ c.translate(W / 2, H / 2); c.scale(lvl, lvl); c.translate(-wcx, -wcy)
5960
+ } else {
5961
+ c.translate(fx, fy); c.scale(lvl, lvl); c.translate(-fx, -fy)
5962
+ }
5850
5963
  ceZs = lvl
5851
5964
  }
5852
5965
  // The card's shadows are cast by a body drawn OFF-CANVAS and brought back
@@ -5988,6 +6101,34 @@ var ON_FRAME = `(ctx, content, dt) => {
5988
6101
  // cursor coordinate space + drawn radius (shared by click effects + the dot)
5989
6102
  var space = (actM ? actM.cursorSpace : d.cursorSpace) || { w: vw, h: vh }
5990
6103
  var curSize = ((d.cursorStyle && d.cursorStyle.size) || 24) * s2 * 0.5
6104
+ // The pointer's shape: an OS-style arrow (tip on the recorded point, a
6105
+ // black body with a white edge, the shape of the pointer on the viewer's
6106
+ // own screen) or the white dot every earlier take was cut with. One
6107
+ // painter for both draw sites (in the card, and the ghost across a
6108
+ // boundary); r is the dot's radius, so an arrow stands 2r tall.
6109
+ var curArrow = !!(d.cursorStyle && d.cursorStyle.style === 'arrow')
6110
+ function drawCur(cc, x, y, r, a, lw) {
6111
+ cc.save()
6112
+ if (curArrow) {
6113
+ var h = 2 * r
6114
+ var P = [[0, 0], [0, 0.72], [0.19, 0.55], [0.31, 0.83], [0.43, 0.78], [0.31, 0.5], [0.55, 0.5]]
6115
+ cc.beginPath()
6116
+ cc.moveTo(x, y)
6117
+ for (var pi = 1; pi < P.length; pi++) cc.lineTo(x + P[pi][0] * h, y + P[pi][1] * h)
6118
+ cc.closePath()
6119
+ cc.lineJoin = 'round'
6120
+ cc.fillStyle = 'rgba(0,0,0,' + (0.92 * a) + ')'
6121
+ cc.strokeStyle = 'rgba(255,255,255,' + (0.95 * a) + ')'
6122
+ cc.lineWidth = Math.max(1, lw * 0.75)
6123
+ cc.fill(); cc.stroke()
6124
+ } else {
6125
+ cc.fillStyle = 'rgba(255,255,255,' + (0.95 * a) + ')'
6126
+ cc.strokeStyle = 'rgba(0,0,0,' + (0.4 * a) + ')'
6127
+ cc.lineWidth = lw
6128
+ cc.beginPath(); cc.arc(x, y, r, 0, Math.PI * 2); cc.fill(); cc.stroke()
6129
+ }
6130
+ cc.restore()
6131
+ }
5991
6132
 
5992
6133
  // click effects \u2014 pure f(t): d.clicks are
5993
6134
  // OUTPUT-anchored records baked at lowering (sorted by ot; re-baked on every
@@ -6161,16 +6302,14 @@ var ON_FRAME = `(ctx, content, dt) => {
6161
6302
  // moving: each pass leaves its point, mapped out of the zoom.
6162
6303
  if (trOut) {
6163
6304
  var tqX = ax, tqY = ay
6164
- if (lvl > 1.001 && !d.zoomSuppressed) { tqX = fx + (ax - fx) * lvl; tqY = fy + (ay - fy) * lvl }
6305
+ if (lvl > 1.001 && !d.zoomSuppressed) {
6306
+ if (camStage) { tqX = W / 2 + (ax - wcx) * lvl; tqY = H / 2 + (ay - wcy) * lvl }
6307
+ else { tqX = fx + (ax - fx) * lvl; tqY = fy + (ay - fy) * lvl }
6308
+ }
6165
6309
  if (tpG) trQa = [tqX, tqY]
6166
6310
  else { trQb = [tqX, tqY]; trQs = curSize * ckPress; trQw = s2 }
6167
6311
  } else if (cuA > 0.01) {
6168
- c.save()
6169
- c.fillStyle = 'rgba(255,255,255,' + (0.95 * cuA) + ')'
6170
- c.strokeStyle = 'rgba(0,0,0,' + (0.4 * cuA) + ')'
6171
- c.lineWidth = 2 * s2
6172
- c.beginPath(); c.arc(ax, ay, curSize * ckPress, 0, Math.PI * 2); c.fill(); c.stroke()
6173
- c.restore()
6312
+ drawCur(c, ax, ay, curSize * ckPress, cuA, 2 * s2)
6174
6313
  }
6175
6314
  }
6176
6315
  if (fitCover) c.restore()
@@ -6255,12 +6394,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6255
6394
  var tqBx = (tqB[0] - W / 2) * trSc + W / 2 + trOx * W, tqBy = (tqB[1] - H / 2) * trSc + H / 2 - trOy * H
6256
6395
  var tqX2 = tqAx + (tqBx - tqAx) * tqP, tqY2 = tqAy + (tqBy - tqAy) * tqP
6257
6396
  var tqR = trQs || (((d.cursorStyle && d.cursorStyle.size) || 24) * s * 0.5)
6258
- ovC.save()
6259
- ovC.fillStyle = 'rgba(255,255,255,0.95)'
6260
- ovC.strokeStyle = 'rgba(0,0,0,0.4)'
6261
- ovC.lineWidth = 2 * (trQw || s)
6262
- ovC.beginPath(); ovC.arc(tqX2, tqY2, tqR, 0, Math.PI * 2); ovC.fill(); ovC.stroke()
6263
- ovC.restore()
6397
+ drawCur(ovC, tqX2, tqY2, tqR, 1, 2 * (trQw || s))
6264
6398
  }
6265
6399
  ov.sig = ovSig
6266
6400
  ov.active = camActiveNow || !!trOut
@@ -6599,6 +6733,7 @@ function lowerToComposition(input) {
6599
6733
  const layout = docCardLayout(doc);
6600
6734
  const meta = doc.source.meta;
6601
6735
  const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
6736
+ const camera = cameraModel(doc.frame);
6602
6737
  const zoomSpans = restSpansThroughTransitions(
6603
6738
  doc.zoom,
6604
6739
  rated,
@@ -6614,13 +6749,17 @@ function lowerToComposition(input) {
6614
6749
  {
6615
6750
  safeRatio: zoomStyle.followSafeRatio,
6616
6751
  recenter: zoomStyle.followRecenter,
6617
- lookahead: zoomStyle.followLookahead
6752
+ lookahead: zoomStyle.followLookahead,
6753
+ camera
6618
6754
  }
6619
6755
  );
6620
6756
  if (f.entry)
6621
6757
  return { ...z, cx: f.entry.cx, cy: f.entry.cy, followEvents: f.events };
6622
6758
  }
6623
- return { ...z, ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout) };
6759
+ return {
6760
+ ...z,
6761
+ ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout, camera)
6762
+ };
6624
6763
  });
6625
6764
  const data = {
6626
6765
  videoSrc: doc.source.videoKey,
@@ -7176,15 +7315,14 @@ function sceneChanges(bins, opts = {}) {
7176
7315
  }
7177
7316
 
7178
7317
  // src/digest/framing.ts
7179
- function zoomWindow(span, layout) {
7318
+ function zoomWindow(span, layout, camera = "card") {
7180
7319
  const L = Math.max(1, span.level);
7181
- const { cx, cy } = clampFocus(span.cx, span.cy, L, layout);
7182
- const fx = layout.dx + cx * layout.dw;
7183
- const fy = layout.dy + cy * layout.dh;
7184
- const px0 = fx - fx / L;
7185
- const px1 = fx + (layout.W - fx) / L;
7186
- const py0 = fy - fy / L;
7187
- const py1 = fy + (layout.H - fy) / L;
7320
+ const { cx, cy } = clampFocus(span.cx, span.cy, L, layout, camera);
7321
+ const v = zoomViewport(L, cx, cy, layout, camera);
7322
+ const px0 = v.x * layout.W;
7323
+ const px1 = (v.x + v.w) * layout.W;
7324
+ const py0 = v.y * layout.H;
7325
+ const py1 = (v.y + v.h) * layout.H;
7188
7326
  return {
7189
7327
  x0: (px0 - layout.dx) / layout.dw,
7190
7328
  x1: (px1 - layout.dx) / layout.dw,
@@ -7192,9 +7330,9 @@ function zoomWindow(span, layout) {
7192
7330
  y1: (py1 - layout.dy) / layout.dh
7193
7331
  };
7194
7332
  }
7195
- function zoomCoversRect(span, rect, layout, tol = 0.02) {
7333
+ function zoomCoversRect(span, rect, layout, tol = 0.02, camera = "card") {
7196
7334
  if (span.level <= 1.001) return true;
7197
- const w = zoomWindow(span, layout);
7335
+ const w = zoomWindow(span, layout, camera);
7198
7336
  return rect.x >= w.x0 - tol && rect.x + rect.w <= w.x1 + tol && rect.y >= w.y0 - tol && rect.y + rect.h <= w.y1 + tol;
7199
7337
  }
7200
7338
 
@@ -9812,6 +9950,8 @@ export {
9812
9950
  AUDIO_PLAN_STEP,
9813
9951
  BACKGROUND_Z,
9814
9952
  BASE_FRAME_STYLE,
9953
+ CAMERA_CENTRE_RAMP,
9954
+ CAMERA_COVER_MIN,
9815
9955
  CAMERA_FAR,
9816
9956
  CAMERA_NEAR,
9817
9957
  CAM_CHAIN_GAP,
@@ -9978,6 +10118,8 @@ export {
9978
10118
  camRectFromPose,
9979
10119
  camRestPose,
9980
10120
  camTrackFromDoc,
10121
+ cameraCentring,
10122
+ cameraModel,
9981
10123
  cardEnter,
9982
10124
  cardExit,
9983
10125
  cardFields,
@@ -10032,6 +10174,7 @@ export {
10032
10174
  exportSizeFor,
10033
10175
  extractClicks,
10034
10176
  focusBounds,
10177
+ focusForViewportCentre,
10035
10178
  followFocusEvents,
10036
10179
  frameGeometry,
10037
10180
  freezeLane,
@@ -10159,6 +10302,8 @@ export {
10159
10302
  zoomLane,
10160
10303
  zoomSpanForRange,
10161
10304
  zoomTrackFromDoc,
10305
+ zoomView,
10306
+ zoomViewport,
10162
10307
  zoomWindow
10163
10308
  };
10164
10309
  //# sourceMappingURL=index.js.map