@real-music-packages/web-core 0.29.0 → 0.31.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.
@@ -1,6 +1,8 @@
1
1
  import {
2
+ STRING_COUNT,
3
+ buildPlacementMap,
2
4
  intervalBySemitones
3
- } from "../chunk-N56UTMWA.js";
5
+ } from "../chunk-ILIXNGPE.js";
4
6
  import {
5
7
  getScaleDegree,
6
8
  getStability,
@@ -666,7 +668,7 @@ function audioPlayheadLine(layout, onsetsMs, tMs, barDurMs, noteCols) {
666
668
  if (alpha <= 0.02) return null;
667
669
  return { x, y0, y1, alpha };
668
670
  }
669
- function vstackAudioPlayheadLine(layout, onsetsMs, tMs, nBars) {
671
+ function vstackAudioPlayheadLine(layout, onsetsMs, tMs, nBars, noteCols) {
670
672
  const padV = 10;
671
673
  const cols = measureColumnsFromLayout(layout.measures);
672
674
  if (!cols.length || !onsetsMs.length || nBars <= 0) return null;
@@ -674,40 +676,59 @@ function vstackAudioPlayheadLine(layout, onsetsMs, tMs, nBars) {
674
676
  const first = onsetsMs[0];
675
677
  const last = onsetsMs[n - 1];
676
678
  const span = last - first;
677
- let progress;
678
- if (tMs <= first || n === 1) progress = 0;
679
- else if (tMs >= last || last <= first) progress = 1;
680
- else {
679
+ const colX = (m, f) => {
680
+ const sx = Math.min(m.noteStartX, m.x + m.w);
681
+ return sx + Math.min(1, Math.max(0, f)) * (m.x + m.w - sx);
682
+ };
683
+ const useEng = !!noteCols && noteCols.length === n;
684
+ const anchorFor = (k) => {
685
+ const pos = useEng ? noteCols[k] : (n === 1 ? 0 : k / (n - 1)) * cols.length;
686
+ const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));
687
+ const m = cols[i];
688
+ return { x: colX(m, pos - i), rowY: m.y, rowH: m.h };
689
+ };
690
+ const sameRow = (ya, yb) => Math.abs(ya - yb) < Math.max(8, ya * 0 + 8);
691
+ const rowRightEdge = (rowY2) => Math.max(...cols.filter((c) => sameRow(c.y, rowY2)).map((c) => c.x + c.w));
692
+ const rowLeftEdge = (rowY2) => Math.min(...cols.filter((c) => sameRow(c.y, rowY2)).map((c) => Math.min(c.noteStartX, c.x)));
693
+ let a, b, frac;
694
+ if (tMs <= first || n === 1) {
695
+ a = b = anchorFor(0);
696
+ frac = 0;
697
+ } else if (tMs >= last) {
698
+ a = b = anchorFor(n - 1);
699
+ frac = 0;
700
+ } else {
681
701
  let lo = 0, hi = n - 1;
682
702
  while (lo < hi) {
683
703
  const mid = lo + hi + 1 >> 1;
684
704
  if (onsetsMs[mid] <= tMs) lo = mid;
685
705
  else hi = mid - 1;
686
706
  }
687
- const segFrac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);
688
- progress = (lo + segFrac) / (n - 1);
689
- }
690
- const colX = (m2, f) => {
691
- const sx = Math.min(m2.noteStartX, m2.x + m2.w);
692
- return sx + Math.min(1, Math.max(0, f)) * (m2.x + m2.w - sx);
693
- };
694
- const posBars = Math.min(nBars - 1e-6, Math.max(0, progress * nBars));
695
- const i = Math.min(cols.length - 1, Math.floor(posBars));
696
- const barFrac = posBars - i;
697
- const m = cols[i];
698
- let x = colX(m, barFrac);
699
- let y0 = m.y - padV;
700
- let y1 = m.y + m.h + padV;
701
- if (i + 1 < cols.length) {
702
- const nxt = cols[i + 1];
703
- const crossesSystem = Math.abs(nxt.y - m.y) > m.h * 1.5;
704
- if (crossesSystem) {
705
- const s = cubicEaseInOut(Math.min(1, Math.max(0, (barFrac - 0.66) / 0.34)));
706
- x = x + (colX(nxt, 0) - x) * s;
707
- y0 = y0 + (nxt.y - padV - y0) * s;
708
- y1 = y1 + (nxt.y + nxt.h + padV - y1) * s;
707
+ a = anchorFor(lo);
708
+ b = anchorFor(lo + 1);
709
+ frac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);
710
+ }
711
+ let x, rowY, rowH;
712
+ if (sameRow(a.rowY, b.rowY)) {
713
+ x = a.x + (b.x - a.x) * frac;
714
+ rowY = a.rowY;
715
+ rowH = a.rowH;
716
+ } else {
717
+ if (frac < 0.5) {
718
+ const s = frac / 0.5;
719
+ x = a.x + (rowRightEdge(a.rowY) - a.x) * s;
720
+ rowY = a.rowY;
721
+ rowH = a.rowH;
722
+ } else {
723
+ const s = (frac - 0.5) / 0.5;
724
+ const nl = rowLeftEdge(b.rowY);
725
+ x = nl + (b.x - nl) * s;
726
+ rowY = b.rowY;
727
+ rowH = b.rowH;
709
728
  }
710
729
  }
730
+ let y0 = rowY - padV;
731
+ let y1 = rowY + rowH + padV;
711
732
  const bandY0 = layout.rect.dy;
712
733
  const bandY1 = layout.rect.dy + layout.rect.dh;
713
734
  const h = Math.max(1, y1 - y0);
@@ -935,7 +956,7 @@ function scrollCursorLayer() {
935
956
  const onsets = onsetsFor(ctx);
936
957
  let line;
937
958
  if (scrollMode === "vstack") {
938
- line = vstackAudioPlayheadLine(layout, onsets, tMs, measureCount(eng.rendered));
959
+ line = vstackAudioPlayheadLine(layout, onsets, tMs, measureCount(eng.rendered), noteColsFor(ctx));
939
960
  } else {
940
961
  line = audioPlayheadLine(layout, onsets, tMs, barDurMs, noteColsFor(ctx));
941
962
  }
@@ -1277,6 +1298,123 @@ var keyboardFactory = {
1277
1298
  }
1278
1299
  };
1279
1300
 
1301
+ // src/scene/layers/fretboard.ts
1302
+ var INLAY_FRETS = [3, 5, 7, 9, 12];
1303
+ var NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
1304
+ function fretboardLayer() {
1305
+ let placement = /* @__PURE__ */ new Map();
1306
+ let visibleFrets = 12;
1307
+ let hands = { R: "#7b2436", L: "#c8a55b" };
1308
+ let showLabels = false;
1309
+ let rect = { x: 0, y: 0, w: 0, h: 0 };
1310
+ return {
1311
+ key: "fretboard",
1312
+ init(ctx, props) {
1313
+ hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
1314
+ showLabels = props.showLabels ?? false;
1315
+ const midis = (ctx.score?.notes ?? []).map((n) => n.pitchMidi);
1316
+ placement = buildPlacementMap(midis);
1317
+ let maxFret = 0;
1318
+ for (const p of placement.values()) maxFret = Math.max(maxFret, p.fret);
1319
+ visibleFrets = props.visibleFrets ?? Math.min(15, Math.max(5, maxFret + 1));
1320
+ const sb = ctx.safeBox;
1321
+ const height = props.height ?? 220;
1322
+ const bottomY = props.bottomY ?? sb.bottom;
1323
+ rect = { x: sb.left, y: bottomY - height, w: sb.w, h: height };
1324
+ },
1325
+ draw(ctx, tMs) {
1326
+ const c = ctx.ctx2d;
1327
+ const { x, y, w, h } = rect;
1328
+ const nutW = Math.max(6, w * 0.012);
1329
+ const boardX = x + nutW;
1330
+ const boardW = w - nutW;
1331
+ const fretW = boardW / visibleFrets;
1332
+ const stringGap = h / (STRING_COUNT - 1);
1333
+ const stringY = (s) => y + h - s * stringGap;
1334
+ const fretX = (f) => boardX + f * fretW;
1335
+ const dotX = (f) => f === 0 ? x + nutW / 2 : fretX(f) - fretW / 2;
1336
+ c.save();
1337
+ c.fillStyle = "#f1e7d6";
1338
+ c.fillRect(x, y, w, h);
1339
+ c.fillStyle = "#2a2420";
1340
+ c.fillRect(x, y, nutW, h);
1341
+ c.strokeStyle = "#b8a98c";
1342
+ c.lineWidth = 2;
1343
+ for (let f = 1; f <= visibleFrets; f++) {
1344
+ c.beginPath();
1345
+ c.moveTo(fretX(f), y);
1346
+ c.lineTo(fretX(f), y + h);
1347
+ c.stroke();
1348
+ }
1349
+ c.fillStyle = "rgba(60,50,40,0.28)";
1350
+ for (const f of INLAY_FRETS) {
1351
+ if (f > visibleFrets) continue;
1352
+ const cx = dotX(f);
1353
+ const r = Math.min(stringGap, fretW) * 0.18;
1354
+ if (f === 12) {
1355
+ c.beginPath();
1356
+ c.arc(cx, stringY(STRING_COUNT - 1) + stringGap * 0.5, r, 0, Math.PI * 2);
1357
+ c.fill();
1358
+ c.beginPath();
1359
+ c.arc(cx, stringY(0) - stringGap * 0.5, r, 0, Math.PI * 2);
1360
+ c.fill();
1361
+ } else {
1362
+ c.beginPath();
1363
+ c.arc(cx, y + h / 2, r, 0, Math.PI * 2);
1364
+ c.fill();
1365
+ }
1366
+ }
1367
+ c.strokeStyle = "#5c5345";
1368
+ for (let s = 0; s < STRING_COUNT; s++) {
1369
+ c.lineWidth = 1 + (STRING_COUNT - 1 - s) * 0.5;
1370
+ c.beginPath();
1371
+ c.moveTo(x, stringY(s));
1372
+ c.lineTo(x + w, stringY(s));
1373
+ c.stroke();
1374
+ }
1375
+ const dotR = Math.min(stringGap, fretW) * 0.42;
1376
+ for (const n of ctx.score?.notes ?? []) {
1377
+ if (!(tMs >= n.onsetMs && tMs < n.onsetMs + n.durMs)) continue;
1378
+ const pos = placement.get(n.pitchMidi);
1379
+ if (!pos || pos.fret > visibleFrets) continue;
1380
+ const cx = dotX(pos.fret);
1381
+ const cy = stringY(pos.string);
1382
+ c.fillStyle = n.hand === "L" ? hands.L : hands.R;
1383
+ c.beginPath();
1384
+ c.arc(cx, cy, dotR, 0, Math.PI * 2);
1385
+ c.fill();
1386
+ if (showLabels) {
1387
+ c.fillStyle = "#fff";
1388
+ c.font = `${Math.round(dotR * 1.1)}px sans-serif`;
1389
+ c.textAlign = "center";
1390
+ c.textBaseline = "middle";
1391
+ c.fillText(NOTE_NAMES[n.pitchMidi % 12], cx, cy);
1392
+ }
1393
+ }
1394
+ c.restore();
1395
+ },
1396
+ dispose() {
1397
+ placement = /* @__PURE__ */ new Map();
1398
+ }
1399
+ };
1400
+ }
1401
+ var fretboardFactory = {
1402
+ key: "fretboard",
1403
+ create: fretboardLayer,
1404
+ validateProps(props) {
1405
+ const errs = [];
1406
+ if (props == null || typeof props !== "object") return ["fretboard: props must be an object"];
1407
+ const p = props;
1408
+ if (p.visibleFrets != null && (typeof p.visibleFrets !== "number" || p.visibleFrets < 1 || p.visibleFrets > 24))
1409
+ errs.push("fretboard.visibleFrets must be a number in 1..24");
1410
+ if (p.height != null && (typeof p.height !== "number" || p.height <= 0))
1411
+ errs.push("fretboard.height must be a positive number");
1412
+ if (p.bottomY != null && typeof p.bottomY !== "number") errs.push("fretboard.bottomY must be a number");
1413
+ if (p.showLabels != null && typeof p.showLabels !== "boolean") errs.push("fretboard.showLabels must be a boolean");
1414
+ return errs;
1415
+ }
1416
+ };
1417
+
1280
1418
  // src/scene/layers/fallingNotes.ts
1281
1419
  var DEFAULT_LEAD_MS = 2e3;
1282
1420
  function rgba(color, a) {
@@ -4643,6 +4781,7 @@ registerLayer(captionFactory);
4643
4781
  registerLayer(notationFactory);
4644
4782
  registerLayer(scrollCursorFactory);
4645
4783
  registerLayer(keyboardFactory);
4784
+ registerLayer(fretboardFactory);
4646
4785
  registerLayer(fallingNotesFactory);
4647
4786
  registerLayer(hookFactory);
4648
4787
  registerLayer(revealFactory);
@@ -5484,6 +5623,7 @@ export {
5484
5623
  followWindowStart,
5485
5624
  fracSlotPoint,
5486
5625
  frameRect,
5626
+ fretboardFactory,
5487
5627
  functionColor,
5488
5628
  functionalHarmonyFactory,
5489
5629
  getKeyboardLayout,