@real-music-packages/web-core 0.36.1 → 0.37.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.
@@ -173,7 +173,7 @@ function vstackFollowBox(rn, camProgress01) {
173
173
  function notationLayout(rn, W, H, boxTop, boxH, opts = {}) {
174
174
  const { zoom01 = 1, focusBox = null } = opts;
175
175
  const sb = safeBox(W, H);
176
- const maxW = sb.centeredW;
176
+ const maxW = opts.boxWidth ?? sb.centeredW;
177
177
  const c = rn.content && rn.content.w > 0 && rn.content.h > 0 ? rn.content : { x: 0, y: 0, w: rn.canvas.width || 1400, h: rn.canvas.height || 300 };
178
178
  let src;
179
179
  if (focusBox) {
@@ -450,6 +450,7 @@ function notationLayer() {
450
450
  let propScale = 1;
451
451
  let propBandTop;
452
452
  let propBandHeight;
453
+ let propBandWidth;
453
454
  function bandTop(_ctx, sb) {
454
455
  return propBandTop ?? sb.top;
455
456
  }
@@ -463,6 +464,7 @@ function notationLayer() {
463
464
  propScale = props.scale ?? 1;
464
465
  propBandTop = props.bandTop;
465
466
  propBandHeight = props.bandHeight;
467
+ propBandWidth = props.bandWidth;
466
468
  if (props.rendered) {
467
469
  rn = props.rendered;
468
470
  } else if (props.xml) {
@@ -470,7 +472,8 @@ function notationLayer() {
470
472
  rn = await renderNotation(props.xml, {
471
473
  bars: props.bars,
472
474
  paper: ctx.theme.paper,
473
- scrollMode: props.scrollMode ?? "hstack"
475
+ scrollMode: props.scrollMode ?? "hstack",
476
+ hostWidth: props.hostWidth
474
477
  });
475
478
  } else {
476
479
  throw new Error("notation layer: provide `rendered` or `xml`");
@@ -478,8 +481,8 @@ function notationLayer() {
478
481
  const sb = safeBox(ctx.W, ctx.H);
479
482
  const top = bandTop(ctx, sb);
480
483
  const height = bandHeight(ctx, sb);
481
- base = notationLayout(rn, ctx.W, ctx.H, top, height, {});
482
- setNotationEngraving(ctx, { rendered: rn, base, bandTop: top, bandHeight: height });
484
+ base = notationLayout(rn, ctx.W, ctx.H, top, height, { boxWidth: propBandWidth });
485
+ setNotationEngraving(ctx, { rendered: rn, base, bandTop: top, bandHeight: height, bandWidth: propBandWidth });
483
486
  },
484
487
  draw(ctx, tMs) {
485
488
  if (!rn || !base) return;
@@ -521,6 +524,10 @@ var notationFactory = {
521
524
  errs.push("notation: provide `rendered` (RenderedNotation) or `xml` (string)");
522
525
  if (p.bars != null && (!Array.isArray(p.bars) || p.bars.length !== 2))
523
526
  errs.push("notation.bars must be [from,to]");
527
+ if (p.hostWidth != null && (typeof p.hostWidth !== "number" || p.hostWidth <= 0))
528
+ errs.push("notation.hostWidth must be a positive number");
529
+ if (p.bandWidth != null && (typeof p.bandWidth !== "number" || p.bandWidth <= 0))
530
+ errs.push("notation.bandWidth must be a positive number");
524
531
  return errs;
525
532
  }
526
533
  };
@@ -532,7 +539,7 @@ function followLayoutFor(ctx, progress01, scrollMode) {
532
539
  const nBars = measureCount(eng.rendered);
533
540
  if (nBars <= 0) return eng.base;
534
541
  const focusBox = scrollMode === "vstack" ? vstackFollowBox(eng.rendered, progress01) : followBoxAt(eng.rendered, followWindowStart(eng.rendered, progress01));
535
- return notationLayout(eng.rendered, ctx.W, ctx.H, eng.bandTop, eng.bandHeight, { focusBox });
542
+ return notationLayout(eng.rendered, ctx.W, ctx.H, eng.bandTop, eng.bandHeight, { focusBox, boxWidth: eng.bandWidth });
536
543
  }
537
544
  function scrollCursorLayer() {
538
545
  let scrollMode = "hstack";
@@ -703,4 +710,4 @@ export {
703
710
  notationFactory,
704
711
  scrollCursorFactory
705
712
  };
706
- //# sourceMappingURL=chunk-JVSXE4X3.js.map
713
+ //# sourceMappingURL=chunk-4565POLG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scene/notationGeometry.ts","../src/scene/engravingStore.ts","../src/scene/layers/notation.ts","../src/scene/layers/scrollCursor.ts"],"sourcesContent":["// Notation + scrolling-cursor geometry (S2) — the pure math extracted FAITHFULLY\n// from RSR's stave-web-sightread/src/routes/promo/+page.svelte. No canvas, no\n// behaviour change: these are the exact functions RSR uses to crop/scale the\n// engraving bitmap, scroll the 2-bar follow window, and place the playhead, lifted\n// verbatim so the notation + scroll-cursor layers can reuse them AND a test can\n// prove pixel-equivalence by comparing the geometry these produce against RSR's.\n//\n// Coordinate spaces (kept identical to RSR):\n// - canvas space: pixels of the rasterized OSMD bitmap (RenderedNotation.canvas).\n// - screen space: the W×H output frame. drawNotation maps a canvas `src` rect\n// onto a screen `dest` rect; cursor/highlight read the mapped boxes.\n//\n// IMPORTANT — easing: RSR uses a CUBIC ease-in-out for the zoom + scroll\n// (`cubicEaseInOut` below). web-core's math.easeInOut is SMOOTHSTEP — a different\n// curve. To stay pixel-faithful the extracted geometry uses RSR's cubic, NOT the\n// shared smoothstep. (Documented difference: none in output; the curve is ported.)\n\nimport type { Box, MeasureColumnBox, RenderedNotation, StaffMeasureBox } from '../promo';\nimport { safeBox } from '../video';\n\n// ─── Constants (verbatim from RSR +page.svelte:222-223) ──────────────────────\n\n/** measures (= rows) visible at once in the follow window. */\nexport const FOLLOW_BARS = 2;\n/** breathing room around the follow window. */\nexport const FOLLOW_PAD = 1.06;\n\n// ─── Easing (verbatim from RSR +page.svelte:175-179) ─────────────────────────\n\n/** Cubic ease-in-out, t in [0,1] (RSR port of stave-video core/camera.py). */\nexport function cubicEaseInOut(t: number): number {\n if (t < 0.5) return 4 * t * t * t;\n const f = 2 * t - 2;\n return 0.5 * f * f * f + 1;\n}\n\n// ─── Ledger-line vertical headroom ───────────────────────────────────────────\n//\n// BUG FIX (web-core 0.20.0): the follow/crop boxes below union OSMD's per-(measure,\n// staff) `box`, which is the STAFF's bounding box — it does NOT include ledger\n// lines, high/low notes, or stems that extend above/below the 5 staff lines. The\n// crop derived from those boxes therefore brutally clips ledger-line notes in BOTH\n// hstack and vstack. `rn.content` IS the true ink box (notes + ledgers + stems,\n// from inkBoundingBox), so we expand any measure-derived box VERTICALLY to:\n// (a) a generous fixed headroom of several staff-spaces above & below (covers\n// the common ±2-3 ledger-line case even when content can't bound it), and\n// (b) the full ink extent (rn.content.y .. content.y+content.h) — so nothing\n// engraved on the page is ever cropped away vertically.\n// Clamped to the canvas. Horizontal extent is untouched (the follow window's\n// purpose is to pick the visible MEASURES; their notes' x stay inside the staff x).\n\n/** ~one staff-space in canvas px, estimated from the median staff-measure height\n * (a single staff spans ~4 spaces, so height/4). Falls back to a sane default. */\nfunction staffSpacePx(rn: RenderedNotation): number {\n const hs = (rn.measures ?? []).map((m) => m.box.h).filter((h) => h > 1).sort((a, b) => a - b);\n if (!hs.length) return 14;\n const med = hs[Math.floor(hs.length / 2)];\n return Math.max(6, med / 4);\n}\n\n/**\n * Expand a measure-derived box VERTICALLY so it can never clip ledger-line notes /\n * stems above or below the staff. Adds `spaces` staff-spaces of headroom top &\n * bottom. When `unionInk` is set (hstack — a single row, so the ink box IS this\n * row), also unions with the canvas ink extent (rn.content) so every engraved mark\n * is visible. In vstack `unionInk` MUST be false: rn.content spans every stacked\n * system, so unioning it would balloon a per-system box to the whole page and\n * defeat the fixed-system framing — generous fixed headroom covers ledger lines\n * there instead. Horizontal extent unchanged. Result clamped to the canvas.\n */\nfunction withLedgerHeadroom(rn: RenderedNotation, box: Box, spaces: number, unionInk: boolean): Box {\n const pad = staffSpacePx(rn) * spaces;\n const ch = rn.canvas.height || box.y + box.h;\n let y0 = box.y - pad;\n let y1 = box.y + box.h + pad;\n if (unionInk) {\n const c = rn.content;\n if (c && c.h > 0) {\n y0 = Math.min(y0, c.y);\n y1 = Math.max(y1, c.y + c.h);\n }\n }\n y0 = Math.max(0, y0);\n y1 = Math.min(ch, y1);\n return { x: box.x, y: y0, w: box.w, h: Math.max(1, y1 - y0) };\n}\n\n// ─── Box helpers (verbatim from RSR) ─────────────────────────────────────────\n\n/** Linear interpolation of two boxes. (RSR lerpBox) */\nexport function lerpBox(a: Box, b: Box, e: number): Box {\n return {\n x: a.x + (b.x - a.x) * e,\n y: a.y + (b.y - a.y) * e,\n w: a.w + (b.w - a.w) * e,\n h: a.h + (b.h - a.h) * e,\n };\n}\n\n/** Smallest crop of `aspect` containing `box` padded by `pad`, clamped to the\n * canvas. (RSR cropAroundBox) */\nexport function cropAroundBox(box: Box, aspect: number, pad: number, cw: number, ch: number): Box {\n const bw = box.w * pad;\n const bh = box.h * pad;\n let w = Math.max(bw, bh * aspect);\n let h = w / aspect;\n w = Math.min(w, cw);\n h = Math.min(h, ch);\n const ccx = box.x + box.w / 2;\n const ccy = box.y + box.h / 2;\n let x = ccx - w / 2;\n let y = ccy - h / 2;\n x = Math.max(0, Math.min(cw - w, x));\n y = Math.max(0, Math.min(ch - h, y));\n return { x, y, w, h };\n}\n\n/** Union (canvas coords) of all staff measure boxes with index in [lo,hi).\n * (RSR measureSpanBox) */\nexport function measureSpanBox(rn: RenderedNotation, lo: number, hi: number): Box | null {\n const ms = (rn.measures ?? []).filter((m) => m.index >= lo && m.index < hi).map((m) => m.box);\n if (!ms.length) return null;\n const x0 = Math.min(...ms.map((b) => b.x));\n const y0 = Math.min(...ms.map((b) => b.y));\n const x1 = Math.max(...ms.map((b) => b.x + b.w));\n const y1 = Math.max(...ms.map((b) => b.y + b.h));\n // hstack: all measures share one row, so the ink box IS this row — union it so\n // ledger-line notes above/below the staff are never cropped.\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, true);\n}\n\n/** Union of the index-0 measure boxes — the opening focal. (RSR firstMeasureBox) */\nexport function firstMeasureBox(rn: RenderedNotation): Box | null {\n const first = (rn.measures ?? []).filter((m) => m.index === 0).map((m) => m.box);\n if (!first.length) return rn.systems?.[0] ?? null;\n const x0 = Math.min(...first.map((b) => b.x));\n const y0 = Math.min(...first.map((b) => b.y));\n const x1 = Math.max(...first.map((b) => b.x + b.w));\n const y1 = Math.max(...first.map((b) => b.y + b.h));\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, false);\n}\n\n/** Number of distinct measures in the notation. (RSR measureCount) */\nexport function measureCount(rn: RenderedNotation): number {\n const idx = (rn.measures ?? []).map((m) => m.index);\n return idx.length ? Math.max(...idx) + 1 : 0;\n}\n\n/** The DISTINCT measure indices present in the engraving, ASCENDING. For excerpts\n * these do NOT start at 0 (e.g. bars 5-8 → [4,5,6,7]) and may be non-contiguous,\n * so the follow window must scroll across THESE indices, not a 0..measureCount\n * range. (measureCount returns maxIndex+1 — a count valid only for 0-based scores.) */\nexport function distinctMeasureIndices(rn: RenderedNotation): number[] {\n const set = new Set<number>();\n for (const m of rn.measures ?? []) set.add(m.index);\n return [...set].sort((a, b) => a - b);\n}\n\n/** The follow window (canvas coords) for a continuous measure-start position,\n * spanning FOLLOW_BARS and lerping between adjacent windows. (RSR followBoxAt) */\nexport function followBoxAt(rn: RenderedNotation, posMeasures: number): Box | null {\n const cur = Math.floor(posMeasures);\n const frac = posMeasures - cur;\n const a = measureSpanBox(rn, cur, cur + FOLLOW_BARS);\n const b = measureSpanBox(rn, cur + 1, cur + 1 + FOLLOW_BARS) ?? a;\n if (!a) return b;\n if (!b) return a;\n return lerpBox(a, b, frac);\n}\n\n/**\n * The continuous follow-window START measure for an audio-clock progress 0..1.\n *\n * INVARIANT: the bar the playhead is in (`curBar`) is ALWAYS fully inside the\n * FOLLOW_BARS-wide window. We anchor `curBar` as the BOTTOM (last) visible bar —\n * with the preceding FOLLOW_BARS-1 bars above it for context — so the window\n * holds steady while the playhead works through the visible bars and only scrolls\n * once the playhead reaches the bottom bar. Concretely the resting window start is\n * `curBar - (FOLLOW_BARS - 1)`; over the tail of each bar we ease that forward by\n * one bar so the NEXT bar slides into the bottom slot just as the playhead crosses\n * into it. Because the eased start stays within `[curBar-(FOLLOW_BARS-1),\n * curBar-(FOLLOW_BARS-2)]`, `curBar` never leaves `[start, start+FOLLOW_BARS)` —\n * the current measure (and its playhead) can never ride off the top.\n *\n * The scroll is cubic-eased (smooth, no hard jump) over the last portion of each\n * bar; result is clamped to [0, nBars-FOLLOW_BARS]. (Replaces RSR's original\n * +page.svelte:738-744 logic, which advanced the window to the NEXT bar over the\n * last third of EVERY bar and so pushed the still-current bar off the top.)\n */\n/** Fraction of the FOLLOW_BARS window the playhead holds from the LEFT in hstack —\n * the rest is look-ahead. ~0.35 keeps the playing bar comfortably on screen and\n * off the edges. */\nexport const HSTACK_PLAYHEAD_LEAD = 0.35;\n\n/**\n * The continuous follow-window START measure (ABSOLUTE index) for an hstack\n * audio-clock progress 0..1.\n *\n * hstack (single horizontal staffline): pan CONTINUOUSLY so the playhead sits at a\n * stable fraction (`HSTACK_PLAYHEAD_LEAD`) from the LEFT of the window — the playing\n * bar is always on screen with look-ahead to its right, and the cursor never drifts\n * to the screen edge.\n *\n * BUG FIX (web-core 0.21.0) — playhead pinned at the right edge for EXCERPTS. This\n * used to take `nBars = measureCount(rn) = maxIndex+1` and pan progress across a\n * 0..nBars range. That is only correct when measure indices are 0-based: RSR's\n * excerpts engrave e.g. bars 5-8 → indices [4,5,6,7], so measureCount=8 but only 4\n * measures exist. The window then panned a phantom 0..8 axis while the playhead\n * (which steps over the 4 REAL columns) raced ahead — pinning the cursor at the far\n * right while the music barely scrolled. We now pan across the ACTUAL distinct\n * indices: progress 0..1 maps to absolute index `firstIndex .. firstIndex+nReal`,\n * and `followBoxAt` (which already indexes absolute measures via measureSpanBox)\n * frames the right bars. Clamped so the window never runs past the last measure.\n *\n * Pass the RenderedNotation so the real index range is known. (The legacy 0-based\n * `nBars`-only call still works via the `firstIndex=0, nReal=nBars` fallback.)\n */\nexport function followWindowStart(rn: RenderedNotation | number, camProgress01: number): number {\n let firstIndex: number;\n let nReal: number;\n if (typeof rn === 'number') {\n // Legacy signature: a plain bar count, assumed 0-based & contiguous.\n firstIndex = 0;\n nReal = rn;\n } else {\n const idx = distinctMeasureIndices(rn);\n firstIndex = idx.length ? idx[0] : 0;\n nReal = idx.length;\n }\n const p = Math.max(0, Math.min(1, camProgress01));\n // posBars in ABSOLUTE index space: 0 → firstIndex, 1 → firstIndex + nReal.\n const posBars = firstIndex + p * nReal;\n const start = posBars - HSTACK_PLAYHEAD_LEAD * FOLLOW_BARS;\n const lastIndex = firstIndex + Math.max(0, nReal - 1);\n // The window's leftmost start can be at most `lastIndex - (FOLLOW_BARS-1)` so the\n // final FOLLOW_BARS measures are still framed; never start before firstIndex.\n const minStart = firstIndex;\n const maxStart = Math.max(minStart, lastIndex - (FOLLOW_BARS - 1));\n return Math.max(minStart, Math.min(maxStart, start));\n}\n\n// ─── vstack follow geometry (fixed active system, vertical scroll) ───────────\n//\n// hstack (single horizontal staffline) lets `followBoxAt` pan a HORIZONTAL slice\n// — all measures share one row, so the FOLLOW_BARS window never changes y. vstack\n// re-introduces stacked systems (multiple rows), where `followBoxAt`'s measure\n// window WOULD jump vertically as consecutive bars land on different rows.\n//\n// The vstack camera avoids that by scrolling on the SYSTEM axis, not the measure\n// axis: it frames ONE system (a full row) at a fixed band position and, as the\n// playhead crosses from one system to the next, eases the focusBox VERTICALLY so\n// the next system slides up into the active slot. The playhead then moves L→R\n// within the framed system (its x comes from the bar's column inside that row).\n// The only horizontal reset is right→left WITHIN the same screen row when the\n// camera advances a system — never a leap over intervening staff lines.\n\n/** The system index a measure box belongs to (nearest by vertical center).\n * rn.systems are y-sorted row boxes; we pick the row whose center is closest. */\nfunction systemIndexOfBox(systems: Box[], box: Box): number {\n if (!systems.length) return 0;\n const cy = box.y + box.h / 2;\n let best = 0;\n let bestD = Infinity;\n for (let i = 0; i < systems.length; i++) {\n const s = systems[i];\n // Inside the system's vertical band → exact match.\n if (cy >= s.y && cy <= s.y + s.h) return i;\n const sc = s.y + s.h / 2;\n const d = Math.abs(cy - sc);\n if (d < bestD) { bestD = d; best = i; }\n }\n return best;\n}\n\n/** For each measure index, the system (row) it lives on — ordered by index.\n * Empty when geometry is missing. */\nexport function measureSystemMap(rn: RenderedNotation): number[] {\n const systems = (rn.systems ?? []);\n const n = measureCount(rn);\n const map = new Array<number>(n).fill(0);\n // For each measure index, use any staff's box to find its row.\n for (let idx = 0; idx < n; idx++) {\n const m = (rn.measures ?? []).find((mm) => mm.index === idx);\n map[idx] = m ? systemIndexOfBox(systems, m.box) : 0;\n }\n return map;\n}\n\n/** Union (canvas coords) of every measure box on system `sys`. Falls back to the\n * system's own box when no measures map to it. */\nexport function systemBox(rn: RenderedNotation, sys: number, sysMap?: number[]): Box | null {\n const map = sysMap ?? measureSystemMap(rn);\n const boxes = (rn.measures ?? [])\n .filter((m) => map[m.index] === sys)\n .map((m) => m.box);\n if (!boxes.length) return (rn.systems ?? [])[sys] ?? null;\n const x0 = Math.min(...boxes.map((b) => b.x));\n const y0 = Math.min(...boxes.map((b) => b.y));\n const x1 = Math.max(...boxes.map((b) => b.x + b.w));\n const y1 = Math.max(...boxes.map((b) => b.y + b.h));\n // vstack: per-system box — generous fixed headroom for ledger lines, but NO ink\n // union (rn.content spans every system; unioning would grab the whole page). 8\n // staff-spaces ≈ 3-4 ledger lines of clearance above & below, which covers the\n // overwhelming majority of real notation without zooming the system out much.\n return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 8, false);\n}\n\n/**\n * vstack follow window (canvas coords) for an audio-clock progress 0..1. Frames\n * the ACTIVE system (the row the playhead's current bar is on) and eases\n * VERTICALLY to the NEXT system over the tail of the active system's last bar, so\n * the next row slides up just as the playhead crosses into it. Within a system\n * the box is fixed (the playhead pans L→R inside it). No measure-window vertical\n * jumps: the focusBox always spans an integer-or-tween of FULL systems.\n *\n * The box keeps a constant size (the larger of the two framed systems' extents)\n * so the dest mapping doesn't breathe as it scrolls — the camera just translates.\n */\nexport function vstackFollowBox(rn: RenderedNotation, camProgress01: number): Box | null {\n const sysMap = measureSystemMap(rn);\n const nBars = measureCount(rn);\n const nSys = (rn.systems ?? []).length || (sysMap.length ? Math.max(...sysMap) + 1 : 0);\n if (nBars <= 0 || nSys <= 0) return null;\n\n const posBars = Math.max(0, Math.min(1, camProgress01)) * nBars;\n const curBar = Math.min(nBars - 1, Math.floor(posBars));\n const barFrac = posBars - Math.floor(posBars);\n const curSys = sysMap[curBar] ?? 0;\n\n // Is curBar the LAST bar of its system? If so, ease toward the next system over\n // the tail of this bar so the next row scrolls up as the playhead exits.\n const isLastBarOfSys =\n curBar + 1 >= nBars || (sysMap[curBar + 1] ?? curSys) !== curSys;\n const scrollFrac = isLastBarOfSys\n ? cubicEaseInOut(Math.min(1, Math.max(0, (barFrac - 0.66) / 0.34)))\n : 0;\n const nextSys = Math.min(nSys - 1, curSys + 1);\n\n const a = systemBox(rn, curSys, sysMap);\n const b = systemBox(rn, nextSys, sysMap) ?? a;\n if (!a) return b;\n if (!b) return a;\n\n // Constant-size box (max extent of the two systems) translated between them, so\n // the camera only scrolls (no zoom-breathing as rows differ slightly in size).\n const w = Math.max(a.w, b.w);\n const h = Math.max(a.h, b.h);\n const x = a.x + (b.x - a.x) * scrollFrac;\n const y = a.y + (b.y - a.y) * scrollFrac;\n return { x, y, w, h };\n}\n\n// ─── drawNotation geometry (verbatim from RSR +page.svelte:262-331) ──────────\n\n/** The dest rect a notation frame is drawn into, on screen. */\nexport interface NotationRect {\n dx: number;\n dy: number;\n dw: number;\n dh: number;\n}\n\n/** Geometry result of laying out the notation for one frame: which canvas `src`\n * rect is blitted to which screen `dest` rect, plus the mapped boxes. */\nexport interface NotationLayout {\n /** Canvas-space crop blitted this frame. */\n src: Box;\n /** Screen-space dest rect. */\n rect: NotationRect;\n /** Per-row system boxes mapped into screen coords (for the playhead fallback). */\n systems: Box[];\n /** Per-(measure,staff) boxes mapped into screen coords (for highlights/cursor). */\n measures: StaffMeasureBox[];\n}\n\nexport interface NotationLayoutOpts {\n /** 0 = zoomed on bar 1, 1 = full excerpt (opening camera). Default 1. */\n zoom01?: number;\n /** Follow window (canvas coords); when set, overrides zoom and scrolls. */\n focusBox?: Box | null;\n /**\n * Explicit horizontal fit box (screen px), overriding the default\n * `safeBox(W,H).centeredW`. `safeBox`'s left/right margins (`SAFE_ZONE` in\n * video.ts) reserve ~24% of width for TikTok/Reels-style caption/share-button\n * chrome — correct for the promo/video card this geometry was built for, but\n * wrong for a plain in-app player canvas with no such overlay, which wants\n * (up to) the FULL available width. Omit to keep the existing promo/video\n * framing unchanged (every caller before this option existed still gets\n * exactly `sb.centeredW`).\n */\n boxWidth?: number;\n}\n\n/**\n * Compute the notation layout (src crop + dest rect + mapped boxes) for one\n * frame. This is RSR's `drawNotation` with the single `ctx.drawImage` call REMOVED\n * — pure geometry, so it is testable headless and shared by the notation +\n * scroll-cursor layers. The layer does the drawImage using `src` + `rect`.\n */\nexport function notationLayout(\n rn: RenderedNotation,\n W: number,\n H: number,\n boxTop: number,\n boxH: number,\n opts: NotationLayoutOpts = {},\n): NotationLayout {\n const { zoom01 = 1, focusBox = null } = opts;\n const sb = safeBox(W, H);\n const maxW = opts.boxWidth ?? sb.centeredW;\n const c =\n rn.content && rn.content.w > 0 && rn.content.h > 0\n ? rn.content\n : { x: 0, y: 0, w: rn.canvas.width || 1400, h: rn.canvas.height || 300 };\n\n let src: Box;\n if (focusBox) {\n const px = (focusBox.w * (FOLLOW_PAD - 1)) / 2;\n const py = (focusBox.h * (FOLLOW_PAD - 1)) / 2;\n src = {\n x: Math.max(0, focusBox.x - px),\n y: Math.max(0, focusBox.y - py),\n w: focusBox.w + 2 * px,\n h: focusBox.h + 2 * py,\n };\n src.w = Math.min(src.w, rn.canvas.width - src.x);\n src.h = Math.min(src.h, rn.canvas.height - src.y);\n } else {\n src = c;\n const focal = firstMeasureBox(rn);\n if (zoom01 < 1 && focal) {\n const start = cropAroundBox(focal, c.w / c.h, 1.25, rn.canvas.width, rn.canvas.height);\n src = lerpBox(start, c, cubicEaseInOut(zoom01));\n }\n }\n\n const srcAspect = src.w / src.h;\n let dw = maxW;\n let dh = dw / srcAspect;\n if (dh > boxH) {\n dh = boxH;\n dw = dh * srcAspect;\n }\n const dx = (W - dw) / 2;\n const dy = boxTop + (boxH - dh) / 2;\n const fx = dw / src.w;\n const fy = dh / src.h;\n const map = (b: Box): Box => ({\n x: dx + (b.x - src.x) * fx,\n y: dy + (b.y - src.y) * fy,\n w: b.w * fx,\n h: b.h * fy,\n });\n const systems = (rn.systems ?? []).map(map);\n const measures = (rn.measures ?? []).map((m) => ({\n ...m,\n box: map(m.box),\n noteStartX: dx + (m.noteStartX - src.x) * fx,\n }));\n return { src, rect: { dx, dy, dw, dh }, systems, measures };\n}\n\n// ─── Playhead geometry (verbatim from RSR +page.svelte:446-518) ──────────────\n\n/** Per-measure grand-staff column boxes (both staves unioned) from a frame's\n * mapped measures, in render order. (RSR measureColumns) */\nexport function measureColumnsFromLayout(measures: StaffMeasureBox[]): MeasureColumnBox[] {\n const byIndex = new Map<number, MeasureColumnBox>();\n for (const m of measures) {\n const cur = byIndex.get(m.index);\n if (!cur) {\n byIndex.set(m.index, { ...m.box, noteStartX: m.noteStartX });\n } else {\n const x0 = Math.min(cur.x, m.box.x);\n const y0 = Math.min(cur.y, m.box.y);\n const x1 = Math.max(cur.x + cur.w, m.box.x + m.box.w);\n const y1 = Math.max(cur.y + cur.h, m.box.y + m.box.h);\n byIndex.set(m.index, {\n x: x0,\n y: y0,\n w: x1 - x0,\n h: y1 - y0,\n noteStartX: Math.min(cur.noteStartX, m.noteStartX),\n });\n }\n }\n return [...byIndex.keys()].sort((a, b) => a - b).map((k) => byIndex.get(k)!);\n}\n\n/** The playhead line + alpha for progress `t01`. null when fully faded. (RSR\n * drawPlayhead, with the actual stroke factored out into the layer.) */\nexport interface PlayheadLine {\n x: number;\n y0: number;\n y1: number;\n alpha: number;\n}\n\nexport function playheadLine(layout: NotationLayout, t01: number): PlayheadLine | null {\n const tt = Math.max(0, Math.min(1, t01));\n let alpha = 0.85;\n if (tt < 0.03) alpha *= tt / 0.03; // fade in\n if (tt > 0.94) alpha *= Math.max(0, (1 - tt) / 0.06); // fade out\n if (alpha <= 0.02) return null;\n\n const padV = 10;\n let x: number, y0: number, y1: number;\n const cols = measureColumnsFromLayout(layout.measures);\n if (cols.length) {\n const pos = tt * cols.length;\n const i = Math.min(cols.length - 1, Math.floor(pos));\n const m = cols[i];\n const startX = Math.min(m.noteStartX, m.x + m.w);\n x = startX + (pos - i) * (m.x + m.w - startX);\n y0 = m.y - padV;\n y1 = m.y + m.h + padV;\n } else if (layout.systems.length) {\n const pos = tt * layout.systems.length;\n const row = Math.min(layout.systems.length - 1, Math.floor(pos));\n const s = layout.systems[row];\n x = s.x + (pos - row) * s.w;\n y0 = s.y - padV;\n y1 = s.y + s.h + padV;\n } else {\n const r = layout.rect;\n x = r.dx + tt * r.dw;\n y0 = r.dy - padV;\n y1 = r.dy + r.dh + padV;\n }\n return { x, y0, y1, alpha };\n}\n\n// ─── Audio-driven playhead (FOOLPROOF default) ───────────────────────────────\n//\n// The foolproof cursor never paces a single 0..1 progress across the whole\n// engraving width (the linear `playheadLine` above — the whozart footgun). It\n// instead anchors EACH note to a geometry X (from the same followed layout the\n// notation blits) and steps the cursor BETWEEN those anchors keyed on the audio\n// clock `tMs` against the notes' own `onsetMs`. So:\n// - it lands on each note's X exactly when that note onsets,\n// - it only ever traverses notes whose onset has been reached — a long score\n// with a short audio window can only reach the notes that play, so it\n// CANNOT race the full width regardless of segment duration / score length,\n// - before the first note it holds on the first anchor (faded in); after the\n// last it holds on the last; rests/gaps are spanned by the same time-lerp\n// between the bracketing onsets (smooth hold-then-glide).\n//\n// Per-note anchor X: notes don't carry an engraved X (Score is parsed headless,\n// notation is rasterized separately), but BOTH derive from the same MusicXML in\n// the same order. We therefore spread the score's distinct onsets, IN ORDER,\n// across the measure-column note positions, and read each onset's X off the\n// followed layout's mapped columns. The anchor is a pure function of the note's\n// ORDINAL (not of t / musicMs), so the time axis stays strictly the onset clock\n// — that is what makes the race impossible.\n\nconst clamp01 = (x: number): number => (x < 0 ? 0 : x > 1 ? 1 : x);\n\n/** A note anchor on the engraving: its onset time and its mapped screen X/row. */\ninterface NoteAnchor {\n onsetMs: number;\n x: number;\n y0: number;\n y1: number;\n}\n\n/** Map a fractional column position `pos` (0..nCols) to an X + vertical row span\n * using the followed layout's measure columns. `pos` is clamped to the columns. */\nfunction anchorAtColumnPos(cols: MeasureColumnBox[], pos: number, padV: number): NoteAnchor {\n const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));\n const m = cols[i];\n const startX = Math.min(m.noteStartX, m.x + m.w);\n const frac = Math.min(1, Math.max(0, pos - i));\n return {\n onsetMs: 0,\n x: startX + frac * (m.x + m.w - startX),\n y0: m.y - padV,\n y1: m.y + m.h + padV,\n };\n}\n\n/**\n * Build per-note anchors from the followed layout + the played notes. One anchor\n * per DISTINCT onset (chords share an onset and thus one cursor X), ordered by\n * onset. The k-th of N distinct onsets sits at column position `k/(N-1) * nCols`,\n * so the cursor visits the engraving left→right exactly as the notes sound.\n */\nfunction noteAnchors(\n layout: NotationLayout,\n onsetsMs: number[],\n padV: number,\n barDurMs?: number,\n noteCols?: number[],\n): NoteAnchor[] {\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length || !onsetsMs.length) return [];\n const n = onsetsMs.length;\n const first = onsetsMs[0];\n // BEST: `noteCols[k]` is the note's REAL engraved column (measureIndex + frac\n // across the note region), so the cursor lands exactly on the notehead. Used\n // only when it aligns 1:1 with the onsets (same count). Else TIME-based\n // (onset/barDurMs — rhythmically right but linear-within-measure). Else the\n // legacy ORDINAL spread (k/(n-1)*nCols — drifts on real rhythm).\n const useEngraved = !!noteCols && noteCols.length === n;\n return onsetsMs.map((onsetMs, k) => {\n const pos = useEngraved\n ? noteCols![k]\n : barDurMs && barDurMs > 0\n ? (onsetMs - first) / barDurMs\n : (n === 1 ? 0 : k / (n - 1)) * cols.length;\n const a = anchorAtColumnPos(cols, pos, padV);\n return { ...a, onsetMs };\n });\n}\n\n/** Distinct, sorted note onsets from a Score's notes. */\nexport function distinctOnsets(notes: { onsetMs: number }[]): number[] {\n const set = new Set<number>();\n for (const n of notes) set.add(n.onsetMs);\n return [...set].sort((a, b) => a - b);\n}\n\n/**\n * The FOOLPROOF audio-driven playhead line for absolute audio time `tMs`.\n *\n * `tMs` is the audio clock; `onsetsMs` are the score's distinct note onsets\n * (sorted). The cursor is the time-lerp of the two anchors bracketing `tMs`:\n * before the first onset it holds on anchor 0; after the last it holds on the\n * last; in a gap it eases between the bracketing onsets. Pure function of\n * (layout, onsetsMs, tMs). Returns null only when there is no geometry to anchor\n * to (falls back to the rect sweep in the layer, same as `playheadLine`).\n *\n * Fade in/out mirrors `playheadLine` but is keyed on position in the ONSET span\n * (first→last onset), not on musicMs — so the fade tracks the notes too.\n */\nexport function audioPlayheadLine(\n layout: NotationLayout,\n onsetsMs: number[],\n tMs: number,\n barDurMs?: number,\n noteCols?: number[],\n): PlayheadLine | null {\n const padV = 10;\n const anchors = noteAnchors(layout, onsetsMs, padV, barDurMs, noteCols);\n if (!anchors.length) return null;\n\n const first = anchors[0].onsetMs;\n const last = anchors[anchors.length - 1].onsetMs;\n const span = last - first;\n\n // Locate the bracketing anchors for tMs and the inter-onset fraction.\n let a: NoteAnchor, b: NoteAnchor, frac: number;\n if (tMs <= first || anchors.length === 1) {\n a = b = anchors[0];\n frac = 0;\n } else if (tMs >= last) {\n a = b = anchors[anchors.length - 1];\n frac = 0;\n } else {\n // Find the last anchor with onset <= tMs (binary search; onsets are sorted).\n let lo = 0;\n let hi = anchors.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (anchors[mid].onsetMs <= tMs) lo = mid;\n else hi = mid - 1;\n }\n a = anchors[lo];\n b = anchors[lo + 1];\n const dt = b.onsetMs - a.onsetMs;\n // LINEAR between onsets — the cursor must advance at the SAME constant rate the\n // notation scrolls (the follow camera, notesProgress, lerps linearly between\n // onsets). cubicEaseInOut here made the line lag in the first half of every\n // note gap then RUSH AHEAD in the second half — read as \"playhead moves faster\n // than the audio, getting worse every note\" on dense pieces. Both must use the\n // same interpolation so the line stays glued to its notehead as bars scroll.\n frac = dt > 0 ? (tMs - a.onsetMs) / dt : 0;\n }\n\n const x = a.x + (b.x - a.x) * frac;\n const y0 = a.y0 + (b.y0 - a.y0) * frac;\n const y1 = a.y1 + (b.y1 - a.y1) * frac;\n\n // Edge fades, time-based (NOT a fraction of the span — a fraction would make\n // the cursor vanish for seconds at the start/end of a long score, the very\n // case the foolproof cursor must handle). Fade in over the lead-up to the first\n // onset, fade out after the last; clamp the window so a short clip still fades.\n let alpha = 0.85;\n if (span > 0) {\n const fadeIn = Math.min(250, span * 0.5);\n const fadeOut = Math.min(400, span * 0.5);\n if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));\n if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));\n }\n if (alpha <= 0.02) return null;\n\n return { x, y0, y1, alpha };\n}\n\n// ─── vstack audio playhead (camera-synced, no vertical leap) ──────────────────\n//\n// BUG FIX (web-core 0.20.0) — whozart vstack vertical leap. The generic\n// `audioPlayheadLine` spreads onsets across ALL columns of the followed layout and\n// time-lerps between bracketing onsets. In vstack the followed layout frames ONE\n// system; a note on the NEXT system maps off-band, so lerping to it (or holding on\n// a note that the camera is scrolling off-screen) sent the cursor leaping across\n// staff lines. The whozart audio path (mode:'audio' + scrollMode:'vstack') hit\n// this every system boundary.\n//\n// This variant is PHASE-LOCKED to `vstackFollowBox`: it walks measure-columns by\n// the same onset progress that drives the camera, and at a system boundary it\n// eases the cursor from the last column of the active system to the FIRST column of\n// the next system over the SAME barFrac∈[0.66,1] scroll window the camera uses — so\n// the cursor rides DOWN with the incoming system and is never stranded off-band.\n// Within a system it tracks each note's column left→right on the onset clock\n// (foolproof: never races, lands on each note as it sounds). Fade matches\n// audioPlayheadLine (onset-span based).\nexport function vstackAudioPlayheadLine(\n layout: NotationLayout,\n onsetsMs: number[],\n tMs: number,\n nBars: number,\n noteCols?: number[],\n): PlayheadLine | null {\n const padV = 10;\n const cols = measureColumnsFromLayout(layout.measures);\n if (!cols.length || !onsetsMs.length || nBars <= 0) return null;\n const n = onsetsMs.length;\n const first = onsetsMs[0];\n const last = onsetsMs[n - 1];\n const span = last - first;\n\n const colX = (m: MeasureColumnBox, f: number): number => {\n const sx = Math.min(m.noteStartX, m.x + m.w);\n return sx + Math.min(1, Math.max(0, f)) * (m.x + m.w - sx);\n };\n // Per-onset anchor: its engraved column X + the staff row it sits on. Uses the\n // real engraved noteCols when they pair 1:1 with the onsets (lands on the actual\n // notehead); else the legacy ordinal spread.\n const useEng = !!noteCols && noteCols.length === n;\n const anchorFor = (k: number) => {\n const pos = useEng ? noteCols![k] : (n === 1 ? 0 : k / (n - 1)) * cols.length;\n const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));\n const m = cols[i];\n return { x: colX(m, pos - i), rowY: m.y, rowH: m.h };\n };\n const sameRow = (ya: number, yb: number) => Math.abs(ya - yb) < Math.max(8, ya * 0 + 8);\n const rowRightEdge = (rowY: number) =>\n Math.max(...cols.filter((c) => sameRow(c.y, rowY)).map((c) => c.x + c.w));\n const rowLeftEdge = (rowY: number) =>\n Math.min(...cols.filter((c) => sameRow(c.y, rowY)).map((c) => Math.min(c.noteStartX, c.x)));\n\n // Bracketing onsets + linear inter-onset fraction.\n let a: ReturnType<typeof anchorFor>, b: ReturnType<typeof anchorFor>, frac: number;\n if (tMs <= first || n === 1) { a = b = anchorFor(0); frac = 0; }\n else if (tMs >= last) { a = b = anchorFor(n - 1); frac = 0; }\n else {\n let lo = 0, hi = n - 1;\n while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (onsetsMs[mid] <= tMs) lo = mid; else hi = mid - 1; }\n a = anchorFor(lo); b = anchorFor(lo + 1);\n frac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);\n }\n\n let x: number, rowY: number, rowH: number;\n if (sameRow(a.rowY, b.rowY)) {\n // Same staff row → glide horizontally between the two noteheads.\n x = a.x + (b.x - a.x) * frac;\n rowY = a.rowY; rowH = a.rowH;\n } else {\n // System boundary → CARRIAGE RETURN, read like text: the first half of the gap\n // sweeps OUT to the current row's right edge (still on that row); the second\n // half comes IN from the next row's left edge to the next notehead (on the next\n // row). The bars stay stacked; the cursor wraps left→right, row to row.\n if (frac < 0.5) {\n const s = frac / 0.5;\n x = a.x + (rowRightEdge(a.rowY) - a.x) * s;\n rowY = a.rowY; rowH = a.rowH;\n } else {\n const s = (frac - 0.5) / 0.5;\n const nl = rowLeftEdge(b.rowY);\n x = nl + (b.x - nl) * s;\n rowY = b.rowY; rowH = b.rowH;\n }\n }\n let y0 = rowY - padV;\n let y1 = rowY + rowH + padV;\n\n // Clamp into the drawn band so the cursor can never ride off-screen.\n const bandY0 = layout.rect.dy;\n const bandY1 = layout.rect.dy + layout.rect.dh;\n const h = Math.max(1, y1 - y0);\n if (y0 < bandY0) { y0 = bandY0; y1 = bandY0 + h; }\n if (y1 > bandY1) { y1 = bandY1; y0 = bandY1 - h; }\n\n let alpha = 0.85;\n if (span > 0) {\n const fadeIn = Math.min(250, span * 0.5);\n const fadeOut = Math.min(400, span * 0.5);\n if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));\n if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));\n }\n if (alpha <= 0.02) return null;\n\n return { x, y0, y1, alpha };\n}\n","// Shared engraving handoff (S2) — lets the `notation` + `scroll-cursor` layers\n// cooperate without re-laying-out: notation publishes its rasterized engraving;\n// scroll-cursor publishes the per-frame follow LAYOUT (the src crop + dest rect),\n// which notation then blits. They agree by construction (one geometry source).\n//\n// Why a side store rather than Score.engraving: the spec's Score.engraving holds\n// browser-only canvas types, but Score is parsed headless in Node and is the\n// audio source for whozart (no notation). Keeping the engraving off Score keeps\n// score.ts DOM-free. We key the store by the runner's shared `audioClock` object\n// (one per render) via a WeakMap — per-render, GC-friendly, no globals.\n\nimport type { RenderCtx } from './layer';\nimport type { RenderedNotation } from '../promo';\nimport type { NotationLayout } from './notationGeometry';\n\nexport interface NotationEngraving {\n rendered: RenderedNotation;\n /** The base world layout (full content fitted to the band; zoom01=1). */\n base: NotationLayout;\n /** Notation band rect (screen px) the follow window fits into, per frame. */\n bandTop: number;\n bandHeight: number;\n /** Explicit horizontal fit width (screen px), forwarded to `notationLayout`'s\n * `boxWidth` — undefined keeps the default `safeBox(W,H).centeredW` promo\n * framing (see `notationGeometry.ts`'s `NotationLayoutOpts.boxWidth` doc).\n * Published so `scroll-cursor`'s per-frame follow layout uses the SAME\n * width `notation`'s base layout was computed with — they must agree, or\n * the visible framing jumps between the base frame and the first followed\n * frame. */\n bandWidth?: number;\n /**\n * A pure follow-layout provider, published by scroll-cursor at init. notation\n * calls it each frame to blit the SAME followed-bars window the cursor sweeps —\n * so the two agree by construction AND z-order is correct (notation drawn first,\n * cursor line on top), regardless of which layer the runner draws first.\n * Absent when no scroll-cursor is in the scene (notation-only) -> base layout.\n */\n followLayoutAt?: (ctx: RenderCtx, tMs: number) => NotationLayout;\n}\n\nconst STORE = new WeakMap<object, NotationEngraving>();\n\nfunction keyFor(ctx: RenderCtx): object {\n return ctx.audioClock;\n}\n\nexport function setNotationEngraving(ctx: RenderCtx, eng: NotationEngraving): void {\n STORE.set(keyFor(ctx), eng);\n}\n\nexport function getNotationEngraving(ctx: RenderCtx): NotationEngraving | undefined {\n return STORE.get(keyFor(ctx));\n}\n\n/** scroll-cursor publishes its follow-layout provider; notation reads it. */\nexport function setFollowLayoutProvider(\n ctx: RenderCtx,\n fn: (ctx: RenderCtx, tMs: number) => NotationLayout,\n): void {\n const e = STORE.get(keyFor(ctx));\n if (e) e.followLayoutAt = fn;\n}\n","// `notation` layer (S2) — extracted FAITHFULLY from RSR's render code\n// (stave-web-sightread/src/routes/promo/+page.svelte drawNotation +\n// $lib/promo/notation.ts renderNotation).\n//\n// Responsibilities:\n// - init(): rasterize the engraving ONCE via web-core's renderNotation (OSMD).\n// The expensive OSMD relayout happens here, never per-frame (spec: \"render\n// once, rasterize, pan/scroll the bitmap\").\n// - lay out the bitmap into the base world rect once (RSR's drawNotation with\n// zoom01=1, focusBox=null) — the full content fitted to the notation band.\n// - draw(): blit the bitmap using the PER-FRAME follow layout published by the\n// scroll-cursor layer (RSR re-crops the src window each frame to keep the\n// followed 2 bars filling the band). When no scroll-cursor is present it falls\n// back to the base full-excerpt layout. This is RSR's drawNotation exactly,\n// with only the single drawImage living here.\n//\n// The engraving + base layout are published via the engraving store (set in init)\n// so the scroll-cursor layer reads the SAME geometry without re-laying-out. Both\n// layers therefore agree by construction (one geometry source).\n\nimport type { Layer, LayerFactory, RenderCtx } from '../layer';\nimport type { RenderedNotation } from '../../promo';\nimport { safeBox } from '../../video';\nimport { notationLayout, type NotationLayout } from '../notationGeometry';\nimport { setNotationEngraving, getNotationEngraving } from '../engravingStore';\n\nexport interface NotationProps {\n /** Engraving system: \"grand\" (two staves) or \"single\". Informational for v1 —\n * the layout is driven by the rasterized bitmap's geometry either way. */\n system?: 'grand' | 'single';\n /** Extra scale applied to the band height the notation fits into. Default 1. */\n scale?: number;\n /**\n * A pre-rendered notation (test/headless injection). When omitted, init()\n * calls renderNotation(xml). One of `rendered` or `xml` is required.\n */\n rendered?: RenderedNotation;\n /** MusicXML to engrave (browser path). Ignored when `rendered` is given. */\n xml?: string;\n /** Bar range [from,to] forwarded to renderNotation (RSR drawFrom/drawUpTo). */\n bars?: [number, number];\n /**\n * Engraving scroll mode (forwarded to renderNotation when engraving from\n * `xml`). 'hstack' (default) = single horizontal staffline; 'vstack' = stacked\n * systems. MUST match the scroll-cursor layer's `scrollMode`. Ignored when a\n * pre-rendered `rendered` is supplied (engraving is already laid out).\n */\n scrollMode?: 'hstack' | 'vstack';\n /** Top y of the notation band (screen px). Default safeBox.top. */\n bandTop?: number;\n /** Height of the notation band (screen px). Default safeBox.bottom - bandTop. */\n bandHeight?: number;\n /** Offscreen OSMD host div width in CSS px, forwarded verbatim to\n * `renderNotation`'s `hostWidth` (default 560 there when omitted). This is\n * the width OSMD line-breaks against — a live player wanting engraved\n * systems to use its actual container width (rather than always the\n * 560px default meant for a fixed-size promo card) sets this to its own\n * measured host size. Ignored when `rendered` is supplied (no engrave\n * happens). */\n hostWidth?: number;\n /** Explicit horizontal fit width (screen px), forwarded to `notationLayout`'s\n * `boxWidth` (see its doc comment) — bypasses `safeBox`'s promo-video\n * caption/share-button margins (~24% of width reserved) so the engraving\n * can use the FULL band width instead. Default: unset (existing\n * `safeBox(W,H).centeredW` promo framing, unchanged for every caller that\n * doesn't pass this). */\n bandWidth?: number;\n}\n\nfunction notationLayer(): Layer<NotationProps> {\n let rn: RenderedNotation | null = null;\n let base: NotationLayout | null = null;\n\n let propScale = 1;\n let propBandTop: number | undefined;\n let propBandHeight: number | undefined;\n let propBandWidth: number | undefined;\n\n function bandTop(_ctx: RenderCtx, sb: ReturnType<typeof safeBox>): number {\n return propBandTop ?? sb.top;\n }\n function bandHeight(ctx: RenderCtx, sb: ReturnType<typeof safeBox>): number {\n const top = bandTop(ctx, sb);\n return (propBandHeight ?? sb.bottom - top) * propScale;\n }\n\n return {\n key: 'notation',\n async init(ctx, props) {\n propScale = props.scale ?? 1;\n propBandTop = props.bandTop;\n propBandHeight = props.bandHeight;\n propBandWidth = props.bandWidth;\n\n if (props.rendered) {\n rn = props.rendered;\n } else if (props.xml) {\n // Browser path: rasterize the engraving once (OSMD). Literal import is\n // inside renderNotation so bundlers resolve it.\n const { renderNotation } = await import('../../promo');\n rn = await renderNotation(props.xml, {\n bars: props.bars,\n paper: ctx.theme.paper,\n scrollMode: props.scrollMode ?? 'hstack',\n hostWidth: props.hostWidth,\n });\n } else {\n throw new Error('notation layer: provide `rendered` or `xml`');\n }\n\n // Publish the engraving + base layout + band so the scroll-cursor layer\n // reads the SAME geometry (no second relayout). ctx2d may be null in init.\n const sb = safeBox(ctx.W, ctx.H);\n const top = bandTop(ctx, sb);\n const height = bandHeight(ctx, sb);\n base = notationLayout(rn, ctx.W, ctx.H, top, height, { boxWidth: propBandWidth });\n setNotationEngraving(ctx, { rendered: rn, base, bandTop: top, bandHeight: height, bandWidth: propBandWidth });\n },\n\n draw(ctx, tMs) {\n if (!rn || !base) return;\n // Blit the scroll-cursor's follow window when present (a scroll-cursor layer\n // published a pure follow-layout provider at init); else the base full-\n // excerpt layout (notation-only scene). One drawImage — the sole canvas op of\n // RSR's drawNotation. Calling the provider (rather than reading a value the\n // cursor's draw set) makes blit z-order independent of layer draw order.\n const eng = getNotationEngraving(ctx);\n const l = eng?.followLayoutAt ? eng.followLayoutAt(ctx, tMs) : base;\n const c = ctx.ctx2d;\n c.drawImage(\n rn.canvas,\n l.src.x, l.src.y, l.src.w, l.src.h,\n l.rect.dx, l.rect.dy, l.rect.dw, l.rect.dh,\n );\n },\n\n dispose() {\n rn = null;\n base = null;\n },\n };\n}\n\nexport const notationFactory: LayerFactory<NotationProps> = {\n key: 'notation',\n create: notationLayer,\n validateProps(props) {\n const errs: string[] = [];\n if (props == null || typeof props !== 'object') return ['notation: props must be an object'];\n const p = props as Record<string, unknown>;\n if (p.system != null && p.system !== 'grand' && p.system !== 'single')\n errs.push('notation.system must be \"grand\" | \"single\"');\n if (p.scrollMode != null && p.scrollMode !== 'hstack' && p.scrollMode !== 'vstack')\n errs.push('notation.scrollMode must be \"hstack\" | \"vstack\"');\n if (p.scale != null && (typeof p.scale !== 'number' || p.scale <= 0))\n errs.push('notation.scale must be a positive number');\n if (p.rendered == null && typeof p.xml !== 'string')\n errs.push('notation: provide `rendered` (RenderedNotation) or `xml` (string)');\n if (p.bars != null && (!Array.isArray(p.bars) || p.bars.length !== 2))\n errs.push('notation.bars must be [from,to]');\n if (p.hostWidth != null && (typeof p.hostWidth !== 'number' || p.hostWidth <= 0))\n errs.push('notation.hostWidth must be a positive number');\n if (p.bandWidth != null && (typeof p.bandWidth !== 'number' || p.bandWidth <= 0))\n errs.push('notation.bandWidth must be a positive number');\n return errs;\n },\n};\n","// `scroll-cursor` layer (S2) — the scrolling playhead + 2-bar follow window.\n//\n// AUDIO-ONSET LOCKED, ALWAYS (web-core 0.22.0). The cursor is positioned by the\n// audio clock NOTE-BY-NOTE — never by a free-running linear interpolation. Each\n// played note is anchored to a geometry X (from the followed layout's mapped\n// measure columns); the cursor is the time-lerp of the two notes bracketing the\n// current audio time `t` by their `onsetMs`. It therefore lands on each note as\n// it sounds and only ever traverses the notes that actually play — it CANNOT race\n// the whole score or drift, no matter what `musicMs` / score length / segment\n// duration it is fed. This is the ONLY positioning path: feed the cursor the\n// played notes via `ctx.score` and it is onset-synced by construction.\n//\n// NO LINEAR SWEEP. The legacy \"camProgress 0..1 across musicMs\" measure-linear\n// sweep (the whozart/RSR desync footgun — a plausible-but-wrong interpolation\n// that drifts off the real onsets) has been REMOVED as a positioning path. When\n// there is NO `ctx.score` (no onsets to lock to) the cursor does NOT fake a\n// sweep: it HOLDS at the start (follow window held at bar 0, cursor on the first\n// note's anchor) so a mis-wired scene shows a stationary playhead — an obvious\n// \"no score fed\" signal — instead of a smooth-but-lying scroll. The `mode` prop\n// is retained only for back-compat; both values now resolve to the onset path\n// (it is a no-op, NOT a switch back to the linear sweep).\n//\n// Per frame it:\n// 1. derives the follow progress 0..1 from the cursor's onset clock\n// (notesProgress over the score's distinct onsets; held at 0 with no score),\n// 2. computes the follow-window start + follow box, then the follow LAYOUT\n// (notationLayout with that focusBox) — RSR's drawNotation geometry,\n// 3. publishes that layout so the notation layer blits the followed bars,\n// 4. draws the onset-anchored playhead line (audioPlayheadLine /\n// vstackAudioPlayheadLine).\n//\n// Riding the camera primitive: the follow window is a world-space rect; the same\n// scroll/zoom RSR achieves by re-cropping `src` is expressible as a camera pose\n// (frameRect over the follow rect). `cameraForFollow()` (in ../notationCamera)\n// produces that pose, and scene-camera-equivalence.test.ts proves it reproduces\n// RSR's crop.\n\nimport type { Layer, LayerFactory, RenderCtx } from '../layer';\nimport {\n audioPlayheadLine,\n distinctOnsets,\n followBoxAt,\n followWindowStart,\n measureCount,\n notationLayout,\n vstackAudioPlayheadLine,\n vstackFollowBox,\n type NotationLayout,\n} from '../notationGeometry';\nimport { getNotationEngraving, setFollowLayoutProvider } from '../engravingStore';\n\nexport interface ScrollCursorProps {\n /**\n * DEPRECATED / back-compat only. The cursor is ALWAYS audio-onset locked now —\n * driven note-by-note off the audio clock against each note's `onsetMs` (see\n * `ctx.score`). Both `'audio'` and `'linear'` resolve to that single onset path;\n * `'linear'` no longer re-enables the legacy measure-linear sweep (removed —\n * that was the drift footgun). Prefer leaving this unset.\n */\n mode?: 'audio' | 'linear';\n /**\n * Notation scroll mode — MUST match the engraving's `scrollMode`.\n * 'hstack' (default) — single horizontal staffline: the follow window is a\n * FOLLOW_BARS-wide horizontal slice that pans left→right (followBoxAt /\n * followWindowStart). No vertical movement.\n * 'vstack' — stacked systems: the follow camera frames the ACTIVE system at a\n * fixed band position and scrolls VERTICALLY to the next system as the\n * playhead crosses systems (vstackFollowBox). The playhead pans L→R within\n * the framed system; the only horizontal reset is per-system, never a\n * vertical leap over staff lines.\n */\n scrollMode?: 'hstack' | 'vstack';\n /** Bars visible in the follow window. Default 2 (RSR FOLLOW_BARS). Informational\n * for v1 — the geometry uses the module constant unless overridden here. */\n followBars?: number;\n /** Opening-zoom duration in ms before the music/cursor start (RSR INTRO_MS=900).\n * During this lead-in the follow window is held at the start. Default 900. */\n openingZoomMs?: number;\n /** DEPRECATED. Total music length in ms (RSR musicMs). No longer used to\n * position the cursor (the onset clock drives all pacing); accepted for\n * back-compat and ignored. */\n musicMs?: number;\n /** Cursor stroke colour. Defaults to theme.accent. */\n color?: string;\n /**\n * Duration of ONE measure in ms. When provided, the cursor + follow window are\n * positioned by onset TIME (column position = (onset-first)/barDurMs), so the\n * playhead lands on each note's real rhythmic X. Without it, positioning falls\n * back to the legacy ORDINAL spread (note index / count), which drifts off the\n * noteheads on any non-uniform rhythm. Assumes a constant meter over the\n * excerpt (true for the curated short excerpts).\n */\n barDurMs?: number;\n /**\n * Per-distinct-onset cursor column positions (`ordinal + frac`), 1:1 with the\n * score's sorted distinct onsets. When provided this OVERRIDES the engraving's\n * own `noteCols` — the app computes it from the score's measure timings so it\n * pairs exactly with the onsets even on polyphonic grand-staff music (engraved\n * pixel columns can't: their count diverges from the onset count). `ordinal` is\n * the 0-based measure column within the excerpt; `frac` the onset's time fraction\n * through that measure. Falls through to engraving noteCols → barDurMs when unset\n * or mismatched in length.\n */\n noteCols?: number[];\n}\n\n/** Compute the follow layout (src crop + mapped boxes) for an audio progress.\n * hstack pans a FOLLOW_BARS horizontal slice; vstack frames the active system\n * and scrolls vertically between systems. */\nfunction followLayoutFor(\n ctx: RenderCtx,\n progress01: number,\n scrollMode: 'hstack' | 'vstack',\n): NotationLayout | null {\n const eng = getNotationEngraving(ctx);\n if (!eng) return null;\n const nBars = measureCount(eng.rendered);\n if (nBars <= 0) return eng.base;\n const focusBox =\n scrollMode === 'vstack'\n ? vstackFollowBox(eng.rendered, progress01)\n : followBoxAt(eng.rendered, followWindowStart(eng.rendered, progress01));\n return notationLayout(eng.rendered, ctx.W, ctx.H, eng.bandTop, eng.bandHeight, { focusBox, boxWidth: eng.bandWidth });\n}\n\nfunction scrollCursorLayer(): Layer<ScrollCursorProps> {\n let scrollMode: 'hstack' | 'vstack' = 'hstack';\n let openingZoomMs = 900;\n let color: string | undefined;\n let barDurMs: number | undefined;\n let propNoteCols: number[] | undefined;\n\n /** Distinct note onsets for the score (sorted). [] when no score/notes. */\n function onsetsFor(ctx: RenderCtx): number[] {\n const notes = ctx.score?.notes;\n return notes && notes.length ? distinctOnsets(notes) : [];\n }\n\n /**\n * Follow progress 0..1 = the cursor's fractional position across the distinct\n * onsets at time `tMs`. The follow window therefore tracks the SAME note clock\n * the cursor does, so window + cursor are locked by construction (no musicMs to\n * drift against). Held at 0 during the opening-zoom lead-in. With no score /\n * onsets this returns 0 — the window HOLDS at the start (no fake linear sweep).\n */\n function notesProgress(onsetsMs: number[], tMs: number, totalMeasures: number, noteCols?: number[]): number {\n if (tMs < openingZoomMs) return 0;\n const n = onsetsMs.length;\n if (n <= 1) return 0;\n const first = onsetsMs[0];\n const last = onsetsMs[n - 1];\n if (tMs <= first) return 0;\n if (tMs >= last || last <= first) return 1;\n let lo = 0, hi = n - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (onsetsMs[mid] <= tMs) lo = mid; else hi = mid - 1;\n }\n const segFrac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);\n // Window progress tracks the SAME positions the cursor uses, so the followed\n // bars stay under the playhead: engraved noteCols (best) → time-based\n // (onset/barDurMs) → legacy ordinal. All normalised by total measures.\n if (noteCols && noteCols.length === n && totalMeasures > 0) {\n const pos = noteCols[lo] + (noteCols[lo + 1] - noteCols[lo]) * segFrac;\n return Math.min(1, Math.max(0, pos / totalMeasures));\n }\n if (barDurMs && barDurMs > 0 && totalMeasures > 0) {\n const posLo = (onsetsMs[lo] - first) / barDurMs;\n const posHi = (onsetsMs[lo + 1] - first) / barDurMs;\n const pos = posLo + (posHi - posLo) * segFrac;\n return Math.min(1, Math.max(0, pos / totalMeasures));\n }\n return (lo + segFrac) / (n - 1);\n }\n\n /**\n * Per-onset cursor columns — the cursor MUST land on the real engraved notehead\n * of each onset (not a time-derived guess, which drifts ~½ bar ahead on dense\n * bars because OSMD spaces noteheads non-linearly).\n *\n * `propNoteCols` (app timing) gives the reliable per-onset structure: one entry\n * per onset, `ordinal + time-frac`, so its integer part is the correct measure.\n * `eng` (engraving) gives the real notehead X columns but its count can differ\n * from the onsets (ties/grace notes engrave extra columns). We therefore remap\n * each onset to a REAL engraved column, per measure, in time order:\n * - exact 1:1 overall → use the engraved columns directly;\n * - else, within each measure, map this measure's K onsets onto its E engraved\n * columns proportionally in order (k-th onset → k-th notehead), so every\n * onset still sits on an actual notehead even when E≠K.\n * Falls back to pure timing only when there is no engraving at all.\n */\n function noteColsFor(ctx: RenderCtx): number[] | undefined {\n const eng = getNotationEngraving(ctx)?.rendered?.noteCols;\n const onsets = onsetsFor(ctx);\n const n = onsets.length;\n if (!eng || !eng.length) return propNoteCols;\n if (eng.length === n) return eng; // clean 1:1 — real notehead X per onset\n if (!propNoteCols || propNoteCols.length !== n) return eng;\n\n const measureOf = (c: number) => Math.floor(c + 1e-6);\n const engByMeasure = new Map<number, number[]>();\n for (const e of eng) {\n const m = measureOf(e);\n const a = engByMeasure.get(m);\n if (a) a.push(e); else engByMeasure.set(m, [e]);\n }\n for (const a of engByMeasure.values()) a.sort((x, y) => x - y);\n\n const idxByMeasure = new Map<number, number[]>();\n propNoteCols.forEach((c, i) => {\n const m = measureOf(c);\n const a = idxByMeasure.get(m);\n if (a) a.push(i); else idxByMeasure.set(m, [i]);\n });\n\n const out = propNoteCols.slice();\n for (const [m, idxs] of idxByMeasure) {\n const e = engByMeasure.get(m);\n if (!e || !e.length) continue; // no engraving for this bar → keep timing\n const K = idxs.length;\n idxs.forEach((origIdx, i) => {\n const j = K > 1 ? Math.round((i * (e.length - 1)) / (K - 1)) : 0;\n out[origIdx] = e[Math.min(j, e.length - 1)];\n });\n }\n return out;\n }\n\n /** Follow progress 0..1 — ALWAYS the onset clock (no linear fallback). Held at\n * 0 (window at the start) when there is no score to lock to. */\n function followProgress(ctx: RenderCtx, tMs: number): number {\n const eng = getNotationEngraving(ctx);\n const total = eng ? measureCount(eng.rendered) : 0;\n return notesProgress(onsetsFor(ctx), tMs, total, noteColsFor(ctx));\n }\n\n /** The follow layout for a frame time — the provider notation calls so it\n * blits the SAME window the cursor sweeps. Pure fn of (ctx, tMs). */\n function layoutAt(ctx: RenderCtx, tMs: number): NotationLayout {\n const eng = getNotationEngraving(ctx);\n return followLayoutFor(ctx, followProgress(ctx, tMs), scrollMode) ?? eng!.base;\n }\n\n return {\n key: 'scroll-cursor',\n init(ctx, props) {\n scrollMode = props.scrollMode ?? 'hstack';\n openingZoomMs = props.openingZoomMs ?? 900;\n color = props.color;\n barDurMs = props.barDurMs;\n propNoteCols = Array.isArray(props.noteCols) ? props.noteCols : undefined;\n // `mode` / `musicMs` are accepted for back-compat and intentionally ignored:\n // the cursor is always onset-locked now (no linear sweep). See the header.\n // Publish the provider so notation blits the followed bars (works whichever\n // layer the runner draws first; the cursor LINE below is the only thing that\n // must come after notation, which it does when notation precedes us).\n setFollowLayoutProvider(ctx, layoutAt);\n },\n draw(ctx, tMs) {\n const eng = getNotationEngraving(ctx);\n if (!eng) return; // notation layer absent — nothing to follow\n const layout = layoutAt(ctx, tMs);\n\n // Playhead: ALWAYS onset-anchored off the audio clock (foolproof — cursor +\n // scroll share the onset clock, no full-width sweep). With NO score, the\n // onset list is empty and audioPlayheadLine returns null → the cursor is\n // hidden (held window, no fake sweep) instead of lying.\n const onsets = onsetsFor(ctx);\n let line: ReturnType<typeof audioPlayheadLine>;\n if (scrollMode === 'vstack') {\n // vstack: phase-locked to the follow camera so the cursor rides between\n // systems WITH the vertical scroll instead of leaping across staff lines.\n line = vstackAudioPlayheadLine(layout, onsets, tMs, measureCount(eng.rendered), noteColsFor(ctx));\n } else {\n line = audioPlayheadLine(layout, onsets, tMs, barDurMs, noteColsFor(ctx));\n }\n if (!line) return;\n const c = ctx.ctx2d;\n c.save();\n c.strokeStyle = color ?? ctx.theme.accent;\n c.globalAlpha = line.alpha;\n c.lineWidth = 4;\n c.beginPath();\n c.moveTo(line.x, line.y0);\n c.lineTo(line.x, line.y1);\n c.stroke();\n c.restore();\n },\n };\n}\n\nexport const scrollCursorFactory: LayerFactory<ScrollCursorProps> = {\n key: 'scroll-cursor',\n create: scrollCursorLayer,\n validateProps(props) {\n const errs: string[] = [];\n if (props == null || typeof props !== 'object') return ['scroll-cursor: props must be an object'];\n const p = props as Record<string, unknown>;\n if (p.mode != null && p.mode !== 'audio' && p.mode !== 'linear')\n errs.push('scroll-cursor.mode must be \"audio\" | \"linear\"');\n if (p.scrollMode != null && p.scrollMode !== 'hstack' && p.scrollMode !== 'vstack')\n errs.push('scroll-cursor.scrollMode must be \"hstack\" | \"vstack\"');\n // `mode` + `musicMs` are deprecated (the cursor is always onset-locked); they\n // are accepted for back-compat and ignored, so neither is ever required.\n if (p.musicMs != null && (typeof p.musicMs !== 'number' || !(p.musicMs > 0)))\n errs.push('scroll-cursor.musicMs must be a positive number');\n if (p.followBars != null && (typeof p.followBars !== 'number' || p.followBars < 1))\n errs.push('scroll-cursor.followBars must be a number >= 1');\n if (p.openingZoomMs != null && (typeof p.openingZoomMs !== 'number' || p.openingZoomMs < 0))\n errs.push('scroll-cursor.openingZoomMs must be a number >= 0');\n if (p.color != null && typeof p.color !== 'string') errs.push('scroll-cursor.color must be a string');\n if (p.barDurMs != null && (typeof p.barDurMs !== 'number' || !(p.barDurMs > 0)))\n errs.push('scroll-cursor.barDurMs must be a positive number');\n if (p.noteCols != null && (!Array.isArray(p.noteCols) || (p.noteCols as unknown[]).some((c) => typeof c !== 'number')))\n errs.push('scroll-cursor.noteCols must be a number[]');\n return errs;\n },\n};\n"],"mappings":"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","names":["rowY"]}
@@ -92,6 +92,17 @@ interface NotationLayoutOpts {
92
92
  zoom01?: number;
93
93
  /** Follow window (canvas coords); when set, overrides zoom and scrolls. */
94
94
  focusBox?: Box | null;
95
+ /**
96
+ * Explicit horizontal fit box (screen px), overriding the default
97
+ * `safeBox(W,H).centeredW`. `safeBox`'s left/right margins (`SAFE_ZONE` in
98
+ * video.ts) reserve ~24% of width for TikTok/Reels-style caption/share-button
99
+ * chrome — correct for the promo/video card this geometry was built for, but
100
+ * wrong for a plain in-app player canvas with no such overlay, which wants
101
+ * (up to) the FULL available width. Omit to keep the existing promo/video
102
+ * framing unchanged (every caller before this option existed still gets
103
+ * exactly `sb.centeredW`).
104
+ */
105
+ boxWidth?: number;
95
106
  }
96
107
  /**
97
108
  * Compute the notation layout (src crop + dest rect + mapped boxes) for one
@@ -1,4 +1,4 @@
1
- import { N as NotationLayout } from './notationGeometry-CZJ0U6PQ.js';
1
+ import { N as NotationLayout } from './notationGeometry-CyYXJrUH.js';
2
2
  import { RenderedNotation } from './promo.js';
3
3
  import { PromoTheme } from './video.js';
4
4
 
@@ -24,12 +24,33 @@ import { PromoTheme } from './video.js';
24
24
  * exercised of the two modes for live players (most real players show
25
25
  * multi-line music, which needs vstack) — supported and correct today, but
26
26
  * treat it as the less battle-tested choice.
27
+ *
28
+ * NOTE for `display: 'scroll'` (0.37.0): scroll mode shows the WHOLE score
29
+ * at once with no camera crop, which is only meaningful for a page-wrapped
30
+ * ('vstack') engraving — 'hstack' engraves the whole piece onto ONE very
31
+ * wide row, so a scroll-mode flow layout for it degenerates to a single
32
+ * short, squeezed tile. Scroll mode does not forbid 'hstack' (the geometry
33
+ * is agnostic), but in practice pick 'vstack' for it, same as window mode.
27
34
  */
28
35
  type NotationPlayerMode = 'hstack' | 'vstack';
36
+ /**
37
+ * Which player shell to build. See the module doc's "DISPLAY MODES" section.
38
+ * Default `'window'` — every pre-0.37.0 caller (and promos/whozart, which never
39
+ * set this) keeps the exact camera-band behavior, byte-for-byte.
40
+ */
41
+ type NotationPlayerDisplay = 'window' | 'scroll';
29
42
  interface NotationPlayerTheme extends Partial<PromoTheme> {
30
43
  }
31
44
  interface CreateNotationPlayerOpts {
32
- /** Element the player's canvas is mounted into (fills it). */
45
+ /** Element the player's content is mounted into.
46
+ * - `display:'window'` (default): a single canvas that FILLS the host
47
+ * (`width:100%;height:100%`) — the host's own size is the frame.
48
+ * - `display:'scroll'`: an internal wrapper that fills the host
49
+ * HORIZONTALLY but is left to its NATURAL height (the whole score's
50
+ * height at the host's width) — the host must not force/clip a fixed
51
+ * height (no `overflow:hidden` + fixed height) or native page scroll
52
+ * can't reach the tiles below the fold. This is the one structural
53
+ * assumption scroll mode makes about its host; window mode has none. */
33
54
  host: HTMLElement;
34
55
  /** MusicXML to engrave. */
35
56
  musicXml: string;
@@ -60,6 +81,14 @@ interface CreateNotationPlayerOpts {
60
81
  * (noted as a follow-up, not built: nothing in the shared machinery makes a
61
82
  * live re-layout trivial). */
62
83
  mode?: NotationPlayerMode;
84
+ /**
85
+ * Which player shell to build — `'window'` (default, the original
86
+ * camera-band player) or `'scroll'` (0.37.0, a tiled full-score display the
87
+ * PAGE scrolls natively). See the module doc's "DISPLAY MODES" section and
88
+ * `NotationPlayerDisplay`. Fixed for the life of the instance, same
89
+ * reasoning as `mode`: create a new player if it must change.
90
+ */
91
+ display?: NotationPlayerDisplay;
63
92
  /** Duration of one measure in ms — enables time-accurate cursor/window
64
93
  * placement when `noteCols` isn't supplied (see `scroll-cursor`'s
65
94
  * `barDurMs`). */
@@ -68,16 +97,21 @@ interface CreateNotationPlayerOpts {
68
97
  * bars/drawFrom-drawUpTo) — engrave an excerpt rather than the whole score. */
69
98
  bars?: [number, number];
70
99
  /** Top of the notation band, screen px. Default 0 (fills the host — this is
71
- * a UI widget, not a phone-safe video frame). */
100
+ * a UI widget, not a phone-safe video frame). `display:'scroll'` IGNORES
101
+ * this — there is no fixed band to inset; the whole score is shown. */
72
102
  bandTop?: number;
73
- /** Height of the notation band, screen px. Default the full frame height. */
103
+ /** Height of the notation band, screen px. Default the full frame height.
104
+ * `display:'scroll'` IGNORES this for the same reason as `bandTop`. */
74
105
  bandHeight?: number;
75
106
  /** Theme tokens (colours/fonts) forwarded to the layers. Any field omitted
76
107
  * falls back to a neutral default. */
77
108
  theme?: NotationPlayerTheme;
78
109
  /** Explicit canvas size in device px. Default: host.clientWidth/clientHeight
79
110
  * × devicePixelRatio. Pass this in test/headless environments where the
80
- * host has no real layout (e.g. jsdom, where clientWidth is always 0). */
111
+ * host has no real layout (e.g. jsdom, where clientWidth is always 0).
112
+ * `display:'scroll'` only reads the WIDTH component (`size[0]`) — the
113
+ * display height is derived from the score's own content, so `size[1]` is
114
+ * ignored in that mode. */
81
115
  size?: [number, number];
82
116
  /**
83
117
  * Advanced / test seam: a pre-rasterized engraving, bypassing the browser
@@ -102,13 +136,18 @@ interface NotationPlayer {
102
136
  * playback position; everything else (follow camera, onset-locked
103
137
  * playhead) is a pure function of `tMs`, exactly as the promo/whozart path
104
138
  * drives it.
139
+ *
140
+ * `display:'scroll'`: also feeds the auto-follow discontinuity detector —
141
+ * see `CreateNotationPlayerOpts.display`'s doc for the exact re-arm rule.
105
142
  */
106
143
  setTime(tMs: number): void;
107
- /** Re-measure the host and resize the canvas to match (device-px aware).
144
+ /** Re-measure the host and resize/rebuild to match (device-px aware).
108
145
  * Cheap after the first draw — reuses the already-rasterized engraving
109
- * bitmap, it does not re-run OSMD. Call on host resize / orientation
110
- * change. Fire-and-forget (async internally; the next `setTime` reflects
111
- * the new size once it lands, typically within a microtask). */
146
+ * bitmap unless the host's CSS width crossed the engrave-width breakpoint
147
+ * (see `desiredEngraveWidth`), it does not otherwise re-run OSMD. Call on
148
+ * host resize / orientation change. Fire-and-forget (async internally; the
149
+ * next `setTime` reflects the new size once it lands, typically within a
150
+ * microtask). `display:'scroll'` rebuilds the whole tile stack. */
112
151
  resize(): void;
113
152
  /** Register a measure-click handler: fires with the clicked measure's
114
153
  * engraved index (matching `ScoreNote.measure` numbering) when a click
@@ -116,7 +155,8 @@ interface NotationPlayer {
116
155
  * may be registered (they all fire); returns an unsubscribe function for
117
156
  * that one handler. `destroy()` also clears every remaining listener. */
118
157
  onMeasureClick(cb: (measureIndex: number) => void): () => void;
119
- /** Tear down: removes the canvas from `host` and drops listeners/state. */
158
+ /** Tear down: removes the canvas/tiles from `host` and drops
159
+ * listeners/state (including the `display:'scroll'` page-scroll listener). */
120
160
  destroy(): void;
121
161
  }
122
162
  /**
@@ -127,7 +167,11 @@ interface NotationPlayer {
127
167
  * returned box back to its measure index, which `measureColumnsFromLayout`
128
168
  * intentionally drops — the playhead has no use for it) is new here, and it
129
169
  * is pure array/index bookkeeping, not geometry math. Exported standalone so
130
- * it is unit-testable without a DOM/canvas.
170
+ * it is unit-testable without a DOM/canvas. Shared by BOTH display modes:
171
+ * `display:'window'` hit-tests against the followed (camera-cropped) layout;
172
+ * `display:'scroll'` hit-tests against the full-score `flowLayout` (see
173
+ * below) with the click's y already translated from tile-local into
174
+ * flow-layout space by the caller.
131
175
  *
132
176
  * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in
133
177
  * the same DEST/DEVICE-PIXEL space `layout.measures[].box` is already mapped
@@ -142,17 +186,95 @@ interface NotationPlayer {
142
186
  */
143
187
  declare function hitTestMeasureAt(layout: NotationLayout, mx: number, my: number): number | null;
144
188
  /**
145
- * Build a live, interactive notation player: a thin packaging of the SAME
146
- * working path whozart's live in-browser promo player uses (see the module
147
- * doc). Renders the engraving ONCE (OSMD raster, via the `notation` Layer),
148
- * then every `setTime(tMs)` call blits the current follow window (via
149
- * `followBoxAt`/`vstackFollowBox` `notationLayout`) and draws the
150
- * onset-locked playhead (`audioPlayheadLine`/`vstackAudioPlayheadLine`)
151
- * exactly the `notation` + `scroll-cursor` Layer pair the promo/video runner
152
- * uses, instantiated directly instead of through the SceneSpec/timeline
153
- * runner (which targets a fixed-duration recorded clip, not a live,
154
- * caller-driven widget).
189
+ * Full-score "flow" layout for scroll mode: the ENTIRE engraved raster
190
+ * (`rn.content`, the same tight ink box `notationLayout` itself already falls
191
+ * back to) scaled to fill `dispWdev` device px of WIDTH, with height
192
+ * following naturally from the content's own aspect ratio no camera crop,
193
+ * no bounded box (the opposite of the follow-window layout `notationLayout`
194
+ * computes for window mode via `{focusBox}`). Reuses `notationLayout` for ALL
195
+ * the actual scale/map/dx/dy math no new geometry — via two calls:
196
+ *
197
+ * 1. a PROBE call with an arbitrarily large `boxH` (1e9 — real engraved
198
+ * music's content aspect ratio, height/width, is always many orders of
199
+ * magnitude below that, so this bound is never the true constraint; it
200
+ * exists ONLY so the width-bound branch is guaranteed to be taken,
201
+ * sidestepping a chicken-and-egg height guess) to read back the TRUE
202
+ * fitted height (`rect.dh`) `notationLayout` would compute for this
203
+ * width;
204
+ * 2. an EXACT call with `boxH` set to precisely that height, so the result
205
+ * is TOP-aligned (`rect.dy === 0`) rather than vertically centered
206
+ * inside an oversized probe box. Both calls share the identical `src` /
207
+ * `srcAspect` internally (same `rn`, `focusBox: null`, default
208
+ * `zoom01`), so the two `dh` values are bit-identical and the second
209
+ * call's `dh > boxH` branch is never taken (equal, not greater).
210
+ *
211
+ * Exported so scroll-mode's tile partition (`computeScrollTiles`) and the
212
+ * playhead's y-mapping can be unit-tested against it directly, without a DOM.
213
+ */
214
+ declare function flowLayout(rn: RenderedNotation, dispWdev: number): NotationLayout;
215
+ /** Target tile height, CSS px — ~2x a typical viewport, so a tile shows
216
+ * roughly "one screenful plus one" of context, and system-boundary carriage
217
+ * returns rarely straddle a tile seam. The ACTUAL tile height is this OR the
218
+ * area-cap-derived height (`SCROLL_TILE_MAX_AREA_PX`), whichever is
219
+ * SMALLER — so a high-dpr device automatically gets shorter (CSS-px) tiles
220
+ * rather than ever exceeding the backing-store area cap; see
221
+ * `computeScrollTiles`. */
222
+ declare const SCROLL_TILE_TARGET_CSS_PX = 1600;
223
+ /** Per-tile canvas backing-store area cap, device px². Comfortably under both
224
+ * iOS Safari's ~16.7M px² (4096×4096) canvas-backing-store limit AND
225
+ * `promo.ts`'s own `MAX_RASTER_AREA_PX` (12M — the cap for the SOURCE raster
226
+ * a tile reads FROM): a tile is a separate, smaller destination canvas than
227
+ * the source raster, so it gets its own, tighter cap; 8M leaves comfortable
228
+ * headroom under both limits at any realistic tile width. */
229
+ declare const SCROLL_TILE_MAX_AREA_PX = 8000000;
230
+ /** One vertical tile of the full-score raster, in the same device-px space
231
+ * `flowLayout` maps into. */
232
+ interface ScrollTileSpec {
233
+ /** Number of vertical tiles covering the full score. Always >= 1. */
234
+ count: number;
235
+ /** Per-tile height, device px. `heights.length === count`,
236
+ * `sum(heights) === totalHeightDev` (within float precision). */
237
+ heights: number[];
238
+ /** Per-tile top y-offset, device px, within the full-score raster
239
+ * (`flowLayout`'s coordinate space). `offsets.length === count`,
240
+ * `offsets[0] === 0`, `offsets[i+1] === offsets[i] + heights[i]`. */
241
+ offsets: number[];
242
+ /** Total display height, device px (== the flow layout's `rect.dh`). */
243
+ totalHeightDev: number;
244
+ }
245
+ /**
246
+ * Pure tile-partition math (unit-tested, no DOM) — same shape/discipline as
247
+ * `clampRasterDpr` in `src/promo.ts`: given the full-score display width and
248
+ * height in device px (`dispWdev`/`totalHeightDev` — `flowLayout`'s own
249
+ * `rect.dw`/`rect.dh`) and the active `dpr`, partition the height into N
250
+ * EQUAL-height tiles such that:
251
+ *
252
+ * - each tile's target height is `SCROLL_TILE_TARGET_CSS_PX * dpr` device
253
+ * px (~2x viewport) UNLESS that would push a tile's own backing-store
254
+ * area (`dispWdev * tileHeightDev`) over `capPx2` — in which case the
255
+ * tile height is derived FROM the area cap instead. This is COMPUTED
256
+ * from `dispWdev`/`dpr` every call, not assumed safe at a fixed CSS
257
+ * height — a very wide host at a high dpr still gets a shorter tile, so
258
+ * the cap holds "at any DPR" as the design requires.
259
+ * - tiles split EVENLY (`totalHeightDev / count`), not
260
+ * max-height-tile-then-a-small-remainder — so there's never an oddly
261
+ * short final tile, and every tile (including the last) is <= the area
262
+ * cap by construction (see the proof in the inline comment below).
263
+ * - a score shorter than one tile's max height gets exactly ONE tile (the
264
+ * degenerate/short-score case) — tiling only exists to keep any single
265
+ * canvas's backing-store area under the cap, which is already true for
266
+ * the whole score at that size, so a single tile is simplest.
267
+ */
268
+ declare function computeScrollTiles(dispWdev: number, totalHeightDev: number, dpr: number, opts?: {
269
+ targetTileCssPx?: number;
270
+ capPx2?: number;
271
+ }): ScrollTileSpec;
272
+ /**
273
+ * Build a live, interactive notation player. Dispatches on
274
+ * `opts.display` (default `'window'`) — see the module doc's "DISPLAY MODES"
275
+ * section, `NotationPlayerDisplay`, `createWindowPlayer`, and
276
+ * `createScrollPlayer`.
155
277
  */
156
278
  declare function createNotationPlayer(opts: CreateNotationPlayerOpts): NotationPlayer;
157
279
 
158
- export { type CreateNotationPlayerOpts, type NotationPlayer, type NotationPlayerMode, type NotationPlayerTheme, createNotationPlayer, hitTestMeasureAt };
280
+ export { type CreateNotationPlayerOpts, type NotationPlayer, type NotationPlayerDisplay, type NotationPlayerMode, type NotationPlayerTheme, SCROLL_TILE_MAX_AREA_PX, SCROLL_TILE_TARGET_CSS_PX, type ScrollTileSpec, computeScrollTiles, createNotationPlayer, flowLayout, hitTestMeasureAt };