@real-music-packages/web-core 0.15.0 → 0.17.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.
@@ -717,8 +717,22 @@ declare function measureCount(rn: RenderedNotation): number;
717
717
  declare function followBoxAt(rn: RenderedNotation, posMeasures: number): Box | null;
718
718
  /**
719
719
  * The continuous follow-window START measure for an audio-clock progress 0..1.
720
- * (RSR +page.svelte:738-744 — hold the current pair, ease-scroll over the last
721
- * third of each bar.) Returns the windowStart fed to followBoxAt.
720
+ *
721
+ * INVARIANT: the bar the playhead is in (`curBar`) is ALWAYS fully inside the
722
+ * FOLLOW_BARS-wide window. We anchor `curBar` as the BOTTOM (last) visible bar —
723
+ * with the preceding FOLLOW_BARS-1 bars above it for context — so the window
724
+ * holds steady while the playhead works through the visible bars and only scrolls
725
+ * once the playhead reaches the bottom bar. Concretely the resting window start is
726
+ * `curBar - (FOLLOW_BARS - 1)`; over the tail of each bar we ease that forward by
727
+ * one bar so the NEXT bar slides into the bottom slot just as the playhead crosses
728
+ * into it. Because the eased start stays within `[curBar-(FOLLOW_BARS-1),
729
+ * curBar-(FOLLOW_BARS-2)]`, `curBar` never leaves `[start, start+FOLLOW_BARS)` —
730
+ * the current measure (and its playhead) can never ride off the top.
731
+ *
732
+ * The scroll is cubic-eased (smooth, no hard jump) over the last portion of each
733
+ * bar; result is clamped to [0, nBars-FOLLOW_BARS]. (Replaces RSR's original
734
+ * +page.svelte:738-744 logic, which advanced the window to the NEXT bar over the
735
+ * last third of EVERY bar and so pushed the still-current bar off the top.)
722
736
  */
723
737
  declare function followWindowStart(nBars: number, camProgress01: number): number;
724
738
  /** The dest rect a notation frame is drawn into, on screen. */
@@ -930,6 +944,10 @@ interface KeyboardProps {
930
944
  bottomY?: number;
931
945
  /** Right/left hand colours. Defaults: R = theme.accent, L = theme.gold. */
932
946
  handColors?: HandColors;
947
+ /** Realistic 3-D piano styling: white/black key gradients, rounded key fronts,
948
+ * a felt rail + cast shadow at the top, and a soft glow on lit keys. Default
949
+ * false (the flat two-tone look — unchanged for existing consumers). */
950
+ realistic?: boolean;
933
951
  }
934
952
  declare const keyboardFactory: LayerFactory<KeyboardProps>;
935
953
 
