@real-music-packages/web-core 0.36.2 → 0.38.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.
@@ -242,6 +242,25 @@ function measureColumnsFromLayout(measures) {
242
242
  }
243
243
  return [...byIndex.keys()].sort((a, b) => a - b).map((k) => byIndex.get(k));
244
244
  }
245
+ function hitTestMeasureAt(layout, mx, my) {
246
+ if (!layout.measures.length) return null;
247
+ const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);
248
+ const cols = measureColumnsFromLayout(layout.measures);
249
+ let nearest = null;
250
+ let nearestD = Infinity;
251
+ for (let i = 0; i < cols.length; i++) {
252
+ const b = cols[i];
253
+ if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];
254
+ const cx = b.x + b.w / 2;
255
+ const cy = b.y + b.h / 2;
256
+ const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);
257
+ if (d < nearestD) {
258
+ nearestD = d;
259
+ nearest = indices[i];
260
+ }
261
+ }
262
+ return nearest;
263
+ }
245
264
  function playheadLine(layout, t01) {
246
265
  const tt = Math.max(0, Math.min(1, t01));
247
266
  let alpha = 0.85;
@@ -427,262 +446,6 @@ function vstackAudioPlayheadLine(layout, onsetsMs, tMs, nBars, noteCols) {
427
446
  return { x, y0, y1, alpha };
428
447
  }
429
448
 
430
- // src/scene/engravingStore.ts
431
- var STORE = /* @__PURE__ */ new WeakMap();
432
- function keyFor(ctx) {
433
- return ctx.audioClock;
434
- }
435
- function setNotationEngraving(ctx, eng) {
436
- STORE.set(keyFor(ctx), eng);
437
- }
438
- function getNotationEngraving(ctx) {
439
- return STORE.get(keyFor(ctx));
440
- }
441
- function setFollowLayoutProvider(ctx, fn) {
442
- const e = STORE.get(keyFor(ctx));
443
- if (e) e.followLayoutAt = fn;
444
- }
445
-
446
- // src/scene/layers/notation.ts
447
- function notationLayer() {
448
- let rn = null;
449
- let base = null;
450
- let propScale = 1;
451
- let propBandTop;
452
- let propBandHeight;
453
- let propBandWidth;
454
- function bandTop(_ctx, sb) {
455
- return propBandTop ?? sb.top;
456
- }
457
- function bandHeight(ctx, sb) {
458
- const top = bandTop(ctx, sb);
459
- return (propBandHeight ?? sb.bottom - top) * propScale;
460
- }
461
- return {
462
- key: "notation",
463
- async init(ctx, props) {
464
- propScale = props.scale ?? 1;
465
- propBandTop = props.bandTop;
466
- propBandHeight = props.bandHeight;
467
- propBandWidth = props.bandWidth;
468
- if (props.rendered) {
469
- rn = props.rendered;
470
- } else if (props.xml) {
471
- const { renderNotation } = await import("./promo.js");
472
- rn = await renderNotation(props.xml, {
473
- bars: props.bars,
474
- paper: ctx.theme.paper,
475
- scrollMode: props.scrollMode ?? "hstack",
476
- hostWidth: props.hostWidth
477
- });
478
- } else {
479
- throw new Error("notation layer: provide `rendered` or `xml`");
480
- }
481
- const sb = safeBox(ctx.W, ctx.H);
482
- const top = bandTop(ctx, sb);
483
- const height = bandHeight(ctx, sb);
484
- base = notationLayout(rn, ctx.W, ctx.H, top, height, { boxWidth: propBandWidth });
485
- setNotationEngraving(ctx, { rendered: rn, base, bandTop: top, bandHeight: height, bandWidth: propBandWidth });
486
- },
487
- draw(ctx, tMs) {
488
- if (!rn || !base) return;
489
- const eng = getNotationEngraving(ctx);
490
- const l = eng?.followLayoutAt ? eng.followLayoutAt(ctx, tMs) : base;
491
- const c = ctx.ctx2d;
492
- c.drawImage(
493
- rn.canvas,
494
- l.src.x,
495
- l.src.y,
496
- l.src.w,
497
- l.src.h,
498
- l.rect.dx,
499
- l.rect.dy,
500
- l.rect.dw,
501
- l.rect.dh
502
- );
503
- },
504
- dispose() {
505
- rn = null;
506
- base = null;
507
- }
508
- };
509
- }
510
- var notationFactory = {
511
- key: "notation",
512
- create: notationLayer,
513
- validateProps(props) {
514
- const errs = [];
515
- if (props == null || typeof props !== "object") return ["notation: props must be an object"];
516
- const p = props;
517
- if (p.system != null && p.system !== "grand" && p.system !== "single")
518
- errs.push('notation.system must be "grand" | "single"');
519
- if (p.scrollMode != null && p.scrollMode !== "hstack" && p.scrollMode !== "vstack")
520
- errs.push('notation.scrollMode must be "hstack" | "vstack"');
521
- if (p.scale != null && (typeof p.scale !== "number" || p.scale <= 0))
522
- errs.push("notation.scale must be a positive number");
523
- if (p.rendered == null && typeof p.xml !== "string")
524
- errs.push("notation: provide `rendered` (RenderedNotation) or `xml` (string)");
525
- if (p.bars != null && (!Array.isArray(p.bars) || p.bars.length !== 2))
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");
531
- return errs;
532
- }
533
- };
534
-
535
- // src/scene/layers/scrollCursor.ts
536
- function followLayoutFor(ctx, progress01, scrollMode) {
537
- const eng = getNotationEngraving(ctx);
538
- if (!eng) return null;
539
- const nBars = measureCount(eng.rendered);
540
- if (nBars <= 0) return eng.base;
541
- const focusBox = scrollMode === "vstack" ? vstackFollowBox(eng.rendered, progress01) : followBoxAt(eng.rendered, followWindowStart(eng.rendered, progress01));
542
- return notationLayout(eng.rendered, ctx.W, ctx.H, eng.bandTop, eng.bandHeight, { focusBox, boxWidth: eng.bandWidth });
543
- }
544
- function scrollCursorLayer() {
545
- let scrollMode = "hstack";
546
- let openingZoomMs = 900;
547
- let color;
548
- let barDurMs;
549
- let propNoteCols;
550
- function onsetsFor(ctx) {
551
- const notes = ctx.score?.notes;
552
- return notes && notes.length ? distinctOnsets(notes) : [];
553
- }
554
- function notesProgress(onsetsMs, tMs, totalMeasures, noteCols) {
555
- if (tMs < openingZoomMs) return 0;
556
- const n = onsetsMs.length;
557
- if (n <= 1) return 0;
558
- const first = onsetsMs[0];
559
- const last = onsetsMs[n - 1];
560
- if (tMs <= first) return 0;
561
- if (tMs >= last || last <= first) return 1;
562
- let lo = 0, hi = n - 1;
563
- while (lo < hi) {
564
- const mid = lo + hi + 1 >> 1;
565
- if (onsetsMs[mid] <= tMs) lo = mid;
566
- else hi = mid - 1;
567
- }
568
- const segFrac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);
569
- if (noteCols && noteCols.length === n && totalMeasures > 0) {
570
- const pos = noteCols[lo] + (noteCols[lo + 1] - noteCols[lo]) * segFrac;
571
- return Math.min(1, Math.max(0, pos / totalMeasures));
572
- }
573
- if (barDurMs && barDurMs > 0 && totalMeasures > 0) {
574
- const posLo = (onsetsMs[lo] - first) / barDurMs;
575
- const posHi = (onsetsMs[lo + 1] - first) / barDurMs;
576
- const pos = posLo + (posHi - posLo) * segFrac;
577
- return Math.min(1, Math.max(0, pos / totalMeasures));
578
- }
579
- return (lo + segFrac) / (n - 1);
580
- }
581
- function noteColsFor(ctx) {
582
- const eng = getNotationEngraving(ctx)?.rendered?.noteCols;
583
- const onsets = onsetsFor(ctx);
584
- const n = onsets.length;
585
- if (!eng || !eng.length) return propNoteCols;
586
- if (eng.length === n) return eng;
587
- if (!propNoteCols || propNoteCols.length !== n) return eng;
588
- const measureOf = (c) => Math.floor(c + 1e-6);
589
- const engByMeasure = /* @__PURE__ */ new Map();
590
- for (const e of eng) {
591
- const m = measureOf(e);
592
- const a = engByMeasure.get(m);
593
- if (a) a.push(e);
594
- else engByMeasure.set(m, [e]);
595
- }
596
- for (const a of engByMeasure.values()) a.sort((x, y) => x - y);
597
- const idxByMeasure = /* @__PURE__ */ new Map();
598
- propNoteCols.forEach((c, i) => {
599
- const m = measureOf(c);
600
- const a = idxByMeasure.get(m);
601
- if (a) a.push(i);
602
- else idxByMeasure.set(m, [i]);
603
- });
604
- const out = propNoteCols.slice();
605
- for (const [m, idxs] of idxByMeasure) {
606
- const e = engByMeasure.get(m);
607
- if (!e || !e.length) continue;
608
- const K = idxs.length;
609
- idxs.forEach((origIdx, i) => {
610
- const j = K > 1 ? Math.round(i * (e.length - 1) / (K - 1)) : 0;
611
- out[origIdx] = e[Math.min(j, e.length - 1)];
612
- });
613
- }
614
- return out;
615
- }
616
- function followProgress(ctx, tMs) {
617
- const eng = getNotationEngraving(ctx);
618
- const total = eng ? measureCount(eng.rendered) : 0;
619
- return notesProgress(onsetsFor(ctx), tMs, total, noteColsFor(ctx));
620
- }
621
- function layoutAt(ctx, tMs) {
622
- const eng = getNotationEngraving(ctx);
623
- return followLayoutFor(ctx, followProgress(ctx, tMs), scrollMode) ?? eng.base;
624
- }
625
- return {
626
- key: "scroll-cursor",
627
- init(ctx, props) {
628
- scrollMode = props.scrollMode ?? "hstack";
629
- openingZoomMs = props.openingZoomMs ?? 900;
630
- color = props.color;
631
- barDurMs = props.barDurMs;
632
- propNoteCols = Array.isArray(props.noteCols) ? props.noteCols : void 0;
633
- setFollowLayoutProvider(ctx, layoutAt);
634
- },
635
- draw(ctx, tMs) {
636
- const eng = getNotationEngraving(ctx);
637
- if (!eng) return;
638
- const layout = layoutAt(ctx, tMs);
639
- const onsets = onsetsFor(ctx);
640
- let line;
641
- if (scrollMode === "vstack") {
642
- line = vstackAudioPlayheadLine(layout, onsets, tMs, measureCount(eng.rendered), noteColsFor(ctx));
643
- } else {
644
- line = audioPlayheadLine(layout, onsets, tMs, barDurMs, noteColsFor(ctx));
645
- }
646
- if (!line) return;
647
- const c = ctx.ctx2d;
648
- c.save();
649
- c.strokeStyle = color ?? ctx.theme.accent;
650
- c.globalAlpha = line.alpha;
651
- c.lineWidth = 4;
652
- c.beginPath();
653
- c.moveTo(line.x, line.y0);
654
- c.lineTo(line.x, line.y1);
655
- c.stroke();
656
- c.restore();
657
- }
658
- };
659
- }
660
- var scrollCursorFactory = {
661
- key: "scroll-cursor",
662
- create: scrollCursorLayer,
663
- validateProps(props) {
664
- const errs = [];
665
- if (props == null || typeof props !== "object") return ["scroll-cursor: props must be an object"];
666
- const p = props;
667
- if (p.mode != null && p.mode !== "audio" && p.mode !== "linear")
668
- errs.push('scroll-cursor.mode must be "audio" | "linear"');
669
- if (p.scrollMode != null && p.scrollMode !== "hstack" && p.scrollMode !== "vstack")
670
- errs.push('scroll-cursor.scrollMode must be "hstack" | "vstack"');
671
- if (p.musicMs != null && (typeof p.musicMs !== "number" || !(p.musicMs > 0)))
672
- errs.push("scroll-cursor.musicMs must be a positive number");
673
- if (p.followBars != null && (typeof p.followBars !== "number" || p.followBars < 1))
674
- errs.push("scroll-cursor.followBars must be a number >= 1");
675
- if (p.openingZoomMs != null && (typeof p.openingZoomMs !== "number" || p.openingZoomMs < 0))
676
- errs.push("scroll-cursor.openingZoomMs must be a number >= 0");
677
- if (p.color != null && typeof p.color !== "string") errs.push("scroll-cursor.color must be a string");
678
- if (p.barDurMs != null && (typeof p.barDurMs !== "number" || !(p.barDurMs > 0)))
679
- errs.push("scroll-cursor.barDurMs must be a positive number");
680
- if (p.noteCols != null && (!Array.isArray(p.noteCols) || p.noteCols.some((c) => typeof c !== "number")))
681
- errs.push("scroll-cursor.noteCols must be a number[]");
682
- return errs;
683
- }
684
- };
685
-
686
449
  export {
687
450
  FOLLOW_BARS,
688
451
  FOLLOW_PAD,
@@ -700,14 +463,10 @@ export {
700
463
  vstackFollowBox,
701
464
  notationLayout,
702
465
  measureColumnsFromLayout,
466
+ hitTestMeasureAt,
703
467
  playheadLine,
704
468
  distinctOnsets,
705
469
  audioPlayheadLine,
706
- vstackAudioPlayheadLine,
707
- setNotationEngraving,
708
- getNotationEngraving,
709
- setFollowLayoutProvider,
710
- notationFactory,
711
- scrollCursorFactory
470
+ vstackAudioPlayheadLine
712
471
  };
713
- //# sourceMappingURL=chunk-4565POLG.js.map
472
+ //# sourceMappingURL=chunk-BHRDISMU.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. */\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/**\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\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"],"mappings":"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","names":["rowY"]}