@real-music-packages/web-core 0.39.12 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-CULHIX4S.js → chunk-F4VYTGJQ.js} +2 -2
- package/dist/{chunk-WKJM527Q.js → chunk-HIELHWEZ.js} +2 -1
- package/dist/chunk-HIELHWEZ.js.map +1 -0
- package/dist/{chunk-BKGAQETO.js → chunk-UADJXSFM.js} +2 -2
- package/dist/notationPlayer.js +2 -2
- package/dist/notationPlayerSvg.js +2 -2
- package/dist/notationPlayerVerovio.d.ts +175 -1
- package/dist/notationPlayerVerovio.js +246 -4
- package/dist/notationPlayerVerovio.js.map +1 -1
- package/dist/scene/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-WKJM527Q.js.map +0 -1
- /package/dist/{chunk-CULHIX4S.js.map → chunk-F4VYTGJQ.js.map} +0 -0
- /package/dist/{chunk-BKGAQETO.js.map → chunk-UADJXSFM.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hitTestMeasureAt,
|
|
3
3
|
measureColumnsFromLayout
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-HIELHWEZ.js";
|
|
5
5
|
|
|
6
6
|
// src/notationCommon.ts
|
|
7
7
|
function computeReflowScrollDelta(oldLayout, newLayout, anchorX, anchorY) {
|
|
@@ -150,4 +150,4 @@ export {
|
|
|
150
150
|
hasReachedProgrammaticTarget,
|
|
151
151
|
createFollowController
|
|
152
152
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-F4VYTGJQ.js.map
|
|
@@ -470,6 +470,7 @@ export {
|
|
|
470
470
|
distinctMeasureIndices,
|
|
471
471
|
followBoxAt,
|
|
472
472
|
followWindowStart,
|
|
473
|
+
systemIndexOfBox,
|
|
473
474
|
measureSystemMap,
|
|
474
475
|
systemBox,
|
|
475
476
|
vstackFollowBox,
|
|
@@ -481,4 +482,4 @@ export {
|
|
|
481
482
|
audioPlayheadLine,
|
|
482
483
|
vstackAudioPlayheadLine
|
|
483
484
|
};
|
|
484
|
-
//# sourceMappingURL=chunk-
|
|
485
|
+
//# sourceMappingURL=chunk-HIELHWEZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scene/notationGeometry.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. */\n/** Which system (row) box's vertical band contains `box`'s center — or, when\n * none does, whichever system's center is closest. Exported (0.40.0) so\n * notationPlayerVerovio.ts's `verovioEngravedNotes` can assign\n * `EngravedNote.systemIndex` from real rendered note geometry using the\n * SAME row-assignment rule `measureSystemMap` already uses for measures —\n * one system-membership test, not two independently-derived ones. */\nexport function 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/**\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 * Lives here (not in notationPlayer.ts, its original home through web-core\n * 0.38.0) so BOTH the canvas player (notationPlayer.ts, which re-exports this\n * for backward compat — its public API is unchanged) and the SVG player\n * (notationPlayerSvg.ts) can import it without either one's bundle pulling in\n * the other's implementation. It has no canvas/raster-specific logic — it\n * only ever reads `NotationLayout`, the same backend-agnostic shape both\n * players produce — so this is its natural, shared home alongside\n * `measureColumnsFromLayout`/`vstackAudioPlayheadLine`, not a canvas-only\n * concept that happens to be reusable.\n *\n * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in\n * the same space `layout.measures[].box` is already mapped into by whichever\n * layout builder produced it — the canvas player's `notationLayout()` maps\n * into DEST/DEVICE-PIXEL space (i.e. the player's own `<canvas>` device\n * pixels, origin top-left, NOT CSS px — the same space its own\n * `onCanvasClick` computes via `(clientX - rect.left) * (canvas.width /\n * rect.width)`); the SVG player's `svgNotationLayout()` maps into real CSS px\n * (there is no separate raster/device-px space for an SVG backend). It is NOT\n * the canvas path's raster/src space (`RenderedNotation.canvas`, OSMD's own\n * pre-map bitmap px) — passing src-space coordinates here is exactly the\n * \"classic scale gotcha\" (`extractGeometry`'s `canvas.width / pageW` vs\n * `/(contentRight+contentLeft)`, see promo.ts's 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/** 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 * The printed SYSTEM (row) index this frame's line is drawn on — `-1`/\n * `undefined` when the producing function has no system geometry to\n * resolve one from (e.g. `audioPlayheadLine`, which never sets it — it has\n * no `layout.systems` concept — or `vstackAudioPlayheadLine` on a\n * degenerate layout with an empty `systems` array). Only\n * `vstackAudioPlayheadLine` populates this (web-core 0.39.2), specifically\n * so a caller can detect a \"carriage return\" (a `sys` CHANGE frame-to-frame)\n * without re-deriving row membership itself — see\n * `notationCommon.ts`'s `createFollowController`, the consumer this was\n * added for.\n */\n sys?: 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 out through the last column of the active system, then pins it\n// to the next engraved onset while the camera brings that system in — so the\n// 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\n // Row (= printed SYSTEM) membership per column, resolved from the layout's own\n // system geometry (`layout.systems` — the same per-row boxes ALL THREE\n // extractors, canvas raster / OSMD-SVG / Verovio, already populate) via\n // `systemIndexOfBox` (nearest system band by vertical center; exact match when\n // inside a band). `-1` sentinel when no system geometry is available at all —\n // see `sameRow` below for the fallback this forces.\n const systems = layout.systems ?? [];\n const colSys: number[] = systems.length ? cols.map((m) => systemIndexOfBox(systems, m)) : [];\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, sys: colSys.length ? colSys[i] : -1 };\n };\n\n // \"Same printed row\" (same engraved SYSTEM), NOT a fixed y-tolerance.\n //\n // BUG FIX (web-core 0.39.1): this used to be a fixed 8px absolute-y tolerance\n // (`Math.abs(ya - yb) < 8`). Verovio-engraved layouts have intra-system\n // measure-column height/y variance up to ~16px — ties, accidentals, register,\n // AND (the most COMMON trigger in practice) a staff RESTING for a beat: a rest\n // yields a shorter/shifted measure-column bounding box (no notehead/stem\n // extending it), just like a bare chord vs. a tall ledger-line note does (see\n // stave-web-sightread's `.superpowers/sdd/one-clock-report.md` Part 3, and a\n // real confirmed instance: `Chopin_NocturneCsharpPosth`'s harmony chord-layer\n // render has a genuine RH rest — `.superpowers/verovio-audit/svg/\n // reduction-Chopin_NocturneCsharpPosth-playable.svg`, measure id `t49ii2d`,\n // rest id `n-0-8-1` — whose measure-column sits ~14px off its \"iiø7\" chord\n // neighbor at a representative production canvas scale, same order of\n // magnitude as the report's 8–16px spurious band). This variance, whatever its\n // source, exceeds 8px and got misclassified as a system boundary, firing the\n // full sweep-right/re-enter-left carriage-return animation MID-ROW (29\n // spurious vs. 11 real boundary events measured over a 589-sample\n // written-view run) — the user-visible \"runs through the line and comes back\n // round from the left.\"\n //\n // Fix: classify row membership from the LAYOUT's own system structure\n // (`colSys`, above) instead of a y epsilon — two columns are the \"same row\" iff\n // they resolve to the same system index, however much their raw box y/h differs\n // within that system. This is IMMUNE BY CONSTRUCTION to the variance's cause\n // (ties/accidentals/register/rests/anything else): it never diffs two columns'\n // y/h against each other or against a fixed epsilon at all — each column is\n // independently placed by `systemIndexOfBox` against its nearest/containing\n // system BAND, so no amount of shape variance within a system can flip the\n // classification as long as the column's vertical center stays inside that\n // system's real engraved extent (which it always does — engraving never\n // straddles two systems). A data-derived epsilon (e.g. half the median\n // inter-row gap) would still just be a bigger constant tuned to the LAST\n // dataset measured; it could not make the same construction-level guarantee\n // against a not-yet-measured variance source. This needed no new field on\n // `MeasureColumnBox`/`StaffMeasureBox`: `NotationLayout.systems` (the real\n // per-row system boxes each extractor already knows from its own source — OSMD\n // `MusicSystems` / Verovio `g.system`) was already reaching this function\n // unchanged, and `systemIndexOfBox` (used above by `measureSystemMap`) already\n // existed to map a box to its nearest system band.\n //\n // Falls back to the legacy fixed-8px tolerance only when `layout.systems` is\n // empty — `verovioNotationLayout` can yield non-empty `measures` with no\n // `g.system` elements on malformed/degenerate SVG input (its `systems` array is\n // built independently of the `measures.length` guard) — so a degenerate layout\n // still degrades to the old (imperfect but pre-existing, not a regression)\n // behaviour instead of collapsing every column into \"row 0\".\n const sameRow = (a: { rowY: number; sys: number }, b: { rowY: number; sys: number }) =>\n systems.length ? a.sys === b.sys : Math.abs(a.rowY - b.rowY) < 8;\n const rowRightEdge = (a: { rowY: number; sys: number }) =>\n Math.max(\n ...cols\n .filter((c, i) => (colSys.length ? colSys[i] === a.sys : Math.abs(c.y - a.rowY) < 8))\n .map((c) => c.x + c.w),\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 // `sys` — the CURRENT frame's system index (`PlayheadLine.sys`, 0.39.2):\n // whichever row `rowY`/`rowH` (below) actually land on this frame, i.e.\n // `a.sys` while still visually on the old row (same-row glide, or the\n // first half of a carriage-return sweep) and `b.sys` once the cursor has\n // re-entered on the new row (second half of the sweep). This is exactly\n // the frame where a caller's \"system CHANGED\" check should fire — the\n // moment the cursor visually appears on the next engraved onset,\n // not the moment the bracketing onset pair first straddles a boundary.\n let x: number, rowY: number, rowH: number, sys: number;\n if (sameRow(a, b)) {\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; sys = a.sys;\n } else {\n // System boundary → CARRIAGE RETURN: the first half of the gap sweeps OUT\n // to the current row's right edge. The second half rides the incoming row at\n // the next engraved onset. Do not sweep from the system's left edge: that\n // blank clef/key margin is not a musical position and looks like a phantom\n // jump to \"before bar 1\" before the cursor reaches the actual next bar.\n if (frac < 0.5) {\n const s = frac / 0.5;\n x = a.x + (rowRightEdge(a) - a.x) * s;\n rowY = a.rowY; rowH = a.rowH; sys = a.sys;\n } else {\n x = b.x;\n rowY = b.rowY; rowH = b.rowH; sys = b.sys;\n }\n }\n\n // Row ENVELOPE (0.39.4): the drawn height comes from the union of the\n // current system's column boxes, not the single current column's box.\n // Per-column boxes hug their own engraved content, so their y-extent\n // varies with register/rests/stems (the same 8–16px variance documented\n // at `sameRow` above) — and since the glide reads the LEFT anchor's box\n // per bracketing pair, the cursor's bottom end visibly bobbed at every\n // onset crossing (user report: \"sometimes bottoms out at the bass clef\n // bottom, sometimes goes higher\"). The 0.39.1 fix made row MEMBERSHIP\n // immune to that variance; this makes the drawn extent immune too:\n // constant within a system, changing only at real line breaks. The\n // per-column box remains the no-systems fallback (same degenerate-layout\n // rationale as `sameRow`'s own fallback).\n if (systems.length && sys >= 0) {\n const rowCols = cols.filter((_, i) => colSys[i] === sys);\n if (rowCols.length) {\n const envY = Math.min(...rowCols.map((c) => c.y));\n rowH = Math.max(...rowCols.map((c) => c.y + c.h)) - envY;\n rowY = envY;\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, sys };\n}\n"],"mappings":"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","names":["a","b"]}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
notationLayout,
|
|
8
8
|
vstackAudioPlayheadLine,
|
|
9
9
|
vstackFollowBox
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-HIELHWEZ.js";
|
|
11
11
|
import {
|
|
12
12
|
safeBox
|
|
13
13
|
} from "./chunk-HXTRNE74.js";
|
|
@@ -275,4 +275,4 @@ export {
|
|
|
275
275
|
notationFactory,
|
|
276
276
|
scrollCursorFactory
|
|
277
277
|
};
|
|
278
|
-
//# sourceMappingURL=chunk-
|
|
278
|
+
//# sourceMappingURL=chunk-UADJXSFM.js.map
|
package/dist/notationPlayer.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
getNotationEngraving,
|
|
3
3
|
notationFactory,
|
|
4
4
|
scrollCursorFactory
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-UADJXSFM.js";
|
|
6
6
|
import {
|
|
7
7
|
audioPlayheadLine,
|
|
8
8
|
distinctOnsets,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
measureCount,
|
|
11
11
|
notationLayout,
|
|
12
12
|
vstackAudioPlayheadLine
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-HIELHWEZ.js";
|
|
14
14
|
import {
|
|
15
15
|
safeBox
|
|
16
16
|
} from "./chunk-HXTRNE74.js";
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
createFollowController,
|
|
5
5
|
hasReachedProgrammaticTarget,
|
|
6
6
|
isWithinProgrammaticScroll
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-F4VYTGJQ.js";
|
|
8
8
|
import {
|
|
9
9
|
distinctOnsets,
|
|
10
10
|
hitTestMeasureAt,
|
|
11
11
|
vstackAudioPlayheadLine
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HIELHWEZ.js";
|
|
13
13
|
import "./chunk-HXTRNE74.js";
|
|
14
14
|
|
|
15
15
|
// src/notationPlayerSvg.ts
|
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
import { N as NotationLayout } from './notationGeometry-DqVBgL7F.js';
|
|
2
|
+
import { EngravedNote } from './notationPlayerSvg.js';
|
|
2
3
|
import './promo.js';
|
|
3
4
|
|
|
5
|
+
/** One `<note>`'s MODEL identity — the ground truth `EngravedNote`
|
|
6
|
+
* (notationPlayerSvg.ts) needs that Verovio's rendered SVG cannot supply on
|
|
7
|
+
* its own (a rendered `g.note`/`g.rest` carries geometry, not pitch/tie/
|
|
8
|
+
* duration semantics). Keyed by the note's stamped `id` in
|
|
9
|
+
* `notePositions()`'s return of `verovioEngravedNotes`
|
|
10
|
+
* (notationPlayerVerovio.ts). */
|
|
11
|
+
interface NoteModel {
|
|
12
|
+
/** `12*(octave+1) + stepSemitone + alter` — the standard MusicXML→MIDI
|
|
13
|
+
* conversion (the same formula stave-web-sightread's own
|
|
14
|
+
* `practice/probeInputs.ts:pitchMidi` uses — a universal formula, not app
|
|
15
|
+
* logic, so this is not an owned-layer violation to restate here). `null`
|
|
16
|
+
* for a rest or an `<unpitched>` note (no `<pitch>` child) — mirrors
|
|
17
|
+
* `EngravedNote.midi`'s own "null covers both" contract. */
|
|
18
|
+
midi: number | null;
|
|
19
|
+
/** Has a `<rest/>` child. */
|
|
20
|
+
isRest: boolean;
|
|
21
|
+
/** True for the STOP half of a tie — a direct `<tie type="stop">` child OR
|
|
22
|
+
* `<notations><tied type="stop">` (exporters vary on which they emit;
|
|
23
|
+
* either counts) — matches `EngravedNote.tieContinuation`'s "continuation
|
|
24
|
+
* note of a tie, not the struck start" contract. */
|
|
25
|
+
tieContinuation: boolean;
|
|
26
|
+
/** 0-based, matching `EngravedNote.staffIndex`'s "0 = top staff of the
|
|
27
|
+
* system" contract: every `<part>` is walked in document order, and
|
|
28
|
+
* every DISTINCT staff within it (by `<attributes><staves>` when
|
|
29
|
+
* present, else the highest `<staff>` number any of its notes uses, else
|
|
30
|
+
* 1) is assigned the next index — so a single-part 2-staff piano score
|
|
31
|
+
* numbers 0/1 by `<staff>`, and a 2-part 1-staff-per-part reduction (this
|
|
32
|
+
* app's own chord+bass shape) numbers 0/1 by PART, with neither case
|
|
33
|
+
* needing different code. */
|
|
34
|
+
staffIndex: number;
|
|
35
|
+
/** Whole-note fraction (0.25 = quarter) — `duration / divisions / 4`,
|
|
36
|
+
* divisions tracked per-part from the LAST `<attributes><divisions>`
|
|
37
|
+
* seen at or before this note (MusicXML: divisions persist until
|
|
38
|
+
* overridden, default 1). 0 for a grace note (no `<duration>` child —
|
|
39
|
+
* the true, spec-correct signal; never guessed from `<type>`). */
|
|
40
|
+
durationReal: number;
|
|
41
|
+
/** 0-based position of this note's `<measure>` among its OWN `<part>`'s
|
|
42
|
+
* measure children, in document order. Informational only — the live
|
|
43
|
+
* join (`verovioOnsetColumns`/`verovioEngravedNotes`, both in
|
|
44
|
+
* notationPlayerVerovio.ts) resolves the note's RENDERED measure index
|
|
45
|
+
* from the live SVG DOM independently (Verovio's own render order, which
|
|
46
|
+
* is what geometry/hit-testing must agree with), never from this field. */
|
|
47
|
+
measureIndex: number;
|
|
48
|
+
/** `print-object="no"` on the source `<note>` (stave's `hideDoubledNotes`
|
|
49
|
+
* sets this on editorially-doubled notes/rests before handing MusicXML to
|
|
50
|
+
* this player). Verovio's importer HONORS this for `<note>` elements
|
|
51
|
+
* carrying a `<pitch>` (renders `visibility="hidden"` on its own,
|
|
52
|
+
* empirically confirmed against a real 6.2.0 render) but does NOT honor
|
|
53
|
+
* it for `<rest>` notes (the `<g class="rest">` renders fully visible
|
|
54
|
+
* regardless — same empirical check). `createVerovioNotationPlayer`
|
|
55
|
+
* reads this field to force `visibility="hidden"` after every render for
|
|
56
|
+
* ANY id where it's true — a no-op re-application on notes Verovio
|
|
57
|
+
* already hid, and the actual fix on the rests it doesn't (see that
|
|
58
|
+
* module's `applyPrintObjectHiding`). */
|
|
59
|
+
hidden: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Restated independently, not imported as a VALUE from notationPlayerSvg.ts
|
|
63
|
+
* — same reasoning as `MAX_ENGRAVE_WIDTH_VRV` vs `MAX_ENGRAVE_WIDTH_SVG`
|
|
64
|
+
* (below): a runtime (non-`type`) import from notationPlayerSvg.ts would
|
|
65
|
+
* pull that module's ENTIRE implementation — including its own
|
|
66
|
+
* `createSvgNotationPlayer` closure (harmless at runtime, since its OSMD
|
|
67
|
+
* import stays lazy/dynamic either way) — into this entry's own tsup
|
|
68
|
+
* chunk graph, which is exactly the cross-entry bundle coupling the
|
|
69
|
+
* module doc's "IMPORT PATH" section (and the build report's dist-grep
|
|
70
|
+
* gate) exists to prevent between the two SVG-family players. Must stay
|
|
71
|
+
* byte-identical to `notationPlayerSvg.ts`'s own `MARKED_NOTE_CLASS` — a
|
|
72
|
+
* test in this file's own suite pins that. */
|
|
73
|
+
declare const MARKED_NOTE_CLASS = "rmp-note-marked";
|
|
4
74
|
/** One distinct note-onset instant: the audio-clock time it sounds at, and
|
|
5
75
|
* the stamped `xml:id`s (Task 1, stave repo) of every note that sounds at
|
|
6
76
|
* that instant (>1 for a chord). Multiple entries sharing the same `tMs`
|
|
@@ -47,6 +117,24 @@ interface CreateVerovioNotationPlayerOpts {
|
|
|
47
117
|
* /width but perform no real re-engrave (there is nothing to re-engrave).
|
|
48
118
|
*/
|
|
49
119
|
rendered?: NotationLayout;
|
|
120
|
+
/**
|
|
121
|
+
* Narrow passthrough matching `CreateSvgNotationPlayerOpts.osmdOptions`'s
|
|
122
|
+
* shape exactly, so a caller driving BOTH players behind one interface
|
|
123
|
+
* (the swap this player exists for) never has to branch per backend.
|
|
124
|
+
* `autoBeam` has NO Verovio equivalent — Verovio always beams straight
|
|
125
|
+
* from the MusicXML's own `<beam>` elements (it has no "re-beam from
|
|
126
|
+
* scratch" pass the way OSMD's `autoBeam` option does) — so this is
|
|
127
|
+
* accepted and silently ignored, with a ONE-TIME `console.warn` (module-
|
|
128
|
+
* level, not per-instance — a caller that builds many players with the
|
|
129
|
+
* same options object should not get spammed) rather than a hard error,
|
|
130
|
+
* since ignoring it is genuinely harmless: synthesized rhythm XML with no
|
|
131
|
+
* `<beam>` data renders unbeamed either way, which is a cosmetic
|
|
132
|
+
* difference the caller can fix upstream (emit real `<beam>` elements)
|
|
133
|
+
* rather than this player faking OSMD's re-beam heuristic.
|
|
134
|
+
*/
|
|
135
|
+
osmdOptions?: {
|
|
136
|
+
autoBeam?: boolean;
|
|
137
|
+
};
|
|
50
138
|
}
|
|
51
139
|
interface VerovioNotationPlayer {
|
|
52
140
|
/** Resolves once the engraving is in the DOM and ready to draw. `setTime`/
|
|
@@ -73,6 +161,26 @@ interface VerovioNotationPlayer {
|
|
|
73
161
|
/** Register a measure-click handler (measure index, matching the DOM-order
|
|
74
162
|
* index `verovioNotationLayout` assigns). Returns an unsubscribe fn. */
|
|
75
163
|
onMeasureClick(cb: (measureIndex: number) => void): () => void;
|
|
164
|
+
/**
|
|
165
|
+
* Mark the engraved noteheads/rests nearest the given columns (same
|
|
166
|
+
* `measureIndex + fraction-through-the-measure` units as `onsets`'
|
|
167
|
+
* derived columns); pass `null` or `[]` to clear. Same contract and CSS
|
|
168
|
+
* class (`MARKED_NOTE_CLASS` = `'rmp-note-marked'`) as
|
|
169
|
+
* `SvgNotationPlayer.markNotes` — a consumer's existing CSS keeps working
|
|
170
|
+
* unmodified across a backend swap. Survives re-engraving: reapplied
|
|
171
|
+
* after every `setZoom`/`resize`.
|
|
172
|
+
*/
|
|
173
|
+
markNotes(cols: number[] | null): void;
|
|
174
|
+
/** Every engraved note/rest with MODEL identity (pitch, rest, tie, staff)
|
|
175
|
+
* and host-relative notehead position — same `EngravedNote` shape
|
|
176
|
+
* `SvgNotationPlayer.notePositions()` returns (re-exported from this
|
|
177
|
+
* module, not redefined). `headEl` is Verovio's rendered `.notehead`
|
|
178
|
+
* sub-group when present (a tighter box than the outer `g.note`, closer
|
|
179
|
+
* in spirit to OSMD's `.vf-notehead`), else the outer `g.note`/`g.rest`
|
|
180
|
+
* group. `[]` before the initial engrave resolves or under the
|
|
181
|
+
* `rendered` test seam (no live SVG to join against — same graceful
|
|
182
|
+
* degradation as `verovioOnsetColumns`). */
|
|
183
|
+
notePositions(): EngravedNote[];
|
|
76
184
|
/** Tear down: removes the mounted DOM (engraving + playhead overlay) from
|
|
77
185
|
* `host`, and drops every listener this instance added (click, window
|
|
78
186
|
* scroll) and pending async work (a token guard drops any in-flight
|
|
@@ -115,6 +223,72 @@ declare function verovioNotationLayout(root: Element): NotationLayout;
|
|
|
115
223
|
* only when there is no usable layout/DOM at all to resolve against.
|
|
116
224
|
*/
|
|
117
225
|
declare function verovioOnsetColumns(root: Element, layout: NotationLayout, onsets: VerovioOnset[]): number[] | undefined;
|
|
226
|
+
/**
|
|
227
|
+
* Every rendered note/rest joined to its MODEL identity (`noteModel`, from
|
|
228
|
+
* `noteModelFromXml` — ./notationXml.ts) by stamped id — this player's
|
|
229
|
+
* `notePositions()`. Same shape and host-relative-px contract as
|
|
230
|
+
* notationPlayerSvg.ts's `engravedNotes` (its own doc: "Positions are px
|
|
231
|
+
* relative to the HOST element, at the notehead's center"), but the join
|
|
232
|
+
* itself is simpler here: unlike OSMD (one graphical group per CHORD,
|
|
233
|
+
* requiring the pitch-rank head-sorting `engravedNotes` does), Verovio
|
|
234
|
+
* renders every chord member as its OWN `<g id="..." class="note">`
|
|
235
|
+
* (empirically confirmed — see the module doc's point 2) — so this is a
|
|
236
|
+
* flat id→element→model join, no grouping/sorting.
|
|
237
|
+
*
|
|
238
|
+
* `headEl` prefers the note's own `.notehead` child (Verovio's real nested
|
|
239
|
+
* glyph group — empirically confirmed as `<g class="notehead">` inside
|
|
240
|
+
* `<g class="note">`) over the outer `g.note`/`g.rest` wrapper when
|
|
241
|
+
* present — a TIGHTER box (closer to notationPlayerSvg.ts's
|
|
242
|
+
* `.vf-notehead`-only box) than the wrapper, which also spans the
|
|
243
|
+
* stem/flag/accidental. Falls back to the wrapper itself for a rest (no
|
|
244
|
+
* `.notehead` child) or if that lookup's own `getBBox` fails.
|
|
245
|
+
*
|
|
246
|
+
* `root` is the container holding every rendered `.vrv-page` (this
|
|
247
|
+
* player's `svgHost`); `host` is the player's OWN host element — `x`/`y`
|
|
248
|
+
* are computed relative to `host` (per `EngravedNote`'s contract), NOT to
|
|
249
|
+
* `root`, which may itself sit offset within `host`. Never throws; returns
|
|
250
|
+
* `[]` for any missing/malformed structure (same defensive style as
|
|
251
|
+
* `verovioNotationLayout`).
|
|
252
|
+
*/
|
|
253
|
+
declare function verovioEngravedNotes(root: Element, host: HTMLElement, noteModel: Map<string, NoteModel>): EngravedNote[];
|
|
254
|
+
/**
|
|
255
|
+
* Every rendered note/rest nearest engraved column `col`
|
|
256
|
+
* (`measureIndex + fraction`, same units as `markNotes`' argument and
|
|
257
|
+
* `noteCols`), one per staff of that measure — the Verovio-backend
|
|
258
|
+
* counterpart to notationPlayerSvg.ts's `graphicalNotesAtColumn`. That
|
|
259
|
+
* function searches OSMD's object model (`relInMeasureTimestamp` vs bar
|
|
260
|
+
* duration); this searches the live rendered geometry instead (Verovio
|
|
261
|
+
* exposes no per-element timestamp) — same "nearest entry by position gap,
|
|
262
|
+
* search every staff of the bar" shape, just sourced from `getBBox` instead
|
|
263
|
+
* of a timeline field. The fractional-position FORMULA itself
|
|
264
|
+
* (`(box.x - sx) / denom`) is the exact one `verovioOnsetColumns` already
|
|
265
|
+
* uses (not re-derived) — this function runs it in the opposite direction
|
|
266
|
+
* (nearest note TO a column, rather than a column FROM a note id).
|
|
267
|
+
*/
|
|
268
|
+
declare function verovioNotesAtColumn(root: Element, layout: NotationLayout, col: number): Element[];
|
|
269
|
+
/**
|
|
270
|
+
* CRITICAL empirical finding (verified against a real 6.2.0 render — see
|
|
271
|
+
* the migration spike's follow-up, `.superpowers/…/print-object-test.mjs`
|
|
272
|
+
* equivalent run for this task): Verovio's MusicXML importer HONORS
|
|
273
|
+
* `print-object="no"` for a pitched `<note>` (renders `visibility="hidden"`
|
|
274
|
+
* on its own `<g class="note">`) but does NOT honor it for a `<note><rest/>`
|
|
275
|
+
* — the `<g class="rest">` renders fully visible regardless. This matters
|
|
276
|
+
* because stave-web-sightread's `hideDoubledNotes` feature sets
|
|
277
|
+
* `print-object="no"` on BOTH doubled notes AND the rests of a voice that
|
|
278
|
+
* lost every visible note (see that function's own "second pass" doc) —
|
|
279
|
+
* without this fix, a hidden-doubled-note's voice would still show a
|
|
280
|
+
* floating rest, exactly the "extra voice" clutter that feature exists to
|
|
281
|
+
* remove.
|
|
282
|
+
*
|
|
283
|
+
* Fix: force `visibility="hidden"` on every rendered id whose model says
|
|
284
|
+
* `hidden` (`NoteModel.hidden`, from `noteModelFromXml`). Re-applying it to
|
|
285
|
+
* a NOTE Verovio already hid itself is a harmless no-op; applying it to a
|
|
286
|
+
* REST is the actual fix. Call after every render (`renderAllPages`) — a
|
|
287
|
+
* fresh render is a fresh DOM, so a prior call's effect never carries over
|
|
288
|
+
* (nothing to "undo" on a note/rest that's no longer print-object="no").
|
|
289
|
+
* Never throws.
|
|
290
|
+
*/
|
|
291
|
+
declare function applyPrintObjectHiding(root: Element, noteModel: Map<string, NoteModel>): void;
|
|
118
292
|
/** Verovio glyph-size percent at semantic zoom 1 (matches the migration
|
|
119
293
|
* spike's own default — a normal, readable size at typical host widths). */
|
|
120
294
|
declare const VEROVIO_BASE_SCALE = 40;
|
|
@@ -169,4 +343,4 @@ declare const MAX_ENGRAVE_WIDTH_VRV = 1200;
|
|
|
169
343
|
* (in stave-web-sightread) §2 for the full design. */
|
|
170
344
|
declare function createVerovioNotationPlayer(opts: CreateVerovioNotationPlayerOpts): VerovioNotationPlayer;
|
|
171
345
|
|
|
172
|
-
export { type CreateVerovioNotationPlayerOpts, MAX_ENGRAVE_WIDTH_VRV, VEROVIO_BASE_SCALE, VEROVIO_MAX_SCALE, VEROVIO_MIN_SCALE, type VerovioNotationPlayer, type VerovioOnset, type VerovioRenderOptions, createVerovioNotationPlayer, verovioNotationLayout, verovioOnsetColumns, verovioZoomOptions };
|
|
346
|
+
export { type CreateVerovioNotationPlayerOpts, EngravedNote, MARKED_NOTE_CLASS, MAX_ENGRAVE_WIDTH_VRV, VEROVIO_BASE_SCALE, VEROVIO_MAX_SCALE, VEROVIO_MIN_SCALE, type VerovioNotationPlayer, type VerovioOnset, type VerovioRenderOptions, applyPrintObjectHiding, createVerovioNotationPlayer, verovioEngravedNotes, verovioNotationLayout, verovioNotesAtColumn, verovioOnsetColumns, verovioZoomOptions };
|