@real-music-packages/web-core 0.30.0 → 0.32.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,
@@ -1296,6 +1298,123 @@ var keyboardFactory = {
1296
1298
  }
1297
1299
  };
1298
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 ?? 360;
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
+
1299
1418
  // src/scene/layers/fallingNotes.ts
1300
1419
  var DEFAULT_LEAD_MS = 2e3;
1301
1420
  function rgba(color, a) {
@@ -4646,6 +4765,212 @@ var endCardFactory = {
4646
4765
  }
4647
4766
  };
4648
4767
 
4768
+ // src/scene/layers/imageReveal.ts
4769
+ var DEFAULTS11 = {
4770
+ mode: "pixelate",
4771
+ startMs: 0,
4772
+ durationMs: 6e3,
4773
+ difficulty: 0.5
4774
+ };
4775
+ function clamp017(x) {
4776
+ return x < 0 ? 0 : x > 1 ? 1 : x;
4777
+ }
4778
+ function fitRect(iW, iH, bx, by, bw, bh) {
4779
+ const scale = Math.min(bw / iW, bh / iH);
4780
+ const dw = iW * scale;
4781
+ const dh = iH * scale;
4782
+ return { dx: bx + (bw - dw) / 2, dy: by + (bh - dh) / 2, dw, dh };
4783
+ }
4784
+ function blockSizeAt(p, maxBlock) {
4785
+ return Math.max(1, Math.round(maxBlock * (1 - p) + 1 * p));
4786
+ }
4787
+ function tileHash(i) {
4788
+ let x = i * 2654435761 >>> 0;
4789
+ x ^= x << 13;
4790
+ x ^= x >>> 17;
4791
+ x ^= x << 5;
4792
+ return (x >>> 0) / 4294967295;
4793
+ }
4794
+ function tileHashSigned(i, salt) {
4795
+ return tileHash(i * 1013904223 + salt * 22695477 | 0) * 2 - 1;
4796
+ }
4797
+ function imageRevealLayer() {
4798
+ let mode = DEFAULTS11.mode;
4799
+ let startMs = DEFAULTS11.startMs;
4800
+ let durationMs = DEFAULTS11.durationMs;
4801
+ let difficulty = DEFAULTS11.difficulty;
4802
+ let image = null;
4803
+ let offscreen = null;
4804
+ let offCtx = null;
4805
+ let imgW = 0;
4806
+ let imgH = 0;
4807
+ function ensureOffscreen(w, h) {
4808
+ const needNew = !offscreen || offscreen.width !== w || offscreen.height !== h;
4809
+ if (needNew) {
4810
+ if (typeof OffscreenCanvas !== "undefined") {
4811
+ offscreen = new OffscreenCanvas(w, h);
4812
+ offCtx = offscreen.getContext("2d");
4813
+ } else {
4814
+ const el = typeof document !== "undefined" ? document.createElement("canvas") : null;
4815
+ if (!el) {
4816
+ offscreen = null;
4817
+ offCtx = null;
4818
+ return null;
4819
+ }
4820
+ el.width = w;
4821
+ el.height = h;
4822
+ offscreen = el;
4823
+ offCtx = el.getContext("2d");
4824
+ }
4825
+ }
4826
+ return offCtx;
4827
+ }
4828
+ return {
4829
+ key: "image-reveal",
4830
+ init(_ctx, props) {
4831
+ mode = props.mode ?? DEFAULTS11.mode;
4832
+ startMs = props.startMs ?? DEFAULTS11.startMs;
4833
+ durationMs = props.durationMs ?? DEFAULTS11.durationMs;
4834
+ difficulty = props.difficulty ?? DEFAULTS11.difficulty;
4835
+ image = props.image;
4836
+ const src = props.image;
4837
+ imgW = src["naturalWidth"] ?? src["displayWidth"] ?? src["width"] ?? 0;
4838
+ imgH = src["naturalHeight"] ?? src["displayHeight"] ?? src["height"] ?? 0;
4839
+ },
4840
+ draw(ctx, tMs) {
4841
+ if (!image || imgW === 0 || imgH === 0) return;
4842
+ const p = clamp017((tMs - startMs) / (durationMs > 0 ? durationMs : 1));
4843
+ const sb = ctx.safeBox;
4844
+ const fit = fitRect(imgW, imgH, sb.left, sb.top, sb.w, sb.h);
4845
+ const c = ctx.ctx2d;
4846
+ c.save();
4847
+ c.beginPath();
4848
+ c.rect(fit.dx, fit.dy, fit.dw, fit.dh);
4849
+ c.clip();
4850
+ if (mode === "pixelate") {
4851
+ drawPixelate(c, image, fit, p, difficulty, imgW, imgH, ensureOffscreen);
4852
+ } else if (mode === "blur") {
4853
+ drawBlur(c, image, fit, p, difficulty, imgW, imgH, ensureOffscreen);
4854
+ } else {
4855
+ drawShuffle(c, image, fit, p, difficulty, imgW, imgH);
4856
+ }
4857
+ c.restore();
4858
+ },
4859
+ dispose() {
4860
+ image = null;
4861
+ offscreen = null;
4862
+ offCtx = null;
4863
+ }
4864
+ };
4865
+ }
4866
+ function drawPixelate(c, image, fit, p, difficulty, imgW, imgH, getOffCtx) {
4867
+ const maxBlock = Math.max(2, Math.round(fit.dw * (0.03125 + difficulty * 0.21875)));
4868
+ const blockSize = blockSizeAt(p, maxBlock);
4869
+ if (blockSize <= 1) {
4870
+ c.drawImage(image, 0, 0, imgW, imgH, fit.dx, fit.dy, fit.dw, fit.dh);
4871
+ return;
4872
+ }
4873
+ const smallW = Math.max(1, Math.ceil(fit.dw / blockSize));
4874
+ const smallH = Math.max(1, Math.ceil(fit.dh / blockSize));
4875
+ const oc = getOffCtx(smallW, smallH);
4876
+ if (!oc) {
4877
+ c.drawImage(image, 0, 0, imgW, imgH, fit.dx, fit.dy, fit.dw, fit.dh);
4878
+ return;
4879
+ }
4880
+ oc.clearRect(0, 0, smallW, smallH);
4881
+ oc.drawImage(image, 0, 0, imgW, imgH, 0, 0, smallW, smallH);
4882
+ const prev = c.imageSmoothingEnabled;
4883
+ c.imageSmoothingEnabled = false;
4884
+ const off = oc.canvas;
4885
+ c.drawImage(off, 0, 0, smallW, smallH, fit.dx, fit.dy, fit.dw, fit.dh);
4886
+ c.imageSmoothingEnabled = prev;
4887
+ }
4888
+ function drawBlur(c, image, fit, p, difficulty, imgW, imgH, getOffCtx) {
4889
+ const maxBlur = 8 + difficulty * 72;
4890
+ const blurPx = maxBlur * (1 - p);
4891
+ const cAny = c;
4892
+ const hasFilter = "filter" in c && cAny["filter"] !== void 0;
4893
+ if (hasFilter && blurPx > 0) {
4894
+ const prev = cAny["filter"];
4895
+ cAny["filter"] = `blur(${blurPx.toFixed(1)}px)`;
4896
+ c.drawImage(image, 0, 0, imgW, imgH, fit.dx, fit.dy, fit.dw, fit.dh);
4897
+ cAny["filter"] = prev ?? "none";
4898
+ } else if (blurPx > 0) {
4899
+ const passes = Math.min(6, Math.round(blurPx / 8));
4900
+ if (passes > 0) {
4901
+ let w = Math.max(1, Math.round(fit.dw / Math.pow(2, passes)));
4902
+ let h = Math.max(1, Math.round(fit.dh / Math.pow(2, passes)));
4903
+ const oc = getOffCtx(w, h);
4904
+ if (oc) {
4905
+ oc.clearRect(0, 0, w, h);
4906
+ oc.drawImage(image, 0, 0, imgW, imgH, 0, 0, w, h);
4907
+ c.drawImage(oc.canvas, 0, 0, w, h, fit.dx, fit.dy, fit.dw, fit.dh);
4908
+ } else {
4909
+ c.drawImage(image, 0, 0, imgW, imgH, fit.dx, fit.dy, fit.dw, fit.dh);
4910
+ }
4911
+ } else {
4912
+ c.drawImage(image, 0, 0, imgW, imgH, fit.dx, fit.dy, fit.dw, fit.dh);
4913
+ }
4914
+ } else {
4915
+ c.drawImage(image, 0, 0, imgW, imgH, fit.dx, fit.dy, fit.dw, fit.dh);
4916
+ }
4917
+ }
4918
+ function drawShuffle(c, image, fit, p, difficulty, imgW, imgH) {
4919
+ const cols = 8;
4920
+ const rows = 14;
4921
+ const tileW = fit.dw / cols;
4922
+ const tileH = fit.dh / rows;
4923
+ const srcTileW = imgW / cols;
4924
+ const srcTileH = imgH / rows;
4925
+ const maxOffX = tileW * difficulty * 1.5;
4926
+ const maxOffY = tileH * difficulty * 1.5;
4927
+ const ease = 1 - (1 - p) * (1 - p);
4928
+ for (let row = 0; row < rows; row++) {
4929
+ for (let col = 0; col < cols; col++) {
4930
+ const i = row * cols + col;
4931
+ const offX = tileHashSigned(i, 0) * maxOffX * (1 - ease);
4932
+ const offY = tileHashSigned(i, 1) * maxOffY * (1 - ease);
4933
+ const sx = col * srcTileW;
4934
+ const sy = row * srcTileH;
4935
+ const dxHome = fit.dx + col * tileW;
4936
+ const dyHome = fit.dy + row * tileH;
4937
+ const dx = dxHome + offX;
4938
+ const dy = dyHome + offY;
4939
+ c.drawImage(
4940
+ image,
4941
+ sx,
4942
+ sy,
4943
+ srcTileW,
4944
+ srcTileH,
4945
+ dx,
4946
+ dy,
4947
+ tileW,
4948
+ tileH
4949
+ );
4950
+ }
4951
+ }
4952
+ }
4953
+ var VALID_MODES = ["pixelate", "shuffle", "blur"];
4954
+ var imageRevealFactory = {
4955
+ key: "image-reveal",
4956
+ create: imageRevealLayer,
4957
+ validateProps(props) {
4958
+ if (props == null || typeof props !== "object") return ["image-reveal: props must be an object"];
4959
+ const p = props;
4960
+ const errs = [];
4961
+ if (p["image"] == null) errs.push("image-reveal.image is required");
4962
+ if (p["mode"] != null && !VALID_MODES.includes(p["mode"]))
4963
+ errs.push(`image-reveal.mode must be one of: ${VALID_MODES.join(", ")}`);
4964
+ if (p["startMs"] != null && typeof p["startMs"] !== "number")
4965
+ errs.push("image-reveal.startMs must be a number");
4966
+ if (p["durationMs"] != null && (typeof p["durationMs"] !== "number" || p["durationMs"] <= 0))
4967
+ errs.push("image-reveal.durationMs must be a positive number");
4968
+ if (p["difficulty"] != null && (typeof p["difficulty"] !== "number" || p["difficulty"] < 0 || p["difficulty"] > 1))
4969
+ errs.push("image-reveal.difficulty must be a number in [0, 1]");
4970
+ return errs;
4971
+ }
4972
+ };
4973
+
4649
4974
  // src/scene/registry.ts
