@vosjs/studio-core 0.33.3 → 0.35.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
@@ -1420,7 +1420,7 @@ function docStillTime(doc) {
1420
1420
  }
1421
1421
  }
1422
1422
  if (!(footage > 0)) return r3(clampOut(STILL_DEFAULT));
1423
- let hero = entranceSeconds(cardEnter(doc.frame));
1423
+ let hero = entranceEnd(cardEnter(doc.frame));
1424
1424
  for (const clip of doc.overlays ?? []) {
1425
1425
  if (clip.start > OPENING_WINDOW) continue;
1426
1426
  const step = enterOf(clip.anim);
@@ -1488,6 +1488,14 @@ function entranceSeconds(e) {
1488
1488
  if (!e || e.kind === "none") return 0;
1489
1489
  return Math.max(0.2, Math.min(3, e.seconds ?? ENTRANCE_SECONDS));
1490
1490
  }
1491
+ function entranceAt(e) {
1492
+ if (!entranceSeconds(e)) return 0;
1493
+ const at = e?.at;
1494
+ return typeof at === "number" && at > 0 ? Math.min(30, at) : 0;
1495
+ }
1496
+ function entranceEnd(e) {
1497
+ return entranceAt(e) + entranceSeconds(e);
1498
+ }
1491
1499
  function exitSeconds(e) {
1492
1500
  if (!e || e.kind === "none") return 0;
1493
1501
  const house = e.kind === "fade" ? CARD_FADE_SECONDS : END_CARD_RECEDE;
@@ -1551,22 +1559,26 @@ function withHolds(docSegments, rated) {
1551
1559
  function entranceTiltKeyframes(e) {
1552
1560
  const s = entranceSeconds(e);
1553
1561
  if (!s || e?.kind !== "tilt-in") return [];
1562
+ const a = entranceAt(e);
1554
1563
  return [
1555
1564
  { t: 0, value: [...TILT_IN_POSE], ease: "none" },
1556
- { t: s, value: [0, 0], ease: "power3.out" }
1565
+ ...a ? [{ t: a, value: [...TILT_IN_POSE], ease: "none" }] : [],
1566
+ { t: a + s, value: [0, 0], ease: "power3.out" }
1557
1567
  ];
1558
1568
  }
1559
1569
  function entranceZoomKeyframes(e) {
1560
1570
  const s = entranceSeconds(e);
1561
1571
  if (!s) return [];
1572
+ const a = entranceAt(e);
1562
1573
  if (e?.kind === "pull-out")
1563
1574
  return [
1564
1575
  { t: 0, value: [PULL_OUT_LEVEL, 0.5, 0.42], ease: "none" },
1565
- { t: s, value: [1, 0.5, 0.5], ease: "power3.out" }
1576
+ ...a ? [{ t: a, value: [PULL_OUT_LEVEL, 0.5, 0.42], ease: "none" }] : [],
1577
+ { t: a + s, value: [1, 0.5, 0.5], ease: "power3.out" }
1566
1578
  ];
1567
1579
  return [
1568
1580
  { t: 0, value: [1, 0.5, 0.5], ease: "none" },
1569
- { t: s, value: [1, 0.5, 0.5], ease: "none" }
1581
+ { t: a + s, value: [1, 0.5, 0.5], ease: "none" }
1570
1582
  ];
1571
1583
  }
1572
1584
  function prependEntrance(track, head) {
@@ -1582,15 +1594,18 @@ var RECEDE_MID = [0.91, -0.018, 0.8];
1582
1594
  function cardPoseTrack(enter, exit, footageEnd, outputEnd2 = footageEnd) {
1583
1595
  const keyframes = [];
1584
1596
  const s = entranceSeconds(enter);
1597
+ const a = entranceAt(enter);
1585
1598
  if (s) {
1586
1599
  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];
1600
+ if (a) from[2] = 0;
1587
1601
  keyframes.push({ t: 0, value: from, ease: "none" });
1588
- keyframes.push({ t: s, value: [1, 0, 1], ease: "power3.out" });
1602
+ if (a) keyframes.push({ t: a, value: [...from], ease: "none" });
1603
+ keyframes.push({ t: a + s, value: [1, 0, 1], ease: "power3.out" });
1589
1604
  }
1590
1605
  const x = exitSeconds(exit);
1591
1606
  let last = [1, 0, 1];
1592
1607
  if (x > 0 && footageEnd > 0) {
1593
- const t0 = round3(Math.max(s, footageEnd - x));
1608
+ const t0 = round3(Math.max(a + s, footageEnd - x));
1594
1609
  const t1 = round3(Math.max(t0 + 0.05, footageEnd));
1595
1610
  last = exitPose(exit);
1596
1611
  keyframes.push({ t: t0, value: [1, 0, 1], ease: "none" });
@@ -1812,6 +1827,7 @@ function liveZoomStyleName(name) {
1812
1827
  if (name in ZOOM_STYLES) return name;
1813
1828
  return RETIRED_ZOOM_STYLES[name]?.style ?? null;
1814
1829
  }
1830
+ var HOLD_DRIFT_MAX = 0.15;
1815
1831
  var PUMP_REST = 1;
1816
1832
  function pumpFreeChainGap(p) {
1817
1833
  return p.rampOut + (p.rampIn - p.rampInOverlap) + PUMP_REST;
@@ -2685,7 +2701,10 @@ function camBubbleRect(cam, W, H = 1080) {
2685
2701
  radius: cam.shape === "rounded" ? (cam.radius ?? 18) * s : size / 2
2686
2702
  };
2687
2703
  }
2688
- function focusBounds(level, layout, camera = "card") {
2704
+ function focusBounds(level, layout, camera = "card", screen) {
2705
+ if (camera === "stage" && screen && level > 1.001) {
2706
+ return { minX: 0, maxX: 1, minY: 0, maxY: 1 };
2707
+ }
2689
2708
  if (level <= 1.001) {
2690
2709
  return { minX: 0.5, maxX: 0.5, minY: 0.5, maxY: 0.5 };
2691
2710
  }
@@ -2748,8 +2767,8 @@ function stageAxisBounds(anchorOff, anchorSize, coverOff, coverSize, viewport, l
2748
2767
  }
2749
2768
  return { min: clamp013(lo), max: clamp013(hi) };
2750
2769
  }
2751
- function clampFocus(cx, cy, level, layout, camera = "card") {
2752
- const b = focusBounds(level, layout, camera);
2770
+ function clampFocus(cx, cy, level, layout, camera = "card", screen) {
2771
+ const b = focusBounds(level, layout, camera, screen);
2753
2772
  return {
2754
2773
  cx: Math.min(b.maxX, Math.max(b.minX, cx)),
2755
2774
  cy: Math.min(b.maxY, Math.max(b.minY, cy))
@@ -2758,6 +2777,9 @@ function clampFocus(cx, cy, level, layout, camera = "card") {
2758
2777
  function clamp013(v) {
2759
2778
  return Math.max(0, Math.min(1, v));
2760
2779
  }
2780
+ function screenPoint(layout, camera, screen) {
2781
+ return camera === "stage" && screen ? { ox: screen.x * layout.W, oy: screen.y * layout.H } : { ox: layout.W / 2, oy: layout.H / 2 };
2782
+ }
2761
2783
  function cameraModel(frame) {
2762
2784
  return frame?.camera === "stage" ? "stage" : "card";
2763
2785
  }
@@ -2767,34 +2789,36 @@ function cameraCentring(level) {
2767
2789
  const u = clamp013((level - 1) / CAMERA_CENTRE_RAMP);
2768
2790
  return u * u * (3 - 2 * u);
2769
2791
  }
2770
- function zoomView(level, cx, cy, layout, camera = "card", centring) {
2792
+ function zoomView(level, cx, cy, layout, camera = "card", centring, screen) {
2771
2793
  const L = Math.max(1, level);
2772
2794
  const fx = layout.dx + cx * layout.dw;
2773
2795
  const fy = layout.dy + cy * layout.dh;
2774
- const ox = layout.W / 2;
2775
- const oy = layout.H / 2;
2796
+ const { ox, oy } = screenPoint(layout, camera, screen);
2776
2797
  const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
2777
2798
  const wcx = fx + (ox - fx) / L * (1 - t);
2778
2799
  const wcy = fy + (oy - fy) / L * (1 - t);
2779
2800
  return { level: L, fx, fy, wcx, wcy, ox, oy };
2780
2801
  }
2781
- function zoomViewport(level, cx, cy, layout, camera = "card", centring) {
2782
- const v = zoomView(level, cx, cy, layout, camera, centring);
2802
+ function zoomViewport(level, cx, cy, layout, camera = "card", centring, screen) {
2803
+ const v = zoomView(level, cx, cy, layout, camera, centring, screen);
2783
2804
  const size = 1 / v.level;
2805
+ const mx = v.wcx + (layout.W / 2 - v.ox) / v.level;
2806
+ const my = v.wcy + (layout.H / 2 - v.oy) / v.level;
2784
2807
  return {
2785
- x: (v.wcx - layout.W / (2 * v.level)) / layout.W,
2786
- y: (v.wcy - layout.H / (2 * v.level)) / layout.H,
2808
+ x: (mx - layout.W / (2 * v.level)) / layout.W,
2809
+ y: (my - layout.H / (2 * v.level)) / layout.H,
2787
2810
  w: size,
2788
2811
  h: size
2789
2812
  };
2790
2813
  }
2791
- function focusForViewportCentre(cX, cY, level, layout, camera = "card", centring) {
2814
+ function focusForViewportCentre(cX, cY, level, layout, camera = "card", centring, screen) {
2792
2815
  const L = Math.max(1, level);
2793
2816
  const t = camera === "stage" ? clamp013(centring ?? 1) : 0;
2794
2817
  const a = 1 - (1 - t) / L;
2795
2818
  if (a <= 1e-6) return { cx: 0.5, cy: 0.5 };
2796
- const fx = (cX * layout.W - layout.W / 2 * (1 - t) / L) / a;
2797
- const fy = (cY * layout.H - layout.H / 2 * (1 - t) / L) / a;
2819
+ const { ox, oy } = screenPoint(layout, camera, screen);
2820
+ const fx = (cX * layout.W - ox * (1 - t) / L - (layout.W / 2 - ox) / L) / a;
2821
+ const fy = (cY * layout.H - oy * (1 - t) / L - (layout.H / 2 - oy) / L) / a;
2798
2822
  return { cx: (fx - layout.dx) / layout.dw, cy: (fy - layout.dy) / layout.dh };
2799
2823
  }
2800
2824
  function levelForFocusFraction(frac) {
@@ -3321,14 +3345,17 @@ function overlayFaceFor(clip) {
3321
3345
  function overlayFontFaces(doc) {
3322
3346
  const faces = [...OVERLAY_FONT_FACES];
3323
3347
  const seen = new Set(faces.map((f) => `${f.family}|${f.weight}`));
3324
- for (const o of doc.overlays ?? []) {
3325
- if (o.kind !== "text") continue;
3326
- const face = overlayFaceFor(o);
3327
- if (!face) continue;
3348
+ const add = (face) => {
3349
+ if (!face) return;
3328
3350
  const key = `${face.family}|${face.weight}`;
3329
- if (seen.has(key)) continue;
3351
+ if (seen.has(key)) return;
3330
3352
  seen.add(key);
3331
3353
  faces.push(face);
3354
+ };
3355
+ for (const o of doc.overlays ?? []) {
3356
+ if (o.kind !== "text") continue;
3357
+ add(overlayFaceFor(o));
3358
+ add(resolveEmphasis(o)?.face);
3332
3359
  }
3333
3360
  return faces;
3334
3361
  }
@@ -3336,6 +3363,102 @@ function overlayLines(text) {
3336
3363
  const lines = text.split("\n");
3337
3364
  return lines.length ? lines : [""];
3338
3365
  }
3366
+ var EM_OPEN = "";
3367
+ var EM_CLOSE = "";
3368
+ function parseEmphasis(text) {
3369
+ let any = false;
3370
+ const lines = text.split("\n").map((line) => {
3371
+ const chars = [];
3372
+ const bold = [];
3373
+ const marker = [];
3374
+ for (let i = 0; i < line.length; i++) {
3375
+ if (line[i] === "\\" && line[i + 1] === "*") {
3376
+ chars.push("*");
3377
+ marker.push(false);
3378
+ i++;
3379
+ } else {
3380
+ chars.push(line[i]);
3381
+ marker.push(line[i] === "*");
3382
+ }
3383
+ }
3384
+ const keep = chars.map(() => true);
3385
+ for (let i = 0; i < chars.length; i++) {
3386
+ if (!marker[i]) continue;
3387
+ let j = i + 1;
3388
+ while (j < chars.length && !marker[j]) j++;
3389
+ if (j >= chars.length) break;
3390
+ const inner = chars.slice(i + 1, j).join("");
3391
+ if (!inner.trim()) {
3392
+ i = j - 1;
3393
+ continue;
3394
+ }
3395
+ keep[i] = false;
3396
+ keep[j] = false;
3397
+ for (let k = i + 1; k < j; k++) bold[k] = true;
3398
+ any = true;
3399
+ i = j;
3400
+ }
3401
+ const kept = [];
3402
+ for (let i = 0; i < chars.length; i++) {
3403
+ if (keep[i])
3404
+ kept.push({ c: chars[i], b: !!bold[i] && !/\s/.test(chars[i]) });
3405
+ }
3406
+ let out = "";
3407
+ for (let i = 0; i < kept.length; i++) {
3408
+ const { c, b } = kept[i];
3409
+ if (b && !(i > 0 && kept[i - 1].b)) out += EM_OPEN;
3410
+ out += c;
3411
+ if (b && !(i + 1 < kept.length && kept[i + 1].b)) out += EM_CLOSE;
3412
+ }
3413
+ return out;
3414
+ });
3415
+ return any ? lines.join("\n") : null;
3416
+ }
3417
+ function overlayDisplayText(clip) {
3418
+ if (!clip.emphasis) return clip.text;
3419
+ return parseEmphasis(clip.text) ?? clip.text;
3420
+ }
3421
+ function stripEmphasis(text) {
3422
+ return text.split(EM_OPEN).join("").split(EM_CLOSE).join("");
3423
+ }
3424
+ var EMPHASIS_WEIGHT = 700;
3425
+ function resolveEmphasis(clip) {
3426
+ if (!clip.emphasis || parseEmphasis(clip.text) === null) return null;
3427
+ const style = resolveOverlayStyle(clip);
3428
+ const entry = findFontFamily2(
3429
+ clip.family ?? PRESET_FAMILY[clip.preset in TEXT_PRESETS ? clip.preset : "title"]
3430
+ );
3431
+ const wanted = clip.emphasis?.weight ?? Math.max(EMPHASIS_WEIGHT, style.weight);
3432
+ const weight = entry ? nearestFontWeight2(entry, wanted) : wanted;
3433
+ const inBase = OVERLAY_FONT_FACES.some(
3434
+ (f) => entry && f.family === entry.family && f.weight === weight
3435
+ );
3436
+ return {
3437
+ weight,
3438
+ ...clip.emphasis?.color ? { color: clip.emphasis.color } : {},
3439
+ ...entry && !inBase ? {
3440
+ face: {
3441
+ family: entry.family,
3442
+ weight,
3443
+ url: fontFaceUrl2(entry.slug, weight)
3444
+ }
3445
+ } : {}
3446
+ };
3447
+ }
3448
+ function measureEmphasized(text, measure, font, emFont, bold = false) {
3449
+ let w = 0;
3450
+ let run = "";
3451
+ let b = bold;
3452
+ for (const c of text) {
3453
+ if (c === EM_OPEN || c === EM_CLOSE) {
3454
+ if (run) w += measure(run, b ? emFont : font);
3455
+ run = "";
3456
+ b = c === EM_OPEN;
3457
+ } else run += c;
3458
+ }
3459
+ if (run) w += measure(run, b ? emFont : font);
3460
+ return w;
3461
+ }
3339
3462
  function overlayTokens(line) {
3340
3463
  return line.match(/\S+\s*/g) ?? [line];
3341
3464
  }
@@ -3386,9 +3509,10 @@ function resolveOverlayFx(clip, clipDuration) {
3386
3509
  const spec = overlayFxSpec(clip) ?? clip.fx ?? null;
3387
3510
  if (!spec) return null;
3388
3511
  const unit = spec.unit ?? "block";
3389
- const units = overlaySegments(clip.text, unit);
3512
+ const text = overlayDisplayText(clip);
3513
+ const units = overlaySegments(text, unit);
3390
3514
  const n = unit === "block" ? 1 : units.reduce((sum, line) => sum + line.length, 0);
3391
- const upperN = unit === "line" && clip.maxWidth ? overlayLines(clip.text).reduce(
3515
+ const upperN = unit === "line" && clip.maxWidth ? overlayLines(text).reduce(
3392
3516
  (sum, line) => sum + overlayTokens(line).length,
3393
3517
  0
3394
3518
  ) : n;
@@ -3435,13 +3559,13 @@ function overlayRect(clip, measure, frameW, frameH = 1080, mediaAspect) {
3435
3559
  const style = resolveOverlayStyle(clip);
3436
3560
  const font = overlayFontString(style, scale, 1);
3437
3561
  const ls = style.letterSpacing * scale;
3438
- const lines = clip.maxWidth ? wrapOverlayLines(
3439
- overlayLines(clip.text),
3440
- (t) => measure(t, font, ls),
3441
- clip.maxWidth * frameW
3442
- ) : overlayLines(clip.text);
3562
+ const em = resolveEmphasis(clip);
3563
+ const emFont = em ? overlayFontString({ ...style, weight: em.weight }, scale, 1) : "";
3564
+ const width = (t) => em ? measureEmphasized(t, (run, f) => measure(run, f, ls), font, emFont) : measure(t, font, ls);
3565
+ const text = overlayDisplayText(clip);
3566
+ const lines = clip.maxWidth ? wrapOverlayLines(overlayLines(text), width, clip.maxWidth * frameW) : overlayLines(text);
3443
3567
  let w = 0;
3444
- for (const line of lines) w = Math.max(w, measure(line, font, ls));
3568
+ for (const line of lines) w = Math.max(w, width(line));
3445
3569
  const lineH = style.size * scale * style.lineHeight;
3446
3570
  const box = resolveOverlayBox(clip);
3447
3571
  const padX = box ? box.padX * scale : 0;
@@ -3590,7 +3714,7 @@ function docTransitions(doc) {
3590
3714
  if (open?.kind === "slide" && extents.length) {
3591
3715
  const first = extents[0];
3592
3716
  const m = move(open, "enter", Math.max(0, first.end - first.start) / 2);
3593
- if (m) out.push({ t: 0, d: m.d, in: m });
3717
+ if (m) out.push({ t: round32(entranceAt(open)), d: m.d, in: m });
3594
3718
  }
3595
3719
  for (let i = 0; i + 1 < segments.length; i++) {
3596
3720
  const a = segments[i];
@@ -3947,14 +4071,16 @@ function pinRectOnScreen(rect, t, layout, camera, zoomTrack) {
3947
4071
  let cx = 0.5;
3948
4072
  let cy = 0.5;
3949
4073
  let centring;
4074
+ let screen;
3950
4075
  if (zoomTrack && zoomTrack.keyframes.length) {
3951
4076
  const z = sample(zoomTrack, t, lerpArray);
3952
4077
  level = z[0];
3953
4078
  cx = z[1];
3954
4079
  cy = z[2];
3955
4080
  if (z.length > 3) centring = z[3];
4081
+ if (z.length > 5) screen = { x: z[4], y: z[5] };
3956
4082
  }
3957
- const v = zoomView(level, cx, cy, layout, camera, centring);
4083
+ const v = zoomView(level, cx, cy, layout, camera, centring, screen);
3958
4084
  const map = (nx, ny) => {
3959
4085
  const px = layout.dx + nx * layout.dw;
3960
4086
  const py = layout.dy + ny * layout.dh;
@@ -4996,6 +5122,19 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
4996
5122
  } catch (e) {}
4997
5123
  }
4998
5124
  }
5125
+ // The emphasis weight's face, the same way (a live edit that adds the
5126
+ // first *marked* word paints without a LOAD).
5127
+ if (ol.em && ol.em.face) {
5128
+ var oeKey = ol.em.face.f + '|' + ol.em.face.w
5129
+ if (!ofSet[oeKey]) {
5130
+ ofSet[oeKey] = 1
5131
+ try {
5132
+ var oeFace = new FontFace(ol.em.face.f, 'url(' + ol.em.face.u + ')', { weight: String(ol.em.face.w) })
5133
+ document.fonts.add(oeFace)
5134
+ oeFace.load().catch(function () {})
5135
+ } catch (e) {}
5136
+ }
5137
+ }
4999
5138
  }
5000
5139
  ovC.save()
5001
5140
  ovC.globalAlpha = olA
@@ -5011,6 +5150,69 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5011
5150
  ovC.textAlign = 'center'
5012
5151
  ovC.textBaseline = 'middle'
5013
5152
  ovC.letterSpacing = ((ol.ls || 0) * olMS * s) + 'px'
5153
+ // Emphasis (ol.em, baked only when the text marks a word): the lines
5154
+ // carry \\u0001/\\u0002 around every emphasized word, and the normal and
5155
+ // emphasis fonts alternate at them, run by run. MIRRORS measureEmphasized
5156
+ // in overlayText.ts \u2014 change together. A clip without ol.em never reaches
5157
+ // these helpers: it keeps its plain measureText/fillText calls.
5158
+ var olEm = ol.em || null
5159
+ var olFN = ovC.font
5160
+ var olFB = olEm ? (ol.sty ? ol.sty + ' ' : '') + olEm.w + ' ' + olPx + 'px ' + ol.stack : ''
5161
+ var olMeasure = function (t, b0) {
5162
+ if (!olEm) return ovC.measureText(t).width
5163
+ var mw = 0, mrun = '', mb = !!b0
5164
+ for (var mi = 0; mi < t.length; mi++) {
5165
+ var mc = t.charAt(mi)
5166
+ if (mc === '\\u0001' || mc === '\\u0002') {
5167
+ if (mrun) { ovC.font = mb ? olFB : olFN; mw += ovC.measureText(mrun).width }
5168
+ mrun = ''
5169
+ mb = mc === '\\u0001'
5170
+ } else mrun += mc
5171
+ }
5172
+ if (mrun) { ovC.font = mb ? olFB : olFN; mw += ovC.measureText(mrun).width }
5173
+ ovC.font = olFN
5174
+ return mw
5175
+ }
5176
+ // The emphasis state after a run of text (a char unit starts in it).
5177
+ var olStateAfter = function (t, b0) {
5178
+ var sb = !!b0
5179
+ for (var si = 0; si < t.length; si++) {
5180
+ var sc = t.charAt(si)
5181
+ if (sc === '\\u0001') sb = true
5182
+ else if (sc === '\\u0002') sb = false
5183
+ }
5184
+ return sb
5185
+ }
5186
+ // Draw t LEFT-aligned from x, run by run: the font switches at the
5187
+ // marks, an emphasized run takes ol.em.c when set, stroke under fill.
5188
+ var olDraw = function (t, x, y, b0) {
5189
+ var drun = '', db = !!b0, dx = x
5190
+ var dflush = function () {
5191
+ if (!drun) return
5192
+ ovC.font = db ? olFB : olFN
5193
+ var dfs = ovC.fillStyle
5194
+ if (db && olEm.c) ovC.fillStyle = olEm.c
5195
+ if (ol.stroke) {
5196
+ ovC.strokeStyle = ol.stroke.c
5197
+ ovC.lineWidth = ol.stroke.w * olMS * s
5198
+ ovC.lineJoin = 'round'
5199
+ ovC.strokeText(drun, dx, y)
5200
+ }
5201
+ ovC.fillText(drun, dx, y)
5202
+ dx += ovC.measureText(drun).width
5203
+ ovC.fillStyle = dfs
5204
+ drun = ''
5205
+ }
5206
+ for (var di = 0; di < t.length; di++) {
5207
+ var dc = t.charAt(di)
5208
+ if (dc === '\\u0001' || dc === '\\u0002') {
5209
+ dflush()
5210
+ db = dc === '\\u0001'
5211
+ } else drun += dc
5212
+ }
5213
+ dflush()
5214
+ ovC.font = olFN
5215
+ }
5014
5216
  var olLines = ol.lines || ['']
5015
5217
  // maxWidth wrap (ol.mw = frame-width fraction): greedy over word tokens
5016
5218
  // (/\\S+\\s*/ \u2014 the SAME tokenization fx uses, trailing spaces kept, so
@@ -5022,7 +5224,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5022
5224
  var olWrapped = []
5023
5225
  for (var olwl = 0; olwl < olLines.length; olwl++) {
5024
5226
  var olWLine = olLines[olwl]
5025
- if (!olWLine || ovC.measureText(olWLine).width <= olWMax) {
5227
+ if (!olWLine || olMeasure(olWLine) <= olWMax) {
5026
5228
  olWrapped.push(olWLine)
5027
5229
  continue
5028
5230
  }
@@ -5030,7 +5232,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5030
5232
  var olCur = ''
5031
5233
  for (var olti = 0; olti < olToks.length; olti++) {
5032
5234
  if (!olCur) { olCur = olToks[olti]; continue }
5033
- if (ovC.measureText(olCur + olToks[olti]).width <= olWMax) {
5235
+ if (olMeasure(olCur + olToks[olti]) <= olWMax) {
5034
5236
  olCur += olToks[olti]
5035
5237
  } else {
5036
5238
  olWrapped.push(olCur)
@@ -5080,10 +5282,10 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5080
5282
  olFx = { k: olFx.k, u: olFx.u, d: olFx.d, st: olFx.st, dur: olFx.dur, tt: olFx.tt, units: olRe, n: olReN }
5081
5283
  }
5082
5284
  var olLWs = null, olMaxW = 0
5083
- if (ol.box || ol.align || olFx) {
5285
+ if (ol.box || ol.align || olFx || olEm) {
5084
5286
  olLWs = []
5085
5287
  for (var olwi = 0; olwi < olLines.length; olwi++) {
5086
- var olw = ovC.measureText(olLines[olwi]).width
5288
+ var olw = olMeasure(olLines[olwi])
5087
5289
  olLWs.push(olw)
5088
5290
  if (olw > olMaxW) olMaxW = olw
5089
5291
  }
@@ -5119,6 +5321,12 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5119
5321
  : (olMaxW - olLWs[ok]) / 2
5120
5322
  }
5121
5323
  var olLY = olY0 + ok * olLH
5324
+ if (olEm) {
5325
+ // Runs draw LEFT-aligned from the centred line's left edge.
5326
+ ovC.textAlign = 'left'
5327
+ olDraw(olLines[ok], olXof - olLWs[ok] / 2, olLY, false)
5328
+ continue
5329
+ }
5122
5330
  if (ol.stroke) {
5123
5331
  ovC.strokeStyle = ol.stroke.c
5124
5332
  ovC.lineWidth = ol.stroke.w * olMS * s
@@ -5155,7 +5363,10 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5155
5363
  : olIdx
5156
5364
  var olT2 = olT - olOrd * olFx.st
5157
5365
  var olNext = olPref + olUs[ou]
5158
- var olNW = ovC.measureText(olNext).width
5366
+ var olNW = olMeasure(olNext)
5367
+ // The emphasis state this unit starts in (a char unit inside a
5368
+ // marked word carries no mark of its own).
5369
+ var olUB = olEm ? olStateAfter(olPref) : false
5159
5370
  var olUW = olNW - olPW
5160
5371
  var olUX = olXb + olPW
5161
5372
  var olUA = 1, olUu = 1
@@ -5184,6 +5395,11 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
5184
5395
  ovC.filter = 'blur(' + ((1 - olUu) * olPx * 0.12).toFixed(2) + 'px)'
5185
5396
  }
5186
5397
  }
5398
+ if (olEm) {
5399
+ olDraw(olUnit, olUX, olLY2, olUB)
5400
+ ovC.restore()
5401
+ continue
5402
+ }
5187
5403
  if (ol.stroke) {
5188
5404
  ovC.strokeStyle = ol.stroke.c
5189
5405
  ovC.lineWidth = ol.stroke.w * olMS * s
@@ -5832,15 +6048,22 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
5832
6048
  return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
5833
6049
  });
5834
6050
  const { keyframes, push: push2 } = trackEmitter();
6051
+ const placedAt = (z) => z.screen && z.focusMode !== "auto" ? z.screen : null;
6052
+ const placed = mapped.some(({ z }) => placedAt(z) !== null);
6053
+ const sc = (z) => {
6054
+ if (!placed) return [];
6055
+ const s = placedAt(z);
6056
+ return s ? [s.x, s.y] : [0.5, 0.5];
6057
+ };
5835
6058
  let chained = false;
5836
6059
  for (let i = 0; i < mapped.length; i++) {
5837
6060
  const { z, tIn, tOut } = mapped[i];
5838
- const entry = [clampZoomLevel(z.level), z.cx, z.cy, 1];
6061
+ const entry = [clampZoomLevel(z.level), z.cx, z.cy, 1, ...sc(z)];
5839
6062
  let cur = entry;
5840
6063
  const m = transitionMult(z.transition);
5841
6064
  if (!chained) {
5842
6065
  const startWanted = tIn - (style.rampIn - style.rampInOverlap) * m;
5843
- const start = push2(startWanted, [1, z.cx, z.cy, 0], "none");
6066
+ const start = push2(startWanted, [1, z.cx, z.cy, 0, ...sc(z)], "none");
5844
6067
  push2(
5845
6068
  rampLanding(start, startWanted, style.rampIn * m, tOut),
5846
6069
  entry,
@@ -5852,7 +6075,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
5852
6075
  const eOut = sourceToTimeline(segments, e.t);
5853
6076
  if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
5854
6077
  if (eOut - tIn < RAMP_FLOOR || tOut - eOut < RAMP_FLOOR) continue;
5855
- const next2 = [cur[0], e.cx, e.cy, 1];
6078
+ const next2 = [cur[0], e.cx, e.cy, 1, ...cur.slice(4)];
5856
6079
  if (e.path) {
5857
6080
  if (!inPath) {
5858
6081
  push2(Math.max(tIn, eOut - style.followRecenter), cur, "none");
@@ -5868,11 +6091,31 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
5868
6091
  }
5869
6092
  cur = next2;
5870
6093
  }
5871
- push2(tOut, cur, "none");
6094
+ const drift = Math.max(0, style.holdDrift ?? 0);
6095
+ 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));
6098
+ cur = [
6099
+ clampZoomLevel(cur[0] * (1 + grow)),
6100
+ cur[1],
6101
+ cur[2],
6102
+ cur[3],
6103
+ ...cur.slice(4)
6104
+ ];
6105
+ push2(tOut, cur, "linear");
6106
+ } else {
6107
+ push2(tOut, cur, "none");
6108
+ }
5872
6109
  const next = mapped.at(i + 1);
5873
6110
  if (next && next.tIn - tOut <= style.chainGap) {
5874
6111
  const mNext = transitionMult(next.z.transition);
5875
- const nextValue = [clampZoomLevel(next.z.level), next.z.cx, next.z.cy, 1];
6112
+ const nextValue = [
6113
+ clampZoomLevel(next.z.level),
6114
+ next.z.cx,
6115
+ next.z.cy,
6116
+ 1,
6117
+ ...sc(next.z)
6118
+ ];
5876
6119
  push2(
5877
6120
  Math.max(
5878
6121
  tOut + RAMP_FLOOR,
@@ -5888,7 +6131,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
5888
6131
  } else {
5889
6132
  push2(
5890
6133
  tOut + style.rampOut * m,
5891
- [1, cur[1], cur[2], 0],
6134
+ [1, cur[1], cur[2], 0, ...cur.slice(4)],
5892
6135
  spanEase(z.ease, style.ease)
5893
6136
  );
5894
6137
  chained = false;
@@ -6930,37 +7173,71 @@ var ON_FRAME = `(ctx, content, dt) => {
6930
7173
  bgC.fillStyle = '#0b0b0c'
6931
7174
  bgC.fillStyle = (function () {
6932
7175
  var bgcss = frame.background || '#0b0b0c'
6933
- if (typeof bgcss === 'string' && bgcss.indexOf('linear-gradient') === 0) {
6934
- var inner = bgcss.substring(bgcss.indexOf('(') + 1, bgcss.lastIndexOf(')'))
6935
- var parts = inner.split(',').map(function (x) { return x.trim() })
6936
- var ang = 135, cols = []
6937
- for (var i = 0; i < parts.length; i++) {
6938
- if (parts[i].indexOf('deg') >= 0) ang = parseFloat(parts[i])
6939
- else cols.push(parts[i])
7176
+ if (typeof bgcss !== 'string') return '#0b0b0c'
7177
+ var gKind = bgcss.indexOf('linear-gradient') === 0 ? 'linear' : bgcss.indexOf('radial-gradient') === 0 ? 'radial' : ''
7178
+ if (!gKind) return bgcss
7179
+ // Split on TOP-LEVEL commas only: an rgba() or hsl() stop has commas of
7180
+ // its own, and splitting inside one hands canvas half a colour.
7181
+ var gin = bgcss.substring(bgcss.indexOf('(') + 1, bgcss.lastIndexOf(')'))
7182
+ var gps = [], gDepth = 0, gCur = ''
7183
+ for (var gi = 0; gi < gin.length; gi++) {
7184
+ var gch = gin.charAt(gi)
7185
+ if (gch === '(') gDepth++
7186
+ else if (gch === ')') gDepth--
7187
+ if (gch === ',' && gDepth === 0) { gps.push(gCur.trim()); gCur = '' } else gCur += gch
7188
+ }
7189
+ gps.push(gCur.trim())
7190
+ // The geometry (an angle, a side, a shape and centre) leads; every other
7191
+ // part is a STOP, a colour with an optional position ('#2b6f64 40%').
7192
+ var ang = 135, rcx = 0.5, rcy = 0.5, gStops = []
7193
+ for (var gj = 0; gj < gps.length; gj++) {
7194
+ var gp = gps[gj]
7195
+ if (!gp) continue
7196
+ if (gKind === 'linear' && gj === 0 && gp.indexOf('deg') >= 0) { ang = parseFloat(gp); continue }
7197
+ if (gKind === 'linear' && gj === 0 && gp.indexOf('to ') === 0) {
7198
+ var tdx = gp.indexOf('right') >= 0 ? 1 : gp.indexOf('left') >= 0 ? -1 : 0
7199
+ var tdy = gp.indexOf('bottom') >= 0 ? 1 : gp.indexOf('top') >= 0 ? -1 : 0
7200
+ ang = Math.atan2(tdx, -tdy) * 180 / Math.PI
7201
+ continue
7202
+ }
7203
+ if (gKind === 'radial' && gj === 0 && (gp.indexOf('circle') === 0 || gp.indexOf('ellipse') === 0 || gp.indexOf('at ') === 0 || gp.indexOf('closest') === 0 || gp.indexOf('farthest') === 0)) {
7204
+ var rat = /at\\s+([\\d.]+)%\\s+([\\d.]+)%/.exec(gp)
7205
+ if (rat) { rcx = parseFloat(rat[1]) / 100; rcy = parseFloat(rat[2]) / 100 }
7206
+ continue
6940
7207
  }
6941
- if (cols.length < 2) cols = [cols[0] || '#000', cols[0] || '#000']
7208
+ var gSp = gp.lastIndexOf(' ')
7209
+ var gTail = gSp > 0 ? gp.substring(gSp + 1) : ''
7210
+ if (gTail.charAt(gTail.length - 1) === '%' && !isNaN(parseFloat(gTail))) {
7211
+ gStops.push({ c: gp.substring(0, gSp).trim(), p: parseFloat(gTail) / 100 })
7212
+ } else gStops.push({ c: gp, p: null })
7213
+ }
7214
+ if (!gStops.length) return '#0b0b0c'
7215
+ if (gStops.length === 1) gStops.push({ c: gStops[0].c, p: null })
7216
+ // CSS's own fix-up: an unplaced first stop sits at 0 and last at 1, a
7217
+ // position never runs backwards, and the unplaced stops between two
7218
+ // placed ones spread evenly. Canvas throws outside 0..1, so clamp.
7219
+ var gN = gStops.length
7220
+ if (gStops[0].p === null) gStops[0].p = 0
7221
+ if (gStops[gN - 1].p === null) gStops[gN - 1].p = 1
7222
+ var gMax = 0
7223
+ for (var gk = 0; gk < gN; gk++) {
7224
+ if (gStops[gk].p !== null) { gStops[gk].p = Math.min(1, Math.max(gMax, gStops[gk].p)); gMax = gStops[gk].p }
7225
+ }
7226
+ for (var gm = 1; gm < gN; gm++) {
7227
+ if (gStops[gm].p !== null) continue
7228
+ var gEnd = gm
7229
+ while (gStops[gEnd].p === null) gEnd++
7230
+ var gFrom = gStops[gm - 1].p, gTo = gStops[gEnd].p, gRun = gEnd - gm + 1
7231
+ for (var gq = gm; gq < gEnd; gq++) gStops[gq].p = gFrom + (gTo - gFrom) * (gq - gm + 1) / gRun
7232
+ }
7233
+ var g
7234
+ if (gKind === 'linear') {
6942
7235
  var rad = (ang - 90) * Math.PI / 180
6943
7236
  var ux = Math.cos(rad), uy = Math.sin(rad)
6944
- var g = bgC.createLinearGradient(W / 2 - ux * W / 2, H / 2 - uy * H / 2, W / 2 + ux * W / 2, H / 2 + uy * H / 2)
6945
- g.addColorStop(0, cols[0]); g.addColorStop(1, cols[cols.length - 1])
6946
- return g
6947
- }
6948
- // Radial: 'radial-gradient([circle|ellipse] [at X% Y%,] A, B)'. Canvas
6949
- // cannot take the string, so it is built here like the linear one, with
6950
- // CSS's own default extent (farthest corner) so the second colour lands
6951
- // exactly where a browser would put it.
6952
- if (typeof bgcss === 'string' && bgcss.indexOf('radial-gradient') === 0) {
6953
- var rin = bgcss.substring(bgcss.indexOf('(') + 1, bgcss.lastIndexOf(')'))
6954
- var rps = rin.split(',').map(function (x) { return x.trim() })
6955
- var rcx = 0.5, rcy = 0.5, rcols = []
6956
- for (var ri = 0; ri < rps.length; ri++) {
6957
- var rp = rps[ri]
6958
- if (rp.indexOf('circle') === 0 || rp.indexOf('ellipse') === 0 || rp.indexOf('at ') === 0) {
6959
- var rat = /at\\s+([\\d.]+)%\\s+([\\d.]+)%/.exec(rp)
6960
- if (rat) { rcx = parseFloat(rat[1]) / 100; rcy = parseFloat(rat[2]) / 100 }
6961
- } else rcols.push(rp)
6962
- }
6963
- if (rcols.length < 2) rcols = [rcols[0] || '#000', rcols[0] || '#000']
7237
+ g = bgC.createLinearGradient(W / 2 - ux * W / 2, H / 2 - uy * H / 2, W / 2 + ux * W / 2, H / 2 + uy * H / 2)
7238
+ } else {
7239
+ // CSS's default extent (farthest corner), so the last stop lands
7240
+ // exactly where a browser would put it.
6964
7241
  var rpx = rcx * W, rpy = rcy * H
6965
7242
  var rr = Math.max(
6966
7243
  Math.sqrt(rpx * rpx + rpy * rpy),
@@ -6968,11 +7245,15 @@ var ON_FRAME = `(ctx, content, dt) => {
6968
7245
  Math.sqrt(rpx * rpx + (H - rpy) * (H - rpy)),
6969
7246
  Math.sqrt((W - rpx) * (W - rpx) + (H - rpy) * (H - rpy))
6970
7247
  )
6971
- var rg = bgC.createRadialGradient(rpx, rpy, 0, rpx, rpy, rr)
6972
- rg.addColorStop(0, rcols[0]); rg.addColorStop(1, rcols[rcols.length - 1])
6973
- return rg
7248
+ g = bgC.createRadialGradient(rpx, rpy, 0, rpx, rpy, rr)
6974
7249
  }
6975
- return bgcss
7250
+ // A stop canvas still cannot parse (a colour keyword it lacks, a second
7251
+ // position) lands on the known ground: a background must never take the
7252
+ // frame down with it.
7253
+ try {
7254
+ for (var gs = 0; gs < gN; gs++) g.addColorStop(gStops[gs].p, gStops[gs].c)
7255
+ } catch (e) { return '#0b0b0c' }
7256
+ return g
6976
7257
  })()
6977
7258
  bgC.fillRect(0, 0, W, H)
6978
7259
 
@@ -7078,7 +7359,7 @@ var ON_FRAME = `(ctx, content, dt) => {
7078
7359
  // current zoom \u2014 a standard keyframe track in OUTPUT time (hold + arrival pairs
7079
7360
  // expanded by the lowering), sampled with the shared deterministic interpolator.
7080
7361
  // Sampled here, before the rect math, so a cover crop can follow it.
7081
- var lvl = 1, zx = 0.5, zy = 0.5, zc = -1
7362
+ var lvl = 1, zx = 0.5, zy = 0.5, zc = -1, zsx = 0.5, zsy = 0.5
7082
7363
  var zt = d.zoomTrack
7083
7364
  if (zt && zt.keyframes && zt.keyframes.length) {
7084
7365
  var z = TL.sample(zt, tpT, TL.lerpArray)
@@ -7087,6 +7368,9 @@ var ON_FRAME = `(ctx, content, dt) => {
7087
7368
  // with the level); a three-component track (a document lowered before
7088
7369
  // the component existed) falls back to the level band below.
7089
7370
  if (z.length > 3) zc = z[3]
7371
+ // A span placed on screen carries where its target lands (frame
7372
+ // fractions); a track without them lands it at the centre.
7373
+ if (z.length > 5) { zsx = z[4]; zsy = z[5] }
7090
7374
  }
7091
7375
  var barH = bar.kind && bar.kind !== 'none' ? (bar.height || 44) * s2 : 0
7092
7376
  var availW = Math.max(1, W - ipL - ipR), availH = Math.max(1, H - ipT - ipB - barH)
@@ -7147,6 +7431,10 @@ var ON_FRAME = `(ctx, content, dt) => {
7147
7431
  var camStage = cfr.camera === 'stage'
7148
7432
  var fx = dx + zx * dw, fy = dy + zy * dh
7149
7433
  var wcx = fx, wcy = fy
7434
+ // The point the stage camera lands its focus on: the span's place on
7435
+ // screen, else the centre (0.5\xB7W is W / 2 exactly). MIRRORS zoomView's
7436
+ // screenPoint \u2014 change together.
7437
+ var cox = camStage ? zsx * W : W / 2, coy = camStage ? zsy * H : H / 2
7150
7438
  if (lvl > 1.001 && !d.zoomSuppressed) {
7151
7439
  if (camStage) {
7152
7440
  var ctt = zc
@@ -7154,9 +7442,9 @@ var ON_FRAME = `(ctx, content, dt) => {
7154
7442
  var ctu = Math.max(0, Math.min(1, (lvl - 1) / ${CAMERA_CENTRE_RAMP}))
7155
7443
  ctt = ctu * ctu * (3 - 2 * ctu)
7156
7444
  }
7157
- wcx = fx + ((W / 2 - fx) / lvl) * (1 - ctt)
7158
- wcy = fy + ((H / 2 - fy) / lvl) * (1 - ctt)
7159
- c.translate(W / 2, H / 2); c.scale(lvl, lvl); c.translate(-wcx, -wcy)
7445
+ wcx = fx + ((cox - fx) / lvl) * (1 - ctt)
7446
+ wcy = fy + ((coy - fy) / lvl) * (1 - ctt)
7447
+ c.translate(cox, coy); c.scale(lvl, lvl); c.translate(-wcx, -wcy)
7160
7448
  } else {
7161
7449
  c.translate(fx, fy); c.scale(lvl, lvl); c.translate(-fx, -fy)
7162
7450
  }
@@ -7556,7 +7844,7 @@ var ON_FRAME = `(ctx, content, dt) => {
7556
7844
  if (trOut) {
7557
7845
  var tqX = ax, tqY = ay
7558
7846
  if (lvl > 1.001 && !d.zoomSuppressed) {
7559
- if (camStage) { tqX = W / 2 + (ax - wcx) * lvl; tqY = H / 2 + (ay - wcy) * lvl }
7847
+ if (camStage) { tqX = cox + (ax - wcx) * lvl; tqY = coy + (ay - wcy) * lvl }
7560
7848
  else { tqX = fx + (ax - fx) * lvl; tqY = fy + (ay - fy) * lvl }
7561
7849
  }
7562
7850
  if (tpG) trQa = [tqX, tqY]
@@ -7678,8 +7966,8 @@ var ON_FRAME = `(ctx, content, dt) => {
7678
7966
  }
7679
7967
  if (lvl > 1.001 && !d.zoomSuppressed) {
7680
7968
  if (camStage) {
7681
- pqAx = W / 2 + (pqAx - wcx) * lvl; pqAy = H / 2 + (pqAy - wcy) * lvl
7682
- pqBx = W / 2 + (pqBx - wcx) * lvl; pqBy = H / 2 + (pqBy - wcy) * lvl
7969
+ pqAx = cox + (pqAx - wcx) * lvl; pqAy = coy + (pqAy - wcy) * lvl
7970
+ pqBx = cox + (pqBx - wcx) * lvl; pqBy = coy + (pqBy - wcy) * lvl
7683
7971
  } else {
7684
7972
  pqAx = fx + (pqAx - fx) * lvl; pqAy = fy + (pqAy - fy) * lvl
7685
7973
  pqBx = fx + (pqBx - fx) * lvl; pqBy = fy + (pqBy - fy) * lvl
@@ -7988,10 +8276,27 @@ function studioLayerData(layers, duration, pins) {
7988
8276
  }
7989
8277
  const st = resolveOverlayStyle(o);
7990
8278
  const bx = resolveOverlayBox(o);
8279
+ const em = resolveEmphasis(o);
7991
8280
  return {
7992
8281
  ...base,
7993
8282
  text: o.text,
7994
- lines: overlayLines(o.text),
8283
+ // The DISPLAYED lines: emphasis marks inline, else the source.
8284
+ lines: overlayLines(overlayDisplayText(o)),
8285
+ // Emphasis set (weight, colour, face to lazy-load), only when
8286
+ // the text marks a word: an unmarked clip's data is unchanged.
8287
+ ...em ? {
8288
+ em: {
8289
+ w: em.weight,
8290
+ ...em.color ? { c: em.color } : {},
8291
+ ...em.face ? {
8292
+ face: {
8293
+ f: em.face.family,
8294
+ w: em.face.weight,
8295
+ u: em.face.url
8296
+ }
8297
+ } : {}
8298
+ }
8299
+ } : {},
7995
8300
  fs: st.size,
7996
8301
  weight: st.weight,
7997
8302
  stack: st.stack,
@@ -8235,7 +8540,15 @@ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
8235
8540
  }
8236
8541
  return {
8237
8542
  ...z,
8238
- ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout, camera)
8543
+ // A span placed on screen is the author's composition: no cover band.
8544
+ ...clampFocus(
8545
+ z.cx,
8546
+ z.cy,
8547
+ clampZoomLevel(z.level),
8548
+ layout,
8549
+ camera,
8550
+ z.focusMode !== "auto" ? z.screen : void 0
8551
+ )
8239
8552
  };
8240
8553
  });
8241
8554
  }
@@ -8788,8 +9101,9 @@ function sceneChanges(bins, opts = {}) {
8788
9101
  // src/digest/framing.ts
8789
9102
  function zoomWindow(span, layout, camera = "card") {
8790
9103
  const L = Math.max(1, span.level);
8791
- const { cx, cy } = clampFocus(span.cx, span.cy, L, layout, camera);
8792
- const v = zoomViewport(L, cx, cy, layout, camera);
9104
+ const screen = span.focusMode !== "auto" ? span.screen : void 0;
9105
+ const { cx, cy } = clampFocus(span.cx, span.cy, L, layout, camera, screen);
9106
+ const v = zoomViewport(L, cx, cy, layout, camera, void 0, screen);
8793
9107
  const px0 = v.x * layout.W;
8794
9108
  const px1 = (v.x + v.w) * layout.W;
8795
9109
  const py0 = v.y * layout.H;
@@ -11515,6 +11829,8 @@ export {
11515
11829
  DRAG_FIT_LEVEL,
11516
11830
  DRAG_MIN_S,
11517
11831
  DRAG_MIN_TRAVEL_FRAC,
11832
+ EM_CLOSE,
11833
+ EM_OPEN,
11518
11834
  END_CARD_FROM,
11519
11835
  END_CARD_RECEDE,
11520
11836
  END_CARD_SECONDS,
@@ -11530,6 +11846,7 @@ export {
11530
11846
  FREEZE_DEFAULT_SECONDS,
11531
11847
  FREEZE_SECONDS_MAX,
11532
11848
  FREEZE_SECONDS_MIN,
11849
+ HOLD_DRIFT_MAX,
11533
11850
  HOUSE_GRADIENT,
11534
11851
  HOUSE_REGISTER,
11535
11852
  HTML_LAYER_FOREIGN_FIELDS,
@@ -11689,6 +12006,8 @@ export {
11689
12006
  endCardClips,
11690
12007
  enterKey,
11691
12008
  enterOf,
12009
+ entranceAt,
12010
+ entranceEnd,
11692
12011
  entranceSeconds,
11693
12012
  envelopeAt,
11694
12013
  envelopeValueAt,
@@ -11743,6 +12062,7 @@ export {
11743
12062
  lowerProgramDoc,
11744
12063
  lowerStudioDoc,
11745
12064
  lowerToComposition,
12065
+ measureEmphasized,
11746
12066
  mediaAtOutput,
11747
12067
  mediaDuration,
11748
12068
  mediaFrame,
@@ -11766,6 +12086,7 @@ export {
11766
12086
  outputEnd,
11767
12087
  outputRangeToSource,
11768
12088
  overlapFraction,
12089
+ overlayDisplayText,
11769
12090
  overlayFaceFor,
11770
12091
  overlayFontFaces,
11771
12092
  overlayFontString,
@@ -11780,6 +12101,7 @@ export {
11780
12101
  ownFreezes,
11781
12102
  pageDisplayUrl,
11782
12103
  parseCssColour,
12104
+ parseEmphasis,
11783
12105
  parsePoseId,
11784
12106
  pickStyle,
11785
12107
  pinBox,
@@ -11807,6 +12129,7 @@ export {
11807
12129
  relativeLuminance,
11808
12130
  removeSourceRange,
11809
12131
  removeSpeedInRange,
12132
+ resolveEmphasis,
11810
12133
  resolveExportSize,
11811
12134
  resolveOverlayBox,
11812
12135
  resolveOverlayStyle,
@@ -11827,6 +12150,7 @@ export {
11827
12150
  smoothCursor,
11828
12151
  spanOutputExtent,
11829
12152
  speedLane,
12153
+ stripEmphasis,
11830
12154
  studioAudioPlan,
11831
12155
  studioEntry,
11832
12156
  studioLayerData,