@real-music-packages/web-core 0.36.0 → 0.36.2
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/{chunk-VPC2EKEY.js → chunk-4565POLG.js} +13 -6
- package/dist/chunk-4565POLG.js.map +1 -0
- package/dist/{notationGeometry-CZJ0U6PQ.d.ts → notationGeometry-CyYXJrUH.d.ts} +11 -0
- package/dist/notationPlayer.d.ts +1 -1
- package/dist/notationPlayer.js +33 -7
- package/dist/notationPlayer.js.map +1 -1
- package/dist/promo.d.ts +15 -1
- package/dist/promo.js +29 -1
- package/dist/promo.js.map +1 -1
- package/dist/scene/index.d.ts +25 -2
- package/dist/scene/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VPC2EKEY.js.map +0 -1
|
@@ -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-
|
|
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
|
package/dist/notationPlayer.d.ts
CHANGED
package/dist/notationPlayer.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
measureColumnsFromLayout,
|
|
4
4
|
notationFactory,
|
|
5
5
|
scrollCursorFactory
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-4565POLG.js";
|
|
7
7
|
import {
|
|
8
8
|
safeBox
|
|
9
9
|
} from "./chunk-HXTRNE74.js";
|
|
@@ -66,6 +66,7 @@ function createNotationPlayer(opts) {
|
|
|
66
66
|
canvas.style.display = "block";
|
|
67
67
|
canvas.style.width = "100%";
|
|
68
68
|
canvas.style.height = "100%";
|
|
69
|
+
canvas.style.touchAction = "pan-y";
|
|
69
70
|
host.appendChild(canvas);
|
|
70
71
|
const dpr = typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1;
|
|
71
72
|
function frameSize() {
|
|
@@ -74,6 +75,11 @@ function createNotationPlayer(opts) {
|
|
|
74
75
|
const h = Math.max(1, Math.round((host.clientHeight || 1) * dpr));
|
|
75
76
|
return [w, h];
|
|
76
77
|
}
|
|
78
|
+
const DEFAULT_ENGRAVE_WIDTH = 560;
|
|
79
|
+
const MAX_ENGRAVE_WIDTH = 1150;
|
|
80
|
+
function desiredEngraveWidth() {
|
|
81
|
+
return (host.clientWidth || 0) > DEFAULT_ENGRAVE_WIDTH ? MAX_ENGRAVE_WIDTH : DEFAULT_ENGRAVE_WIDTH;
|
|
82
|
+
}
|
|
77
83
|
const [initW, initH] = frameSize();
|
|
78
84
|
canvas.width = initW;
|
|
79
85
|
canvas.height = initH;
|
|
@@ -93,20 +99,33 @@ function createNotationPlayer(opts) {
|
|
|
93
99
|
};
|
|
94
100
|
const notation = notationFactory.create();
|
|
95
101
|
const scrollCursor = scrollCursorFactory.create();
|
|
96
|
-
function notationProps(rendered) {
|
|
102
|
+
function notationProps(rendered, hostWidth) {
|
|
103
|
+
const widened = hostWidth > DEFAULT_ENGRAVE_WIDTH;
|
|
97
104
|
return {
|
|
98
105
|
...rendered ? { rendered } : { xml: musicXml },
|
|
99
106
|
scrollMode: mode,
|
|
100
107
|
bars,
|
|
101
108
|
bandTop: opts.bandTop ?? 0,
|
|
102
|
-
bandHeight: opts.bandHeight ?? rctx.H
|
|
109
|
+
bandHeight: opts.bandHeight ?? rctx.H,
|
|
110
|
+
hostWidth,
|
|
111
|
+
// Fit to (up to) the full canvas width instead of `notationLayout`'s
|
|
112
|
+
// default `safeBox(W,H).centeredW` — that default reserves ~24% of
|
|
113
|
+
// width for the PROMO video path's caption/share-button chrome (see
|
|
114
|
+
// `NotationLayoutOpts.boxWidth`'s doc comment), which doesn't apply to
|
|
115
|
+
// this plain, chrome-less player canvas. Capped at the same
|
|
116
|
+
// `MAX_ENGRAVE_WIDTH` (converted CSS px → device px) as the engrave
|
|
117
|
+
// width itself, so a very wide desktop host doesn't stretch systems
|
|
118
|
+
// past a readable size even though the canvas has room for it.
|
|
119
|
+
...widened ? { bandWidth: Math.min(MAX_ENGRAVE_WIDTH * dpr, rctx.W) } : {}
|
|
103
120
|
};
|
|
104
121
|
}
|
|
105
122
|
const scrollCursorProps = { scrollMode: mode, barDurMs, noteCols };
|
|
106
123
|
let renderedRn = null;
|
|
107
124
|
let destroyed = false;
|
|
125
|
+
let engravedHostWidthPx = 0;
|
|
108
126
|
const ready = (async () => {
|
|
109
|
-
|
|
127
|
+
engravedHostWidthPx = desiredEngraveWidth();
|
|
128
|
+
await notation.init(rctx, notationProps(opts.rendered, engravedHostWidthPx));
|
|
110
129
|
renderedRn = getNotationEngraving(rctx)?.rendered ?? null;
|
|
111
130
|
await scrollCursor.init(rctx, scrollCursorProps);
|
|
112
131
|
})();
|
|
@@ -126,14 +145,21 @@ function createNotationPlayer(opts) {
|
|
|
126
145
|
}
|
|
127
146
|
async function performResize() {
|
|
128
147
|
const [w, h] = frameSize();
|
|
129
|
-
|
|
148
|
+
const newEngraveWidth = desiredEngraveWidth();
|
|
149
|
+
const needsReEngrave = !opts.rendered && renderedRn != null && newEngraveWidth !== engravedHostWidthPx;
|
|
150
|
+
if (w === canvas.width && h === canvas.height && !needsReEngrave) return;
|
|
130
151
|
canvas.width = w;
|
|
131
152
|
canvas.height = h;
|
|
132
153
|
rctx.W = w;
|
|
133
154
|
rctx.H = h;
|
|
134
155
|
rctx.safeBox = safeBox(w, h);
|
|
135
|
-
if (
|
|
136
|
-
|
|
156
|
+
if (needsReEngrave) {
|
|
157
|
+
engravedHostWidthPx = newEngraveWidth;
|
|
158
|
+
await notation.init(rctx, notationProps(void 0, engravedHostWidthPx));
|
|
159
|
+
renderedRn = getNotationEngraving(rctx)?.rendered ?? null;
|
|
160
|
+
await scrollCursor.init(rctx, scrollCursorProps);
|
|
161
|
+
} else if (renderedRn) {
|
|
162
|
+
await notation.init(rctx, notationProps(renderedRn, engravedHostWidthPx));
|
|
137
163
|
await scrollCursor.init(rctx, scrollCursorProps);
|
|
138
164
|
}
|
|
139
165
|
if (!destroyed) setTime(lastTMs);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/notationPlayer.ts"],"sourcesContent":["// createNotationPlayer — THE canonical notation-playback component: one\n// scrolling-notation-plus-gliding-playhead implementation, used for BOTH live\n// in-app players (this module's reason to exist) and the promo/video-recording\n// path (src/scene/layers/notation.ts + scrollCursor.ts, driven by the SceneSpec\n// runner for a fixed-duration capture). Both consume the SAME geometry —\n// `notationLayout`, `followBoxAt`/`vstackFollowBox`, `audioPlayheadLine`/\n// `vstackAudioPlayheadLine`, `measureColumnsFromLayout` — which still lives in\n// `src/scene/` (notationGeometry.ts, engravingStore.ts, layers/notation.ts,\n// layers/scrollCursor.ts) for now; this module is their canonical PUBLIC\n// surface for interactive, caller-driven playback. It does not reimplement or\n// fork any of that math — it just instantiates the two `Layer`s directly\n// (bypassing the SceneSpec/timeline runner, which is built for a fixed-length\n// recorded clip, not a live `setTime()`-driven widget) and adds the two things\n// a live player needs that a recorded promo never did: resize, and\n// measure-click hit-testing.\n//\n// WHY THIS EXISTS: a prior in-app player (stave-web-sightread's /bach/play)\n// hand-rolled its own playhead interpolation directly against OSMD's graphical\n// output instead of reusing this path, and the result was a janky cursor.\n// Hand-rolling notation/playhead geometry against OSMD is a KNOWN FAILURE MODE\n// — OSMD's rasterized bitmap does not scale by the naive `canvas.width / pageW`\n// whenever the engraving overflows the nominal page width (which the `hstack`\n// single-staffline layout does routinely), so a from-scratch cursor drifts\n// steadily off the noteheads over a piece. That exact bug was found and fixed\n// ONCE, here, in `extractGeometry` (src/promo.ts — `canvas.width /\n// (contentRight + contentLeft)`, not `/pageW`); every consumer of THIS module\n// inherits the fix for free. Don't re-derive playhead/scroll geometry against\n// OSMD anywhere else — wrap this component instead.\n//\n// Follow-up (noted, not done here): src/scene/layers/notation.ts +\n// scrollCursor.ts could themselves be rebuilt on top of this module instead of\n// duplicating the init/draw wiring; deferred to keep this change additive.\n//\n// IMPORT PATH: this module is SUBPATH-ONLY —\n// `import { createNotationPlayer } from '@real-music-packages/web-core/notationPlayer'`\n// — matching every other non-trivial module in this package (promo.ts,\n// video.ts, scene/, server.ts, streak.ts, playback.ts). It is intentionally\n// NOT re-exported from the root barrel (src/index.ts): the root barrel is\n// theory-only (notes/frequency/scales/intervals/chords/fretboard/shareCard,\n// all zero-dependency) so apps that only want pitch/theory helpers don't\n// statically pull in this module's scene/video runtime deps.\n\nimport { notationFactory, type NotationProps } from './scene/layers/notation';\nimport { scrollCursorFactory, type ScrollCursorProps } from './scene/layers/scrollCursor';\nimport { getNotationEngraving } from './scene/engravingStore';\nimport { measureColumnsFromLayout, type NotationLayout } from './scene/notationGeometry';\nimport type { AudioClock, RenderCtx } from './scene/layer';\nimport type { Score, ScoreNote } from './scene/score';\nimport type { RenderedNotation } from './promo';\nimport { safeBox, type PromoTheme } from './video';\n\n/**\n * Notation scroll/camera mode — forwarded verbatim to the underlying engraving\n * + follow-camera (see `RenderNotationOpts.scrollMode` in src/promo.ts and\n * `notationGeometry.ts`'s hstack/vstack follow functions). These are the ONLY\n * two modes the shared machinery implements; there is no third.\n *\n * 'vstack' (DEFAULT here) — classic stacked systems (page-wrap engraving).\n * The follow camera frames one system at a time and scrolls vertically\n * (with a \"carriage return\" playhead) as the piece crosses system breaks.\n * This is the actively-exercised path (whozart's live player, the RSR/RET/\n * RMT promo videos) and the one the memory note \"use vstack everywhere\"\n * refers to — pick it unless you have a specific reason not to.\n *\n * 'hstack' — single horizontal staffline; the follow window pans purely\n * left→right. The cursor is onset-locked here too (audioPlayheadLine, not\n * a free-running sweep — that linear-sweep footgun was removed from the\n * shared cursor entirely in web-core 0.22.0), and the earlier\n * `canvas.width/pageW` scale bug that made hstack drift is fixed (0.30+\n * `extractGeometry`, see the module doc above). It is still the LESS\n * exercised of the two modes for live players (most real players show\n * multi-line music, which needs vstack) — supported and correct today, but\n * treat it as the less battle-tested choice.\n */\nexport type NotationPlayerMode = 'hstack' | 'vstack';\n\nexport interface NotationPlayerTheme extends Partial<PromoTheme> {}\n\nexport interface CreateNotationPlayerOpts {\n /** Element the player's canvas is mounted into (fills it). */\n host: HTMLElement;\n /** MusicXML to engrave. */\n musicXml: string;\n /** Distinct note onsets (ms, sorted or not — de-duped/sorted internally via\n * the shared score model) the playhead locks to. One per played note (or\n * the caller's own onset schedule) — this IS the audio clock's timing. */\n onsetsMs: number[];\n /**\n * Per-onset engraved column positions (`measureIndex + frac`), 1:1 with\n * `onsetsMs`. Optional — falls back to the shared machinery's own\n * time/ordinal-based placement when omitted (see `scroll-cursor`'s\n * `noteCols`). Strongly recommended for anything beyond a demo: it is what\n * lands the cursor on the actual notehead instead of a time-derived guess.\n *\n * GOTCHA (pre-existing in the wrapped machinery, not introduced here):\n * internally the cursor works off `distinctOnsets(onsetsMs)` (deduped +\n * sorted), so if `onsetsMs` contains duplicate onsets (simultaneous chord\n * notes sharing one onset time — a legitimate input), the deduped length no\n * longer matches `noteCols.length` and the `1:1` alignment silently breaks,\n * degrading to the `barDurMs`/ordinal fallback with no warning. Build\n * `noteCols` against the DEDUPED, sorted onset list, not the raw one.\n */\n noteCols?: number[];\n /** Scroll/camera mode. Default 'vstack'. See `NotationPlayerMode`. Fixed for\n * the life of the instance — switching modes needs a fresh OSMD engrave\n * (different `RenderSingleHorizontalStaffline` layout), so it is NOT a\n * runtime-switchable option; create a new player if the mode must change\n * (noted as a follow-up, not built: nothing in the shared machinery makes a\n * live re-layout trivial). */\n mode?: NotationPlayerMode;\n /** Duration of one measure in ms — enables time-accurate cursor/window\n * placement when `noteCols` isn't supplied (see `scroll-cursor`'s\n * `barDurMs`). */\n barDurMs?: number;\n /** Bar range [from,to] forwarded to the engraver (renderNotation\n * bars/drawFrom-drawUpTo) — engrave an excerpt rather than the whole score. */\n bars?: [number, number];\n /** Top of the notation band, screen px. Default 0 (fills the host — this is\n * a UI widget, not a phone-safe video frame). */\n bandTop?: number;\n /** Height of the notation band, screen px. Default the full frame height. */\n bandHeight?: number;\n /** Theme tokens (colours/fonts) forwarded to the layers. Any field omitted\n * falls back to a neutral default. */\n theme?: NotationPlayerTheme;\n /** Explicit canvas size in device px. Default: host.clientWidth/clientHeight\n * × devicePixelRatio. Pass this in test/headless environments where the\n * host has no real layout (e.g. jsdom, where clientWidth is always 0). */\n size?: [number, number];\n /**\n * Advanced / test seam: a pre-rasterized engraving, bypassing the browser\n * OSMD raster (`musicXml` is still required by the type but is ignored when\n * this is set). Mirrors `NotationProps.rendered` in\n * `src/scene/layers/notation.ts` (\"test/headless injection\") — real OSMD\n * *rendering* (as opposed to parsing) needs a real 2D canvas context (glyph\n * metrics for its line-breaking pass) that headless/jsdom test contexts\n * can't fully provide, so this is how this module (and the layer it wraps)\n * stays unit-testable in Node. Not needed in a real browser host.\n */\n rendered?: RenderedNotation;\n}\n\nexport interface NotationPlayer {\n /** Resolves once the engraving has been rasterized and is ready to draw.\n * `setTime`/click hit-testing are safe to call before this resolves — they\n * simply no-op (draw a blank frame / report no measure) until ready. */\n readonly ready: Promise<void>;\n /**\n * Draw the frame for absolute playback time `tMs`. The caller owns the\n * audio clock + the rAF loop — call this every frame with the current\n * playback position; everything else (follow camera, onset-locked\n * playhead) is a pure function of `tMs`, exactly as the promo/whozart path\n * drives it.\n */\n setTime(tMs: number): void;\n /** Re-measure the host and resize the canvas to match (device-px aware).\n * Cheap after the first draw — reuses the already-rasterized engraving\n * bitmap, it does not re-run OSMD. Call on host resize / orientation\n * change. Fire-and-forget (async internally; the next `setTime` reflects\n * the new size once it lands, typically within a microtask). */\n resize(): void;\n /** Register a measure-click handler: fires with the clicked measure's\n * engraved index (matching `ScoreNote.measure` numbering) when a click\n * lands on — or nearest to — a rendered measure column. Multiple handlers\n * may be registered (they all fire); returns an unsubscribe function for\n * that one handler. `destroy()` also clears every remaining listener. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the canvas from `host` and drops listeners/state. */\n destroy(): void;\n}\n\nconst DEFAULT_THEME: PromoTheme = {\n paper: '#faf7f0', ink: '#1a1614', accent: '#7b2436', sepia: '#6d5d4d', gold: '#c8a55b',\n fontDisplay: 'Georgia, serif', fontBody: 'Georgia, serif', brand: '',\n};\n\n/** A minimal but valid `Score` carrying only the onset timings the shared\n * scroll-cursor needs (`distinctOnsets(ctx.score.notes)`). Pitch/hand/measure\n * fields are placeholders — the notation + scroll-cursor layers never read\n * them (they read the RASTERIZED engraving's own geometry for position; the\n * Score here only supplies the audio-onset clock). */\nfunction scoreFromOnsets(onsetsMs: number[]): Score {\n const notes: ScoreNote[] = onsetsMs.map((onsetMs) => ({\n pitchMidi: 60, step: 'C', alter: 0, octave: 4,\n onsetMs, durMs: 0, staff: 0, voice: 0, hand: 'R', measure: 1,\n }));\n const durationMs = onsetsMs.length ? Math.max(...onsetsMs) : 0;\n return {\n notes,\n tempoMap: { source: 'fallback', segments: [{ atMs: 0, bpm: 120 }] },\n durationMs,\n };\n}\n\n/**\n * Pure hit-test: which measure (by its engraved index) contains — or is\n * nearest to — point (mx, my) in a given followed layout. Reuses\n * `measureColumnsFromLayout` (the SAME per-measure union boxes the playhead\n * anchors to) for the boxes; only the index bookkeeping (matching each\n * returned box back to its measure index, which `measureColumnsFromLayout`\n * intentionally drops — the playhead has no use for it) is new here, and it\n * is pure array/index bookkeeping, not geometry math. Exported standalone so\n * it is unit-testable without a DOM/canvas.\n *\n * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in\n * the same DEST/DEVICE-PIXEL space `layout.measures[].box` is already mapped\n * into by `notationLayout()` (i.e. the player's own `<canvas>` device pixels,\n * origin top-left, NOT CSS px) — the same space this module's own\n * `onCanvasClick` computes via `(clientX - rect.left) * (canvas.width /\n * rect.width)`. It is NOT the raster/src space (`RenderedNotation.canvas`,\n * OSMD's own pre-map bitmap px) — passing src-space coordinates here is\n * exactly the \"classic scale gotcha\" (`extractGeometry`'s `canvas.width /\n * pageW` vs `/(contentRight+contentLeft)`, see the module doc) this component\n * exists to make impossible; don't reintroduce it at the call site.\n */\nexport function hitTestMeasureAt(layout: NotationLayout, mx: number, my: number): number | null {\n if (!layout.measures.length) return null;\n const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const cols = measureColumnsFromLayout(layout.measures);\n let nearest: number | null = null;\n let nearestD = Infinity;\n for (let i = 0; i < cols.length; i++) {\n const b = cols[i];\n if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];\n const cx = b.x + b.w / 2;\n const cy = b.y + b.h / 2;\n const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);\n if (d < nearestD) {\n nearestD = d;\n nearest = indices[i];\n }\n }\n return nearest;\n}\n\n/**\n * Build a live, interactive notation player: a thin packaging of the SAME\n * working path whozart's live in-browser promo player uses (see the module\n * doc). Renders the engraving ONCE (OSMD raster, via the `notation` Layer),\n * then every `setTime(tMs)` call blits the current follow window (via\n * `followBoxAt`/`vstackFollowBox` → `notationLayout`) and draws the\n * onset-locked playhead (`audioPlayheadLine`/`vstackAudioPlayheadLine`) —\n * exactly the `notation` + `scroll-cursor` Layer pair the promo/video runner\n * uses, instantiated directly instead of through the SceneSpec/timeline\n * runner (which targets a fixed-duration recorded clip, not a live,\n * caller-driven widget).\n */\nexport function createNotationPlayer(opts: CreateNotationPlayerOpts): NotationPlayer {\n const { host, musicXml, onsetsMs, noteCols, barDurMs, bars } = opts;\n const mode: NotationPlayerMode = opts.mode ?? 'vstack';\n const theme: PromoTheme = { ...DEFAULT_THEME, ...opts.theme };\n\n const canvas = document.createElement('canvas');\n canvas.style.display = 'block';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n host.appendChild(canvas);\n\n const dpr = typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;\n function frameSize(): [number, number] {\n if (opts.size) return opts.size;\n const w = Math.max(1, Math.round((host.clientWidth || 1) * dpr));\n const h = Math.max(1, Math.round((host.clientHeight || 1) * dpr));\n return [w, h];\n }\n\n const [initW, initH] = frameSize();\n canvas.width = initW;\n canvas.height = initH;\n const ctx2d = canvas.getContext('2d');\n if (!ctx2d) throw new Error('createNotationPlayer: 2D canvas context unavailable');\n\n let lastTMs = 0;\n const audioClock: AudioClock = { nowMs: () => lastTMs };\n const rctx: RenderCtx = {\n ctx2d, W: initW, H: initH,\n score: scoreFromOnsets(onsetsMs),\n audioClock, theme, safeBox: safeBox(initW, initH), fps: 30,\n };\n\n const notation = notationFactory.create();\n const scrollCursor = scrollCursorFactory.create();\n\n function notationProps(rendered?: RenderedNotation): NotationProps {\n return {\n ...(rendered ? { rendered } : { xml: musicXml }),\n scrollMode: mode,\n bars,\n bandTop: opts.bandTop ?? 0,\n bandHeight: opts.bandHeight ?? rctx.H,\n };\n }\n const scrollCursorProps: ScrollCursorProps = { scrollMode: mode, barDurMs, noteCols };\n\n let renderedRn: RenderedNotation | null = null;\n let destroyed = false;\n\n const ready = (async () => {\n await notation.init(rctx, notationProps(opts.rendered));\n renderedRn = getNotationEngraving(rctx)?.rendered ?? null;\n await scrollCursor.init(rctx, scrollCursorProps);\n })();\n\n function clear(): void {\n ctx2d!.save();\n ctx2d!.setTransform(1, 0, 0, 1, 0, 0);\n ctx2d!.fillStyle = theme.paper;\n ctx2d!.fillRect(0, 0, canvas.width, canvas.height);\n ctx2d!.restore();\n }\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n lastTMs = tMs;\n clear();\n notation.draw(rctx, tMs);\n scrollCursor.draw(rctx, tMs);\n }\n\n async function performResize(): Promise<void> {\n const [w, h] = frameSize();\n if (w === canvas.width && h === canvas.height) return;\n canvas.width = w;\n canvas.height = h;\n rctx.W = w;\n rctx.H = h;\n rctx.safeBox = safeBox(w, h);\n if (renderedRn) {\n // Cheap re-layout: reuses the already-rasterized bitmap (no OSMD re-run).\n await notation.init(rctx, notationProps(renderedRn));\n await scrollCursor.init(rctx, scrollCursorProps);\n }\n if (!destroyed) setTime(lastTMs);\n }\n\n function currentLayout(): NotationLayout | null {\n const eng = getNotationEngraving(rctx);\n if (!eng) return null;\n return eng.followLayoutAt ? eng.followLayoutAt(rctx, lastTMs) : eng.base;\n }\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onCanvasClick(e: MouseEvent): void {\n const rect = canvas.getBoundingClientRect();\n if (!(rect.width > 0) || !(rect.height > 0)) return;\n const mx = (e.clientX - rect.left) * (canvas.width / rect.width);\n const my = (e.clientY - rect.top) * (canvas.height / rect.height);\n const layout = currentLayout();\n if (!layout) return;\n const idx = hitTestMeasureAt(layout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n canvas.addEventListener('click', onCanvasClick);\n\n return {\n ready,\n setTime,\n resize(): void {\n void performResize();\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n canvas.removeEventListener('click', onCanvasClick);\n notation.dispose?.();\n scrollCursor.dispose?.();\n clickListeners.length = 0;\n if (canvas.parentNode === host) host.removeChild(canvas);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/notationPlayer.ts"],"sourcesContent":["// createNotationPlayer — THE canonical notation-playback component: one\n// scrolling-notation-plus-gliding-playhead implementation, used for BOTH live\n// in-app players (this module's reason to exist) and the promo/video-recording\n// path (src/scene/layers/notation.ts + scrollCursor.ts, driven by the SceneSpec\n// runner for a fixed-duration capture). Both consume the SAME geometry —\n// `notationLayout`, `followBoxAt`/`vstackFollowBox`, `audioPlayheadLine`/\n// `vstackAudioPlayheadLine`, `measureColumnsFromLayout` — which still lives in\n// `src/scene/` (notationGeometry.ts, engravingStore.ts, layers/notation.ts,\n// layers/scrollCursor.ts) for now; this module is their canonical PUBLIC\n// surface for interactive, caller-driven playback. It does not reimplement or\n// fork any of that math — it just instantiates the two `Layer`s directly\n// (bypassing the SceneSpec/timeline runner, which is built for a fixed-length\n// recorded clip, not a live `setTime()`-driven widget) and adds the two things\n// a live player needs that a recorded promo never did: resize, and\n// measure-click hit-testing.\n//\n// WHY THIS EXISTS: a prior in-app player (stave-web-sightread's /bach/play)\n// hand-rolled its own playhead interpolation directly against OSMD's graphical\n// output instead of reusing this path, and the result was a janky cursor.\n// Hand-rolling notation/playhead geometry against OSMD is a KNOWN FAILURE MODE\n// — OSMD's rasterized bitmap does not scale by the naive `canvas.width / pageW`\n// whenever the engraving overflows the nominal page width (which the `hstack`\n// single-staffline layout does routinely), so a from-scratch cursor drifts\n// steadily off the noteheads over a piece. That exact bug was found and fixed\n// ONCE, here, in `extractGeometry` (src/promo.ts — `canvas.width /\n// (contentRight + contentLeft)`, not `/pageW`); every consumer of THIS module\n// inherits the fix for free. Don't re-derive playhead/scroll geometry against\n// OSMD anywhere else — wrap this component instead.\n//\n// Follow-up (noted, not done here): src/scene/layers/notation.ts +\n// scrollCursor.ts could themselves be rebuilt on top of this module instead of\n// duplicating the init/draw wiring; deferred to keep this change additive.\n//\n// IMPORT PATH: this module is SUBPATH-ONLY —\n// `import { createNotationPlayer } from '@real-music-packages/web-core/notationPlayer'`\n// — matching every other non-trivial module in this package (promo.ts,\n// video.ts, scene/, server.ts, streak.ts, playback.ts). It is intentionally\n// NOT re-exported from the root barrel (src/index.ts): the root barrel is\n// theory-only (notes/frequency/scales/intervals/chords/fretboard/shareCard,\n// all zero-dependency) so apps that only want pitch/theory helpers don't\n// statically pull in this module's scene/video runtime deps.\n\nimport { notationFactory, type NotationProps } from './scene/layers/notation';\nimport { scrollCursorFactory, type ScrollCursorProps } from './scene/layers/scrollCursor';\nimport { getNotationEngraving } from './scene/engravingStore';\nimport { measureColumnsFromLayout, type NotationLayout } from './scene/notationGeometry';\nimport type { AudioClock, RenderCtx } from './scene/layer';\nimport type { Score, ScoreNote } from './scene/score';\nimport type { RenderedNotation } from './promo';\nimport { safeBox, type PromoTheme } from './video';\n\n/**\n * Notation scroll/camera mode — forwarded verbatim to the underlying engraving\n * + follow-camera (see `RenderNotationOpts.scrollMode` in src/promo.ts and\n * `notationGeometry.ts`'s hstack/vstack follow functions). These are the ONLY\n * two modes the shared machinery implements; there is no third.\n *\n * 'vstack' (DEFAULT here) — classic stacked systems (page-wrap engraving).\n * The follow camera frames one system at a time and scrolls vertically\n * (with a \"carriage return\" playhead) as the piece crosses system breaks.\n * This is the actively-exercised path (whozart's live player, the RSR/RET/\n * RMT promo videos) and the one the memory note \"use vstack everywhere\"\n * refers to — pick it unless you have a specific reason not to.\n *\n * 'hstack' — single horizontal staffline; the follow window pans purely\n * left→right. The cursor is onset-locked here too (audioPlayheadLine, not\n * a free-running sweep — that linear-sweep footgun was removed from the\n * shared cursor entirely in web-core 0.22.0), and the earlier\n * `canvas.width/pageW` scale bug that made hstack drift is fixed (0.30+\n * `extractGeometry`, see the module doc above). It is still the LESS\n * exercised of the two modes for live players (most real players show\n * multi-line music, which needs vstack) — supported and correct today, but\n * treat it as the less battle-tested choice.\n */\nexport type NotationPlayerMode = 'hstack' | 'vstack';\n\nexport interface NotationPlayerTheme extends Partial<PromoTheme> {}\n\nexport interface CreateNotationPlayerOpts {\n /** Element the player's canvas is mounted into (fills it). */\n host: HTMLElement;\n /** MusicXML to engrave. */\n musicXml: string;\n /** Distinct note onsets (ms, sorted or not — de-duped/sorted internally via\n * the shared score model) the playhead locks to. One per played note (or\n * the caller's own onset schedule) — this IS the audio clock's timing. */\n onsetsMs: number[];\n /**\n * Per-onset engraved column positions (`measureIndex + frac`), 1:1 with\n * `onsetsMs`. Optional — falls back to the shared machinery's own\n * time/ordinal-based placement when omitted (see `scroll-cursor`'s\n * `noteCols`). Strongly recommended for anything beyond a demo: it is what\n * lands the cursor on the actual notehead instead of a time-derived guess.\n *\n * GOTCHA (pre-existing in the wrapped machinery, not introduced here):\n * internally the cursor works off `distinctOnsets(onsetsMs)` (deduped +\n * sorted), so if `onsetsMs` contains duplicate onsets (simultaneous chord\n * notes sharing one onset time — a legitimate input), the deduped length no\n * longer matches `noteCols.length` and the `1:1` alignment silently breaks,\n * degrading to the `barDurMs`/ordinal fallback with no warning. Build\n * `noteCols` against the DEDUPED, sorted onset list, not the raw one.\n */\n noteCols?: number[];\n /** Scroll/camera mode. Default 'vstack'. See `NotationPlayerMode`. Fixed for\n * the life of the instance — switching modes needs a fresh OSMD engrave\n * (different `RenderSingleHorizontalStaffline` layout), so it is NOT a\n * runtime-switchable option; create a new player if the mode must change\n * (noted as a follow-up, not built: nothing in the shared machinery makes a\n * live re-layout trivial). */\n mode?: NotationPlayerMode;\n /** Duration of one measure in ms — enables time-accurate cursor/window\n * placement when `noteCols` isn't supplied (see `scroll-cursor`'s\n * `barDurMs`). */\n barDurMs?: number;\n /** Bar range [from,to] forwarded to the engraver (renderNotation\n * bars/drawFrom-drawUpTo) — engrave an excerpt rather than the whole score. */\n bars?: [number, number];\n /** Top of the notation band, screen px. Default 0 (fills the host — this is\n * a UI widget, not a phone-safe video frame). */\n bandTop?: number;\n /** Height of the notation band, screen px. Default the full frame height. */\n bandHeight?: number;\n /** Theme tokens (colours/fonts) forwarded to the layers. Any field omitted\n * falls back to a neutral default. */\n theme?: NotationPlayerTheme;\n /** Explicit canvas size in device px. Default: host.clientWidth/clientHeight\n * × devicePixelRatio. Pass this in test/headless environments where the\n * host has no real layout (e.g. jsdom, where clientWidth is always 0). */\n size?: [number, number];\n /**\n * Advanced / test seam: a pre-rasterized engraving, bypassing the browser\n * OSMD raster (`musicXml` is still required by the type but is ignored when\n * this is set). Mirrors `NotationProps.rendered` in\n * `src/scene/layers/notation.ts` (\"test/headless injection\") — real OSMD\n * *rendering* (as opposed to parsing) needs a real 2D canvas context (glyph\n * metrics for its line-breaking pass) that headless/jsdom test contexts\n * can't fully provide, so this is how this module (and the layer it wraps)\n * stays unit-testable in Node. Not needed in a real browser host.\n */\n rendered?: RenderedNotation;\n}\n\nexport interface NotationPlayer {\n /** Resolves once the engraving has been rasterized and is ready to draw.\n * `setTime`/click hit-testing are safe to call before this resolves — they\n * simply no-op (draw a blank frame / report no measure) until ready. */\n readonly ready: Promise<void>;\n /**\n * Draw the frame for absolute playback time `tMs`. The caller owns the\n * audio clock + the rAF loop — call this every frame with the current\n * playback position; everything else (follow camera, onset-locked\n * playhead) is a pure function of `tMs`, exactly as the promo/whozart path\n * drives it.\n */\n setTime(tMs: number): void;\n /** Re-measure the host and resize the canvas to match (device-px aware).\n * Cheap after the first draw — reuses the already-rasterized engraving\n * bitmap, it does not re-run OSMD. Call on host resize / orientation\n * change. Fire-and-forget (async internally; the next `setTime` reflects\n * the new size once it lands, typically within a microtask). */\n resize(): void;\n /** Register a measure-click handler: fires with the clicked measure's\n * engraved index (matching `ScoreNote.measure` numbering) when a click\n * lands on — or nearest to — a rendered measure column. Multiple handlers\n * may be registered (they all fire); returns an unsubscribe function for\n * that one handler. `destroy()` also clears every remaining listener. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the canvas from `host` and drops listeners/state. */\n destroy(): void;\n}\n\nconst DEFAULT_THEME: PromoTheme = {\n paper: '#faf7f0', ink: '#1a1614', accent: '#7b2436', sepia: '#6d5d4d', gold: '#c8a55b',\n fontDisplay: 'Georgia, serif', fontBody: 'Georgia, serif', brand: '',\n};\n\n/** A minimal but valid `Score` carrying only the onset timings the shared\n * scroll-cursor needs (`distinctOnsets(ctx.score.notes)`). Pitch/hand/measure\n * fields are placeholders — the notation + scroll-cursor layers never read\n * them (they read the RASTERIZED engraving's own geometry for position; the\n * Score here only supplies the audio-onset clock). */\nfunction scoreFromOnsets(onsetsMs: number[]): Score {\n const notes: ScoreNote[] = onsetsMs.map((onsetMs) => ({\n pitchMidi: 60, step: 'C', alter: 0, octave: 4,\n onsetMs, durMs: 0, staff: 0, voice: 0, hand: 'R', measure: 1,\n }));\n const durationMs = onsetsMs.length ? Math.max(...onsetsMs) : 0;\n return {\n notes,\n tempoMap: { source: 'fallback', segments: [{ atMs: 0, bpm: 120 }] },\n durationMs,\n };\n}\n\n/**\n * Pure hit-test: which measure (by its engraved index) contains — or is\n * nearest to — point (mx, my) in a given followed layout. Reuses\n * `measureColumnsFromLayout` (the SAME per-measure union boxes the playhead\n * anchors to) for the boxes; only the index bookkeeping (matching each\n * returned box back to its measure index, which `measureColumnsFromLayout`\n * intentionally drops — the playhead has no use for it) is new here, and it\n * is pure array/index bookkeeping, not geometry math. Exported standalone so\n * it is unit-testable without a DOM/canvas.\n *\n * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in\n * the same DEST/DEVICE-PIXEL space `layout.measures[].box` is already mapped\n * into by `notationLayout()` (i.e. the player's own `<canvas>` device pixels,\n * origin top-left, NOT CSS px) — the same space this module's own\n * `onCanvasClick` computes via `(clientX - rect.left) * (canvas.width /\n * rect.width)`. It is NOT the raster/src space (`RenderedNotation.canvas`,\n * OSMD's own pre-map bitmap px) — passing src-space coordinates here is\n * exactly the \"classic scale gotcha\" (`extractGeometry`'s `canvas.width /\n * pageW` vs `/(contentRight+contentLeft)`, see the module doc) this component\n * exists to make impossible; don't reintroduce it at the call site.\n */\nexport function hitTestMeasureAt(layout: NotationLayout, mx: number, my: number): number | null {\n if (!layout.measures.length) return null;\n const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const cols = measureColumnsFromLayout(layout.measures);\n let nearest: number | null = null;\n let nearestD = Infinity;\n for (let i = 0; i < cols.length; i++) {\n const b = cols[i];\n if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];\n const cx = b.x + b.w / 2;\n const cy = b.y + b.h / 2;\n const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);\n if (d < nearestD) {\n nearestD = d;\n nearest = indices[i];\n }\n }\n return nearest;\n}\n\n/**\n * Build a live, interactive notation player: a thin packaging of the SAME\n * working path whozart's live in-browser promo player uses (see the module\n * doc). Renders the engraving ONCE (OSMD raster, via the `notation` Layer),\n * then every `setTime(tMs)` call blits the current follow window (via\n * `followBoxAt`/`vstackFollowBox` → `notationLayout`) and draws the\n * onset-locked playhead (`audioPlayheadLine`/`vstackAudioPlayheadLine`) —\n * exactly the `notation` + `scroll-cursor` Layer pair the promo/video runner\n * uses, instantiated directly instead of through the SceneSpec/timeline\n * runner (which targets a fixed-duration recorded clip, not a live,\n * caller-driven widget).\n */\nexport function createNotationPlayer(opts: CreateNotationPlayerOpts): NotationPlayer {\n const { host, musicXml, onsetsMs, noteCols, barDurMs, bars } = opts;\n const mode: NotationPlayerMode = opts.mode ?? 'vstack';\n const theme: PromoTheme = { ...DEFAULT_THEME, ...opts.theme };\n\n const canvas = document.createElement('canvas');\n canvas.style.display = 'block';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n // Let the browser handle vertical page-scroll gestures that start over the\n // canvas (a live player is normally embedded in a scrolling page, unlike\n // the fixed-frame promo/video path this module shares its draw code with).\n // Without this, WebKit/iOS routinely demotes a swipe that starts on an\n // element carrying a pointer/click listener (see `onCanvasClick` below) to\n // its slow-path gesture disambiguation, which can read as \"the page won't\n // scroll\" even though nothing here calls `preventDefault()`. `pan-y` keeps\n // vertical panning on the browser's fast path while still leaving the\n // synthesized `click` (tap, near-zero movement) to reach `onCanvasClick` —\n // browsers already withhold `click` after a scrolling gesture, so tap vs.\n // swipe still discriminates for free.\n canvas.style.touchAction = 'pan-y';\n host.appendChild(canvas);\n\n const dpr = typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;\n function frameSize(): [number, number] {\n if (opts.size) return opts.size;\n const w = Math.max(1, Math.round((host.clientWidth || 1) * dpr));\n const h = Math.max(1, Math.round((host.clientHeight || 1) * dpr));\n return [w, h];\n }\n\n // The width OSMD engraves against (CSS px, forwarded as `renderNotation`'s\n // `hostWidth`) — NOT the same thing as `frameSize()`'s canvas device-px\n // size above, which only controls how big the (already-engraved) raster is\n // blitted. Left unset, `renderNotation` defaults to a fixed 560px host,\n // sized for the promo card this draw path was built for; a live player\n // embedded in a wide desktop container inherits that same narrow 560px\n // line-breaking regardless of how big its own host is, so systems come out\n // engraved for a phone-width column and then get letterboxed (aspect-fit,\n // see `notationLayout`) into the middle of the real, wider canvas — narrow\n // systems on a lot of unused horizontal whitespace.\n //\n // `DEFAULT_ENGRAVE_WIDTH` is a floor: hosts at or narrower than it (phones)\n // get exactly `renderNotation`'s own 560px default, unchanged — never\n // narrower. Any host WIDER than that always engraves at the single fixed\n // `MAX_ENGRAVE_WIDTH` ceiling, rather than tracking the host's exact CSS\n // width 1:1: the engrave step decides how many measures line-break onto one\n // system (i.e. the system's raw width:height aspect ratio) — the MORE\n // measures per line, the wider that aspect ratio, and a wider aspect ratio\n // is what lets the SEPARATE display-time fit (`notationLayout`'s\n // width-vs-height-bound choice) actually reach a wide `dw` before it runs\n // out of `boxH` — so engraving at less than the max on, say, a\n // 936px-wide-but-only-438px-tall card leaves the aspect ratio too narrow\n // for the fit to ever get wide even though the container has width to\n // spare (measured: srcAspect 1.65 tops out at ~73% of a 934px canvas at\n // boxH=438; engraving at the full 1150px ceiling instead reaches srcAspect\n // ~2.0+, wide enough to fill it). The container's OWN width still governs\n // how far that gets displayed (`bandWidth`, capped at `rctx.W`), so a host\n // narrower than 1150px never actually shows more than it has room for —\n // only the OFFSCREEN raster is engraved wider than strictly necessary, at\n // the (cheap, one-time) cost of engraving a system with a couple of unused\n // measures's worth of headroom baked in.\n const DEFAULT_ENGRAVE_WIDTH = 560;\n const MAX_ENGRAVE_WIDTH = 1150;\n function desiredEngraveWidth(): number {\n return (host.clientWidth || 0) > DEFAULT_ENGRAVE_WIDTH ? MAX_ENGRAVE_WIDTH : DEFAULT_ENGRAVE_WIDTH;\n }\n\n const [initW, initH] = frameSize();\n canvas.width = initW;\n canvas.height = initH;\n const ctx2d = canvas.getContext('2d');\n if (!ctx2d) throw new Error('createNotationPlayer: 2D canvas context unavailable');\n\n let lastTMs = 0;\n const audioClock: AudioClock = { nowMs: () => lastTMs };\n const rctx: RenderCtx = {\n ctx2d, W: initW, H: initH,\n score: scoreFromOnsets(onsetsMs),\n audioClock, theme, safeBox: safeBox(initW, initH), fps: 30,\n };\n\n const notation = notationFactory.create();\n const scrollCursor = scrollCursorFactory.create();\n\n function notationProps(rendered: RenderedNotation | undefined, hostWidth: number): NotationProps {\n // Only claim (up to) the full canvas width for the DISPLAY fit when the\n // engrave itself was actually widened past `renderNotation`'s own\n // phone-card default (`hostWidth` here is always `desiredEngraveWidth()`'s\n // output — see its doc comment: a floor, never narrower than\n // DEFAULT_ENGRAVE_WIDTH). On a narrow/mobile host this is false, so\n // `bandWidth` is omitted and `notationLayout` falls back to its original\n // `safeBox(W,H).centeredW` — mobile's display fit stays BYTE-IDENTICAL to\n // pre-fix behavior, not just visually similar.\n const widened = hostWidth > DEFAULT_ENGRAVE_WIDTH;\n return {\n ...(rendered ? { rendered } : { xml: musicXml }),\n scrollMode: mode,\n bars,\n bandTop: opts.bandTop ?? 0,\n bandHeight: opts.bandHeight ?? rctx.H,\n hostWidth,\n // Fit to (up to) the full canvas width instead of `notationLayout`'s\n // default `safeBox(W,H).centeredW` — that default reserves ~24% of\n // width for the PROMO video path's caption/share-button chrome (see\n // `NotationLayoutOpts.boxWidth`'s doc comment), which doesn't apply to\n // this plain, chrome-less player canvas. Capped at the same\n // `MAX_ENGRAVE_WIDTH` (converted CSS px → device px) as the engrave\n // width itself, so a very wide desktop host doesn't stretch systems\n // past a readable size even though the canvas has room for it.\n ...(widened ? { bandWidth: Math.min(MAX_ENGRAVE_WIDTH * dpr, rctx.W) } : {}),\n };\n }\n const scrollCursorProps: ScrollCursorProps = { scrollMode: mode, barDurMs, noteCols };\n\n let renderedRn: RenderedNotation | null = null;\n let destroyed = false;\n // The host width the CURRENT `renderedRn` was actually engraved at — kept\n // in sync with every real (re-)engrave so `performResize` can tell \"host\n // got bigger/smaller, systems should re-flow\" apart from \"canvas device\n // pixels changed but the CSS width driving line-breaking didn't\" (e.g. a\n // pure devicePixelRatio or height-only change), which stays on the cheap\n // reuse-the-bitmap path.\n let engravedHostWidthPx = 0;\n\n const ready = (async () => {\n engravedHostWidthPx = desiredEngraveWidth();\n await notation.init(rctx, notationProps(opts.rendered, engravedHostWidthPx));\n renderedRn = getNotationEngraving(rctx)?.rendered ?? null;\n await scrollCursor.init(rctx, scrollCursorProps);\n })();\n\n function clear(): void {\n ctx2d!.save();\n ctx2d!.setTransform(1, 0, 0, 1, 0, 0);\n ctx2d!.fillStyle = theme.paper;\n ctx2d!.fillRect(0, 0, canvas.width, canvas.height);\n ctx2d!.restore();\n }\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n lastTMs = tMs;\n clear();\n notation.draw(rctx, tMs);\n scrollCursor.draw(rctx, tMs);\n }\n\n async function performResize(): Promise<void> {\n const [w, h] = frameSize();\n const newEngraveWidth = desiredEngraveWidth();\n // Only a REAL OSMD re-engrave (not the cheap bitmap-reuse relayout below)\n // can change how many measures fit per system — see `desiredEngraveWidth`'s\n // doc comment. Gated on `!opts.rendered`: a caller that supplied a\n // pre-rasterized `rendered` (the headless/test seam) never had a real xml\n // engrave to begin with, and `notation.init` can't run OSMD against jsdom\n // — re-deriving `engravedHostWidthPx` for that seam would just make this\n // check misfire on the next resize, so it's skipped entirely.\n const needsReEngrave = !opts.rendered && renderedRn != null && newEngraveWidth !== engravedHostWidthPx;\n if (w === canvas.width && h === canvas.height && !needsReEngrave) return;\n canvas.width = w;\n canvas.height = h;\n rctx.W = w;\n rctx.H = h;\n rctx.safeBox = safeBox(w, h);\n if (needsReEngrave) {\n // Host width materially changed (e.g. mobile→desktop, a fullscreen\n // toggle, or a page-layout reflow) — re-run OSMD so systems re-flow at\n // the new width instead of just rescaling the old, narrower engrave.\n engravedHostWidthPx = newEngraveWidth;\n await notation.init(rctx, notationProps(undefined, engravedHostWidthPx));\n renderedRn = getNotationEngraving(rctx)?.rendered ?? null;\n await scrollCursor.init(rctx, scrollCursorProps);\n } else if (renderedRn) {\n // Cheap re-layout: reuses the already-rasterized bitmap (no OSMD re-run).\n await notation.init(rctx, notationProps(renderedRn, engravedHostWidthPx));\n await scrollCursor.init(rctx, scrollCursorProps);\n }\n if (!destroyed) setTime(lastTMs);\n }\n\n function currentLayout(): NotationLayout | null {\n const eng = getNotationEngraving(rctx);\n if (!eng) return null;\n return eng.followLayoutAt ? eng.followLayoutAt(rctx, lastTMs) : eng.base;\n }\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onCanvasClick(e: MouseEvent): void {\n const rect = canvas.getBoundingClientRect();\n if (!(rect.width > 0) || !(rect.height > 0)) return;\n const mx = (e.clientX - rect.left) * (canvas.width / rect.width);\n const my = (e.clientY - rect.top) * (canvas.height / rect.height);\n const layout = currentLayout();\n if (!layout) return;\n const idx = hitTestMeasureAt(layout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n canvas.addEventListener('click', onCanvasClick);\n\n return {\n ready,\n setTime,\n resize(): void {\n void performResize();\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n canvas.removeEventListener('click', onCanvasClick);\n notation.dispose?.();\n scrollCursor.dispose?.();\n clickListeners.length = 0;\n if (canvas.parentNode === host) host.removeChild(canvas);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/promo.d.ts
CHANGED
|
@@ -87,6 +87,20 @@ interface RenderNotationOpts {
|
|
|
87
87
|
*/
|
|
88
88
|
singleRow?: boolean;
|
|
89
89
|
}
|
|
90
|
+
declare const MAX_RASTER_AREA_PX = 12000000;
|
|
91
|
+
/**
|
|
92
|
+
* Pure clamp math (unit-tested, no DOM): given the CSS-px content dims OSMD
|
|
93
|
+
* is about to rasterize (`cssW`/`cssH` — the SAME layout regardless of dpr)
|
|
94
|
+
* and the browser's native `devicePixelRatio`, return the largest dpr `<=
|
|
95
|
+
* nativeDpr` whose backing-store area (`cssW*dpr × cssH*dpr`) stays within
|
|
96
|
+
* `capPx2`. Returns `nativeDpr` unchanged whenever the native-resolution
|
|
97
|
+
* raster already fits — that no-op path is what keeps desktop/short-excerpt
|
|
98
|
+
* rendering byte-for-byte unchanged by this clamp. Never returns less than 1
|
|
99
|
+
* (native CSS-px density is the floor — always legible, never sub-1x blur)
|
|
100
|
+
* unless `nativeDpr` itself is below 1 (not expected in a real browser, but
|
|
101
|
+
* handled rather than dividing by/sqrt-ing a negative).
|
|
102
|
+
*/
|
|
103
|
+
declare function clampRasterDpr(cssW: number, cssH: number, nativeDpr: number, capPx2?: number): number;
|
|
90
104
|
/** Render a MusicXML string to a detached canvas + geometry.
|
|
91
105
|
* Browser-only (requires document + opensheetmusicdisplay dynamic import). */
|
|
92
106
|
declare function renderNotation(xml: string, opts?: RenderNotationOpts): Promise<RenderedNotation>;
|
|
@@ -125,4 +139,4 @@ declare function createPromoSampler(opts?: {
|
|
|
125
139
|
analyser?: boolean;
|
|
126
140
|
}): Promise<PromoSampler>;
|
|
127
141
|
|
|
128
|
-
export { type Box, type MeasureColumnBox, type MidiNote, type ParsedMidi, type PromoSampler, type PromoVoicing, type RenderNotationOpts, type RenderedNotation, type StaffMeasureBox, createPromoSampler, midiDurationMs, parseMidi, renderNotation };
|
|
142
|
+
export { type Box, MAX_RASTER_AREA_PX, type MeasureColumnBox, type MidiNote, type ParsedMidi, type PromoSampler, type PromoVoicing, type RenderNotationOpts, type RenderedNotation, type StaffMeasureBox, clampRasterDpr, createPromoSampler, midiDurationMs, parseMidi, renderNotation };
|
package/dist/promo.js
CHANGED
|
@@ -301,6 +301,15 @@ function extractGeometry(osmd, canvas, inkSumThreshold) {
|
|
|
301
301
|
return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
|
+
var MAX_RASTER_AREA_PX = 12e6;
|
|
305
|
+
function clampRasterDpr(cssW, cssH, nativeDpr, capPx2 = MAX_RASTER_AREA_PX) {
|
|
306
|
+
const safeDpr = nativeDpr > 0 ? nativeDpr : 1;
|
|
307
|
+
if (!(cssW > 0) || !(cssH > 0)) return safeDpr;
|
|
308
|
+
const nativeArea = cssW * cssH * safeDpr * safeDpr;
|
|
309
|
+
if (nativeArea <= capPx2) return safeDpr;
|
|
310
|
+
const cappedDpr = Math.sqrt(capPx2 / (cssW * cssH));
|
|
311
|
+
return Math.max(1, Math.min(safeDpr, cappedDpr));
|
|
312
|
+
}
|
|
304
313
|
async function renderNotation(xml, opts) {
|
|
305
314
|
const paper = opts?.paper ?? "#faf7f0";
|
|
306
315
|
const inkSumThreshold = opts?.inkSumThreshold ?? 690;
|
|
@@ -332,7 +341,24 @@ async function renderNotation(xml, opts) {
|
|
|
332
341
|
});
|
|
333
342
|
}
|
|
334
343
|
osmd.render();
|
|
335
|
-
|
|
344
|
+
let canvas = host.querySelector("canvas");
|
|
345
|
+
if (canvas && canvas.width * canvas.height > MAX_RASTER_AREA_PX) {
|
|
346
|
+
const nativeDpr = typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1;
|
|
347
|
+
const cssW = canvas.width / nativeDpr;
|
|
348
|
+
const cssH = canvas.height / nativeDpr;
|
|
349
|
+
const clampedDpr = clampRasterDpr(cssW, cssH, nativeDpr);
|
|
350
|
+
if (clampedDpr < nativeDpr) {
|
|
351
|
+
const desc = Object.getOwnPropertyDescriptor(window, "devicePixelRatio");
|
|
352
|
+
try {
|
|
353
|
+
Object.defineProperty(window, "devicePixelRatio", { value: clampedDpr, configurable: true });
|
|
354
|
+
osmd.render();
|
|
355
|
+
canvas = host.querySelector("canvas");
|
|
356
|
+
} finally {
|
|
357
|
+
if (desc) Object.defineProperty(window, "devicePixelRatio", desc);
|
|
358
|
+
else delete window.devicePixelRatio;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
336
362
|
if (canvas) {
|
|
337
363
|
const { systems, measures, measureColumns, content, noteCols } = extractGeometry(osmd, canvas, inkSumThreshold);
|
|
338
364
|
document.body.removeChild(host);
|
|
@@ -473,6 +499,8 @@ async function createPromoSampler(opts) {
|
|
|
473
499
|
};
|
|
474
500
|
}
|
|
475
501
|
export {
|
|
502
|
+
MAX_RASTER_AREA_PX,
|
|
503
|
+
clampRasterDpr,
|
|
476
504
|
createPromoSampler,
|
|
477
505
|
midiDurationMs,
|
|
478
506
|
parseMidi,
|
package/dist/promo.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/promo.ts"],"sourcesContent":["// Promo utilities — browser-only except for parseMidi/midiDurationMs which are\n// pure (no DOM).\n//\n// Exports:\n// parseMidi / midiDurationMs — pure MIDI parser (no DOM; testable in Node)\n// renderNotation — OSMD canvas renderer (browser/OSMD only)\n// createPromoSampler — Tone.js sampler factory (browser/Tone only)\n//\n// Unit tests cover parseMidi (tests/midi.test.ts).\n// renderNotation and createPromoSampler are browser-only — consumers' dom tests\n// cover them (OSMD and Tone.js require a browser context).\n\n// ─── MIDI parser ──────────────────────────────────────────────────────────────\n// Moved verbatim from realmusictheory/site/src/lib/promo/midiDuration.ts.\n\nexport interface MidiNote {\n midi: number;\n startMs: number;\n /** Audible duration in ms — extended while the sustain pedal is held. */\n durMs: number;\n /** 0..1 */\n velocity: number;\n}\n\nexport interface ParsedMidi {\n /** When the last notated note is released (pre-pedal-tail), in ms — paces the playhead. */\n durationMs: number;\n notes: MidiNote[];\n}\n\ninterface RawEvent {\n tick: number;\n kind: 'on' | 'off' | 'sustain' | 'tempo';\n midi?: number;\n velocity?: number;\n on?: boolean; // sustain down?\n us?: number; // tempo in µs/beat\n}\n\nexport function parseMidi(buf: ArrayBuffer): ParsedMidi {\n try {\n const dv = new DataView(buf);\n let p = 0;\n const u8 = () => dv.getUint8(p++);\n const u16 = () => { const v = dv.getUint16(p); p += 2; return v; };\n const u32 = () => { const v = dv.getUint32(p); p += 4; return v; };\n\n if (u32() !== 0x4d546864) return { durationMs: 0, notes: [] }; // 'MThd'\n const headerLen = u32();\n u16(); // format\n const ntrk = u16();\n const division = u16();\n p = 8 + headerLen;\n if (division & 0x8000) return { durationMs: 0, notes: [] }; // SMPTE — not handled\n const tpq = division || 480;\n\n const events: RawEvent[] = [];\n for (let t = 0; t < ntrk; t++) {\n if (p + 8 > dv.byteLength || u32() !== 0x4d54726b) break; // 'MTrk'\n const len = u32();\n const end = Math.min(p + len, dv.byteLength);\n let tick = 0;\n let running = 0;\n while (p < end) {\n let dt = 0, b: number;\n do { b = u8(); dt = (dt << 7) | (b & 0x7f); } while (b & 0x80 && p < end);\n tick += dt;\n let status = dv.getUint8(p);\n if (status & 0x80) { p++; running = status; } else { status = running; }\n if (status === 0xff) {\n const type = u8();\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n if (type === 0x51 && l === 3) {\n events.push({\n tick,\n kind: 'tempo',\n us: (dv.getUint8(p) << 16) | (dv.getUint8(p + 1) << 8) | dv.getUint8(p + 2),\n });\n }\n p += l;\n } else if (status === 0xf0 || status === 0xf7) {\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n p += l;\n } else {\n const hi = status & 0xf0;\n if (hi === 0x90 || hi === 0x80) {\n const midi = u8();\n const vel = u8();\n if (hi === 0x90 && vel > 0) events.push({ tick, kind: 'on', midi, velocity: vel / 127 });\n else events.push({ tick, kind: 'off', midi });\n } else if (hi === 0xb0) {\n const cc = u8();\n const val = u8();\n if (cc === 64) events.push({ tick, kind: 'sustain', on: val >= 64 });\n } else {\n p += hi === 0xc0 || hi === 0xd0 ? 1 : 2;\n }\n }\n }\n p = end;\n }\n\n // tick → ms via the tempo map\n const tempos = events.filter((e) => e.kind === 'tempo').sort((a, b) => a.tick - b.tick);\n if (!tempos.length || tempos[0].tick > 0) tempos.unshift({ tick: 0, kind: 'tempo', us: 500000 });\n const tickToMs = (tick: number): number => {\n let ms = 0;\n for (let i = 0; i < tempos.length; i++) {\n const segStart = tempos[i].tick;\n if (segStart >= tick) break;\n const segEnd = i + 1 < tempos.length ? Math.min(tempos[i + 1].tick, tick) : tick;\n ms += ((segEnd - segStart) / tpq) * ((tempos[i].us ?? 500000) / 1000);\n }\n return ms;\n };\n\n // Sustain spans (tick ranges where the pedal is down)\n const sustainEvents = events.filter((e) => e.kind === 'sustain').sort((a, b) => a.tick - b.tick);\n const pedalUpAfter = (tick: number): number | null => {\n for (const s of sustainEvents) if (!s.on && s.tick >= tick) return s.tick;\n return null;\n };\n const pedalDownAt = (tick: number): boolean => {\n let down = false;\n for (const s of sustainEvents) { if (s.tick > tick) break; down = !!s.on; }\n return down;\n };\n\n // Pair note-ons with the next matching note-off\n const ordered = events.filter((e) => e.kind === 'on' || e.kind === 'off').sort((a, b) => a.tick - b.tick);\n const open: Record<number, { tick: number; vel: number }[]> = {};\n const notes: MidiNote[] = [];\n let lastOffTick = 0;\n for (const e of ordered) {\n const m = e.midi!;\n if (e.kind === 'on') {\n (open[m] ??= []).push({ tick: e.tick, vel: e.velocity ?? 0.7 });\n } else {\n const stack = open[m];\n if (stack && stack.length) {\n const start = stack.shift()!;\n let endTick = e.tick;\n lastOffTick = Math.max(lastOffTick, endTick);\n // Extend while pedal is held past the note-off.\n if (pedalDownAt(endTick)) {\n const up = pedalUpAfter(endTick);\n if (up != null) endTick = up;\n }\n const startMs = tickToMs(start.tick);\n notes.push({\n midi: m,\n startMs,\n durMs: Math.max(60, tickToMs(endTick) - startMs),\n velocity: start.vel,\n });\n }\n }\n }\n\n return { durationMs: tickToMs(lastOffTick), notes };\n } catch {\n return { durationMs: 0, notes: [] };\n }\n}\n\n/** Total playback duration in ms (0 if unparseable). */\nexport function midiDurationMs(buf: ArrayBuffer): number {\n return parseMidi(buf).durationMs;\n}\n\n// ─── Notation renderer ────────────────────────────────────────────────────────\n// Superset of RSR (stave-web-sightread/src/lib/promo/notation.ts) and RMT\n// (realmusictheory/site/src/lib/promo/notation.ts). Both per-staff measure boxes\n// (RSR) and per-measure column union boxes (RMT) are computed every render.\n//\n// Browser-only: requires document + opensheetmusicdisplay. No unit tests here —\n// consumers' dom tests cover renderNotation.\n\nexport interface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/** One staff's slice of a measure, in canvas px. */\nexport interface StaffMeasureBox {\n index: number; // 0-based measure position within the rendered range\n staff: number; // 0 = top staff (RH/treble), 1 = bottom staff (LH/bass)\n box: Box;\n /** x of the measure's FIRST note (canvas px) — past any clef/key/time signature. */\n noteStartX: number;\n}\n\n/** Per-measure column box: union across all staves for that measure, in canvas px. */\nexport interface MeasureColumnBox extends Box {\n noteStartX: number;\n}\n\nexport interface RenderedNotation {\n canvas: HTMLCanvasElement;\n /** Per-row grand-staff system boxes in canvas px, top-to-bottom. */\n systems: Box[];\n /** Per-(measure, staff) boxes — RSR's geometry. */\n measures: StaffMeasureBox[];\n /** Per-measure column union across staves — RMT's geometry. */\n measureColumns: MeasureColumnBox[];\n /** Tight bounding box of all notation in canvas px (page whitespace cropped). */\n content: Box;\n /**\n * Per-distinct-onset engraved column position (measureIndex + fraction across\n * the note region), in time (left→right) order — one entry per played onset,\n * rests excluded, chords/both-hands collapsed. The scroll-cursor anchors to\n * THESE (the notes' real engraved X) instead of a time-fraction, so the\n * playhead lands exactly on each notehead. Empty/absent when extraction fails\n * or the engraving predates this field (cursor falls back to time/ordinal).\n */\n noteCols?: number[];\n}\n\nexport interface RenderNotationOpts {\n /** OSMD drawFrom/drawUpToMeasureNumber. Applied only when provided. */\n bars?: [number, number];\n /** Background fill colour. Default '#faf7f0' (RSR paper). */\n paper?: string;\n /** Pixel-sum threshold below which a pixel is counted as ink.\n * Default 690 (RSR: paper #faf7f0 sum ≈ 737). RMT uses 620. */\n inkSumThreshold?: number;\n /** Host div width in CSS px. Default 560 (RSR). RMT uses 620. */\n hostWidth?: number;\n /**\n * Notation scroll mode — drives BOTH the engraving layout here and the\n * follow-camera in the scroll-cursor/notation layers.\n *\n * 'hstack' (default) — engrave all measures on ONE horizontal staffline\n * (OSMD RenderSingleHorizontalStaffline). The follow window is a purely\n * HORIZONTAL slice so the playhead pans left→right with no vertical\n * row-break jump. The rasterized bitmap is one very wide row; geometry\n * (systems/measures) all share one row of y.\n *\n * 'vstack' — engrave the classic STACKED systems (normal page wrap into\n * multiple rows). The follow camera frames the ACTIVE system at a fixed\n * band position and scrolls VERTICALLY (eased) to the next system as the\n * playhead crosses systems — never a vertical leap across staff lines.\n *\n * Default 'hstack' (preserves the current single-row behavior).\n */\n scrollMode?: 'hstack' | 'vstack';\n /**\n * @deprecated Use `scrollMode` instead. Back-compat alias: `singleRow:true`\n * ⇔ `scrollMode:'hstack'`, `singleRow:false` ⇔ `scrollMode:'vstack'`.\n * When both are given, `scrollMode` wins.\n */\n singleRow?: boolean;\n}\n\n/** Padded union of boxes, clamped to the canvas. */\nfunction unionBox(boxes: Box[], canvas: HTMLCanvasElement, pad: number): Box {\n if (!boxes.length) return { x: 0, y: 0, w: canvas.width, h: canvas.height };\n const minX = Math.min(...boxes.map((b) => b.x));\n const minY = Math.min(...boxes.map((b) => b.y));\n const maxX = Math.max(...boxes.map((b) => b.x + b.w));\n const maxY = Math.max(...boxes.map((b) => b.y + b.h));\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(canvas.width, maxX + pad) - x,\n h: Math.min(canvas.height, maxY + pad) - y,\n };\n}\n\n/** Tight box around all non-paper pixels (notes, ledger lines, stems), padded. */\nfunction inkBoundingBox(canvas: HTMLCanvasElement, inkSumThreshold: number): Box | null {\n try {\n const ctx = canvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return null;\n const { width: W, height: H } = canvas;\n const data = ctx.getImageData(0, 0, W, H).data;\n let minX = W, minY = H, maxX = -1, maxY = -1;\n const step = 2;\n for (let y = 0; y < H; y += step) {\n for (let x = 0; x < W; x += step) {\n const i = (y * W + x) * 4;\n if (data[i + 3] < 16) continue;\n if (data[i] + data[i + 1] + data[i + 2] < inkSumThreshold) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n }\n if (maxX < 0) return null;\n const pad = 16;\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(W, maxX + pad) - x,\n h: Math.min(H, maxY + pad) - y,\n };\n } catch {\n return null;\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\nfunction extractGeometry(\n osmd: any,\n canvas: HTMLCanvasElement,\n inkSumThreshold: number,\n): { systems: Box[]; measures: StaffMeasureBox[]; measureColumns: MeasureColumnBox[]; content: Box; noteCols: number[] } {\n const full: Box = { x: 0, y: 0, w: canvas.width, h: canvas.height };\n try {\n const graphic: any = osmd.GraphicSheet;\n const page: any = graphic?.MusicPages?.[0];\n const pageW: number = page?.PositionAndShape?.Size?.width;\n const musicSystems: any[] = page?.MusicSystems ?? [];\n if (!pageW || !musicSystems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n // OSMD units → canvas px.\n //\n // BUG FIX: the naive `canvas.width / pageW` is WRONG whenever the engraving\n // OVERFLOWS the nominal page width — which `renderSingleHorizontalStaffline`\n // (hstack) does routinely: one wide row of measures spills past pageW, and OSMD\n // sizes the canvas to the CONTENT (with its page margins), not to pageW. Using\n // canvas.width/pageW then OVER-scales every position (~5% on a 5-bar row), so\n // the cursor/highlight geometry drifts steadily RIGHT of the real noteheads and\n // DOWN in y (the same scale is applied to both axes) — accumulating to ~half a\n // bar by the end of the clip.\n //\n // OSMD renders at a single uniform scale `s` (px per OSMD unit) with symmetric\n // page margins, so `canvas.width = (contentRight + contentLeft) * s` where\n // contentLeft is the left margin. Hence `s = canvas.width / (contentRight +\n // contentLeft)`. This SELF-CALIBRATES to OSMD's zoom and, when the content fits\n // the page (vstack: contentRight ≈ pageW - margin, contentLeft = margin), it\n // reduces to canvas.width/pageW — so it's correct for both layouts. Falls back\n // to pageW if the content extent can't be measured.\n let contentRightU = -Infinity, contentLeftU = Infinity;\n for (const staves of (graphic?.MeasureList ?? []) as any[][]) {\n for (const m of (staves ?? [])) {\n const ps = m?.PositionAndShape;\n if (!ps?.AbsolutePosition || !ps?.Size || !(ps.Size.width > 0.1)) continue;\n contentRightU = Math.max(contentRightU, ps.AbsolutePosition.x + ps.Size.width);\n contentLeftU = Math.min(contentLeftU, ps.AbsolutePosition.x);\n }\n }\n const spanU = Number.isFinite(contentRightU) && Number.isFinite(contentLeftU)\n ? contentRightU + contentLeftU\n : pageW;\n const f = canvas.width / (spanU > 0 ? spanU : pageW);\n const toBox = (pas: any): Box | null => {\n const p = pas?.AbsolutePosition;\n const sz = pas?.Size;\n if (!p || !sz) return null;\n return { x: p.x * f, y: p.y * f, w: sz.width * f, h: sz.height * f };\n };\n\n const systems: Box[] = musicSystems\n .map((s) => toBox(s?.PositionAndShape))\n .filter((b): b is Box => !!b && b.w > 1 && b.h > 1)\n .sort((a, b) => a.y - b.y);\n if (!systems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n const measureList: any[][] = graphic?.MeasureList ?? [];\n\n // RSR geometry: per-(measure, staff) individual boxes\n const measures: StaffMeasureBox[] = [];\n measureList.forEach((staves, index) => {\n (staves ?? []).forEach((m: any, staff: number) => {\n const box = toBox(m?.PositionAndShape);\n if (box && box.w > 1 && box.h > 1) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n const noteStartX = typeof seX === 'number' ? seX * f : box.x;\n measures.push({ index, staff, box, noteStartX });\n }\n });\n });\n\n // RMT geometry: per-measure column boxes (union across staves)\n const measureColumns: MeasureColumnBox[] = measureList\n .map((staves) => {\n const arr = staves ?? [];\n const boxes = arr\n .map((m: any) => toBox(m?.PositionAndShape))\n .filter((b: Box | null): b is Box => !!b && b.w > 1 && b.h > 1);\n if (!boxes.length) return 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 // First note x across all staves; clamped into the column box\n let nx = Infinity;\n for (const m of arr) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof seX === 'number') nx = Math.min(nx, seX * f);\n }\n const noteStartX = Number.isFinite(nx) ? Math.max(x0, Math.min(nx, x1)) : x0;\n return { x: x0, y: y0, w: x1 - x0, h: y1 - y0, noteStartX };\n })\n .filter((b): b is MeasureColumnBox => !!b);\n\n // Per-distinct-onset engraved columns, in render (= time) order, rests excluded\n // — the cursor's TRUE anchor X. Each value is `ordinal + frac`, where `ordinal`\n // is the column's position among the DRAWN measures (matching the array index of\n // measureColumnsFromLayout — NOT the absolute MeasureList index, which would\n // overflow the columns array for excerpts whose first measure isn't bar 0), and\n // `frac` is the notehead's position across the measure's note region. Crucially\n // we iterate ONLY drawn measures (a valid box): with drawFrom/drawUpTo, OSMD's\n // MeasureList still holds EVERY source measure, so without this filter the whole\n // score leaks in (e.g. 128 cols for a 22-note excerpt) and never pairs 1:1 with\n // the played onsets, disabling the engraved-x anchor.\n // We key each note's engraved X by its rhythmic TIMESTAMP within the measure,\n // NOT by raw distinct X. Two staves' notes at the SAME beat engrave at slightly\n // different X (the grand staff isn't pixel-aligned), so deduping by X yielded\n // MORE columns than there are onsets (e.g. 45 cols vs 40 onsets) — which broke\n // the 1:1 pairing with the played onsets and forced the cursor onto a\n // linear-time fallback that drifts ~½ bar ahead of the real noteheads on dense\n // bars. Keying by timestamp collapses each beat to ONE column at the (leftmost)\n // engraved X of its noteheads, so the column count equals the distinct-onset\n // count and the cursor lands on the actual notehead, beat for beat.\n const noteCols: number[] = [];\n let ordinal = 0;\n measureList.forEach((staves) => {\n const arr = staves ?? [];\n let mx0 = Infinity, mx1 = -Infinity, nsx = Infinity;\n const byTs = new Map<number, number>(); // rhythmic timestamp → leftmost note X (px)\n for (const m of arr) {\n const mb = toBox(m?.PositionAndShape);\n if (mb) { mx0 = Math.min(mx0, mb.x); mx1 = Math.max(mx1, mb.x + mb.w); }\n const se0 = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof se0 === 'number') nsx = Math.min(nsx, se0 * f);\n for (const se of (m?.staffEntries ?? [])) {\n const sx = se?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof sx !== 'number') continue;\n const hasNote = (se?.graphicalVoiceEntries ?? []).some(\n (gve: any) => (gve?.notes ?? []).some(\n (gn: any) => !(gn?.sourceNote?.isRestFlag ?? gn?.sourceNote?.IsRest ?? false)));\n if (!hasNote) continue;\n // Within-measure rhythmic position (whole notes). Round to a fine grid so\n // float noise doesn't split a beat; falls back to X if unavailable.\n const tsRaw =\n se?.relInMeasureTimestamp?.RealValue ??\n se?.getAbsoluteTimestamp?.()?.RealValue ??\n se?.sourceStaffEntry?.Timestamp?.RealValue;\n const key = typeof tsRaw === 'number' ? Math.round(tsRaw * 10000) : Math.round(sx * f);\n const px = sx * f;\n const prev = byTs.get(key);\n if (prev === undefined || px < prev) byTs.set(key, px);\n }\n }\n // Skip undrawn measures (no valid box / zero width) — only drawn measures get\n // a column, so `ordinal` stays in lockstep with measureColumnsFromLayout.\n if (!Number.isFinite(mx0) || (mx1 - mx0) <= 1) return;\n const startX = Number.isFinite(nsx) ? Math.max(mx0, Math.min(nsx, mx1)) : mx0;\n const denom = (mx1 - startX) || 1;\n // Emit one column per distinct beat, in time order (the byTs keys are the\n // rounded timestamps), so column k ↔ onset k.\n for (const key of [...byTs.keys()].sort((a, b) => a - b)) {\n const x = byTs.get(key)!;\n noteCols.push(ordinal + Math.min(1, Math.max(0, (x - startX) / denom)));\n }\n ordinal++;\n });\n\n const content = inkBoundingBox(canvas, inkSumThreshold) ?? unionBox(systems, canvas, 14);\n return { systems, measures, measureColumns, content, noteCols };\n } catch {\n return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/** Render a MusicXML string to a detached canvas + geometry.\n * Browser-only (requires document + opensheetmusicdisplay dynamic import). */\nexport async function renderNotation(\n xml: string,\n opts?: RenderNotationOpts,\n): Promise<RenderedNotation> {\n const paper = opts?.paper ?? '#faf7f0';\n const inkSumThreshold = opts?.inkSumThreshold ?? 690;\n const hostWidth = opts?.hostWidth ?? 560;\n // scrollMode drives the engraving layout; singleRow is the deprecated alias.\n // Default 'hstack' (single horizontal staffline) preserves current behavior.\n const scrollMode: 'hstack' | 'vstack' =\n opts?.scrollMode ?? (opts?.singleRow === false ? 'vstack' : 'hstack');\n const singleRow = scrollMode === 'hstack';\n\n // Literal dynamic import: consumers' bundlers (Vite/Rollup) must be able to\n // statically see the specifier to resolve + code-split it — a variable\n // specifier would reach the browser as a bare import and fail at runtime.\n const { OpenSheetMusicDisplay } = await import('opensheetmusicdisplay');\n\n const host = document.createElement('div');\n host.style.cssText = `position:fixed;left:-9999px;top:0;width:${hostWidth}px;background:${paper};`;\n document.body.appendChild(host);\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const osmd: any = new OpenSheetMusicDisplay(host, {\n backend: 'canvas',\n autoResize: false,\n drawTitle: false,\n drawSubtitle: false,\n drawComposer: false,\n drawLyricist: false,\n drawPartNames: false,\n });\n\n await osmd.load(xml);\n\n // hstack: engrave on a single horizontal staffline so the follow window pans\n // purely HORIZONTALLY (no stacked-system row breaks → no vertical playhead\n // jump). vstack: leave OSMD's default page wrap → classic stacked systems\n // (the follow camera then scrolls vertically between systems). Must be set\n // before render(); when on, the rasterized canvas becomes one wide row.\n if (singleRow) {\n osmd.setOptions({ renderSingleHorizontalStaffline: true } as never);\n }\n\n // Apply bar range only when provided (RSR's drawFrom/drawUpTo).\n if (opts?.bars) {\n osmd.setOptions({\n drawFromMeasureNumber: opts.bars[0],\n drawUpToMeasureNumber: opts.bars[1],\n } as never);\n }\n\n osmd.render();\n\n const canvas = host.querySelector('canvas');\n if (canvas) {\n const { systems, measures, measureColumns, content, noteCols } = extractGeometry(osmd, canvas, inkSumThreshold);\n document.body.removeChild(host);\n return { canvas, systems, measures, measureColumns, content, noteCols };\n }\n\n // SVG fallback: rasterize into a canvas so callers always get a canvas.\n const svg = host.querySelector('svg');\n if (svg) {\n const w = svg.clientWidth || hostWidth;\n const h = svg.clientHeight || 300;\n const svgStr = new XMLSerializer().serializeToString(svg);\n const dataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));\n const img = new Image();\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error('svg rasterize failed'));\n img.src = dataUrl;\n });\n const out = document.createElement('canvas');\n out.width = w;\n out.height = h;\n const c2d = out.getContext('2d');\n if (!c2d) throw new Error('2d context unavailable');\n c2d.fillStyle = paper;\n c2d.fillRect(0, 0, w, h);\n c2d.drawImage(img, 0, 0, w, h);\n document.body.removeChild(host);\n return {\n canvas: out,\n systems: [],\n measures: [],\n measureColumns: [],\n content: { x: 0, y: 0, w, h },\n noteCols: [],\n };\n }\n\n document.body.removeChild(host);\n throw new Error('OSMD produced neither canvas nor SVG');\n } catch (e) {\n if (host.parentNode) document.body.removeChild(host);\n throw e;\n }\n}\n\n// ─── Promo sampler ────────────────────────────────────────────────────────────\n// The shared Tone.js setup prefix under both apps' createPromoAudio.\n// Scheduling (playEvents / playMidi / playWindowSolo / playForeground) stays\n// in each app.\n//\n// Browser-only: requires Tone.js dynamic import. No unit tests here —\n// consumers' dom tests cover createPromoSampler.\n\nimport { createSalamanderSampler, generateReverb } from './audioHelpers';\nimport { SALAMANDER_CDN_BASE, SALAMANDER_URLS_8, SALAMANDER_URLS_FULL } from './salamander';\n\nexport interface PromoSampler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Tone: any; // typeof Tone — typed loose like audioHelpers.ts does for Tone\n sampler: unknown; // Tone.Sampler; typed loose like audioHelpers\n getStream(): MediaStream;\n /** Master-bus analyser (post-chain, sink-only) for reactive visualizers, or\n * null if `analyser` wasn't requested. whozart's \"listen\" spectrum reads it. */\n getAnalyser(): AnalyserNode | null;\n /** Current audio-clock time in seconds (Tone.now()). */\n audioNow(): number;\n /** releaseAll on the sampler; safe no-op on failure. */\n stop(): void;\n}\n\n/** Per-app tone shaping for the shared promo mastering bus. Ported from\n * whozart's audio-impl chain (the richest of the fleet); `reading` is a drier,\n * closer voicing so sight-reading notation stays legible note-to-note. Apps\n * pick one via `createPromoSampler({ voicing })`. */\nexport type PromoVoicing = 'concertHall' | 'reading' | 'dry';\n\ninterface VoicingPreset {\n release: number;\n attack: number;\n eq: { low: number; mid: number; high: number; lowFrequency: number; highFrequency: number };\n comp: { threshold: number; ratio: number; attack: number; release: number; knee: number };\n widen: number;\n reverb: { decay: number; preDelay: number; wet: number };\n}\n\nconst PROMO_VOICINGS: Record<PromoVoicing, VoicingPreset> = {\n // whozart's hall sound: long ringing release, lifted lows/air, gentle glue\n // compression, wide stereo, a real-hall reverb tail.\n concertHall: {\n release: 1.8, attack: 0.005,\n eq: { low: 1.5, mid: -1, high: 1.5, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -18, ratio: 1.6, attack: 0.012, release: 0.3, knee: 18 },\n widen: 0.30,\n reverb: { decay: 3.5, preDelay: 0.04, wet: 0.22 },\n },\n // Sight-reading: shorter release + much drier/shorter reverb so consecutive\n // notes don't smear into each other while the eye tracks the staff. Small\n // presence bump in the mids for note-attack clarity, narrower stereo.\n reading: {\n release: 1.1, attack: 0.004,\n eq: { low: 0.5, mid: 1, high: 1, lowFrequency: 280, highFrequency: 5000 },\n comp: { threshold: -16, ratio: 2.0, attack: 0.008, release: 0.25, knee: 12 },\n widen: 0.18,\n reverb: { decay: 1.4, preDelay: 0.02, wet: 0.10 },\n },\n // Bone-dry — no reverb at all (e.g. a debugging/reference voicing).\n dry: {\n release: 1.0, attack: 0.004,\n eq: { low: 0, mid: 0, high: 0, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -14, ratio: 2.0, attack: 0.008, release: 0.25, knee: 10 },\n widen: 0,\n reverb: { decay: 1.0, preDelay: 0, wet: 0 },\n },\n};\n\nexport async function createPromoSampler(opts?: {\n /** Feed a 0-value ConstantSource into the capture stream so the recorder's\n * audio track is live from t=0 (silent intro scenes aren't dropped).\n * Default false (RSR behavior). RMT passes true. */\n keepAlive?: boolean;\n /** Override the voicing's sampler release time (seconds). */\n release?: number;\n /** Sampler volume in dB. Default -2. */\n volumeDb?: number;\n /** Per-app tone shaping (see PromoVoicing). Default 'concertHall'. */\n voicing?: PromoVoicing;\n /** Use the full A0..C8 Salamander anchor set instead of the 8-anchor set —\n * fuller tone, more samples to fetch. Default false (8-anchor). */\n fullSamples?: boolean;\n /** Expose a master-bus analyser (for reactive visualizers). Default false. */\n analyser?: boolean;\n}): Promise<PromoSampler> {\n const voicing = PROMO_VOICINGS[opts?.voicing ?? 'concertHall'];\n const release = opts?.release ?? voicing.release;\n const volumeDb = opts?.volumeDb ?? -2;\n const keepAlive = opts?.keepAlive ?? false;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const Tone = (await import('tone')) as any;\n await Tone.start();\n\n // Unrouted sampler — we build the mastering bus off it (don't send the dry\n // sampler to the speakers, which would bypass the chain).\n const sampler = createSalamanderSampler(Tone, {\n urls: opts?.fullSamples ? SALAMANDER_URLS_FULL : SALAMANDER_URLS_8,\n baseUrl: SALAMANDER_CDN_BASE,\n release,\n attack: voicing.attack,\n volumeDb,\n connectToDestination: false,\n });\n\n // Mastering bus (ported from whozart): EQ tilt → glue compressor → stereo\n // widener → reverb → brick-wall limiter. Reverb is generated with a timeout\n // fallback so a slow/again-failing IR can never hang the render; on failure\n // the chain simply omits reverb.\n const eq = new Tone.EQ3(voicing.eq);\n const compressor = new Tone.Compressor(voicing.comp);\n const widener = new Tone.StereoWidener(voicing.widen);\n const limiter = new Tone.Limiter(-1.5);\n\n const reverb =\n voicing.reverb.wet > 0\n ? await generateReverb(\n Tone,\n { decay: voicing.reverb.decay, wet: voicing.reverb.wet },\n 4000,\n )\n : null;\n if (reverb) reverb.preDelay = voicing.reverb.preDelay;\n\n // chain() wires node→node in order; tail node feeds the taps below.\n const chainNodes = reverb ? [eq, compressor, widener, reverb, limiter] : [eq, compressor, widener, limiter];\n sampler.chain(...chainNodes);\n\n const ctx = Tone.getContext().rawContext as AudioContext;\n const mediaDest = ctx.createMediaStreamDestination();\n limiter.connect(mediaDest);\n // Also monitor through the speakers so a non-headless preview is audible.\n limiter.connect(Tone.getDestination());\n\n // Optional master-bus analyser (sink-only — no onward connection so it can't\n // double the signal). Drives whozart's reactive \"listen\" visualizer.\n let analyserNode: AnalyserNode | null = null;\n if (opts?.analyser) {\n analyserNode = ctx.createAnalyser();\n analyserNode.fftSize = 2048;\n analyserNode.smoothingTimeConstant = 0.8;\n limiter.connect(analyserNode);\n }\n\n // Optional: keep the audio track alive from t=0 so the recorder doesn't drop\n // silent intro scenes. RMT uses this; RSR does not.\n if (keepAlive) {\n const source = ctx.createConstantSource();\n source.offset.value = 0;\n source.connect(mediaDest);\n source.start();\n }\n\n await Tone.loaded();\n\n return {\n Tone,\n sampler,\n getStream(): MediaStream {\n return mediaDest.stream;\n },\n getAnalyser(): AnalyserNode | null {\n return analyserNode;\n },\n audioNow(): number {\n return Tone.now();\n },\n stop(): void {\n try {\n (sampler as unknown as { releaseAll?: () => void }).releaseAll?.();\n } catch {\n /* no-op */\n }\n },\n };\n}\n"],"mappings":"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","names":["y","x"]}
|
|
1
|
+
{"version":3,"sources":["../src/promo.ts"],"sourcesContent":["// Promo utilities — browser-only except for parseMidi/midiDurationMs which are\n// pure (no DOM).\n//\n// Exports:\n// parseMidi / midiDurationMs — pure MIDI parser (no DOM; testable in Node)\n// renderNotation — OSMD canvas renderer (browser/OSMD only)\n// createPromoSampler — Tone.js sampler factory (browser/Tone only)\n//\n// Unit tests cover parseMidi (tests/midi.test.ts).\n// renderNotation and createPromoSampler are browser-only — consumers' dom tests\n// cover them (OSMD and Tone.js require a browser context).\n\n// ─── MIDI parser ──────────────────────────────────────────────────────────────\n// Moved verbatim from realmusictheory/site/src/lib/promo/midiDuration.ts.\n\nexport interface MidiNote {\n midi: number;\n startMs: number;\n /** Audible duration in ms — extended while the sustain pedal is held. */\n durMs: number;\n /** 0..1 */\n velocity: number;\n}\n\nexport interface ParsedMidi {\n /** When the last notated note is released (pre-pedal-tail), in ms — paces the playhead. */\n durationMs: number;\n notes: MidiNote[];\n}\n\ninterface RawEvent {\n tick: number;\n kind: 'on' | 'off' | 'sustain' | 'tempo';\n midi?: number;\n velocity?: number;\n on?: boolean; // sustain down?\n us?: number; // tempo in µs/beat\n}\n\nexport function parseMidi(buf: ArrayBuffer): ParsedMidi {\n try {\n const dv = new DataView(buf);\n let p = 0;\n const u8 = () => dv.getUint8(p++);\n const u16 = () => { const v = dv.getUint16(p); p += 2; return v; };\n const u32 = () => { const v = dv.getUint32(p); p += 4; return v; };\n\n if (u32() !== 0x4d546864) return { durationMs: 0, notes: [] }; // 'MThd'\n const headerLen = u32();\n u16(); // format\n const ntrk = u16();\n const division = u16();\n p = 8 + headerLen;\n if (division & 0x8000) return { durationMs: 0, notes: [] }; // SMPTE — not handled\n const tpq = division || 480;\n\n const events: RawEvent[] = [];\n for (let t = 0; t < ntrk; t++) {\n if (p + 8 > dv.byteLength || u32() !== 0x4d54726b) break; // 'MTrk'\n const len = u32();\n const end = Math.min(p + len, dv.byteLength);\n let tick = 0;\n let running = 0;\n while (p < end) {\n let dt = 0, b: number;\n do { b = u8(); dt = (dt << 7) | (b & 0x7f); } while (b & 0x80 && p < end);\n tick += dt;\n let status = dv.getUint8(p);\n if (status & 0x80) { p++; running = status; } else { status = running; }\n if (status === 0xff) {\n const type = u8();\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n if (type === 0x51 && l === 3) {\n events.push({\n tick,\n kind: 'tempo',\n us: (dv.getUint8(p) << 16) | (dv.getUint8(p + 1) << 8) | dv.getUint8(p + 2),\n });\n }\n p += l;\n } else if (status === 0xf0 || status === 0xf7) {\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n p += l;\n } else {\n const hi = status & 0xf0;\n if (hi === 0x90 || hi === 0x80) {\n const midi = u8();\n const vel = u8();\n if (hi === 0x90 && vel > 0) events.push({ tick, kind: 'on', midi, velocity: vel / 127 });\n else events.push({ tick, kind: 'off', midi });\n } else if (hi === 0xb0) {\n const cc = u8();\n const val = u8();\n if (cc === 64) events.push({ tick, kind: 'sustain', on: val >= 64 });\n } else {\n p += hi === 0xc0 || hi === 0xd0 ? 1 : 2;\n }\n }\n }\n p = end;\n }\n\n // tick → ms via the tempo map\n const tempos = events.filter((e) => e.kind === 'tempo').sort((a, b) => a.tick - b.tick);\n if (!tempos.length || tempos[0].tick > 0) tempos.unshift({ tick: 0, kind: 'tempo', us: 500000 });\n const tickToMs = (tick: number): number => {\n let ms = 0;\n for (let i = 0; i < tempos.length; i++) {\n const segStart = tempos[i].tick;\n if (segStart >= tick) break;\n const segEnd = i + 1 < tempos.length ? Math.min(tempos[i + 1].tick, tick) : tick;\n ms += ((segEnd - segStart) / tpq) * ((tempos[i].us ?? 500000) / 1000);\n }\n return ms;\n };\n\n // Sustain spans (tick ranges where the pedal is down)\n const sustainEvents = events.filter((e) => e.kind === 'sustain').sort((a, b) => a.tick - b.tick);\n const pedalUpAfter = (tick: number): number | null => {\n for (const s of sustainEvents) if (!s.on && s.tick >= tick) return s.tick;\n return null;\n };\n const pedalDownAt = (tick: number): boolean => {\n let down = false;\n for (const s of sustainEvents) { if (s.tick > tick) break; down = !!s.on; }\n return down;\n };\n\n // Pair note-ons with the next matching note-off\n const ordered = events.filter((e) => e.kind === 'on' || e.kind === 'off').sort((a, b) => a.tick - b.tick);\n const open: Record<number, { tick: number; vel: number }[]> = {};\n const notes: MidiNote[] = [];\n let lastOffTick = 0;\n for (const e of ordered) {\n const m = e.midi!;\n if (e.kind === 'on') {\n (open[m] ??= []).push({ tick: e.tick, vel: e.velocity ?? 0.7 });\n } else {\n const stack = open[m];\n if (stack && stack.length) {\n const start = stack.shift()!;\n let endTick = e.tick;\n lastOffTick = Math.max(lastOffTick, endTick);\n // Extend while pedal is held past the note-off.\n if (pedalDownAt(endTick)) {\n const up = pedalUpAfter(endTick);\n if (up != null) endTick = up;\n }\n const startMs = tickToMs(start.tick);\n notes.push({\n midi: m,\n startMs,\n durMs: Math.max(60, tickToMs(endTick) - startMs),\n velocity: start.vel,\n });\n }\n }\n }\n\n return { durationMs: tickToMs(lastOffTick), notes };\n } catch {\n return { durationMs: 0, notes: [] };\n }\n}\n\n/** Total playback duration in ms (0 if unparseable). */\nexport function midiDurationMs(buf: ArrayBuffer): number {\n return parseMidi(buf).durationMs;\n}\n\n// ─── Notation renderer ────────────────────────────────────────────────────────\n// Superset of RSR (stave-web-sightread/src/lib/promo/notation.ts) and RMT\n// (realmusictheory/site/src/lib/promo/notation.ts). Both per-staff measure boxes\n// (RSR) and per-measure column union boxes (RMT) are computed every render.\n//\n// Browser-only: requires document + opensheetmusicdisplay. No unit tests here —\n// consumers' dom tests cover renderNotation.\n\nexport interface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/** One staff's slice of a measure, in canvas px. */\nexport interface StaffMeasureBox {\n index: number; // 0-based measure position within the rendered range\n staff: number; // 0 = top staff (RH/treble), 1 = bottom staff (LH/bass)\n box: Box;\n /** x of the measure's FIRST note (canvas px) — past any clef/key/time signature. */\n noteStartX: number;\n}\n\n/** Per-measure column box: union across all staves for that measure, in canvas px. */\nexport interface MeasureColumnBox extends Box {\n noteStartX: number;\n}\n\nexport interface RenderedNotation {\n canvas: HTMLCanvasElement;\n /** Per-row grand-staff system boxes in canvas px, top-to-bottom. */\n systems: Box[];\n /** Per-(measure, staff) boxes — RSR's geometry. */\n measures: StaffMeasureBox[];\n /** Per-measure column union across staves — RMT's geometry. */\n measureColumns: MeasureColumnBox[];\n /** Tight bounding box of all notation in canvas px (page whitespace cropped). */\n content: Box;\n /**\n * Per-distinct-onset engraved column position (measureIndex + fraction across\n * the note region), in time (left→right) order — one entry per played onset,\n * rests excluded, chords/both-hands collapsed. The scroll-cursor anchors to\n * THESE (the notes' real engraved X) instead of a time-fraction, so the\n * playhead lands exactly on each notehead. Empty/absent when extraction fails\n * or the engraving predates this field (cursor falls back to time/ordinal).\n */\n noteCols?: number[];\n}\n\nexport interface RenderNotationOpts {\n /** OSMD drawFrom/drawUpToMeasureNumber. Applied only when provided. */\n bars?: [number, number];\n /** Background fill colour. Default '#faf7f0' (RSR paper). */\n paper?: string;\n /** Pixel-sum threshold below which a pixel is counted as ink.\n * Default 690 (RSR: paper #faf7f0 sum ≈ 737). RMT uses 620. */\n inkSumThreshold?: number;\n /** Host div width in CSS px. Default 560 (RSR). RMT uses 620. */\n hostWidth?: number;\n /**\n * Notation scroll mode — drives BOTH the engraving layout here and the\n * follow-camera in the scroll-cursor/notation layers.\n *\n * 'hstack' (default) — engrave all measures on ONE horizontal staffline\n * (OSMD RenderSingleHorizontalStaffline). The follow window is a purely\n * HORIZONTAL slice so the playhead pans left→right with no vertical\n * row-break jump. The rasterized bitmap is one very wide row; geometry\n * (systems/measures) all share one row of y.\n *\n * 'vstack' — engrave the classic STACKED systems (normal page wrap into\n * multiple rows). The follow camera frames the ACTIVE system at a fixed\n * band position and scrolls VERTICALLY (eased) to the next system as the\n * playhead crosses systems — never a vertical leap across staff lines.\n *\n * Default 'hstack' (preserves the current single-row behavior).\n */\n scrollMode?: 'hstack' | 'vstack';\n /**\n * @deprecated Use `scrollMode` instead. Back-compat alias: `singleRow:true`\n * ⇔ `scrollMode:'hstack'`, `singleRow:false` ⇔ `scrollMode:'vstack'`.\n * When both are given, `scrollMode` wins.\n */\n singleRow?: boolean;\n}\n\n/** Padded union of boxes, clamped to the canvas. */\nfunction unionBox(boxes: Box[], canvas: HTMLCanvasElement, pad: number): Box {\n if (!boxes.length) return { x: 0, y: 0, w: canvas.width, h: canvas.height };\n const minX = Math.min(...boxes.map((b) => b.x));\n const minY = Math.min(...boxes.map((b) => b.y));\n const maxX = Math.max(...boxes.map((b) => b.x + b.w));\n const maxY = Math.max(...boxes.map((b) => b.y + b.h));\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(canvas.width, maxX + pad) - x,\n h: Math.min(canvas.height, maxY + pad) - y,\n };\n}\n\n/** Tight box around all non-paper pixels (notes, ledger lines, stems), padded. */\nfunction inkBoundingBox(canvas: HTMLCanvasElement, inkSumThreshold: number): Box | null {\n try {\n const ctx = canvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return null;\n const { width: W, height: H } = canvas;\n const data = ctx.getImageData(0, 0, W, H).data;\n let minX = W, minY = H, maxX = -1, maxY = -1;\n const step = 2;\n for (let y = 0; y < H; y += step) {\n for (let x = 0; x < W; x += step) {\n const i = (y * W + x) * 4;\n if (data[i + 3] < 16) continue;\n if (data[i] + data[i + 1] + data[i + 2] < inkSumThreshold) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n }\n if (maxX < 0) return null;\n const pad = 16;\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(W, maxX + pad) - x,\n h: Math.min(H, maxY + pad) - y,\n };\n } catch {\n return null;\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\nfunction extractGeometry(\n osmd: any,\n canvas: HTMLCanvasElement,\n inkSumThreshold: number,\n): { systems: Box[]; measures: StaffMeasureBox[]; measureColumns: MeasureColumnBox[]; content: Box; noteCols: number[] } {\n const full: Box = { x: 0, y: 0, w: canvas.width, h: canvas.height };\n try {\n const graphic: any = osmd.GraphicSheet;\n const page: any = graphic?.MusicPages?.[0];\n const pageW: number = page?.PositionAndShape?.Size?.width;\n const musicSystems: any[] = page?.MusicSystems ?? [];\n if (!pageW || !musicSystems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n // OSMD units → canvas px.\n //\n // BUG FIX: the naive `canvas.width / pageW` is WRONG whenever the engraving\n // OVERFLOWS the nominal page width — which `renderSingleHorizontalStaffline`\n // (hstack) does routinely: one wide row of measures spills past pageW, and OSMD\n // sizes the canvas to the CONTENT (with its page margins), not to pageW. Using\n // canvas.width/pageW then OVER-scales every position (~5% on a 5-bar row), so\n // the cursor/highlight geometry drifts steadily RIGHT of the real noteheads and\n // DOWN in y (the same scale is applied to both axes) — accumulating to ~half a\n // bar by the end of the clip.\n //\n // OSMD renders at a single uniform scale `s` (px per OSMD unit) with symmetric\n // page margins, so `canvas.width = (contentRight + contentLeft) * s` where\n // contentLeft is the left margin. Hence `s = canvas.width / (contentRight +\n // contentLeft)`. This SELF-CALIBRATES to OSMD's zoom and, when the content fits\n // the page (vstack: contentRight ≈ pageW - margin, contentLeft = margin), it\n // reduces to canvas.width/pageW — so it's correct for both layouts. Falls back\n // to pageW if the content extent can't be measured.\n let contentRightU = -Infinity, contentLeftU = Infinity;\n for (const staves of (graphic?.MeasureList ?? []) as any[][]) {\n for (const m of (staves ?? [])) {\n const ps = m?.PositionAndShape;\n if (!ps?.AbsolutePosition || !ps?.Size || !(ps.Size.width > 0.1)) continue;\n contentRightU = Math.max(contentRightU, ps.AbsolutePosition.x + ps.Size.width);\n contentLeftU = Math.min(contentLeftU, ps.AbsolutePosition.x);\n }\n }\n const spanU = Number.isFinite(contentRightU) && Number.isFinite(contentLeftU)\n ? contentRightU + contentLeftU\n : pageW;\n const f = canvas.width / (spanU > 0 ? spanU : pageW);\n const toBox = (pas: any): Box | null => {\n const p = pas?.AbsolutePosition;\n const sz = pas?.Size;\n if (!p || !sz) return null;\n return { x: p.x * f, y: p.y * f, w: sz.width * f, h: sz.height * f };\n };\n\n const systems: Box[] = musicSystems\n .map((s) => toBox(s?.PositionAndShape))\n .filter((b): b is Box => !!b && b.w > 1 && b.h > 1)\n .sort((a, b) => a.y - b.y);\n if (!systems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n const measureList: any[][] = graphic?.MeasureList ?? [];\n\n // RSR geometry: per-(measure, staff) individual boxes\n const measures: StaffMeasureBox[] = [];\n measureList.forEach((staves, index) => {\n (staves ?? []).forEach((m: any, staff: number) => {\n const box = toBox(m?.PositionAndShape);\n if (box && box.w > 1 && box.h > 1) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n const noteStartX = typeof seX === 'number' ? seX * f : box.x;\n measures.push({ index, staff, box, noteStartX });\n }\n });\n });\n\n // RMT geometry: per-measure column boxes (union across staves)\n const measureColumns: MeasureColumnBox[] = measureList\n .map((staves) => {\n const arr = staves ?? [];\n const boxes = arr\n .map((m: any) => toBox(m?.PositionAndShape))\n .filter((b: Box | null): b is Box => !!b && b.w > 1 && b.h > 1);\n if (!boxes.length) return 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 // First note x across all staves; clamped into the column box\n let nx = Infinity;\n for (const m of arr) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof seX === 'number') nx = Math.min(nx, seX * f);\n }\n const noteStartX = Number.isFinite(nx) ? Math.max(x0, Math.min(nx, x1)) : x0;\n return { x: x0, y: y0, w: x1 - x0, h: y1 - y0, noteStartX };\n })\n .filter((b): b is MeasureColumnBox => !!b);\n\n // Per-distinct-onset engraved columns, in render (= time) order, rests excluded\n // — the cursor's TRUE anchor X. Each value is `ordinal + frac`, where `ordinal`\n // is the column's position among the DRAWN measures (matching the array index of\n // measureColumnsFromLayout — NOT the absolute MeasureList index, which would\n // overflow the columns array for excerpts whose first measure isn't bar 0), and\n // `frac` is the notehead's position across the measure's note region. Crucially\n // we iterate ONLY drawn measures (a valid box): with drawFrom/drawUpTo, OSMD's\n // MeasureList still holds EVERY source measure, so without this filter the whole\n // score leaks in (e.g. 128 cols for a 22-note excerpt) and never pairs 1:1 with\n // the played onsets, disabling the engraved-x anchor.\n // We key each note's engraved X by its rhythmic TIMESTAMP within the measure,\n // NOT by raw distinct X. Two staves' notes at the SAME beat engrave at slightly\n // different X (the grand staff isn't pixel-aligned), so deduping by X yielded\n // MORE columns than there are onsets (e.g. 45 cols vs 40 onsets) — which broke\n // the 1:1 pairing with the played onsets and forced the cursor onto a\n // linear-time fallback that drifts ~½ bar ahead of the real noteheads on dense\n // bars. Keying by timestamp collapses each beat to ONE column at the (leftmost)\n // engraved X of its noteheads, so the column count equals the distinct-onset\n // count and the cursor lands on the actual notehead, beat for beat.\n const noteCols: number[] = [];\n let ordinal = 0;\n measureList.forEach((staves) => {\n const arr = staves ?? [];\n let mx0 = Infinity, mx1 = -Infinity, nsx = Infinity;\n const byTs = new Map<number, number>(); // rhythmic timestamp → leftmost note X (px)\n for (const m of arr) {\n const mb = toBox(m?.PositionAndShape);\n if (mb) { mx0 = Math.min(mx0, mb.x); mx1 = Math.max(mx1, mb.x + mb.w); }\n const se0 = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof se0 === 'number') nsx = Math.min(nsx, se0 * f);\n for (const se of (m?.staffEntries ?? [])) {\n const sx = se?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof sx !== 'number') continue;\n const hasNote = (se?.graphicalVoiceEntries ?? []).some(\n (gve: any) => (gve?.notes ?? []).some(\n (gn: any) => !(gn?.sourceNote?.isRestFlag ?? gn?.sourceNote?.IsRest ?? false)));\n if (!hasNote) continue;\n // Within-measure rhythmic position (whole notes). Round to a fine grid so\n // float noise doesn't split a beat; falls back to X if unavailable.\n const tsRaw =\n se?.relInMeasureTimestamp?.RealValue ??\n se?.getAbsoluteTimestamp?.()?.RealValue ??\n se?.sourceStaffEntry?.Timestamp?.RealValue;\n const key = typeof tsRaw === 'number' ? Math.round(tsRaw * 10000) : Math.round(sx * f);\n const px = sx * f;\n const prev = byTs.get(key);\n if (prev === undefined || px < prev) byTs.set(key, px);\n }\n }\n // Skip undrawn measures (no valid box / zero width) — only drawn measures get\n // a column, so `ordinal` stays in lockstep with measureColumnsFromLayout.\n if (!Number.isFinite(mx0) || (mx1 - mx0) <= 1) return;\n const startX = Number.isFinite(nsx) ? Math.max(mx0, Math.min(nsx, mx1)) : mx0;\n const denom = (mx1 - startX) || 1;\n // Emit one column per distinct beat, in time order (the byTs keys are the\n // rounded timestamps), so column k ↔ onset k.\n for (const key of [...byTs.keys()].sort((a, b) => a - b)) {\n const x = byTs.get(key)!;\n noteCols.push(ordinal + Math.min(1, Math.max(0, (x - startX) / denom)));\n }\n ordinal++;\n });\n\n const content = inkBoundingBox(canvas, inkSumThreshold) ?? unionBox(systems, canvas, 14);\n return { systems, measures, measureColumns, content, noteCols };\n } catch {\n return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n// ─── Mobile canvas-area clamp ──────────────────────────────────────────────\n//\n// WHY THIS EXISTS: OSMD's canvas backend (opensheetmusicdisplay's vendored\n// VexFlow-canvas resize()) multiplies its CSS-px backing-store dims by\n// `window.devicePixelRatio` with NO area check — it only guards each\n// DIMENSION against a 32767px ceiling (`SanitizeCanvasDims`), applied BEFORE\n// the devicePixelRatio multiply, so the guard does nothing for the failure\n// mode that actually bites: a long `vstack` (page-wrap) engraving stacks one\n// system per row, so total CSS height grows with piece length — a 65-bar\n// piece easily reaches ~14,000 CSS px tall — and at a mobile `devicePixelRatio`\n// of 2–3 that height alone lands near or past 32767 px in device pixels, while\n// the resulting AREA (width_px × height_px) blows straight through iOS\n// Safari's well-documented ~16,777,216px² (4096×4096) canvas backing-store\n// limit. Past that limit iOS Safari does not throw or warn — it silently\n// clears/no-ops the canvas, so `renderNotation` returns what LOOKS like a\n// valid non-null `RenderedNotation` (real dims, real geometry from\n// `extractGeometry`) whose `canvas` is actually blank. That is the root cause\n// of the /bach/play \"As written\" mobile-blank bug (E7M — 2026-08-10): the\n// FULL 65-bar Chopin piece rasterizes to 1680×41936 device px (~70.5M px²,\n// 4.2× the iOS cap) at devicePixelRatio 3. Confirmed via a raw-canvas-op trace\n// (Playwright, iPhone profile) — see docs/superpowers/sdd/written-mobile-blank-report.md.\n//\n// FIX: after OSMD's normal (native-dpr) render, check the resulting canvas's\n// device-px AREA against a conservative cap comfortably under iOS's real\n// limit. If it fits, this is a complete no-op (byte-identical to before this\n// change — desktop and any short/promo excerpt never re-renders). If it\n// doesn't, re-render ONCE more at a reduced effective devicePixelRatio\n// (`clampRasterDpr`, pure/testable) that brings the SAME CSS-px layout back\n// under the cap — the engraving loses some sharpness on very long pieces\n// (fewer device px per notehead) but stays fully legible and, critically,\n// actually paints. This is a resolution clamp, not a content/layout change:\n// `osmd`'s CSS-px page layout (line breaks, systems, bar placement) is\n// unaffected — only the backing-store pixel density is reduced — so\n// `extractGeometry`'s CSS-px-independent geometry (systems/measures/content\n// boxes, all read in CANVAS px post-render) still lines up with the smaller\n// canvas.\n//\n// HOW: OSMD's canvas backend reads `window.devicePixelRatio` directly (no\n// public hook to override per-call), so this temporarily overrides the\n// global `devicePixelRatio` accessor for the DURATION OF ONE SYNCHRONOUS\n// `osmd.render()` CALL ONLY, then restores the original descriptor in a\n// `finally`. Safe because `osmd.render()` is called (both here and in the\n// pre-existing first pass above) without `await` — canvas-backend rendering\n// is synchronous — so no other code can observe the override mid-flight\n// (single-threaded JS; nothing yields between the override and the restore).\nexport const MAX_RASTER_AREA_PX = 12_000_000;\n\n/**\n * Pure clamp math (unit-tested, no DOM): given the CSS-px content dims OSMD\n * is about to rasterize (`cssW`/`cssH` — the SAME layout regardless of dpr)\n * and the browser's native `devicePixelRatio`, return the largest dpr `<=\n * nativeDpr` whose backing-store area (`cssW*dpr × cssH*dpr`) stays within\n * `capPx2`. Returns `nativeDpr` unchanged whenever the native-resolution\n * raster already fits — that no-op path is what keeps desktop/short-excerpt\n * rendering byte-for-byte unchanged by this clamp. Never returns less than 1\n * (native CSS-px density is the floor — always legible, never sub-1x blur)\n * unless `nativeDpr` itself is below 1 (not expected in a real browser, but\n * handled rather than dividing by/sqrt-ing a negative).\n */\nexport function clampRasterDpr(\n cssW: number,\n cssH: number,\n nativeDpr: number,\n capPx2: number = MAX_RASTER_AREA_PX,\n): number {\n const safeDpr = nativeDpr > 0 ? nativeDpr : 1;\n if (!(cssW > 0) || !(cssH > 0)) return safeDpr;\n const nativeArea = cssW * cssH * safeDpr * safeDpr;\n if (nativeArea <= capPx2) return safeDpr;\n const cappedDpr = Math.sqrt(capPx2 / (cssW * cssH));\n return Math.max(1, Math.min(safeDpr, cappedDpr));\n}\n\n/** Render a MusicXML string to a detached canvas + geometry.\n * Browser-only (requires document + opensheetmusicdisplay dynamic import). */\nexport async function renderNotation(\n xml: string,\n opts?: RenderNotationOpts,\n): Promise<RenderedNotation> {\n const paper = opts?.paper ?? '#faf7f0';\n const inkSumThreshold = opts?.inkSumThreshold ?? 690;\n const hostWidth = opts?.hostWidth ?? 560;\n // scrollMode drives the engraving layout; singleRow is the deprecated alias.\n // Default 'hstack' (single horizontal staffline) preserves current behavior.\n const scrollMode: 'hstack' | 'vstack' =\n opts?.scrollMode ?? (opts?.singleRow === false ? 'vstack' : 'hstack');\n const singleRow = scrollMode === 'hstack';\n\n // Literal dynamic import: consumers' bundlers (Vite/Rollup) must be able to\n // statically see the specifier to resolve + code-split it — a variable\n // specifier would reach the browser as a bare import and fail at runtime.\n const { OpenSheetMusicDisplay } = await import('opensheetmusicdisplay');\n\n const host = document.createElement('div');\n host.style.cssText = `position:fixed;left:-9999px;top:0;width:${hostWidth}px;background:${paper};`;\n document.body.appendChild(host);\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const osmd: any = new OpenSheetMusicDisplay(host, {\n backend: 'canvas',\n autoResize: false,\n drawTitle: false,\n drawSubtitle: false,\n drawComposer: false,\n drawLyricist: false,\n drawPartNames: false,\n });\n\n await osmd.load(xml);\n\n // hstack: engrave on a single horizontal staffline so the follow window pans\n // purely HORIZONTALLY (no stacked-system row breaks → no vertical playhead\n // jump). vstack: leave OSMD's default page wrap → classic stacked systems\n // (the follow camera then scrolls vertically between systems). Must be set\n // before render(); when on, the rasterized canvas becomes one wide row.\n if (singleRow) {\n osmd.setOptions({ renderSingleHorizontalStaffline: true } as never);\n }\n\n // Apply bar range only when provided (RSR's drawFrom/drawUpTo).\n if (opts?.bars) {\n osmd.setOptions({\n drawFromMeasureNumber: opts.bars[0],\n drawUpToMeasureNumber: opts.bars[1],\n } as never);\n }\n\n osmd.render();\n\n let canvas = host.querySelector('canvas');\n\n // Mobile canvas-area clamp — see the module doc above `clampRasterDpr`.\n // No-ops (skips straight through) whenever the native-dpr raster already\n // fits the cap, which is every desktop render and every short/promo\n // excerpt — only a long vstack piece at a high mobile dpr re-renders.\n if (canvas && canvas.width * canvas.height > MAX_RASTER_AREA_PX) {\n const nativeDpr = typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;\n const cssW = canvas.width / nativeDpr;\n const cssH = canvas.height / nativeDpr;\n const clampedDpr = clampRasterDpr(cssW, cssH, nativeDpr);\n if (clampedDpr < nativeDpr) {\n const desc = Object.getOwnPropertyDescriptor(window, 'devicePixelRatio');\n try {\n Object.defineProperty(window, 'devicePixelRatio', { value: clampedDpr, configurable: true });\n osmd.render();\n canvas = host.querySelector('canvas');\n } finally {\n if (desc) Object.defineProperty(window, 'devicePixelRatio', desc);\n else delete (window as unknown as Record<string, unknown>).devicePixelRatio;\n }\n }\n }\n\n if (canvas) {\n const { systems, measures, measureColumns, content, noteCols } = extractGeometry(osmd, canvas, inkSumThreshold);\n document.body.removeChild(host);\n return { canvas, systems, measures, measureColumns, content, noteCols };\n }\n\n // SVG fallback: rasterize into a canvas so callers always get a canvas.\n const svg = host.querySelector('svg');\n if (svg) {\n const w = svg.clientWidth || hostWidth;\n const h = svg.clientHeight || 300;\n const svgStr = new XMLSerializer().serializeToString(svg);\n const dataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));\n const img = new Image();\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error('svg rasterize failed'));\n img.src = dataUrl;\n });\n const out = document.createElement('canvas');\n out.width = w;\n out.height = h;\n const c2d = out.getContext('2d');\n if (!c2d) throw new Error('2d context unavailable');\n c2d.fillStyle = paper;\n c2d.fillRect(0, 0, w, h);\n c2d.drawImage(img, 0, 0, w, h);\n document.body.removeChild(host);\n return {\n canvas: out,\n systems: [],\n measures: [],\n measureColumns: [],\n content: { x: 0, y: 0, w, h },\n noteCols: [],\n };\n }\n\n document.body.removeChild(host);\n throw new Error('OSMD produced neither canvas nor SVG');\n } catch (e) {\n if (host.parentNode) document.body.removeChild(host);\n throw e;\n }\n}\n\n// ─── Promo sampler ────────────────────────────────────────────────────────────\n// The shared Tone.js setup prefix under both apps' createPromoAudio.\n// Scheduling (playEvents / playMidi / playWindowSolo / playForeground) stays\n// in each app.\n//\n// Browser-only: requires Tone.js dynamic import. No unit tests here —\n// consumers' dom tests cover createPromoSampler.\n\nimport { createSalamanderSampler, generateReverb } from './audioHelpers';\nimport { SALAMANDER_CDN_BASE, SALAMANDER_URLS_8, SALAMANDER_URLS_FULL } from './salamander';\n\nexport interface PromoSampler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Tone: any; // typeof Tone — typed loose like audioHelpers.ts does for Tone\n sampler: unknown; // Tone.Sampler; typed loose like audioHelpers\n getStream(): MediaStream;\n /** Master-bus analyser (post-chain, sink-only) for reactive visualizers, or\n * null if `analyser` wasn't requested. whozart's \"listen\" spectrum reads it. */\n getAnalyser(): AnalyserNode | null;\n /** Current audio-clock time in seconds (Tone.now()). */\n audioNow(): number;\n /** releaseAll on the sampler; safe no-op on failure. */\n stop(): void;\n}\n\n/** Per-app tone shaping for the shared promo mastering bus. Ported from\n * whozart's audio-impl chain (the richest of the fleet); `reading` is a drier,\n * closer voicing so sight-reading notation stays legible note-to-note. Apps\n * pick one via `createPromoSampler({ voicing })`. */\nexport type PromoVoicing = 'concertHall' | 'reading' | 'dry';\n\ninterface VoicingPreset {\n release: number;\n attack: number;\n eq: { low: number; mid: number; high: number; lowFrequency: number; highFrequency: number };\n comp: { threshold: number; ratio: number; attack: number; release: number; knee: number };\n widen: number;\n reverb: { decay: number; preDelay: number; wet: number };\n}\n\nconst PROMO_VOICINGS: Record<PromoVoicing, VoicingPreset> = {\n // whozart's hall sound: long ringing release, lifted lows/air, gentle glue\n // compression, wide stereo, a real-hall reverb tail.\n concertHall: {\n release: 1.8, attack: 0.005,\n eq: { low: 1.5, mid: -1, high: 1.5, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -18, ratio: 1.6, attack: 0.012, release: 0.3, knee: 18 },\n widen: 0.30,\n reverb: { decay: 3.5, preDelay: 0.04, wet: 0.22 },\n },\n // Sight-reading: shorter release + much drier/shorter reverb so consecutive\n // notes don't smear into each other while the eye tracks the staff. Small\n // presence bump in the mids for note-attack clarity, narrower stereo.\n reading: {\n release: 1.1, attack: 0.004,\n eq: { low: 0.5, mid: 1, high: 1, lowFrequency: 280, highFrequency: 5000 },\n comp: { threshold: -16, ratio: 2.0, attack: 0.008, release: 0.25, knee: 12 },\n widen: 0.18,\n reverb: { decay: 1.4, preDelay: 0.02, wet: 0.10 },\n },\n // Bone-dry — no reverb at all (e.g. a debugging/reference voicing).\n dry: {\n release: 1.0, attack: 0.004,\n eq: { low: 0, mid: 0, high: 0, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -14, ratio: 2.0, attack: 0.008, release: 0.25, knee: 10 },\n widen: 0,\n reverb: { decay: 1.0, preDelay: 0, wet: 0 },\n },\n};\n\nexport async function createPromoSampler(opts?: {\n /** Feed a 0-value ConstantSource into the capture stream so the recorder's\n * audio track is live from t=0 (silent intro scenes aren't dropped).\n * Default false (RSR behavior). RMT passes true. */\n keepAlive?: boolean;\n /** Override the voicing's sampler release time (seconds). */\n release?: number;\n /** Sampler volume in dB. Default -2. */\n volumeDb?: number;\n /** Per-app tone shaping (see PromoVoicing). Default 'concertHall'. */\n voicing?: PromoVoicing;\n /** Use the full A0..C8 Salamander anchor set instead of the 8-anchor set —\n * fuller tone, more samples to fetch. Default false (8-anchor). */\n fullSamples?: boolean;\n /** Expose a master-bus analyser (for reactive visualizers). Default false. */\n analyser?: boolean;\n}): Promise<PromoSampler> {\n const voicing = PROMO_VOICINGS[opts?.voicing ?? 'concertHall'];\n const release = opts?.release ?? voicing.release;\n const volumeDb = opts?.volumeDb ?? -2;\n const keepAlive = opts?.keepAlive ?? false;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const Tone = (await import('tone')) as any;\n await Tone.start();\n\n // Unrouted sampler — we build the mastering bus off it (don't send the dry\n // sampler to the speakers, which would bypass the chain).\n const sampler = createSalamanderSampler(Tone, {\n urls: opts?.fullSamples ? SALAMANDER_URLS_FULL : SALAMANDER_URLS_8,\n baseUrl: SALAMANDER_CDN_BASE,\n release,\n attack: voicing.attack,\n volumeDb,\n connectToDestination: false,\n });\n\n // Mastering bus (ported from whozart): EQ tilt → glue compressor → stereo\n // widener → reverb → brick-wall limiter. Reverb is generated with a timeout\n // fallback so a slow/again-failing IR can never hang the render; on failure\n // the chain simply omits reverb.\n const eq = new Tone.EQ3(voicing.eq);\n const compressor = new Tone.Compressor(voicing.comp);\n const widener = new Tone.StereoWidener(voicing.widen);\n const limiter = new Tone.Limiter(-1.5);\n\n const reverb =\n voicing.reverb.wet > 0\n ? await generateReverb(\n Tone,\n { decay: voicing.reverb.decay, wet: voicing.reverb.wet },\n 4000,\n )\n : null;\n if (reverb) reverb.preDelay = voicing.reverb.preDelay;\n\n // chain() wires node→node in order; tail node feeds the taps below.\n const chainNodes = reverb ? [eq, compressor, widener, reverb, limiter] : [eq, compressor, widener, limiter];\n sampler.chain(...chainNodes);\n\n const ctx = Tone.getContext().rawContext as AudioContext;\n const mediaDest = ctx.createMediaStreamDestination();\n limiter.connect(mediaDest);\n // Also monitor through the speakers so a non-headless preview is audible.\n limiter.connect(Tone.getDestination());\n\n // Optional master-bus analyser (sink-only — no onward connection so it can't\n // double the signal). Drives whozart's reactive \"listen\" visualizer.\n let analyserNode: AnalyserNode | null = null;\n if (opts?.analyser) {\n analyserNode = ctx.createAnalyser();\n analyserNode.fftSize = 2048;\n analyserNode.smoothingTimeConstant = 0.8;\n limiter.connect(analyserNode);\n }\n\n // Optional: keep the audio track alive from t=0 so the recorder doesn't drop\n // silent intro scenes. RMT uses this; RSR does not.\n if (keepAlive) {\n const source = ctx.createConstantSource();\n source.offset.value = 0;\n source.connect(mediaDest);\n source.start();\n }\n\n await Tone.loaded();\n\n return {\n Tone,\n sampler,\n getStream(): MediaStream {\n return mediaDest.stream;\n },\n getAnalyser(): AnalyserNode | null {\n return analyserNode;\n },\n audioNow(): number {\n return Tone.now();\n },\n stop(): void {\n try {\n (sampler as unknown as { releaseAll?: () => void }).releaseAll?.();\n } catch {\n /* no-op */\n }\n },\n };\n}\n"],"mappings":"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","names":["y","x"]}
|
package/dist/scene/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { A as AudioClock, S as Score, a as LayerFactory, R as RenderCtx, b as Sc
|
|
|
2
2
|
export { L as Layer, d as SpectrumInput, e as SpectrumProps, W as WaveformInput, f as WaveformProps, s as scoreFromMusicXML, g as spectrumFactory, w as waveformFactory } from '../waveform-DdSMAbYQ.js';
|
|
3
3
|
import { PromoTheme, RecordOpts, Scene, SafeBox } from '../video.js';
|
|
4
4
|
import { RenderedNotation, Box } from '../promo.js';
|
|
5
|
-
import { N as NotationLayout } from '../notationGeometry-
|
|
6
|
-
export { F as FOLLOW_BARS, a as FOLLOW_PAD, b as NotationLayoutOpts, c as NotationRect, P as PlayheadLine, d as audioPlayheadLine, e as cropAroundBox, f as cubicEaseInOut, g as distinctMeasureIndices, h as distinctOnsets, i as firstMeasureBox, j as followBoxAt, k as followWindowStart, l as lerpBox, m as measureColumnsFromLayout, n as measureCount, o as measureSpanBox, p as measureSystemMap, q as notationLayout, r as playheadLine, s as systemBox, v as vstackAudioPlayheadLine, t as vstackFollowBox } from '../notationGeometry-
|
|
5
|
+
import { N as NotationLayout } from '../notationGeometry-CyYXJrUH.js';
|
|
6
|
+
export { F as FOLLOW_BARS, a as FOLLOW_PAD, b as NotationLayoutOpts, c as NotationRect, P as PlayheadLine, d as audioPlayheadLine, e as cropAroundBox, f as cubicEaseInOut, g as distinctMeasureIndices, h as distinctOnsets, i as firstMeasureBox, j as followBoxAt, k as followWindowStart, l as lerpBox, m as measureColumnsFromLayout, n as measureCount, o as measureSpanBox, p as measureSystemMap, q as notationLayout, r as playheadLine, s as systemBox, v as vstackAudioPlayheadLine, t as vstackFollowBox } from '../notationGeometry-CyYXJrUH.js';
|
|
7
7
|
|
|
8
8
|
/** One scheduled audio event, in seconds RELATIVE to the schedule start. */
|
|
9
9
|
interface AudioEvent {
|
|
@@ -538,6 +538,21 @@ interface NotationProps {
|
|
|
538
538
|
bandTop?: number;
|
|
539
539
|
/** Height of the notation band (screen px). Default safeBox.bottom - bandTop. */
|
|
540
540
|
bandHeight?: number;
|
|
541
|
+
/** Offscreen OSMD host div width in CSS px, forwarded verbatim to
|
|
542
|
+
* `renderNotation`'s `hostWidth` (default 560 there when omitted). This is
|
|
543
|
+
* the width OSMD line-breaks against — a live player wanting engraved
|
|
544
|
+
* systems to use its actual container width (rather than always the
|
|
545
|
+
* 560px default meant for a fixed-size promo card) sets this to its own
|
|
546
|
+
* measured host size. Ignored when `rendered` is supplied (no engrave
|
|
547
|
+
* happens). */
|
|
548
|
+
hostWidth?: number;
|
|
549
|
+
/** Explicit horizontal fit width (screen px), forwarded to `notationLayout`'s
|
|
550
|
+
* `boxWidth` (see its doc comment) — bypasses `safeBox`'s promo-video
|
|
551
|
+
* caption/share-button margins (~24% of width reserved) so the engraving
|
|
552
|
+
* can use the FULL band width instead. Default: unset (existing
|
|
553
|
+
* `safeBox(W,H).centeredW` promo framing, unchanged for every caller that
|
|
554
|
+
* doesn't pass this). */
|
|
555
|
+
bandWidth?: number;
|
|
541
556
|
}
|
|
542
557
|
declare const notationFactory: LayerFactory<NotationProps>;
|
|
543
558
|
|
|
@@ -621,6 +636,14 @@ interface NotationEngraving {
|
|
|
621
636
|
/** Notation band rect (screen px) the follow window fits into, per frame. */
|
|
622
637
|
bandTop: number;
|
|
623
638
|
bandHeight: number;
|
|
639
|
+
/** Explicit horizontal fit width (screen px), forwarded to `notationLayout`'s
|
|
640
|
+
* `boxWidth` — undefined keeps the default `safeBox(W,H).centeredW` promo
|
|
641
|
+
* framing (see `notationGeometry.ts`'s `NotationLayoutOpts.boxWidth` doc).
|
|
642
|
+
* Published so `scroll-cursor`'s per-frame follow layout uses the SAME
|
|
643
|
+
* width `notation`'s base layout was computed with — they must agree, or
|
|
644
|
+
* the visible framing jumps between the base frame and the first followed
|
|
645
|
+
* frame. */
|
|
646
|
+
bandWidth?: number;
|
|
624
647
|
/**
|
|
625
648
|
* A pure follow-layout provider, published by scroll-cursor at init. notation
|
|
626
649
|
* calls it each frame to blit the SAME followed-bars window the cursor sweeps —
|
package/dist/scene/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
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\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 = 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 /**\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}\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\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\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 });\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, {});\n setNotationEngraving(ctx, { rendered: rn, base, bandTop: top, bandHeight: height });\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 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 });\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"]}
|