@real-music-packages/web-core 0.35.0 → 0.36.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-VPC2EKEY.js +706 -0
- package/dist/chunk-VPC2EKEY.js.map +1 -0
- package/dist/notationGeometry-CZJ0U6PQ.d.ts +135 -0
- package/dist/notationPlayer.d.ts +158 -0
- package/dist/notationPlayer.js +186 -0
- package/dist/notationPlayer.js.map +1 -0
- package/dist/scene/index.d.ts +4 -134
- package/dist/scene/index.js +46 -693
- package/dist/scene/index.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getNotationEngraving,
|
|
3
|
+
measureColumnsFromLayout,
|
|
4
|
+
notationFactory,
|
|
5
|
+
scrollCursorFactory
|
|
6
|
+
} from "./chunk-VPC2EKEY.js";
|
|
7
|
+
import {
|
|
8
|
+
safeBox
|
|
9
|
+
} from "./chunk-HXTRNE74.js";
|
|
10
|
+
|
|
11
|
+
// src/notationPlayer.ts
|
|
12
|
+
var DEFAULT_THEME = {
|
|
13
|
+
paper: "#faf7f0",
|
|
14
|
+
ink: "#1a1614",
|
|
15
|
+
accent: "#7b2436",
|
|
16
|
+
sepia: "#6d5d4d",
|
|
17
|
+
gold: "#c8a55b",
|
|
18
|
+
fontDisplay: "Georgia, serif",
|
|
19
|
+
fontBody: "Georgia, serif",
|
|
20
|
+
brand: ""
|
|
21
|
+
};
|
|
22
|
+
function scoreFromOnsets(onsetsMs) {
|
|
23
|
+
const notes = onsetsMs.map((onsetMs) => ({
|
|
24
|
+
pitchMidi: 60,
|
|
25
|
+
step: "C",
|
|
26
|
+
alter: 0,
|
|
27
|
+
octave: 4,
|
|
28
|
+
onsetMs,
|
|
29
|
+
durMs: 0,
|
|
30
|
+
staff: 0,
|
|
31
|
+
voice: 0,
|
|
32
|
+
hand: "R",
|
|
33
|
+
measure: 1
|
|
34
|
+
}));
|
|
35
|
+
const durationMs = onsetsMs.length ? Math.max(...onsetsMs) : 0;
|
|
36
|
+
return {
|
|
37
|
+
notes,
|
|
38
|
+
tempoMap: { source: "fallback", segments: [{ atMs: 0, bpm: 120 }] },
|
|
39
|
+
durationMs
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function hitTestMeasureAt(layout, mx, my) {
|
|
43
|
+
if (!layout.measures.length) return null;
|
|
44
|
+
const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);
|
|
45
|
+
const cols = measureColumnsFromLayout(layout.measures);
|
|
46
|
+
let nearest = null;
|
|
47
|
+
let nearestD = Infinity;
|
|
48
|
+
for (let i = 0; i < cols.length; i++) {
|
|
49
|
+
const b = cols[i];
|
|
50
|
+
if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];
|
|
51
|
+
const cx = b.x + b.w / 2;
|
|
52
|
+
const cy = b.y + b.h / 2;
|
|
53
|
+
const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);
|
|
54
|
+
if (d < nearestD) {
|
|
55
|
+
nearestD = d;
|
|
56
|
+
nearest = indices[i];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return nearest;
|
|
60
|
+
}
|
|
61
|
+
function createNotationPlayer(opts) {
|
|
62
|
+
const { host, musicXml, onsetsMs, noteCols, barDurMs, bars } = opts;
|
|
63
|
+
const mode = opts.mode ?? "vstack";
|
|
64
|
+
const theme = { ...DEFAULT_THEME, ...opts.theme };
|
|
65
|
+
const canvas = document.createElement("canvas");
|
|
66
|
+
canvas.style.display = "block";
|
|
67
|
+
canvas.style.width = "100%";
|
|
68
|
+
canvas.style.height = "100%";
|
|
69
|
+
host.appendChild(canvas);
|
|
70
|
+
const dpr = typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1;
|
|
71
|
+
function frameSize() {
|
|
72
|
+
if (opts.size) return opts.size;
|
|
73
|
+
const w = Math.max(1, Math.round((host.clientWidth || 1) * dpr));
|
|
74
|
+
const h = Math.max(1, Math.round((host.clientHeight || 1) * dpr));
|
|
75
|
+
return [w, h];
|
|
76
|
+
}
|
|
77
|
+
const [initW, initH] = frameSize();
|
|
78
|
+
canvas.width = initW;
|
|
79
|
+
canvas.height = initH;
|
|
80
|
+
const ctx2d = canvas.getContext("2d");
|
|
81
|
+
if (!ctx2d) throw new Error("createNotationPlayer: 2D canvas context unavailable");
|
|
82
|
+
let lastTMs = 0;
|
|
83
|
+
const audioClock = { nowMs: () => lastTMs };
|
|
84
|
+
const rctx = {
|
|
85
|
+
ctx2d,
|
|
86
|
+
W: initW,
|
|
87
|
+
H: initH,
|
|
88
|
+
score: scoreFromOnsets(onsetsMs),
|
|
89
|
+
audioClock,
|
|
90
|
+
theme,
|
|
91
|
+
safeBox: safeBox(initW, initH),
|
|
92
|
+
fps: 30
|
|
93
|
+
};
|
|
94
|
+
const notation = notationFactory.create();
|
|
95
|
+
const scrollCursor = scrollCursorFactory.create();
|
|
96
|
+
function notationProps(rendered) {
|
|
97
|
+
return {
|
|
98
|
+
...rendered ? { rendered } : { xml: musicXml },
|
|
99
|
+
scrollMode: mode,
|
|
100
|
+
bars,
|
|
101
|
+
bandTop: opts.bandTop ?? 0,
|
|
102
|
+
bandHeight: opts.bandHeight ?? rctx.H
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const scrollCursorProps = { scrollMode: mode, barDurMs, noteCols };
|
|
106
|
+
let renderedRn = null;
|
|
107
|
+
let destroyed = false;
|
|
108
|
+
const ready = (async () => {
|
|
109
|
+
await notation.init(rctx, notationProps(opts.rendered));
|
|
110
|
+
renderedRn = getNotationEngraving(rctx)?.rendered ?? null;
|
|
111
|
+
await scrollCursor.init(rctx, scrollCursorProps);
|
|
112
|
+
})();
|
|
113
|
+
function clear() {
|
|
114
|
+
ctx2d.save();
|
|
115
|
+
ctx2d.setTransform(1, 0, 0, 1, 0, 0);
|
|
116
|
+
ctx2d.fillStyle = theme.paper;
|
|
117
|
+
ctx2d.fillRect(0, 0, canvas.width, canvas.height);
|
|
118
|
+
ctx2d.restore();
|
|
119
|
+
}
|
|
120
|
+
function setTime(tMs) {
|
|
121
|
+
if (destroyed) return;
|
|
122
|
+
lastTMs = tMs;
|
|
123
|
+
clear();
|
|
124
|
+
notation.draw(rctx, tMs);
|
|
125
|
+
scrollCursor.draw(rctx, tMs);
|
|
126
|
+
}
|
|
127
|
+
async function performResize() {
|
|
128
|
+
const [w, h] = frameSize();
|
|
129
|
+
if (w === canvas.width && h === canvas.height) return;
|
|
130
|
+
canvas.width = w;
|
|
131
|
+
canvas.height = h;
|
|
132
|
+
rctx.W = w;
|
|
133
|
+
rctx.H = h;
|
|
134
|
+
rctx.safeBox = safeBox(w, h);
|
|
135
|
+
if (renderedRn) {
|
|
136
|
+
await notation.init(rctx, notationProps(renderedRn));
|
|
137
|
+
await scrollCursor.init(rctx, scrollCursorProps);
|
|
138
|
+
}
|
|
139
|
+
if (!destroyed) setTime(lastTMs);
|
|
140
|
+
}
|
|
141
|
+
function currentLayout() {
|
|
142
|
+
const eng = getNotationEngraving(rctx);
|
|
143
|
+
if (!eng) return null;
|
|
144
|
+
return eng.followLayoutAt ? eng.followLayoutAt(rctx, lastTMs) : eng.base;
|
|
145
|
+
}
|
|
146
|
+
const clickListeners = [];
|
|
147
|
+
function onCanvasClick(e) {
|
|
148
|
+
const rect = canvas.getBoundingClientRect();
|
|
149
|
+
if (!(rect.width > 0) || !(rect.height > 0)) return;
|
|
150
|
+
const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
|
|
151
|
+
const my = (e.clientY - rect.top) * (canvas.height / rect.height);
|
|
152
|
+
const layout = currentLayout();
|
|
153
|
+
if (!layout) return;
|
|
154
|
+
const idx = hitTestMeasureAt(layout, mx, my);
|
|
155
|
+
if (idx != null) for (const cb of clickListeners) cb(idx);
|
|
156
|
+
}
|
|
157
|
+
canvas.addEventListener("click", onCanvasClick);
|
|
158
|
+
return {
|
|
159
|
+
ready,
|
|
160
|
+
setTime,
|
|
161
|
+
resize() {
|
|
162
|
+
void performResize();
|
|
163
|
+
},
|
|
164
|
+
onMeasureClick(cb) {
|
|
165
|
+
clickListeners.push(cb);
|
|
166
|
+
return () => {
|
|
167
|
+
const i = clickListeners.indexOf(cb);
|
|
168
|
+
if (i >= 0) clickListeners.splice(i, 1);
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
destroy() {
|
|
172
|
+
if (destroyed) return;
|
|
173
|
+
destroyed = true;
|
|
174
|
+
canvas.removeEventListener("click", onCanvasClick);
|
|
175
|
+
notation.dispose?.();
|
|
176
|
+
scrollCursor.dispose?.();
|
|
177
|
+
clickListeners.length = 0;
|
|
178
|
+
if (canvas.parentNode === host) host.removeChild(canvas);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
createNotationPlayer,
|
|
184
|
+
hitTestMeasureAt
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=notationPlayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notationPlayer.ts"],"sourcesContent":["// createNotationPlayer — THE canonical notation-playback component: one\n// scrolling-notation-plus-gliding-playhead implementation, used for BOTH live\n// in-app players (this module's reason to exist) and the promo/video-recording\n// path (src/scene/layers/notation.ts + scrollCursor.ts, driven by the SceneSpec\n// runner for a fixed-duration capture). Both consume the SAME geometry —\n// `notationLayout`, `followBoxAt`/`vstackFollowBox`, `audioPlayheadLine`/\n// `vstackAudioPlayheadLine`, `measureColumnsFromLayout` — which still lives in\n// `src/scene/` (notationGeometry.ts, engravingStore.ts, layers/notation.ts,\n// layers/scrollCursor.ts) for now; this module is their canonical PUBLIC\n// surface for interactive, caller-driven playback. It does not reimplement or\n// fork any of that math — it just instantiates the two `Layer`s directly\n// (bypassing the SceneSpec/timeline runner, which is built for a fixed-length\n// recorded clip, not a live `setTime()`-driven widget) and adds the two things\n// a live player needs that a recorded promo never did: resize, and\n// measure-click hit-testing.\n//\n// WHY THIS EXISTS: a prior in-app player (stave-web-sightread's /bach/play)\n// hand-rolled its own playhead interpolation directly against OSMD's graphical\n// output instead of reusing this path, and the result was a janky cursor.\n// Hand-rolling notation/playhead geometry against OSMD is a KNOWN FAILURE MODE\n// — OSMD's rasterized bitmap does not scale by the naive `canvas.width / pageW`\n// whenever the engraving overflows the nominal page width (which the `hstack`\n// single-staffline layout does routinely), so a from-scratch cursor drifts\n// steadily off the noteheads over a piece. That exact bug was found and fixed\n// ONCE, here, in `extractGeometry` (src/promo.ts — `canvas.width /\n// (contentRight + contentLeft)`, not `/pageW`); every consumer of THIS module\n// inherits the fix for free. Don't re-derive playhead/scroll geometry against\n// OSMD anywhere else — wrap this component instead.\n//\n// Follow-up (noted, not done here): src/scene/layers/notation.ts +\n// scrollCursor.ts could themselves be rebuilt on top of this module instead of\n// duplicating the init/draw wiring; deferred to keep this change additive.\n//\n// IMPORT PATH: this module is SUBPATH-ONLY —\n// `import { createNotationPlayer } from '@real-music-packages/web-core/notationPlayer'`\n// — matching every other non-trivial module in this package (promo.ts,\n// video.ts, scene/, server.ts, streak.ts, playback.ts). It is intentionally\n// NOT re-exported from the root barrel (src/index.ts): the root barrel is\n// theory-only (notes/frequency/scales/intervals/chords/fretboard/shareCard,\n// all zero-dependency) so apps that only want pitch/theory helpers don't\n// statically pull in this module's scene/video runtime deps.\n\nimport { notationFactory, type NotationProps } from './scene/layers/notation';\nimport { scrollCursorFactory, type ScrollCursorProps } from './scene/layers/scrollCursor';\nimport { getNotationEngraving } from './scene/engravingStore';\nimport { measureColumnsFromLayout, type NotationLayout } from './scene/notationGeometry';\nimport type { AudioClock, RenderCtx } from './scene/layer';\nimport type { Score, ScoreNote } from './scene/score';\nimport type { RenderedNotation } from './promo';\nimport { safeBox, type PromoTheme } from './video';\n\n/**\n * Notation scroll/camera mode — forwarded verbatim to the underlying engraving\n * + follow-camera (see `RenderNotationOpts.scrollMode` in src/promo.ts and\n * `notationGeometry.ts`'s hstack/vstack follow functions). These are the ONLY\n * two modes the shared machinery implements; there is no third.\n *\n * 'vstack' (DEFAULT here) — classic stacked systems (page-wrap engraving).\n * The follow camera frames one system at a time and scrolls vertically\n * (with a \"carriage return\" playhead) as the piece crosses system breaks.\n * This is the actively-exercised path (whozart's live player, the RSR/RET/\n * RMT promo videos) and the one the memory note \"use vstack everywhere\"\n * refers to — pick it unless you have a specific reason not to.\n *\n * 'hstack' — single horizontal staffline; the follow window pans purely\n * left→right. The cursor is onset-locked here too (audioPlayheadLine, not\n * a free-running sweep — that linear-sweep footgun was removed from the\n * shared cursor entirely in web-core 0.22.0), and the earlier\n * `canvas.width/pageW` scale bug that made hstack drift is fixed (0.30+\n * `extractGeometry`, see the module doc above). It is still the LESS\n * exercised of the two modes for live players (most real players show\n * multi-line music, which needs vstack) — supported and correct today, but\n * treat it as the less battle-tested choice.\n */\nexport type NotationPlayerMode = 'hstack' | 'vstack';\n\nexport interface NotationPlayerTheme extends Partial<PromoTheme> {}\n\nexport interface CreateNotationPlayerOpts {\n /** Element the player's canvas is mounted into (fills it). */\n host: HTMLElement;\n /** MusicXML to engrave. */\n musicXml: string;\n /** Distinct note onsets (ms, sorted or not — de-duped/sorted internally via\n * the shared score model) the playhead locks to. One per played note (or\n * the caller's own onset schedule) — this IS the audio clock's timing. */\n onsetsMs: number[];\n /**\n * Per-onset engraved column positions (`measureIndex + frac`), 1:1 with\n * `onsetsMs`. Optional — falls back to the shared machinery's own\n * time/ordinal-based placement when omitted (see `scroll-cursor`'s\n * `noteCols`). Strongly recommended for anything beyond a demo: it is what\n * lands the cursor on the actual notehead instead of a time-derived guess.\n *\n * GOTCHA (pre-existing in the wrapped machinery, not introduced here):\n * internally the cursor works off `distinctOnsets(onsetsMs)` (deduped +\n * sorted), so if `onsetsMs` contains duplicate onsets (simultaneous chord\n * notes sharing one onset time — a legitimate input), the deduped length no\n * longer matches `noteCols.length` and the `1:1` alignment silently breaks,\n * degrading to the `barDurMs`/ordinal fallback with no warning. Build\n * `noteCols` against the DEDUPED, sorted onset list, not the raw one.\n */\n noteCols?: number[];\n /** Scroll/camera mode. Default 'vstack'. See `NotationPlayerMode`. Fixed for\n * the life of the instance — switching modes needs a fresh OSMD engrave\n * (different `RenderSingleHorizontalStaffline` layout), so it is NOT a\n * runtime-switchable option; create a new player if the mode must change\n * (noted as a follow-up, not built: nothing in the shared machinery makes a\n * live re-layout trivial). */\n mode?: NotationPlayerMode;\n /** Duration of one measure in ms — enables time-accurate cursor/window\n * placement when `noteCols` isn't supplied (see `scroll-cursor`'s\n * `barDurMs`). */\n barDurMs?: number;\n /** Bar range [from,to] forwarded to the engraver (renderNotation\n * bars/drawFrom-drawUpTo) — engrave an excerpt rather than the whole score. */\n bars?: [number, number];\n /** Top of the notation band, screen px. Default 0 (fills the host — this is\n * a UI widget, not a phone-safe video frame). */\n bandTop?: number;\n /** Height of the notation band, screen px. Default the full frame height. */\n bandHeight?: number;\n /** Theme tokens (colours/fonts) forwarded to the layers. Any field omitted\n * falls back to a neutral default. */\n theme?: NotationPlayerTheme;\n /** Explicit canvas size in device px. Default: host.clientWidth/clientHeight\n * × devicePixelRatio. Pass this in test/headless environments where the\n * host has no real layout (e.g. jsdom, where clientWidth is always 0). */\n size?: [number, number];\n /**\n * Advanced / test seam: a pre-rasterized engraving, bypassing the browser\n * OSMD raster (`musicXml` is still required by the type but is ignored when\n * this is set). Mirrors `NotationProps.rendered` in\n * `src/scene/layers/notation.ts` (\"test/headless injection\") — real OSMD\n * *rendering* (as opposed to parsing) needs a real 2D canvas context (glyph\n * metrics for its line-breaking pass) that headless/jsdom test contexts\n * can't fully provide, so this is how this module (and the layer it wraps)\n * stays unit-testable in Node. Not needed in a real browser host.\n */\n rendered?: RenderedNotation;\n}\n\nexport interface NotationPlayer {\n /** Resolves once the engraving has been rasterized and is ready to draw.\n * `setTime`/click hit-testing are safe to call before this resolves — they\n * simply no-op (draw a blank frame / report no measure) until ready. */\n readonly ready: Promise<void>;\n /**\n * Draw the frame for absolute playback time `tMs`. The caller owns the\n * audio clock + the rAF loop — call this every frame with the current\n * playback position; everything else (follow camera, onset-locked\n * playhead) is a pure function of `tMs`, exactly as the promo/whozart path\n * drives it.\n */\n setTime(tMs: number): void;\n /** Re-measure the host and resize the canvas to match (device-px aware).\n * Cheap after the first draw — reuses the already-rasterized engraving\n * bitmap, it does not re-run OSMD. Call on host resize / orientation\n * change. Fire-and-forget (async internally; the next `setTime` reflects\n * the new size once it lands, typically within a microtask). */\n resize(): void;\n /** Register a measure-click handler: fires with the clicked measure's\n * engraved index (matching `ScoreNote.measure` numbering) when a click\n * lands on — or nearest to — a rendered measure column. Multiple handlers\n * may be registered (they all fire); returns an unsubscribe function for\n * that one handler. `destroy()` also clears every remaining listener. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the canvas from `host` and drops listeners/state. */\n destroy(): void;\n}\n\nconst DEFAULT_THEME: PromoTheme = {\n paper: '#faf7f0', ink: '#1a1614', accent: '#7b2436', sepia: '#6d5d4d', gold: '#c8a55b',\n fontDisplay: 'Georgia, serif', fontBody: 'Georgia, serif', brand: '',\n};\n\n/** A minimal but valid `Score` carrying only the onset timings the shared\n * scroll-cursor needs (`distinctOnsets(ctx.score.notes)`). Pitch/hand/measure\n * fields are placeholders — the notation + scroll-cursor layers never read\n * them (they read the RASTERIZED engraving's own geometry for position; the\n * Score here only supplies the audio-onset clock). */\nfunction scoreFromOnsets(onsetsMs: number[]): Score {\n const notes: ScoreNote[] = onsetsMs.map((onsetMs) => ({\n pitchMidi: 60, step: 'C', alter: 0, octave: 4,\n onsetMs, durMs: 0, staff: 0, voice: 0, hand: 'R', measure: 1,\n }));\n const durationMs = onsetsMs.length ? Math.max(...onsetsMs) : 0;\n return {\n notes,\n tempoMap: { source: 'fallback', segments: [{ atMs: 0, bpm: 120 }] },\n durationMs,\n };\n}\n\n/**\n * Pure hit-test: which measure (by its engraved index) contains — or is\n * nearest to — point (mx, my) in a given followed layout. Reuses\n * `measureColumnsFromLayout` (the SAME per-measure union boxes the playhead\n * anchors to) for the boxes; only the index bookkeeping (matching each\n * returned box back to its measure index, which `measureColumnsFromLayout`\n * intentionally drops — the playhead has no use for it) is new here, and it\n * is pure array/index bookkeeping, not geometry math. Exported standalone so\n * it is unit-testable without a DOM/canvas.\n *\n * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in\n * the same DEST/DEVICE-PIXEL space `layout.measures[].box` is already mapped\n * into by `notationLayout()` (i.e. the player's own `<canvas>` device pixels,\n * origin top-left, NOT CSS px) — the same space this module's own\n * `onCanvasClick` computes via `(clientX - rect.left) * (canvas.width /\n * rect.width)`. It is NOT the raster/src space (`RenderedNotation.canvas`,\n * OSMD's own pre-map bitmap px) — passing src-space coordinates here is\n * exactly the \"classic scale gotcha\" (`extractGeometry`'s `canvas.width /\n * pageW` vs `/(contentRight+contentLeft)`, see the module doc) this component\n * exists to make impossible; don't reintroduce it at the call site.\n */\nexport function hitTestMeasureAt(layout: NotationLayout, mx: number, my: number): number | null {\n if (!layout.measures.length) return null;\n const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const cols = measureColumnsFromLayout(layout.measures);\n let nearest: number | null = null;\n let nearestD = Infinity;\n for (let i = 0; i < cols.length; i++) {\n const b = cols[i];\n if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];\n const cx = b.x + b.w / 2;\n const cy = b.y + b.h / 2;\n const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);\n if (d < nearestD) {\n nearestD = d;\n nearest = indices[i];\n }\n }\n return nearest;\n}\n\n/**\n * Build a live, interactive notation player: a thin packaging of the SAME\n * working path whozart's live in-browser promo player uses (see the module\n * doc). Renders the engraving ONCE (OSMD raster, via the `notation` Layer),\n * then every `setTime(tMs)` call blits the current follow window (via\n * `followBoxAt`/`vstackFollowBox` → `notationLayout`) and draws the\n * onset-locked playhead (`audioPlayheadLine`/`vstackAudioPlayheadLine`) —\n * exactly the `notation` + `scroll-cursor` Layer pair the promo/video runner\n * uses, instantiated directly instead of through the SceneSpec/timeline\n * runner (which targets a fixed-duration recorded clip, not a live,\n * caller-driven widget).\n */\nexport function createNotationPlayer(opts: CreateNotationPlayerOpts): NotationPlayer {\n const { host, musicXml, onsetsMs, noteCols, barDurMs, bars } = opts;\n const mode: NotationPlayerMode = opts.mode ?? 'vstack';\n const theme: PromoTheme = { ...DEFAULT_THEME, ...opts.theme };\n\n const canvas = document.createElement('canvas');\n canvas.style.display = 'block';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n host.appendChild(canvas);\n\n const dpr = typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;\n function frameSize(): [number, number] {\n if (opts.size) return opts.size;\n const w = Math.max(1, Math.round((host.clientWidth || 1) * dpr));\n const h = Math.max(1, Math.round((host.clientHeight || 1) * dpr));\n return [w, h];\n }\n\n const [initW, initH] = frameSize();\n canvas.width = initW;\n canvas.height = initH;\n const ctx2d = canvas.getContext('2d');\n if (!ctx2d) throw new Error('createNotationPlayer: 2D canvas context unavailable');\n\n let lastTMs = 0;\n const audioClock: AudioClock = { nowMs: () => lastTMs };\n const rctx: RenderCtx = {\n ctx2d, W: initW, H: initH,\n score: scoreFromOnsets(onsetsMs),\n audioClock, theme, safeBox: safeBox(initW, initH), fps: 30,\n };\n\n const notation = notationFactory.create();\n const scrollCursor = scrollCursorFactory.create();\n\n function notationProps(rendered?: RenderedNotation): NotationProps {\n return {\n ...(rendered ? { rendered } : { xml: musicXml }),\n scrollMode: mode,\n bars,\n bandTop: opts.bandTop ?? 0,\n bandHeight: opts.bandHeight ?? rctx.H,\n };\n }\n const scrollCursorProps: ScrollCursorProps = { scrollMode: mode, barDurMs, noteCols };\n\n let renderedRn: RenderedNotation | null = null;\n let destroyed = false;\n\n const ready = (async () => {\n await notation.init(rctx, notationProps(opts.rendered));\n renderedRn = getNotationEngraving(rctx)?.rendered ?? null;\n await scrollCursor.init(rctx, scrollCursorProps);\n })();\n\n function clear(): void {\n ctx2d!.save();\n ctx2d!.setTransform(1, 0, 0, 1, 0, 0);\n ctx2d!.fillStyle = theme.paper;\n ctx2d!.fillRect(0, 0, canvas.width, canvas.height);\n ctx2d!.restore();\n }\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n lastTMs = tMs;\n clear();\n notation.draw(rctx, tMs);\n scrollCursor.draw(rctx, tMs);\n }\n\n async function performResize(): Promise<void> {\n const [w, h] = frameSize();\n if (w === canvas.width && h === canvas.height) return;\n canvas.width = w;\n canvas.height = h;\n rctx.W = w;\n rctx.H = h;\n rctx.safeBox = safeBox(w, h);\n if (renderedRn) {\n // Cheap re-layout: reuses the already-rasterized bitmap (no OSMD re-run).\n await notation.init(rctx, notationProps(renderedRn));\n await scrollCursor.init(rctx, scrollCursorProps);\n }\n if (!destroyed) setTime(lastTMs);\n }\n\n function currentLayout(): NotationLayout | null {\n const eng = getNotationEngraving(rctx);\n if (!eng) return null;\n return eng.followLayoutAt ? eng.followLayoutAt(rctx, lastTMs) : eng.base;\n }\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onCanvasClick(e: MouseEvent): void {\n const rect = canvas.getBoundingClientRect();\n if (!(rect.width > 0) || !(rect.height > 0)) return;\n const mx = (e.clientX - rect.left) * (canvas.width / rect.width);\n const my = (e.clientY - rect.top) * (canvas.height / rect.height);\n const layout = currentLayout();\n if (!layout) return;\n const idx = hitTestMeasureAt(layout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n canvas.addEventListener('click', onCanvasClick);\n\n return {\n ready,\n setTime,\n resize(): void {\n void performResize();\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n canvas.removeEventListener('click', onCanvasClick);\n notation.dispose?.();\n scrollCursor.dispose?.();\n clickListeners.length = 0;\n if (canvas.parentNode === host) host.removeChild(canvas);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/scene/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { A as AudioClock, S as Score, a as LayerFactory, R as RenderCtx, b as ScoreFromMusicXMLOpts, T as TempoMap, c as ScoreNote } from '../waveform-DdSMAbYQ.js';
|
|
2
2
|
export { L as Layer, d as SpectrumInput, e as SpectrumProps, W as WaveformInput, f as WaveformProps, s as scoreFromMusicXML, g as spectrumFactory, w as waveformFactory } from '../waveform-DdSMAbYQ.js';
|
|
3
3
|
import { PromoTheme, RecordOpts, Scene, SafeBox } from '../video.js';
|
|
4
|
-
import { RenderedNotation, Box
|
|
4
|
+
import { RenderedNotation, Box } from '../promo.js';
|
|
5
|
+
import { N as NotationLayout } from '../notationGeometry-CZJ0U6PQ.js';
|
|
6
|
+
export { F as FOLLOW_BARS, a as FOLLOW_PAD, b as NotationLayoutOpts, c as NotationRect, P as PlayheadLine, d as audioPlayheadLine, e as cropAroundBox, f as cubicEaseInOut, g as distinctMeasureIndices, h as distinctOnsets, i as firstMeasureBox, j as followBoxAt, k as followWindowStart, l as lerpBox, m as measureColumnsFromLayout, n as measureCount, o as measureSpanBox, p as measureSystemMap, q as notationLayout, r as playheadLine, s as systemBox, v as vstackAudioPlayheadLine, t as vstackFollowBox } from '../notationGeometry-CZJ0U6PQ.js';
|
|
5
7
|
|
|
6
8
|
/** One scheduled audio event, in seconds RELATIVE to the schedule start. */
|
|
7
9
|
interface AudioEvent {
|
|
@@ -595,138 +597,6 @@ interface ScrollCursorProps {
|
|
|
595
597
|
}
|
|
596
598
|
declare const scrollCursorFactory: LayerFactory<ScrollCursorProps>;
|
|
597
599
|
|
|
598
|
-
/** measures (= rows) visible at once in the follow window. */
|
|
599
|
-
declare const FOLLOW_BARS = 2;
|
|
600
|
-
/** breathing room around the follow window. */
|
|
601
|
-
declare const FOLLOW_PAD = 1.06;
|
|
602
|
-
/** Cubic ease-in-out, t in [0,1] (RSR port of stave-video core/camera.py). */
|
|
603
|
-
declare function cubicEaseInOut(t: number): number;
|
|
604
|
-
/** Linear interpolation of two boxes. (RSR lerpBox) */
|
|
605
|
-
declare function lerpBox(a: Box, b: Box, e: number): Box;
|
|
606
|
-
/** Smallest crop of `aspect` containing `box` padded by `pad`, clamped to the
|
|
607
|
-
* canvas. (RSR cropAroundBox) */
|
|
608
|
-
declare function cropAroundBox(box: Box, aspect: number, pad: number, cw: number, ch: number): Box;
|
|
609
|
-
/** Union (canvas coords) of all staff measure boxes with index in [lo,hi).
|
|
610
|
-
* (RSR measureSpanBox) */
|
|
611
|
-
declare function measureSpanBox(rn: RenderedNotation, lo: number, hi: number): Box | null;
|
|
612
|
-
/** Union of the index-0 measure boxes — the opening focal. (RSR firstMeasureBox) */
|
|
613
|
-
declare function firstMeasureBox(rn: RenderedNotation): Box | null;
|
|
614
|
-
/** Number of distinct measures in the notation. (RSR measureCount) */
|
|
615
|
-
declare function measureCount(rn: RenderedNotation): number;
|
|
616
|
-
/** The DISTINCT measure indices present in the engraving, ASCENDING. For excerpts
|
|
617
|
-
* these do NOT start at 0 (e.g. bars 5-8 → [4,5,6,7]) and may be non-contiguous,
|
|
618
|
-
* so the follow window must scroll across THESE indices, not a 0..measureCount
|
|
619
|
-
* range. (measureCount returns maxIndex+1 — a count valid only for 0-based scores.) */
|
|
620
|
-
declare function distinctMeasureIndices(rn: RenderedNotation): number[];
|
|
621
|
-
/** The follow window (canvas coords) for a continuous measure-start position,
|
|
622
|
-
* spanning FOLLOW_BARS and lerping between adjacent windows. (RSR followBoxAt) */
|
|
623
|
-
declare function followBoxAt(rn: RenderedNotation, posMeasures: number): Box | null;
|
|
624
|
-
/**
|
|
625
|
-
* The continuous follow-window START measure (ABSOLUTE index) for an hstack
|
|
626
|
-
* audio-clock progress 0..1.
|
|
627
|
-
*
|
|
628
|
-
* hstack (single horizontal staffline): pan CONTINUOUSLY so the playhead sits at a
|
|
629
|
-
* stable fraction (`HSTACK_PLAYHEAD_LEAD`) from the LEFT of the window — the playing
|
|
630
|
-
* bar is always on screen with look-ahead to its right, and the cursor never drifts
|
|
631
|
-
* to the screen edge.
|
|
632
|
-
*
|
|
633
|
-
* BUG FIX (web-core 0.21.0) — playhead pinned at the right edge for EXCERPTS. This
|
|
634
|
-
* used to take `nBars = measureCount(rn) = maxIndex+1` and pan progress across a
|
|
635
|
-
* 0..nBars range. That is only correct when measure indices are 0-based: RSR's
|
|
636
|
-
* excerpts engrave e.g. bars 5-8 → indices [4,5,6,7], so measureCount=8 but only 4
|
|
637
|
-
* measures exist. The window then panned a phantom 0..8 axis while the playhead
|
|
638
|
-
* (which steps over the 4 REAL columns) raced ahead — pinning the cursor at the far
|
|
639
|
-
* right while the music barely scrolled. We now pan across the ACTUAL distinct
|
|
640
|
-
* indices: progress 0..1 maps to absolute index `firstIndex .. firstIndex+nReal`,
|
|
641
|
-
* and `followBoxAt` (which already indexes absolute measures via measureSpanBox)
|
|
642
|
-
* frames the right bars. Clamped so the window never runs past the last measure.
|
|
643
|
-
*
|
|
644
|
-
* Pass the RenderedNotation so the real index range is known. (The legacy 0-based
|
|
645
|
-
* `nBars`-only call still works via the `firstIndex=0, nReal=nBars` fallback.)
|
|
646
|
-
*/
|
|
647
|
-
declare function followWindowStart(rn: RenderedNotation | number, camProgress01: number): number;
|
|
648
|
-
/** For each measure index, the system (row) it lives on — ordered by index.
|
|
649
|
-
* Empty when geometry is missing. */
|
|
650
|
-
declare function measureSystemMap(rn: RenderedNotation): number[];
|
|
651
|
-
/** Union (canvas coords) of every measure box on system `sys`. Falls back to the
|
|
652
|
-
* system's own box when no measures map to it. */
|
|
653
|
-
declare function systemBox(rn: RenderedNotation, sys: number, sysMap?: number[]): Box | null;
|
|
654
|
-
/**
|
|
655
|
-
* vstack follow window (canvas coords) for an audio-clock progress 0..1. Frames
|
|
656
|
-
* the ACTIVE system (the row the playhead's current bar is on) and eases
|
|
657
|
-
* VERTICALLY to the NEXT system over the tail of the active system's last bar, so
|
|
658
|
-
* the next row slides up just as the playhead crosses into it. Within a system
|
|
659
|
-
* the box is fixed (the playhead pans L→R inside it). No measure-window vertical
|
|
660
|
-
* jumps: the focusBox always spans an integer-or-tween of FULL systems.
|
|
661
|
-
*
|
|
662
|
-
* The box keeps a constant size (the larger of the two framed systems' extents)
|
|
663
|
-
* so the dest mapping doesn't breathe as it scrolls — the camera just translates.
|
|
664
|
-
*/
|
|
665
|
-
declare function vstackFollowBox(rn: RenderedNotation, camProgress01: number): Box | null;
|
|
666
|
-
/** The dest rect a notation frame is drawn into, on screen. */
|
|
667
|
-
interface NotationRect {
|
|
668
|
-
dx: number;
|
|
669
|
-
dy: number;
|
|
670
|
-
dw: number;
|
|
671
|
-
dh: number;
|
|
672
|
-
}
|
|
673
|
-
/** Geometry result of laying out the notation for one frame: which canvas `src`
|
|
674
|
-
* rect is blitted to which screen `dest` rect, plus the mapped boxes. */
|
|
675
|
-
interface NotationLayout {
|
|
676
|
-
/** Canvas-space crop blitted this frame. */
|
|
677
|
-
src: Box;
|
|
678
|
-
/** Screen-space dest rect. */
|
|
679
|
-
rect: NotationRect;
|
|
680
|
-
/** Per-row system boxes mapped into screen coords (for the playhead fallback). */
|
|
681
|
-
systems: Box[];
|
|
682
|
-
/** Per-(measure,staff) boxes mapped into screen coords (for highlights/cursor). */
|
|
683
|
-
measures: StaffMeasureBox[];
|
|
684
|
-
}
|
|
685
|
-
interface NotationLayoutOpts {
|
|
686
|
-
/** 0 = zoomed on bar 1, 1 = full excerpt (opening camera). Default 1. */
|
|
687
|
-
zoom01?: number;
|
|
688
|
-
/** Follow window (canvas coords); when set, overrides zoom and scrolls. */
|
|
689
|
-
focusBox?: Box | null;
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* Compute the notation layout (src crop + dest rect + mapped boxes) for one
|
|
693
|
-
* frame. This is RSR's `drawNotation` with the single `ctx.drawImage` call REMOVED
|
|
694
|
-
* — pure geometry, so it is testable headless and shared by the notation +
|
|
695
|
-
* scroll-cursor layers. The layer does the drawImage using `src` + `rect`.
|
|
696
|
-
*/
|
|
697
|
-
declare function notationLayout(rn: RenderedNotation, W: number, H: number, boxTop: number, boxH: number, opts?: NotationLayoutOpts): NotationLayout;
|
|
698
|
-
/** Per-measure grand-staff column boxes (both staves unioned) from a frame's
|
|
699
|
-
* mapped measures, in render order. (RSR measureColumns) */
|
|
700
|
-
declare function measureColumnsFromLayout(measures: StaffMeasureBox[]): MeasureColumnBox[];
|
|
701
|
-
/** The playhead line + alpha for progress `t01`. null when fully faded. (RSR
|
|
702
|
-
* drawPlayhead, with the actual stroke factored out into the layer.) */
|
|
703
|
-
interface PlayheadLine {
|
|
704
|
-
x: number;
|
|
705
|
-
y0: number;
|
|
706
|
-
y1: number;
|
|
707
|
-
alpha: number;
|
|
708
|
-
}
|
|
709
|
-
declare function playheadLine(layout: NotationLayout, t01: number): PlayheadLine | null;
|
|
710
|
-
/** Distinct, sorted note onsets from a Score's notes. */
|
|
711
|
-
declare function distinctOnsets(notes: {
|
|
712
|
-
onsetMs: number;
|
|
713
|
-
}[]): number[];
|
|
714
|
-
/**
|
|
715
|
-
* The FOOLPROOF audio-driven playhead line for absolute audio time `tMs`.
|
|
716
|
-
*
|
|
717
|
-
* `tMs` is the audio clock; `onsetsMs` are the score's distinct note onsets
|
|
718
|
-
* (sorted). The cursor is the time-lerp of the two anchors bracketing `tMs`:
|
|
719
|
-
* before the first onset it holds on anchor 0; after the last it holds on the
|
|
720
|
-
* last; in a gap it eases between the bracketing onsets. Pure function of
|
|
721
|
-
* (layout, onsetsMs, tMs). Returns null only when there is no geometry to anchor
|
|
722
|
-
* to (falls back to the rect sweep in the layer, same as `playheadLine`).
|
|
723
|
-
*
|
|
724
|
-
* Fade in/out mirrors `playheadLine` but is keyed on position in the ONSET span
|
|
725
|
-
* (first→last onset), not on musicMs — so the fade tracks the notes too.
|
|
726
|
-
*/
|
|
727
|
-
declare function audioPlayheadLine(layout: NotationLayout, onsetsMs: number[], tMs: number, barDurMs?: number, noteCols?: number[]): PlayheadLine | null;
|
|
728
|
-
declare function vstackAudioPlayheadLine(layout: NotationLayout, onsetsMs: number[], tMs: number, nBars: number, noteCols?: number[]): PlayheadLine | null;
|
|
729
|
-
|
|
730
600
|
/** Map a canvas-space box through a base notation layout into world/screen coords. */
|
|
731
601
|
declare function mapBoxThroughLayout(base: NotationLayout, b: Box): Box;
|
|
732
602
|
/** RSR's per-frame `src` crop from a follow box (FOLLOW_PAD expand + clamp) —
|
|
@@ -1825,4 +1695,4 @@ interface ImageRevealProps {
|
|
|
1825
1695
|
}
|
|
1826
1696
|
declare const imageRevealFactory: LayerFactory<ImageRevealProps>;
|
|
1827
1697
|
|
|
1828
|
-
export { type Affine, AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatPulseProps, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordRibbonProps, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type ContourPlot, type ContourPoint, type CountInOpts, type CountdownProps, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type EndCardProps, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR,
|
|
1698
|
+
export { type Affine, AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatPulseProps, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordRibbonProps, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type ContourPlot, type ContourPoint, type CountInOpts, type CountdownProps, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type EndCardProps, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, type FallingKeyboardDemoOpts, type FallingNotesProps, type FretboardProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type ImageRevealMode, type ImageRevealProps, type IntervalArcsProps, type KaraokeCaptionProps, type KaraokeWord, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type KineticMode, type KineticTextProps, type LabelMode, LayerFactory, type McqCardProps, type NotationEngraving, NotationLayout, type NotationProps, type OutputProbe, PIANO_HIGH, PIANO_LOW, type ParticleBurstProps, type PitchContourProps, type Placement, type PortraitProps, type ProgressRingProps, type PromoCardsDemoOpts, type PulseStyle, type Quiz, type QuizOption, type QuizPhase, type RadialSpectrumProps, type RayEndpoints, type RecordSceneSpecOpts, type Rect, RenderCtx, type ResolvedSegment, type RevealEasing, type RevealProps, SCALE_INTERVALS, type SafeGuidesProps, type ScaleHighlightProps, type SceneSpec, type ScheduleTarget, Score, ScoreFromMusicXMLOpts, ScoreNote, type ScrollCursorProps, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type StaffKeyboardRayProps, type StatCounterProps, TempoMap, type TensionGraphProps, type TensionPoint, type TextureProps, type TimeAnchor, type TimelineSegment, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, ballArc, ballX, beatGrid, beatPhase, beatPulseFactory, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, chordRibbonFactory, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, contourMinimapDemoSpec, contourPoints, contourPolyline, countInLeadSec, countInSchedule, countdownFactory, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, ctaFactory, cueOpacity, degreeLabel, degreeLabelsFactory, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, endCardFactory, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, followSrcBox, fracSlotPoint, frameRect, fretboardFactory, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, imageRevealFactory, inRange, intervalArcsFactory, invLerp, isBlackKey, karaokeCaptionFactory, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, kineticTextFactory, lerp, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureSpans, timeToX as minimapTimeToX, msPerBeat, notationFactory, noteColor, noteSetXRange, parseKey, parseTimeSig, particleBurstFactory, pcToSlot, pitchAt, pitchContourFactory, pitchRange, portraitFactory, progress01, progressRingFactory, projectPoint, promoCardsDemoSpec, pulseAt, quizPhase, radialSpectrumFactory, rayEndpoints, rayPointAt, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scaleHighlightFactory, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, statCounterFactory, tensionGraphFactory, textureFactory, transitionProgress, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, whiteKeys, worldToViewport };
|