@real-music-packages/web-core 0.35.0 → 0.36.1

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.
@@ -0,0 +1,706 @@
1
+ import {
2
+ safeBox
3
+ } from "./chunk-HXTRNE74.js";
4
+
5
+ // src/scene/notationGeometry.ts
6
+ var FOLLOW_BARS = 2;
7
+ var FOLLOW_PAD = 1.06;
8
+ function cubicEaseInOut(t) {
9
+ if (t < 0.5) return 4 * t * t * t;
10
+ const f = 2 * t - 2;
11
+ return 0.5 * f * f * f + 1;
12
+ }
13
+ function staffSpacePx(rn) {
14
+ const hs = (rn.measures ?? []).map((m) => m.box.h).filter((h) => h > 1).sort((a, b) => a - b);
15
+ if (!hs.length) return 14;
16
+ const med = hs[Math.floor(hs.length / 2)];
17
+ return Math.max(6, med / 4);
18
+ }
19
+ function withLedgerHeadroom(rn, box, spaces, unionInk) {
20
+ const pad = staffSpacePx(rn) * spaces;
21
+ const ch = rn.canvas.height || box.y + box.h;
22
+ let y0 = box.y - pad;
23
+ let y1 = box.y + box.h + pad;
24
+ if (unionInk) {
25
+ const c = rn.content;
26
+ if (c && c.h > 0) {
27
+ y0 = Math.min(y0, c.y);
28
+ y1 = Math.max(y1, c.y + c.h);
29
+ }
30
+ }
31
+ y0 = Math.max(0, y0);
32
+ y1 = Math.min(ch, y1);
33
+ return { x: box.x, y: y0, w: box.w, h: Math.max(1, y1 - y0) };
34
+ }
35
+ function lerpBox(a, b, e) {
36
+ return {
37
+ x: a.x + (b.x - a.x) * e,
38
+ y: a.y + (b.y - a.y) * e,
39
+ w: a.w + (b.w - a.w) * e,
40
+ h: a.h + (b.h - a.h) * e
41
+ };
42
+ }
43
+ function cropAroundBox(box, aspect, pad, cw, ch) {
44
+ const bw = box.w * pad;
45
+ const bh = box.h * pad;
46
+ let w = Math.max(bw, bh * aspect);
47
+ let h = w / aspect;
48
+ w = Math.min(w, cw);
49
+ h = Math.min(h, ch);
50
+ const ccx = box.x + box.w / 2;
51
+ const ccy = box.y + box.h / 2;
52
+ let x = ccx - w / 2;
53
+ let y = ccy - h / 2;
54
+ x = Math.max(0, Math.min(cw - w, x));
55
+ y = Math.max(0, Math.min(ch - h, y));
56
+ return { x, y, w, h };
57
+ }
58
+ function measureSpanBox(rn, lo, hi) {
59
+ const ms = (rn.measures ?? []).filter((m) => m.index >= lo && m.index < hi).map((m) => m.box);
60
+ if (!ms.length) return null;
61
+ const x0 = Math.min(...ms.map((b) => b.x));
62
+ const y0 = Math.min(...ms.map((b) => b.y));
63
+ const x1 = Math.max(...ms.map((b) => b.x + b.w));
64
+ const y1 = Math.max(...ms.map((b) => b.y + b.h));
65
+ return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, true);
66
+ }
67
+ function firstMeasureBox(rn) {
68
+ const first = (rn.measures ?? []).filter((m) => m.index === 0).map((m) => m.box);
69
+ if (!first.length) return rn.systems?.[0] ?? null;
70
+ const x0 = Math.min(...first.map((b) => b.x));
71
+ const y0 = Math.min(...first.map((b) => b.y));
72
+ const x1 = Math.max(...first.map((b) => b.x + b.w));
73
+ const y1 = Math.max(...first.map((b) => b.y + b.h));
74
+ return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 6, false);
75
+ }
76
+ function measureCount(rn) {
77
+ const idx = (rn.measures ?? []).map((m) => m.index);
78
+ return idx.length ? Math.max(...idx) + 1 : 0;
79
+ }
80
+ function distinctMeasureIndices(rn) {
81
+ const set = /* @__PURE__ */ new Set();
82
+ for (const m of rn.measures ?? []) set.add(m.index);
83
+ return [...set].sort((a, b) => a - b);
84
+ }
85
+ function followBoxAt(rn, posMeasures) {
86
+ const cur = Math.floor(posMeasures);
87
+ const frac = posMeasures - cur;
88
+ const a = measureSpanBox(rn, cur, cur + FOLLOW_BARS);
89
+ const b = measureSpanBox(rn, cur + 1, cur + 1 + FOLLOW_BARS) ?? a;
90
+ if (!a) return b;
91
+ if (!b) return a;
92
+ return lerpBox(a, b, frac);
93
+ }
94
+ var HSTACK_PLAYHEAD_LEAD = 0.35;
95
+ function followWindowStart(rn, camProgress01) {
96
+ let firstIndex;
97
+ let nReal;
98
+ if (typeof rn === "number") {
99
+ firstIndex = 0;
100
+ nReal = rn;
101
+ } else {
102
+ const idx = distinctMeasureIndices(rn);
103
+ firstIndex = idx.length ? idx[0] : 0;
104
+ nReal = idx.length;
105
+ }
106
+ const p = Math.max(0, Math.min(1, camProgress01));
107
+ const posBars = firstIndex + p * nReal;
108
+ const start = posBars - HSTACK_PLAYHEAD_LEAD * FOLLOW_BARS;
109
+ const lastIndex = firstIndex + Math.max(0, nReal - 1);
110
+ const minStart = firstIndex;
111
+ const maxStart = Math.max(minStart, lastIndex - (FOLLOW_BARS - 1));
112
+ return Math.max(minStart, Math.min(maxStart, start));
113
+ }
114
+ function systemIndexOfBox(systems, box) {
115
+ if (!systems.length) return 0;
116
+ const cy = box.y + box.h / 2;
117
+ let best = 0;
118
+ let bestD = Infinity;
119
+ for (let i = 0; i < systems.length; i++) {
120
+ const s = systems[i];
121
+ if (cy >= s.y && cy <= s.y + s.h) return i;
122
+ const sc = s.y + s.h / 2;
123
+ const d = Math.abs(cy - sc);
124
+ if (d < bestD) {
125
+ bestD = d;
126
+ best = i;
127
+ }
128
+ }
129
+ return best;
130
+ }
131
+ function measureSystemMap(rn) {
132
+ const systems = rn.systems ?? [];
133
+ const n = measureCount(rn);
134
+ const map = new Array(n).fill(0);
135
+ for (let idx = 0; idx < n; idx++) {
136
+ const m = (rn.measures ?? []).find((mm) => mm.index === idx);
137
+ map[idx] = m ? systemIndexOfBox(systems, m.box) : 0;
138
+ }
139
+ return map;
140
+ }
141
+ function systemBox(rn, sys, sysMap) {
142
+ const map = sysMap ?? measureSystemMap(rn);
143
+ const boxes = (rn.measures ?? []).filter((m) => map[m.index] === sys).map((m) => m.box);
144
+ if (!boxes.length) return (rn.systems ?? [])[sys] ?? null;
145
+ const x0 = Math.min(...boxes.map((b) => b.x));
146
+ const y0 = Math.min(...boxes.map((b) => b.y));
147
+ const x1 = Math.max(...boxes.map((b) => b.x + b.w));
148
+ const y1 = Math.max(...boxes.map((b) => b.y + b.h));
149
+ return withLedgerHeadroom(rn, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }, 8, false);
150
+ }
151
+ function vstackFollowBox(rn, camProgress01) {
152
+ const sysMap = measureSystemMap(rn);
153
+ const nBars = measureCount(rn);
154
+ const nSys = (rn.systems ?? []).length || (sysMap.length ? Math.max(...sysMap) + 1 : 0);
155
+ if (nBars <= 0 || nSys <= 0) return null;
156
+ const posBars = Math.max(0, Math.min(1, camProgress01)) * nBars;
157
+ const curBar = Math.min(nBars - 1, Math.floor(posBars));
158
+ const barFrac = posBars - Math.floor(posBars);
159
+ const curSys = sysMap[curBar] ?? 0;
160
+ const isLastBarOfSys = curBar + 1 >= nBars || (sysMap[curBar + 1] ?? curSys) !== curSys;
161
+ const scrollFrac = isLastBarOfSys ? cubicEaseInOut(Math.min(1, Math.max(0, (barFrac - 0.66) / 0.34))) : 0;
162
+ const nextSys = Math.min(nSys - 1, curSys + 1);
163
+ const a = systemBox(rn, curSys, sysMap);
164
+ const b = systemBox(rn, nextSys, sysMap) ?? a;
165
+ if (!a) return b;
166
+ if (!b) return a;
167
+ const w = Math.max(a.w, b.w);
168
+ const h = Math.max(a.h, b.h);
169
+ const x = a.x + (b.x - a.x) * scrollFrac;
170
+ const y = a.y + (b.y - a.y) * scrollFrac;
171
+ return { x, y, w, h };
172
+ }
173
+ function notationLayout(rn, W, H, boxTop, boxH, opts = {}) {
174
+ const { zoom01 = 1, focusBox = null } = opts;
175
+ const sb = safeBox(W, H);
176
+ const maxW = sb.centeredW;
177
+ const c = rn.content && rn.content.w > 0 && rn.content.h > 0 ? rn.content : { x: 0, y: 0, w: rn.canvas.width || 1400, h: rn.canvas.height || 300 };
178
+ let src;
179
+ if (focusBox) {
180
+ const px = focusBox.w * (FOLLOW_PAD - 1) / 2;
181
+ const py = focusBox.h * (FOLLOW_PAD - 1) / 2;
182
+ src = {
183
+ x: Math.max(0, focusBox.x - px),
184
+ y: Math.max(0, focusBox.y - py),
185
+ w: focusBox.w + 2 * px,
186
+ h: focusBox.h + 2 * py
187
+ };
188
+ src.w = Math.min(src.w, rn.canvas.width - src.x);
189
+ src.h = Math.min(src.h, rn.canvas.height - src.y);
190
+ } else {
191
+ src = c;
192
+ const focal = firstMeasureBox(rn);
193
+ if (zoom01 < 1 && focal) {
194
+ const start = cropAroundBox(focal, c.w / c.h, 1.25, rn.canvas.width, rn.canvas.height);
195
+ src = lerpBox(start, c, cubicEaseInOut(zoom01));
196
+ }
197
+ }
198
+ const srcAspect = src.w / src.h;
199
+ let dw = maxW;
200
+ let dh = dw / srcAspect;
201
+ if (dh > boxH) {
202
+ dh = boxH;
203
+ dw = dh * srcAspect;
204
+ }
205
+ const dx = (W - dw) / 2;
206
+ const dy = boxTop + (boxH - dh) / 2;
207
+ const fx = dw / src.w;
208
+ const fy = dh / src.h;
209
+ const map = (b) => ({
210
+ x: dx + (b.x - src.x) * fx,
211
+ y: dy + (b.y - src.y) * fy,
212
+ w: b.w * fx,
213
+ h: b.h * fy
214
+ });
215
+ const systems = (rn.systems ?? []).map(map);
216
+ const measures = (rn.measures ?? []).map((m) => ({
217
+ ...m,
218
+ box: map(m.box),
219
+ noteStartX: dx + (m.noteStartX - src.x) * fx
220
+ }));
221
+ return { src, rect: { dx, dy, dw, dh }, systems, measures };
222
+ }
223
+ function measureColumnsFromLayout(measures) {
224
+ const byIndex = /* @__PURE__ */ new Map();
225
+ for (const m of measures) {
226
+ const cur = byIndex.get(m.index);
227
+ if (!cur) {
228
+ byIndex.set(m.index, { ...m.box, noteStartX: m.noteStartX });
229
+ } else {
230
+ const x0 = Math.min(cur.x, m.box.x);
231
+ const y0 = Math.min(cur.y, m.box.y);
232
+ const x1 = Math.max(cur.x + cur.w, m.box.x + m.box.w);
233
+ const y1 = Math.max(cur.y + cur.h, m.box.y + m.box.h);
234
+ byIndex.set(m.index, {
235
+ x: x0,
236
+ y: y0,
237
+ w: x1 - x0,
238
+ h: y1 - y0,
239
+ noteStartX: Math.min(cur.noteStartX, m.noteStartX)
240
+ });
241
+ }
242
+ }
243
+ return [...byIndex.keys()].sort((a, b) => a - b).map((k) => byIndex.get(k));
244
+ }
245
+ function playheadLine(layout, t01) {
246
+ const tt = Math.max(0, Math.min(1, t01));
247
+ let alpha = 0.85;
248
+ if (tt < 0.03) alpha *= tt / 0.03;
249
+ if (tt > 0.94) alpha *= Math.max(0, (1 - tt) / 0.06);
250
+ if (alpha <= 0.02) return null;
251
+ const padV = 10;
252
+ let x, y0, y1;
253
+ const cols = measureColumnsFromLayout(layout.measures);
254
+ if (cols.length) {
255
+ const pos = tt * cols.length;
256
+ const i = Math.min(cols.length - 1, Math.floor(pos));
257
+ const m = cols[i];
258
+ const startX = Math.min(m.noteStartX, m.x + m.w);
259
+ x = startX + (pos - i) * (m.x + m.w - startX);
260
+ y0 = m.y - padV;
261
+ y1 = m.y + m.h + padV;
262
+ } else if (layout.systems.length) {
263
+ const pos = tt * layout.systems.length;
264
+ const row = Math.min(layout.systems.length - 1, Math.floor(pos));
265
+ const s = layout.systems[row];
266
+ x = s.x + (pos - row) * s.w;
267
+ y0 = s.y - padV;
268
+ y1 = s.y + s.h + padV;
269
+ } else {
270
+ const r = layout.rect;
271
+ x = r.dx + tt * r.dw;
272
+ y0 = r.dy - padV;
273
+ y1 = r.dy + r.dh + padV;
274
+ }
275
+ return { x, y0, y1, alpha };
276
+ }
277
+ var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
278
+ function anchorAtColumnPos(cols, pos, padV) {
279
+ const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));
280
+ const m = cols[i];
281
+ const startX = Math.min(m.noteStartX, m.x + m.w);
282
+ const frac = Math.min(1, Math.max(0, pos - i));
283
+ return {
284
+ onsetMs: 0,
285
+ x: startX + frac * (m.x + m.w - startX),
286
+ y0: m.y - padV,
287
+ y1: m.y + m.h + padV
288
+ };
289
+ }
290
+ function noteAnchors(layout, onsetsMs, padV, barDurMs, noteCols) {
291
+ const cols = measureColumnsFromLayout(layout.measures);
292
+ if (!cols.length || !onsetsMs.length) return [];
293
+ const n = onsetsMs.length;
294
+ const first = onsetsMs[0];
295
+ const useEngraved = !!noteCols && noteCols.length === n;
296
+ return onsetsMs.map((onsetMs, k) => {
297
+ const pos = useEngraved ? noteCols[k] : barDurMs && barDurMs > 0 ? (onsetMs - first) / barDurMs : (n === 1 ? 0 : k / (n - 1)) * cols.length;
298
+ const a = anchorAtColumnPos(cols, pos, padV);
299
+ return { ...a, onsetMs };
300
+ });
301
+ }
302
+ function distinctOnsets(notes) {
303
+ const set = /* @__PURE__ */ new Set();
304
+ for (const n of notes) set.add(n.onsetMs);
305
+ return [...set].sort((a, b) => a - b);
306
+ }
307
+ function audioPlayheadLine(layout, onsetsMs, tMs, barDurMs, noteCols) {
308
+ const padV = 10;
309
+ const anchors = noteAnchors(layout, onsetsMs, padV, barDurMs, noteCols);
310
+ if (!anchors.length) return null;
311
+ const first = anchors[0].onsetMs;
312
+ const last = anchors[anchors.length - 1].onsetMs;
313
+ const span = last - first;
314
+ let a, b, frac;
315
+ if (tMs <= first || anchors.length === 1) {
316
+ a = b = anchors[0];
317
+ frac = 0;
318
+ } else if (tMs >= last) {
319
+ a = b = anchors[anchors.length - 1];
320
+ frac = 0;
321
+ } else {
322
+ let lo = 0;
323
+ let hi = anchors.length - 1;
324
+ while (lo < hi) {
325
+ const mid = lo + hi + 1 >> 1;
326
+ if (anchors[mid].onsetMs <= tMs) lo = mid;
327
+ else hi = mid - 1;
328
+ }
329
+ a = anchors[lo];
330
+ b = anchors[lo + 1];
331
+ const dt = b.onsetMs - a.onsetMs;
332
+ frac = dt > 0 ? (tMs - a.onsetMs) / dt : 0;
333
+ }
334
+ const x = a.x + (b.x - a.x) * frac;
335
+ const y0 = a.y0 + (b.y0 - a.y0) * frac;
336
+ const y1 = a.y1 + (b.y1 - a.y1) * frac;
337
+ let alpha = 0.85;
338
+ if (span > 0) {
339
+ const fadeIn = Math.min(250, span * 0.5);
340
+ const fadeOut = Math.min(400, span * 0.5);
341
+ if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));
342
+ if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));
343
+ }
344
+ if (alpha <= 0.02) return null;
345
+ return { x, y0, y1, alpha };
346
+ }
347
+ function vstackAudioPlayheadLine(layout, onsetsMs, tMs, nBars, noteCols) {
348
+ const padV = 10;
349
+ const cols = measureColumnsFromLayout(layout.measures);
350
+ if (!cols.length || !onsetsMs.length || nBars <= 0) return null;
351
+ const n = onsetsMs.length;
352
+ const first = onsetsMs[0];
353
+ const last = onsetsMs[n - 1];
354
+ const span = last - first;
355
+ const colX = (m, f) => {
356
+ const sx = Math.min(m.noteStartX, m.x + m.w);
357
+ return sx + Math.min(1, Math.max(0, f)) * (m.x + m.w - sx);
358
+ };
359
+ const useEng = !!noteCols && noteCols.length === n;
360
+ const anchorFor = (k) => {
361
+ const pos = useEng ? noteCols[k] : (n === 1 ? 0 : k / (n - 1)) * cols.length;
362
+ const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));
363
+ const m = cols[i];
364
+ return { x: colX(m, pos - i), rowY: m.y, rowH: m.h };
365
+ };
366
+ const sameRow = (ya, yb) => Math.abs(ya - yb) < Math.max(8, ya * 0 + 8);
367
+ const rowRightEdge = (rowY2) => Math.max(...cols.filter((c) => sameRow(c.y, rowY2)).map((c) => c.x + c.w));
368
+ const rowLeftEdge = (rowY2) => Math.min(...cols.filter((c) => sameRow(c.y, rowY2)).map((c) => Math.min(c.noteStartX, c.x)));
369
+ let a, b, frac;
370
+ if (tMs <= first || n === 1) {
371
+ a = b = anchorFor(0);
372
+ frac = 0;
373
+ } else if (tMs >= last) {
374
+ a = b = anchorFor(n - 1);
375
+ frac = 0;
376
+ } else {
377
+ let lo = 0, hi = n - 1;
378
+ while (lo < hi) {
379
+ const mid = lo + hi + 1 >> 1;
380
+ if (onsetsMs[mid] <= tMs) lo = mid;
381
+ else hi = mid - 1;
382
+ }
383
+ a = anchorFor(lo);
384
+ b = anchorFor(lo + 1);
385
+ frac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);
386
+ }
387
+ let x, rowY, rowH;
388
+ if (sameRow(a.rowY, b.rowY)) {
389
+ x = a.x + (b.x - a.x) * frac;
390
+ rowY = a.rowY;
391
+ rowH = a.rowH;
392
+ } else {
393
+ if (frac < 0.5) {
394
+ const s = frac / 0.5;
395
+ x = a.x + (rowRightEdge(a.rowY) - a.x) * s;
396
+ rowY = a.rowY;
397
+ rowH = a.rowH;
398
+ } else {
399
+ const s = (frac - 0.5) / 0.5;
400
+ const nl = rowLeftEdge(b.rowY);
401
+ x = nl + (b.x - nl) * s;
402
+ rowY = b.rowY;
403
+ rowH = b.rowH;
404
+ }
405
+ }
406
+ let y0 = rowY - padV;
407
+ let y1 = rowY + rowH + padV;
408
+ const bandY0 = layout.rect.dy;
409
+ const bandY1 = layout.rect.dy + layout.rect.dh;
410
+ const h = Math.max(1, y1 - y0);
411
+ if (y0 < bandY0) {
412
+ y0 = bandY0;
413
+ y1 = bandY0 + h;
414
+ }
415
+ if (y1 > bandY1) {
416
+ y1 = bandY1;
417
+ y0 = bandY1 - h;
418
+ }
419
+ let alpha = 0.85;
420
+ if (span > 0) {
421
+ const fadeIn = Math.min(250, span * 0.5);
422
+ const fadeOut = Math.min(400, span * 0.5);
423
+ if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));
424
+ if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));
425
+ }
426
+ if (alpha <= 0.02) return null;
427
+ return { x, y0, y1, alpha };
428
+ }
429
+
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
+ function bandTop(_ctx, sb) {
454
+ return propBandTop ?? sb.top;
455
+ }
456
+ function bandHeight(ctx, sb) {
457
+ const top = bandTop(ctx, sb);
458
+ return (propBandHeight ?? sb.bottom - top) * propScale;
459
+ }
460
+ return {
461
+ key: "notation",
462
+ async init(ctx, props) {
463
+ propScale = props.scale ?? 1;
464
+ propBandTop = props.bandTop;
465
+ propBandHeight = props.bandHeight;
466
+ if (props.rendered) {
467
+ rn = props.rendered;
468
+ } else if (props.xml) {
469
+ const { renderNotation } = await import("./promo.js");
470
+ rn = await renderNotation(props.xml, {
471
+ bars: props.bars,
472
+ paper: ctx.theme.paper,
473
+ scrollMode: props.scrollMode ?? "hstack"
474
+ });
475
+ } else {
476
+ throw new Error("notation layer: provide `rendered` or `xml`");
477
+ }
478
+ const sb = safeBox(ctx.W, ctx.H);
479
+ const top = bandTop(ctx, sb);
480
+ const height = bandHeight(ctx, sb);
481
+ base = notationLayout(rn, ctx.W, ctx.H, top, height, {});
482
+ setNotationEngraving(ctx, { rendered: rn, base, bandTop: top, bandHeight: height });
483
+ },
484
+ draw(ctx, tMs) {
485
+ if (!rn || !base) return;
486
+ const eng = getNotationEngraving(ctx);
487
+ const l = eng?.followLayoutAt ? eng.followLayoutAt(ctx, tMs) : base;
488
+ const c = ctx.ctx2d;
489
+ c.drawImage(
490
+ rn.canvas,
491
+ l.src.x,
492
+ l.src.y,
493
+ l.src.w,
494
+ l.src.h,
495
+ l.rect.dx,
496
+ l.rect.dy,
497
+ l.rect.dw,
498
+ l.rect.dh
499
+ );
500
+ },
501
+ dispose() {
502
+ rn = null;
503
+ base = null;
504
+ }
505
+ };
506
+ }
507
+ var notationFactory = {
508
+ key: "notation",
509
+ create: notationLayer,
510
+ validateProps(props) {
511
+ const errs = [];
512
+ if (props == null || typeof props !== "object") return ["notation: props must be an object"];
513
+ const p = props;
514
+ if (p.system != null && p.system !== "grand" && p.system !== "single")
515
+ errs.push('notation.system must be "grand" | "single"');
516
+ if (p.scrollMode != null && p.scrollMode !== "hstack" && p.scrollMode !== "vstack")
517
+ errs.push('notation.scrollMode must be "hstack" | "vstack"');
518
+ if (p.scale != null && (typeof p.scale !== "number" || p.scale <= 0))
519
+ errs.push("notation.scale must be a positive number");
520
+ if (p.rendered == null && typeof p.xml !== "string")
521
+ errs.push("notation: provide `rendered` (RenderedNotation) or `xml` (string)");
522
+ if (p.bars != null && (!Array.isArray(p.bars) || p.bars.length !== 2))
523
+ errs.push("notation.bars must be [from,to]");
524
+ return errs;
525
+ }
526
+ };
527
+
528
+ // src/scene/layers/scrollCursor.ts
529
+ function followLayoutFor(ctx, progress01, scrollMode) {
530
+ const eng = getNotationEngraving(ctx);
531
+ if (!eng) return null;
532
+ const nBars = measureCount(eng.rendered);
533
+ if (nBars <= 0) return eng.base;
534
+ const focusBox = scrollMode === "vstack" ? vstackFollowBox(eng.rendered, progress01) : followBoxAt(eng.rendered, followWindowStart(eng.rendered, progress01));
535
+ return notationLayout(eng.rendered, ctx.W, ctx.H, eng.bandTop, eng.bandHeight, { focusBox });
536
+ }
537
+ function scrollCursorLayer() {
538
+ let scrollMode = "hstack";
539
+ let openingZoomMs = 900;
540
+ let color;
541
+ let barDurMs;
542
+ let propNoteCols;
543
+ function onsetsFor(ctx) {
544
+ const notes = ctx.score?.notes;
545
+ return notes && notes.length ? distinctOnsets(notes) : [];
546
+ }
547
+ function notesProgress(onsetsMs, tMs, totalMeasures, noteCols) {
548
+ if (tMs < openingZoomMs) return 0;
549
+ const n = onsetsMs.length;
550
+ if (n <= 1) return 0;
551
+ const first = onsetsMs[0];
552
+ const last = onsetsMs[n - 1];
553
+ if (tMs <= first) return 0;
554
+ if (tMs >= last || last <= first) return 1;
555
+ let lo = 0, hi = n - 1;
556
+ while (lo < hi) {
557
+ const mid = lo + hi + 1 >> 1;
558
+ if (onsetsMs[mid] <= tMs) lo = mid;
559
+ else hi = mid - 1;
560
+ }
561
+ const segFrac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);
562
+ if (noteCols && noteCols.length === n && totalMeasures > 0) {
563
+ const pos = noteCols[lo] + (noteCols[lo + 1] - noteCols[lo]) * segFrac;
564
+ return Math.min(1, Math.max(0, pos / totalMeasures));
565
+ }
566
+ if (barDurMs && barDurMs > 0 && totalMeasures > 0) {
567
+ const posLo = (onsetsMs[lo] - first) / barDurMs;
568
+ const posHi = (onsetsMs[lo + 1] - first) / barDurMs;
569
+ const pos = posLo + (posHi - posLo) * segFrac;
570
+ return Math.min(1, Math.max(0, pos / totalMeasures));
571
+ }
572
+ return (lo + segFrac) / (n - 1);
573
+ }
574
+ function noteColsFor(ctx) {
575
+ const eng = getNotationEngraving(ctx)?.rendered?.noteCols;
576
+ const onsets = onsetsFor(ctx);
577
+ const n = onsets.length;
578
+ if (!eng || !eng.length) return propNoteCols;
579
+ if (eng.length === n) return eng;
580
+ if (!propNoteCols || propNoteCols.length !== n) return eng;
581
+ const measureOf = (c) => Math.floor(c + 1e-6);
582
+ const engByMeasure = /* @__PURE__ */ new Map();
583
+ for (const e of eng) {
584
+ const m = measureOf(e);
585
+ const a = engByMeasure.get(m);
586
+ if (a) a.push(e);
587
+ else engByMeasure.set(m, [e]);
588
+ }
589
+ for (const a of engByMeasure.values()) a.sort((x, y) => x - y);
590
+ const idxByMeasure = /* @__PURE__ */ new Map();
591
+ propNoteCols.forEach((c, i) => {
592
+ const m = measureOf(c);
593
+ const a = idxByMeasure.get(m);
594
+ if (a) a.push(i);
595
+ else idxByMeasure.set(m, [i]);
596
+ });
597
+ const out = propNoteCols.slice();
598
+ for (const [m, idxs] of idxByMeasure) {
599
+ const e = engByMeasure.get(m);
600
+ if (!e || !e.length) continue;
601
+ const K = idxs.length;
602
+ idxs.forEach((origIdx, i) => {
603
+ const j = K > 1 ? Math.round(i * (e.length - 1) / (K - 1)) : 0;
604
+ out[origIdx] = e[Math.min(j, e.length - 1)];
605
+ });
606
+ }
607
+ return out;
608
+ }
609
+ function followProgress(ctx, tMs) {
610
+ const eng = getNotationEngraving(ctx);
611
+ const total = eng ? measureCount(eng.rendered) : 0;
612
+ return notesProgress(onsetsFor(ctx), tMs, total, noteColsFor(ctx));
613
+ }
614
+ function layoutAt(ctx, tMs) {
615
+ const eng = getNotationEngraving(ctx);
616
+ return followLayoutFor(ctx, followProgress(ctx, tMs), scrollMode) ?? eng.base;
617
+ }
618
+ return {
619
+ key: "scroll-cursor",
620
+ init(ctx, props) {
621
+ scrollMode = props.scrollMode ?? "hstack";
622
+ openingZoomMs = props.openingZoomMs ?? 900;
623
+ color = props.color;
624
+ barDurMs = props.barDurMs;
625
+ propNoteCols = Array.isArray(props.noteCols) ? props.noteCols : void 0;
626
+ setFollowLayoutProvider(ctx, layoutAt);
627
+ },
628
+ draw(ctx, tMs) {
629
+ const eng = getNotationEngraving(ctx);
630
+ if (!eng) return;
631
+ const layout = layoutAt(ctx, tMs);
632
+ const onsets = onsetsFor(ctx);
633
+ let line;
634
+ if (scrollMode === "vstack") {
635
+ line = vstackAudioPlayheadLine(layout, onsets, tMs, measureCount(eng.rendered), noteColsFor(ctx));
636
+ } else {
637
+ line = audioPlayheadLine(layout, onsets, tMs, barDurMs, noteColsFor(ctx));
638
+ }
639
+ if (!line) return;
640
+ const c = ctx.ctx2d;
641
+ c.save();
642
+ c.strokeStyle = color ?? ctx.theme.accent;
643
+ c.globalAlpha = line.alpha;
644
+ c.lineWidth = 4;
645
+ c.beginPath();
646
+ c.moveTo(line.x, line.y0);
647
+ c.lineTo(line.x, line.y1);
648
+ c.stroke();
649
+ c.restore();
650
+ }
651
+ };
652
+ }
653
+ var scrollCursorFactory = {
654
+ key: "scroll-cursor",
655
+ create: scrollCursorLayer,
656
+ validateProps(props) {
657
+ const errs = [];
658
+ if (props == null || typeof props !== "object") return ["scroll-cursor: props must be an object"];
659
+ const p = props;
660
+ if (p.mode != null && p.mode !== "audio" && p.mode !== "linear")
661
+ errs.push('scroll-cursor.mode must be "audio" | "linear"');
662
+ if (p.scrollMode != null && p.scrollMode !== "hstack" && p.scrollMode !== "vstack")
663
+ errs.push('scroll-cursor.scrollMode must be "hstack" | "vstack"');
664
+ if (p.musicMs != null && (typeof p.musicMs !== "number" || !(p.musicMs > 0)))
665
+ errs.push("scroll-cursor.musicMs must be a positive number");
666
+ if (p.followBars != null && (typeof p.followBars !== "number" || p.followBars < 1))
667
+ errs.push("scroll-cursor.followBars must be a number >= 1");
668
+ if (p.openingZoomMs != null && (typeof p.openingZoomMs !== "number" || p.openingZoomMs < 0))
669
+ errs.push("scroll-cursor.openingZoomMs must be a number >= 0");
670
+ if (p.color != null && typeof p.color !== "string") errs.push("scroll-cursor.color must be a string");
671
+ if (p.barDurMs != null && (typeof p.barDurMs !== "number" || !(p.barDurMs > 0)))
672
+ errs.push("scroll-cursor.barDurMs must be a positive number");
673
+ if (p.noteCols != null && (!Array.isArray(p.noteCols) || p.noteCols.some((c) => typeof c !== "number")))
674
+ errs.push("scroll-cursor.noteCols must be a number[]");
675
+ return errs;
676
+ }
677
+ };
678
+
679
+ export {
680
+ FOLLOW_BARS,
681
+ FOLLOW_PAD,
682
+ cubicEaseInOut,
683
+ lerpBox,
684
+ cropAroundBox,
685
+ measureSpanBox,
686
+ firstMeasureBox,
687
+ measureCount,
688
+ distinctMeasureIndices,
689
+ followBoxAt,
690
+ followWindowStart,
691
+ measureSystemMap,
692
+ systemBox,
693
+ vstackFollowBox,
694
+ notationLayout,
695
+ measureColumnsFromLayout,
696
+ playheadLine,
697
+ distinctOnsets,
698
+ audioPlayheadLine,
699
+ vstackAudioPlayheadLine,
700
+ setNotationEngraving,
701
+ getNotationEngraving,
702
+ setFollowLayoutProvider,
703
+ notationFactory,
704
+ scrollCursorFactory
705
+ };
706
+ //# sourceMappingURL=chunk-JVSXE4X3.js.map