4650
4975
  var REGISTRY = /* @__PURE__ */ new Map();
4651
4976
  function registerLayer(factory) {
@@ -4662,6 +4987,7 @@ registerLayer(captionFactory);
4662
4987
  registerLayer(notationFactory);
4663
4988
  registerLayer(scrollCursorFactory);
4664
4989
  registerLayer(keyboardFactory);
4990
+ registerLayer(fretboardFactory);
4665
4991
  registerLayer(fallingNotesFactory);
4666
4992
  registerLayer(hookFactory);
4667
4993
  registerLayer(revealFactory);
@@ -4693,6 +5019,7 @@ registerLayer(tensionGraphFactory);
4693
5019
  registerLayer(textureFactory);
4694
5020
  registerLayer(statCounterFactory);
4695
5021
  registerLayer(endCardFactory);
5022
+ registerLayer(imageRevealFactory);
4696
5023
 
4697
5024
  // src/scene/audioLayers.ts
4698
5025
  function countInSchedule(opts) {
@@ -5503,6 +5830,7 @@ export {
5503
5830
  followWindowStart,
5504
5831
  fracSlotPoint,
5505
5832
  frameRect,
5833
+ fretboardFactory,
5506
5834
  functionColor,
5507
5835
  functionalHarmonyFactory,
5508
5836
  getKeyboardLayout,
@@ -5512,6 +5840,7 @@ export {
5512
5840
  highlightIntensity,
5513
5841
  hookFactory,
5514
5842
  identityCamera,
5843
+ imageRevealFactory,
5515
5844
  inRange,
5516
5845
  intervalArcsFactory,
5517
5846
  invLerp,