@@ -946,6 +964,10 @@ interface FallingNotesProps {
946
964
  speed?: number;
947
965
  /** Glow flash at the hit-line while a note sounds. Default true. */
948
966
  hitGlow?: boolean;
967
+ /** Glowy/translucent block styling: rounded corners, a vertical gradient that
968
+ * fades toward the top, a soft bloom (shadow) in the note colour, and a bright
969
+ * leading-edge cap. Default false (flat opaque rectangles — unchanged). */
970
+ glow?: boolean;
949
971
  /** Standalone-only: keyboard range when `keyboard:false`. Default "auto". */
950
972
  range?: '88' | 'auto';
951
973
  /** Standalone-only: hit-line (world y). Default safeBox.bottom - 220. */
@@ -382,8 +382,9 @@ function followWindowStart(nBars, camProgress01) {
382
382
  const curBar = Math.floor(posBars);
383
383
  const frac = posBars - curBar;
384
384
  const scrollFrac = cubicEaseInOut(Math.min(1, Math.max(0, (frac - 0.66) / 0.34)));
385
+ const restStart = curBar - (FOLLOW_BARS - 1);
385
386
  const maxStart = Math.max(0, nBars - FOLLOW_BARS);
386
- return Math.max(0, Math.min(maxStart, curBar + scrollFrac));
387
+ return Math.max(0, Math.min(maxStart, restStart + scrollFrac));
387
388
  }
388
389
  function notationLayout(rn, W, H, boxTop, boxH, opts = {}) {
389
390
  const { zoom01 = 1, focusBox = null } = opts;
@@ -761,7 +762,7 @@ var BLACK_PC = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
761
762
  function whiteIndexAtOrBelow(midi) {
762
763
  const oct = Math.floor(midi / 12);
763
764
  const pc = midi - oct * 12;
764
- const WHITES_BELOW = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6];
765
+ const WHITES_BELOW = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
765
766
  return oct * 7 + WHITES_BELOW[pc];
766
767
  }
767
768
  function isBlackKey(midi) {
@@ -884,15 +885,34 @@ function getKeyboardLayout(ctx) {
884
885
  }
885
886
 
886
887
  // src/scene/layers/keyboard.ts
888
+ function roundedPath(c, x, y, w, h, radii) {
889
+ c.beginPath();
890
+ const rr = c.roundRect;
891
+ if (typeof rr === "function") {
892
+ rr.call(c, x, y, w, h, radii);
893
+ return;
894
+ }
895
+ c.rect(x, y, w, h);
896
+ }
897
+ function shade(hex, f) {
898
+ const n = parseInt(hex.slice(1), 16);
899
+ const t = f < 0 ? 0 : 255, p = Math.abs(f);
900
+ const r = Math.round((n >> 16 & 255) + (t - (n >> 16 & 255)) * p);
901
+ const g = Math.round((n >> 8 & 255) + (t - (n >> 8 & 255)) * p);
902
+ const b = Math.round((n & 255) + (t - (n & 255)) * p);
903
+ return `rgb(${r},${g},${b})`;
904
+ }
887
905
  function keyboardLayer() {
888
906
  let layout = null;
889
907
  let colorBy = "hand";
890
908
  let hands = { R: "#7b2436", L: "#c8a55b" };
909
+ let realistic = false;
891
910
  return {
892
911
  key: "keyboard",
893
912
  init(ctx, props) {
894
913
  colorBy = props.colorBy ?? "hand";
895
914
  hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
915
+ realistic = props.realistic ?? false;
896
916
  const sb = ctx.safeBox;
897
917
  const height = props.height ?? 220;
898
918
  const bottomY = props.bottomY ?? sb.bottom;
@@ -915,6 +935,10 @@ function keyboardLayer() {
915
935
  for (const n of ctx.score?.notes ?? []) {
916
936
  if (tMs >= n.onsetMs && tMs < n.onsetMs + n.durMs) lit.set(n.pitchMidi, n.hand);
917
937
  }
938
+ if (realistic) {
939
+ drawRealisticKeyboard(c, L, lit, colorBy, hands);
940
+ return;
941
+ }
918
942
  c.save();
919
943
  for (const m of whiteKeys(L)) {
920
944
  const r = keyRect(L, m);
@@ -938,6 +962,78 @@ function keyboardLayer() {
938
962
  }
939
963
  };
940
964
  }
965
+ function drawRealisticKeyboard(c, L, lit, colorBy, hands) {
966
+ {
967
+ const wr = keyRect(L, whiteKeys(L)[0] ?? 0);
968
+ const wRad = Math.max(3, Math.min(8, wr.w * 0.18));
969
+ c.save();
970
+ for (const m of whiteKeys(L)) {
971
+ const r = keyRect(L, m);
972
+ const onHand = lit.get(m);
973
+ const x = r.x + 0.75, w = r.w - 1.5;
974
+ roundedPath(c, x, r.y, w, r.h, [2, 2, wRad, wRad]);
975
+ if (onHand) {
976
+ const base = noteColor(m, onHand, colorBy, hands);
977
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
978
+ g.addColorStop(0, shade(base, 0.18));
979
+ g.addColorStop(1, base);
980
+ c.fillStyle = g;
981
+ } else {
982
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
983
+ g.addColorStop(0, "#e9e4da");
984
+ g.addColorStop(0.12, "#fbfaf7");
985
+ g.addColorStop(1, "#ffffff");
986
+ c.fillStyle = g;
987
+ }
988
+ c.fill();
989
+ c.strokeStyle = "#cdc6ba";
990
+ c.lineWidth = 1;
991
+ c.stroke();
992
+ }
993
+ const railH = Math.max(4, L.height * 0.03);
994
+ c.fillStyle = "#7b2436";
995
+ c.fillRect(L.x, L.top - railH, L.w, railH);
996
+ const sh = c.createLinearGradient(0, L.top, 0, L.top + L.height * 0.14);
997
+ sh.addColorStop(0, "rgba(0,0,0,0.22)");
998
+ sh.addColorStop(1, "rgba(0,0,0,0)");
999
+ c.fillStyle = sh;
1000
+ c.fillRect(L.x, L.top, L.w, L.height * 0.14);
1001
+ for (const m of blackKeys(L)) {
1002
+ const r = keyRect(L, m);
1003
+ const bRad = Math.max(2, r.w * 0.22);
1004
+ const onHand = lit.get(m);
1005
+ roundedPath(c, r.x, r.y, r.w, r.h, [1, 1, bRad, bRad]);
1006
+ if (onHand) {
1007
+ const base = noteColor(m, onHand, colorBy, hands);
1008
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
1009
+ g.addColorStop(0, shade(base, -0.25));
1010
+ g.addColorStop(1, base);
1011
+ c.fillStyle = g;
1012
+ } else {
1013
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
1014
+ g.addColorStop(0, "#15110e");
1015
+ g.addColorStop(0.82, "#2c2622");
1016
+ g.addColorStop(1, "#453d36");
1017
+ c.fillStyle = g;
1018
+ }
1019
+ c.fill();
1020
+ c.fillStyle = onHand ? "rgba(255,255,255,0.28)" : "rgba(255,255,255,0.10)";
1021
+ roundedPath(c, r.x + r.w * 0.18, r.y + r.h - Math.max(2, r.h * 0.06), r.w * 0.64, Math.max(1.5, r.h * 0.035), [1, 1, 1, 1]);
1022
+ c.fill();
1023
+ }
1024
+ for (const [m, hand] of lit) {
1025
+ const r = keyRect(L, m);
1026
+ c.save();
1027
+ c.shadowColor = noteColor(m, hand, colorBy, hands);
1028
+ c.shadowBlur = 22;
1029
+ c.globalAlpha = 0.55;
1030
+ c.fillStyle = noteColor(m, hand, colorBy, hands);
1031
+ c.fillRect(r.x + r.w * 0.2, r.y + r.h * 0.5, r.w * 0.6, r.h * 0.3);
1032
+ c.restore();
1033
+ }
1034
+ c.restore();
1035
+ }
1036
+ }
941
1037
  var keyboardFactory = {
942
1038
  key: "keyboard",
943
1039
  create: keyboardLayer,
@@ -953,17 +1049,68 @@ var keyboardFactory = {
953
1049
  errs.push("keyboard.height must be a positive number");
954
1050
  if (p.bottomY != null && typeof p.bottomY !== "number")
955
1051
  errs.push("keyboard.bottomY must be a number");
1052
+ if (p.realistic != null && typeof p.realistic !== "boolean")
1053
+ errs.push("keyboard.realistic must be a boolean");
956
1054
  return errs;
957
1055
  }
958
1056
  };
959
1057
 
960
1058
  // src/scene/layers/fallingNotes.ts
961
1059
  var DEFAULT_LEAD_MS = 2e3;
1060
+ function rgba(color, a) {
1061
+ if (/^#[0-9a-fA-F]{6}$/.test(color)) {
1062
+ const n = parseInt(color.slice(1), 16);
1063
+ return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
1064
+ }
1065
+ return color;
1066
+ }
1067
+ function roundedRect(c, x, y, w, h, r) {
1068
+ c.beginPath();
1069
+ const rr = c.roundRect;
1070
+ if (typeof rr === "function") {
1071
+ rr.call(c, x, y, w, h, r);
1072
+ return;
1073
+ }
1074
+ c.rect(x, y, w, h);
1075
+ }
1076
+ function drawGlowBlock(c, x, y, w, hRaw, color) {
1077
+ const h = Math.max(2, hRaw);
1078
+ const r = Math.min(w * 0.42, 8);
1079
+ const bx = x + 1, bw = Math.max(1, w - 2);
1080
+ c.save();
1081
+ c.shadowColor = rgba(color, 1);
1082
+ c.shadowBlur = 22;
1083
+ c.fillStyle = rgba(color, 0.5);
1084
+ roundedRect(c, bx, y, bw, h, r);
1085
+ c.fill();
1086
+ c.fill();
1087
+ c.shadowBlur = 0;
1088
+ const g = c.createLinearGradient(0, y, 0, y + h);
1089
+ g.addColorStop(0, rgba(color, 0.06));
1090
+ g.addColorStop(0.5, rgba(color, 0.38));
1091
+ g.addColorStop(1, rgba(color, 0.78));
1092
+ c.fillStyle = g;
1093
+ roundedRect(c, bx, y, bw, h, r);
1094
+ c.fill();
1095
+ const hl = c.createLinearGradient(bx, 0, bx + bw, 0);
1096
+ hl.addColorStop(0, "rgba(255,255,255,0)");
1097
+ hl.addColorStop(0.5, "rgba(255,255,255,0.22)");
1098
+ hl.addColorStop(1, "rgba(255,255,255,0)");
1099
+ c.fillStyle = hl;
1100
+ roundedRect(c, bx + bw * 0.18, y, bw * 0.46, h, Math.min(r, bw * 0.2));
1101
+ c.fill();
1102
+ const capH = Math.min(7, h);
1103
+ c.fillStyle = rgba(color, 0.98);
1104
+ roundedRect(c, bx, y + h - capH, bw, capH, Math.min(r, capH / 2));
1105
+ c.fill();
1106
+ c.restore();
1107
+ }
962
1108
  function fallingNotesLayer() {
963
1109
  let paired = true;
964
1110
  let colorBy = "hand";
965
1111
  let hands = { R: "#7b2436", L: "#c8a55b" };
966
1112
  let hitGlow = true;
1113
+ let glow = false;
967
1114
  let ownLayout = null;
968
1115
  let topY = 0;
969
1116
  let leadMsProp;
@@ -987,6 +1134,7 @@ function fallingNotesLayer() {
987
1134
  colorBy = props.colorBy ?? "hand";
988
1135
  hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
989
1136
  hitGlow = props.hitGlow ?? true;
1137
+ glow = props.glow ?? false;
990
1138
  leadMsProp = props.leadMs;
991
1139
  speedProp = props.speed;
992
1140
  const sb = ctx.safeBox;
@@ -1024,11 +1172,15 @@ function fallingNotesLayer() {
1024
1172
  const drawTop = Math.max(topY, topEdge);
1025
1173
  const drawBottom = Math.min(hit, bottomY);
1026
1174
  const fill = noteColor(n.pitchMidi, n.hand, colorBy, hands);
1027
- c.save();
1028
- c.fillStyle = fill;
1029
- c.globalAlpha = 0.92;
1030
- c.fillRect(cx - w / 2, drawTop, w, Math.max(1, drawBottom - drawTop));
1031
- c.restore();
1175
+ if (glow) {
1176
+ drawGlowBlock(c, cx - w / 2, drawTop, w, drawBottom - drawTop, fill);
1177
+ } else {
1178
+ c.save();
1179
+ c.fillStyle = fill;
1180
+ c.globalAlpha = 0.92;
1181
+ c.fillRect(cx - w / 2, drawTop, w, Math.max(1, drawBottom - drawTop));
1182
+ c.restore();
1183
+ }
1032
1184
  }
1033
1185
  if (hitGlow) {
1034
1186
  for (const n of ctx.score?.notes ?? []) {
@@ -1066,6 +1218,8 @@ var fallingNotesFactory = {
1066
1218
  errs.push("falling-notes.speed must be a positive number");
1067
1219
  if (p.hitGlow != null && typeof p.hitGlow !== "boolean")
1068
1220
  errs.push("falling-notes.hitGlow must be a boolean");
1221
+ if (p.glow != null && typeof p.glow !== "boolean")
1222
+ errs.push("falling-notes.glow must be a boolean");
1069
1223
  if (p.range != null && p.range !== "88" && p.range !== "auto")
1070
1224
  errs.push('falling-notes.range must be "88" | "auto"');
1071
1225
  return errs;