@vosjs/studio-core 0.20.0 → 0.22.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
@@ -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,29 +1143,35 @@ 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,
1138
- minClusterClicks: 2,
1155
+ minClusterClicks: 1,
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
+ ease: "css-bezier(0.16, 1, 0.3, 1)",
1172
+ panEase: "css-bezier(0.22, 1, 0.36, 1)",
1156
1173
  followSafeRatio: 0.45,
1157
- followRecenter: 0.8,
1174
+ followRecenter: 0.55,
1158
1175
  followLookahead: 0.4,
1159
1176
  tilt: { intensity: "off" }
1160
1177
  },
@@ -1308,7 +1325,7 @@ var ZOOM_STYLES = {
1308
1325
  keynote: {
1309
1326
  autoZoom: true,
1310
1327
  clusterGap: 3,
1311
- minClusterClicks: 2,
1328
+ minClusterClicks: 1,
1312
1329
  targetFill: 0.42,
1313
1330
  minLevel: 1.3,
1314
1331
  maxLevel: 1.8,
@@ -1417,13 +1434,17 @@ var DWELL_MIN = 0.45;
1417
1434
  var DWELL_MAX = 2.6;
1418
1435
  var DWELL_SPACING = 1.8;
1419
1436
  var DRAG_FIT_LEVEL = 1.15;
1437
+ var PRESS_ECHO_MS = 4;
1438
+ var PRESS_ECHO_PX = 2;
1420
1439
  var TYPING_MIN_PINGS = 2;
1421
1440
  var TYPING_MIN_DUR = 0.5;
1422
1441
  var TYPING_CLICK_ABSORB = 1.5;
1423
1442
  var TYPING_REFOCUS_FRAC = 0.08;
1424
1443
  function groupTrack(track, opts) {
1425
- const { width, height, clusterGap, typingGap, typingZoom } = opts;
1426
- const clicks = track.filter((e) => e.type === "down").map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
1444
+ const { width, height, clusterGap, typingGap, typingZoom, targetFill } = opts;
1445
+ const clicks = dedupePresses(
1446
+ track.filter((e) => e.type === "down")
1447
+ ).map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
1427
1448
  const sessions = typingZoom ? typingSessions(track, width, height, typingGap) : [];
1428
1449
  const absorbed = /* @__PURE__ */ new Set();
1429
1450
  for (const s of sessions) {
@@ -1441,15 +1462,43 @@ function groupTrack(track, opts) {
1441
1462
  s.events = [best, ...s.events];
1442
1463
  }
1443
1464
  }
1444
- const clusters = [];
1465
+ const targets = [];
1466
+ const surfaces = [];
1445
1467
  for (const c of clicks) {
1446
1468
  if (absorbed.has(c)) continue;
1447
- const last = clusters.at(-1);
1448
- const prev = last?.at(-1);
1449
- if (last && prev && c.t - prev.t <= clusterGap) last.push(c);
1450
- else clusters.push([c]);
1469
+ if (isSurfacePress(c, width, height, targetFill)) surfaces.push(c);
1470
+ else targets.push(c);
1471
+ }
1472
+ const chain = (list) => {
1473
+ const clusters = [];
1474
+ for (const c of list) {
1475
+ const last = clusters.at(-1);
1476
+ const prev = last?.at(-1);
1477
+ if (last && prev && c.t - prev.t <= clusterGap) last.push(c);
1478
+ else clusters.push([c]);
1479
+ }
1480
+ return clusters;
1481
+ };
1482
+ return { sessions, clusters: chain(targets), surfaces: chain(surfaces) };
1483
+ }
1484
+ function dedupePresses(downs) {
1485
+ const kept = [];
1486
+ for (const e of downs) {
1487
+ const prev = kept.at(-1);
1488
+ if (prev && e.t - prev.t <= PRESS_ECHO_MS && Math.abs(e.x - prev.x) <= PRESS_ECHO_PX && Math.abs(e.y - prev.y) <= PRESS_ECHO_PX) {
1489
+ continue;
1490
+ }
1491
+ kept.push(e);
1451
1492
  }
1452
- return { sessions, clusters };
1493
+ return kept;
1494
+ }
1495
+ function fitLevel(rect, width, height, targetFill) {
1496
+ if (!rect || rect.w <= 0 || rect.h <= 0) return null;
1497
+ return Math.min(width * targetFill / rect.w, height * targetFill / rect.h);
1498
+ }
1499
+ function isSurfacePress(c, width, height, targetFill) {
1500
+ const fit = fitLevel(c.rect, width, height, targetFill);
1501
+ return fit !== null && fit < DRAG_FIT_LEVEL;
1453
1502
  }
1454
1503
  function planAutoZoom(track, options) {
1455
1504
  const style = resolveZoomStyle(options.style, options.params);
@@ -1470,31 +1519,31 @@ function planAutoZoom(track, options) {
1470
1519
  typingHold = style.typingHold,
1471
1520
  typingMinLevel = style.typingMinLevel
1472
1521
  } = options;
1473
- const { sessions, clusters } = groupTrack(track, {
1522
+ const { sessions, clusters, surfaces } = groupTrack(track, {
1474
1523
  width,
1475
1524
  height,
1476
1525
  clusterGap,
1477
1526
  typingGap,
1478
- typingZoom
1527
+ typingZoom,
1528
+ targetFill
1479
1529
  });
1480
1530
  const eligible = clusters.filter((c) => c.length >= minClusterClicks);
1481
1531
  const clickSpans = [];
1482
- const dragReserved = [];
1532
+ const dragReserved = surfaces.map((cluster, i) => {
1533
+ const f = clusterFocus(cluster, width, height);
1534
+ return {
1535
+ id: `drag${i}`,
1536
+ in: Math.max(0, cluster[0].t - lead),
1537
+ out: cluster[cluster.length - 1].t + hold,
1538
+ level: 1,
1539
+ cx: f.cx,
1540
+ cy: f.cy
1541
+ };
1542
+ });
1483
1543
  for (const cluster of eligible) {
1484
1544
  const first = cluster[0];
1485
1545
  const last = cluster[cluster.length - 1];
1486
1546
  const f = focusFor(cluster, width, height, targetFill, minLevel, maxLevel);
1487
- if (f.fit !== null && f.fit < DRAG_FIT_LEVEL) {
1488
- dragReserved.push({
1489
- id: `drag${dragReserved.length}`,
1490
- in: Math.max(0, first.t - lead),
1491
- out: last.t + hold,
1492
- level: 1,
1493
- cx: f.cx,
1494
- cy: f.cy
1495
- });
1496
- continue;
1497
- }
1498
1547
  clickSpans.push({
1499
1548
  in: Math.max(0, first.t - lead),
1500
1549
  out: last.t + hold,
@@ -1668,15 +1717,8 @@ function focusFor(cluster, width, height, targetFill, minLevel, maxLevel) {
1668
1717
  const n = cluster.length;
1669
1718
  const cx = clamp012(sx / n / width);
1670
1719
  const cy = clamp012(sy / n / height);
1671
- let level = maxLevel;
1672
- let fit = null;
1673
- if (maxW > 0 && maxH > 0) {
1674
- const fitX = width * targetFill / maxW;
1675
- const fitY = height * targetFill / maxH;
1676
- level = Math.min(fitX, fitY);
1677
- fit = level;
1678
- }
1679
- return { cx, cy, level: clamp2(level, minLevel, maxLevel), fit };
1720
+ const level = fitLevel({ x: 0, y: 0, w: maxW, h: maxH }, width, height, targetFill) ?? maxLevel;
1721
+ return { cx, cy, level: clamp2(level, minLevel, maxLevel) };
1680
1722
  }
1681
1723
  function clusterFocus(events, width, height) {
1682
1724
  let sx = 0;
@@ -1840,10 +1882,29 @@ function camBubbleRect(cam, W, H = 1080) {
1840
1882
  radius: cam.shape === "rounded" ? (cam.radius ?? 18) * s : size / 2
1841
1883
  };
1842
1884
  }
1843
- function focusBounds(level, layout) {
1885
+ function focusBounds(level, layout, camera = "card") {
1844
1886
  if (level <= 1.001) {
1845
1887
  return { minX: 0.5, maxX: 0.5, minY: 0.5, maxY: 0.5 };
1846
1888
  }
1889
+ if (camera === "stage") {
1890
+ const x2 = stageAxisBounds(
1891
+ layout.dx,
1892
+ layout.dw,
1893
+ layout.cardX,
1894
+ layout.cardW,
1895
+ layout.W,
1896
+ level
1897
+ );
1898
+ const y2 = stageAxisBounds(
1899
+ layout.dy,
1900
+ layout.dh,
1901
+ layout.cardY,
1902
+ layout.cardH,
1903
+ layout.H,
1904
+ level
1905
+ );
1906
+ return { minX: x2.min, maxX: x2.max, minY: y2.min, maxY: y2.max };
1907
+ }
1847
1908
  const x = axisBounds(
1848
1909
  layout.dx,
1849
1910
  layout.dw,
@@ -1873,8 +1934,19 @@ function axisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level)
1873
1934
  }
1874
1935
  return { min: clamp013(lo), max: clamp013(hi) };
1875
1936
  }
1876
- function clampFocus(cx, cy, level, layout) {
1877
- const b = focusBounds(level, layout);
1937
+ function stageAxisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, level) {
1938
+ const b = CAMERA_COVER_MIN / 2 * viewport;
1939
+ let lo = (coverOff + b / level - anchorOff) / anchorSize;
1940
+ let hi = (coverOff + coverSize - b / level - anchorOff) / anchorSize;
1941
+ if (lo > hi) {
1942
+ const mid = (lo + hi) / 2;
1943
+ lo = mid;
1944
+ hi = mid;
1945
+ }
1946
+ return { min: clamp013(lo), max: clamp013(hi) };
1947
+ }
1948
+ function clampFocus(cx, cy, level, layout, camera = "card") {
1949
+ const b = focusBounds(level, layout, camera);
1878
1950
  return {
1879
1951
  cx: Math.min(b.maxX, Math.max(b.minX, cx)),
1880
1952
  cy: Math.min(b.maxY, Math.max(b.minY, cy))
@@ -1883,6 +1955,45 @@ function clampFocus(cx, cy, level, layout) {
1883
1955
  function clamp013(v) {
1884
1956
  return Math.max(0, Math.min(1, v));
1885
1957
  }
1958
+ function cameraModel(frame) {
1959
+ return frame?.camera === "stage" ? "stage" : "card";
1960
+ }
1961
+ var CAMERA_CENTRE_RAMP = 0.3;
1962
+ var CAMERA_COVER_MIN = 0.8;
1963
+ function cameraCentring(level) {
1964
+ const u = clamp013((level - 1) / CAMERA_CENTRE_RAMP);
1965
+ return u * u * (3 - 2 * u);
1966
+ }
1967
+ function zoomView(level, cx, cy, layout, camera = "card") {
1968
+ const L = Math.max(1, level);
1969
+ const fx = layout.dx + cx * layout.dw;
1970
+ const fy = layout.dy + cy * layout.dh;
1971
+ const ox = layout.W / 2;
1972
+ const oy = layout.H / 2;
1973
+ const t = camera === "stage" ? cameraCentring(L) : 0;
1974
+ const wcx = fx + (ox - fx) / L * (1 - t);
1975
+ const wcy = fy + (oy - fy) / L * (1 - t);
1976
+ return { level: L, fx, fy, wcx, wcy, ox, oy };
1977
+ }
1978
+ function zoomViewport(level, cx, cy, layout, camera = "card") {
1979
+ const v = zoomView(level, cx, cy, layout, camera);
1980
+ const size = 1 / v.level;
1981
+ return {
1982
+ x: (v.wcx - layout.W / (2 * v.level)) / layout.W,
1983
+ y: (v.wcy - layout.H / (2 * v.level)) / layout.H,
1984
+ w: size,
1985
+ h: size
1986
+ };
1987
+ }
1988
+ function focusForViewportCentre(cX, cY, level, layout, camera = "card") {
1989
+ const L = Math.max(1, level);
1990
+ const t = camera === "stage" ? cameraCentring(L) : 0;
1991
+ const a = 1 - (1 - t) / L;
1992
+ if (a <= 1e-6) return { cx: 0.5, cy: 0.5 };
1993
+ const fx = (cX * layout.W - layout.W / 2 * (1 - t) / L) / a;
1994
+ const fy = (cY * layout.H - layout.H / 2 * (1 - t) / L) / a;
1995
+ return { cx: (fx - layout.dx) / layout.dw, cy: (fy - layout.dy) / layout.dh };
1996
+ }
1886
1997
  function levelForFocusFraction(frac) {
1887
1998
  if (!(frac > 0)) return clampZoomLevel(Number.POSITIVE_INFINITY);
1888
1999
  return clampZoomLevel(Math.max(1.1, 1 / frac));
@@ -2787,6 +2898,7 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
2787
2898
  const safeRatio = options.safeRatio ?? FOLLOW_SAFE_RATIO;
2788
2899
  const recenter = options.recenter ?? FOLLOW_RECENTER;
2789
2900
  const lookahead = options.lookahead ?? 0;
2901
+ const camera = options.camera ?? "card";
2790
2902
  const level = clampZoomLevel(span.level);
2791
2903
  if (!cursor.length || !space.w || !space.h || level <= 1.001) {
2792
2904
  return { entry: null, events: [] };
@@ -2797,12 +2909,7 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
2797
2909
  ny: clamp014(e.y / space.h)
2798
2910
  }));
2799
2911
  if (!pts.length) return { entry: null, events: [] };
2800
- let entryPt = pts[0];
2801
- for (const p of pts) {
2802
- if (p.t > span.in) break;
2803
- entryPt = p;
2804
- }
2805
- const entry = clampFocus(entryPt.nx, entryPt.ny, level, layout);
2912
+ const entry = clampFocus(span.cx, span.cy, level, layout, camera);
2806
2913
  const thrX = safeRatio * layout.W / (2 * level * layout.dw);
2807
2914
  const thrY = safeRatio * layout.H / (2 * level * layout.dh);
2808
2915
  const events = [];
@@ -2815,7 +2922,7 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
2815
2922
  if (p.t < nextAllowed) continue;
2816
2923
  if (Math.abs(p.nx - cx) > thrX || Math.abs(p.ny - cy) > thrY) {
2817
2924
  const target = lookahead > 0 ? sampleAt2(pts, Math.min(p.t + lookahead, span.out)) : p;
2818
- const f = clampFocus(target.nx, target.ny, level, layout);
2925
+ const f = clampFocus(target.nx, target.ny, level, layout, camera);
2819
2926
  if (Math.abs(f.cx - cx) < 1e-3 && Math.abs(f.cy - cy) < 1e-3) continue;
2820
2927
  events.push({ t: round2(p.t), cx: round2(f.cx), cy: round2(f.cy) });
2821
2928
  cx = f.cx;
@@ -5819,9 +5926,28 @@ var ON_FRAME = `(ctx, content, dt) => {
5819
5926
  // ceZs = the zoom transform's device scale, for the two values below that
5820
5927
  // are sized in DEVICE pixels inside a scaled user space.
5821
5928
  var ceZs = 1
5929
+ // The camera model (cfr.camera; MIRRORS layout.ts zoomView \u2014 change them
5930
+ // together). The magnifier scales the card about the focus, which stays
5931
+ // where it is on screen; the stage camera also slides the card so the
5932
+ // focus lands at the frame's centre, blended in over CAMERA_CENTRE_RAMP
5933
+ // of level (a smoothstep, 0 at level 1) so a ramp through the band slides
5934
+ // without a kink, and past the card's edge the ground shows. wcx/wcy is
5935
+ // the content point at the frame's centre under either model; the two
5936
+ // branches are the same map, the magnifier's kept in its old float order
5937
+ // so every existing document renders byte-identically.
5938
+ var camStage = cfr.camera === 'stage'
5939
+ var fx = dx + zx * dw, fy = dy + zy * dh
5940
+ var wcx = fx, wcy = fy
5822
5941
  if (lvl > 1.001 && !d.zoomSuppressed) {
5823
- var fx = dx + zx * dw, fy = dy + zy * dh
5824
- c.translate(fx, fy); c.scale(lvl, lvl); c.translate(-fx, -fy)
5942
+ if (camStage) {
5943
+ var ctu = Math.max(0, Math.min(1, (lvl - 1) / ${CAMERA_CENTRE_RAMP}))
5944
+ var ctt = ctu * ctu * (3 - 2 * ctu)
5945
+ wcx = fx + ((W / 2 - fx) / lvl) * (1 - ctt)
5946
+ wcy = fy + ((H / 2 - fy) / lvl) * (1 - ctt)
5947
+ c.translate(W / 2, H / 2); c.scale(lvl, lvl); c.translate(-wcx, -wcy)
5948
+ } else {
5949
+ c.translate(fx, fy); c.scale(lvl, lvl); c.translate(-fx, -fy)
5950
+ }
5825
5951
  ceZs = lvl
5826
5952
  }
5827
5953
  // The card's shadows are cast by a body drawn OFF-CANVAS and brought back
@@ -5963,6 +6089,34 @@ var ON_FRAME = `(ctx, content, dt) => {
5963
6089
  // cursor coordinate space + drawn radius (shared by click effects + the dot)
5964
6090
  var space = (actM ? actM.cursorSpace : d.cursorSpace) || { w: vw, h: vh }
5965
6091
  var curSize = ((d.cursorStyle && d.cursorStyle.size) || 24) * s2 * 0.5
6092
+ // The pointer's shape: an OS-style arrow (tip on the recorded point, a
6093
+ // black body with a white edge, the shape of the pointer on the viewer's
6094
+ // own screen) or the white dot every earlier take was cut with. One
6095
+ // painter for both draw sites (in the card, and the ghost across a
6096
+ // boundary); r is the dot's radius, so an arrow stands 2r tall.
6097
+ var curArrow = !!(d.cursorStyle && d.cursorStyle.style === 'arrow')
6098
+ function drawCur(cc, x, y, r, a, lw) {
6099
+ cc.save()
6100
+ if (curArrow) {
6101
+ var h = 2 * r
6102
+ 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]]
6103
+ cc.beginPath()
6104
+ cc.moveTo(x, y)
6105
+ for (var pi = 1; pi < P.length; pi++) cc.lineTo(x + P[pi][0] * h, y + P[pi][1] * h)
6106
+ cc.closePath()
6107
+ cc.lineJoin = 'round'
6108
+ cc.fillStyle = 'rgba(0,0,0,' + (0.92 * a) + ')'
6109
+ cc.strokeStyle = 'rgba(255,255,255,' + (0.95 * a) + ')'
6110
+ cc.lineWidth = Math.max(1, lw * 0.75)
6111
+ cc.fill(); cc.stroke()
6112
+ } else {
6113
+ cc.fillStyle = 'rgba(255,255,255,' + (0.95 * a) + ')'
6114
+ cc.strokeStyle = 'rgba(0,0,0,' + (0.4 * a) + ')'
6115
+ cc.lineWidth = lw
6116
+ cc.beginPath(); cc.arc(x, y, r, 0, Math.PI * 2); cc.fill(); cc.stroke()
6117
+ }
6118
+ cc.restore()
6119
+ }
5966
6120
 
5967
6121
  // click effects \u2014 pure f(t): d.clicks are
5968
6122
  // OUTPUT-anchored records baked at lowering (sorted by ot; re-baked on every
@@ -6136,16 +6290,14 @@ var ON_FRAME = `(ctx, content, dt) => {
6136
6290
  // moving: each pass leaves its point, mapped out of the zoom.
6137
6291
  if (trOut) {
6138
6292
  var tqX = ax, tqY = ay
6139
- if (lvl > 1.001 && !d.zoomSuppressed) { tqX = fx + (ax - fx) * lvl; tqY = fy + (ay - fy) * lvl }
6293
+ if (lvl > 1.001 && !d.zoomSuppressed) {
6294
+ if (camStage) { tqX = W / 2 + (ax - wcx) * lvl; tqY = H / 2 + (ay - wcy) * lvl }
6295
+ else { tqX = fx + (ax - fx) * lvl; tqY = fy + (ay - fy) * lvl }
6296
+ }
6140
6297
  if (tpG) trQa = [tqX, tqY]
6141
6298
  else { trQb = [tqX, tqY]; trQs = curSize * ckPress; trQw = s2 }
6142
6299
  } else if (cuA > 0.01) {
6143
- c.save()
6144
- c.fillStyle = 'rgba(255,255,255,' + (0.95 * cuA) + ')'
6145
- c.strokeStyle = 'rgba(0,0,0,' + (0.4 * cuA) + ')'
6146
- c.lineWidth = 2 * s2
6147
- c.beginPath(); c.arc(ax, ay, curSize * ckPress, 0, Math.PI * 2); c.fill(); c.stroke()
6148
- c.restore()
6300
+ drawCur(c, ax, ay, curSize * ckPress, cuA, 2 * s2)
6149
6301
  }
6150
6302
  }
6151
6303
  if (fitCover) c.restore()
@@ -6230,12 +6382,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6230
6382
  var tqBx = (tqB[0] - W / 2) * trSc + W / 2 + trOx * W, tqBy = (tqB[1] - H / 2) * trSc + H / 2 - trOy * H
6231
6383
  var tqX2 = tqAx + (tqBx - tqAx) * tqP, tqY2 = tqAy + (tqBy - tqAy) * tqP
6232
6384
  var tqR = trQs || (((d.cursorStyle && d.cursorStyle.size) || 24) * s * 0.5)
6233
- ovC.save()
6234
- ovC.fillStyle = 'rgba(255,255,255,0.95)'
6235
- ovC.strokeStyle = 'rgba(0,0,0,0.4)'
6236
- ovC.lineWidth = 2 * (trQw || s)
6237
- ovC.beginPath(); ovC.arc(tqX2, tqY2, tqR, 0, Math.PI * 2); ovC.fill(); ovC.stroke()
6238
- ovC.restore()
6385
+ drawCur(ovC, tqX2, tqY2, tqR, 1, 2 * (trQw || s))
6239
6386
  }
6240
6387
  ov.sig = ovSig
6241
6388
  ov.active = camActiveNow || !!trOut
@@ -6574,6 +6721,7 @@ function lowerToComposition(input) {
6574
6721
  const layout = docCardLayout(doc);
6575
6722
  const meta = doc.source.meta;
6576
6723
  const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
6724
+ const camera = cameraModel(doc.frame);
6577
6725
  const zoomSpans = restSpansThroughTransitions(
6578
6726
  doc.zoom,
6579
6727
  rated,
@@ -6589,13 +6737,17 @@ function lowerToComposition(input) {
6589
6737
  {
6590
6738
  safeRatio: zoomStyle.followSafeRatio,
6591
6739
  recenter: zoomStyle.followRecenter,
6592
- lookahead: zoomStyle.followLookahead
6740
+ lookahead: zoomStyle.followLookahead,
6741
+ camera
6593
6742
  }
6594
6743
  );
6595
6744
  if (f.entry)
6596
6745
  return { ...z, cx: f.entry.cx, cy: f.entry.cy, followEvents: f.events };
6597
6746
  }
6598
- return { ...z, ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout) };
6747
+ return {
6748
+ ...z,
6749
+ ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout, camera)
6750
+ };
6599
6751
  });
6600
6752
  const data = {
6601
6753
  videoSrc: doc.source.videoKey,
@@ -6962,14 +7114,15 @@ function momentsFromDoc(doc, plan, opts = {}) {
6962
7114
  rect: null
6963
7115
  });
6964
7116
  if (track.length) {
6965
- const { sessions, clusters } = groupTrack(track, {
7117
+ const { sessions, clusters, surfaces } = groupTrack(track, {
6966
7118
  width,
6967
7119
  height,
6968
7120
  clusterGap: style.clusterGap,
6969
7121
  typingGap: style.typingGap,
6970
- typingZoom: style.typingZoom
7122
+ typingZoom: style.typingZoom,
7123
+ targetFill: style.targetFill
6971
7124
  });
6972
- for (const c of clusters) {
7125
+ const pressed = (c, surface) => {
6973
7126
  const f = clusterFocus(c, width, height);
6974
7127
  drafts.push({
6975
7128
  kind: "click",
@@ -6980,9 +7133,12 @@ function momentsFromDoc(doc, plan, opts = {}) {
6980
7133
  at: c[0].t,
6981
7134
  focus: { cx: f.cx, cy: f.cy },
6982
7135
  rect: f.rect,
6983
- clicks: c.length
7136
+ clicks: c.length,
7137
+ ...surface ? { surface: true } : {}
6984
7138
  });
6985
- }
7139
+ };
7140
+ for (const c of clusters) pressed(c, false);
7141
+ for (const c of surfaces) pressed(c, true);
6986
7142
  for (const s of sessions) {
6987
7143
  const f = clusterFocus(s.events, width, height);
6988
7144
  drafts.push({
@@ -7102,6 +7258,7 @@ function momentsFromDoc(doc, plan, opts = {}) {
7102
7258
  h: round8(d.rect.h)
7103
7259
  } : null,
7104
7260
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
7261
+ ...d.surface ? { surface: true } : {},
7105
7262
  ...d.pings !== void 0 ? { pings: d.pings } : {},
7106
7263
  activity: activityOf(opts.bins, d),
7107
7264
  proposed,
@@ -7146,15 +7303,14 @@ function sceneChanges(bins, opts = {}) {
7146
7303
  }
7147
7304
 
7148
7305
  // src/digest/framing.ts
7149
- function zoomWindow(span, layout) {
7306
+ function zoomWindow(span, layout, camera = "card") {
7150
7307
  const L = Math.max(1, span.level);
7151
- const { cx, cy } = clampFocus(span.cx, span.cy, L, layout);
7152
- const fx = layout.dx + cx * layout.dw;
7153
- const fy = layout.dy + cy * layout.dh;
7154
- const px0 = fx - fx / L;
7155
- const px1 = fx + (layout.W - fx) / L;
7156
- const py0 = fy - fy / L;
7157
- const py1 = fy + (layout.H - fy) / L;
7308
+ const { cx, cy } = clampFocus(span.cx, span.cy, L, layout, camera);
7309
+ const v = zoomViewport(L, cx, cy, layout, camera);
7310
+ const px0 = v.x * layout.W;
7311
+ const px1 = (v.x + v.w) * layout.W;
7312
+ const py0 = v.y * layout.H;
7313
+ const py1 = (v.y + v.h) * layout.H;
7158
7314
  return {
7159
7315
  x0: (px0 - layout.dx) / layout.dw,
7160
7316
  x1: (px1 - layout.dx) / layout.dw,
@@ -7162,9 +7318,9 @@ function zoomWindow(span, layout) {
7162
7318
  y1: (py1 - layout.dy) / layout.dh
7163
7319
  };
7164
7320
  }
7165
- function zoomCoversRect(span, rect, layout, tol = 0.02) {
7321
+ function zoomCoversRect(span, rect, layout, tol = 0.02, camera = "card") {
7166
7322
  if (span.level <= 1.001) return true;
7167
- const w = zoomWindow(span, layout);
7323
+ const w = zoomWindow(span, layout, camera);
7168
7324
  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;
7169
7325
  }
7170
7326
 
@@ -9782,6 +9938,8 @@ export {
9782
9938
  AUDIO_PLAN_STEP,
9783
9939
  BACKGROUND_Z,
9784
9940
  BASE_FRAME_STYLE,
9941
+ CAMERA_CENTRE_RAMP,
9942
+ CAMERA_COVER_MIN,
9785
9943
  CAMERA_FAR,
9786
9944
  CAMERA_NEAR,
9787
9945
  CAM_CHAIN_GAP,
@@ -9948,6 +10106,8 @@ export {
9948
10106
  camRectFromPose,
9949
10107
  camRestPose,
9950
10108
  camTrackFromDoc,
10109
+ cameraCentring,
10110
+ cameraModel,
9951
10111
  cardEnter,
9952
10112
  cardExit,
9953
10113
  cardFields,
@@ -10002,6 +10162,7 @@ export {
10002
10162
  exportSizeFor,
10003
10163
  extractClicks,
10004
10164
  focusBounds,
10165
+ focusForViewportCentre,
10005
10166
  followFocusEvents,
10006
10167
  frameGeometry,
10007
10168
  freezeLane,
@@ -10129,6 +10290,8 @@ export {
10129
10290
  zoomLane,
10130
10291
  zoomSpanForRange,
10131
10292
  zoomTrackFromDoc,
10293
+ zoomView,
10294
+ zoomViewport,
10132
10295
  zoomWindow
10133
10296
  };
10134
10297
  //# sourceMappingURL=index.js.map