@real-music-packages/web-core 0.15.0 → 0.16.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.
@@ -930,6 +930,10 @@ interface KeyboardProps {
930
930
  bottomY?: number;
931
931
  /** Right/left hand colours. Defaults: R = theme.accent, L = theme.gold. */
932
932
  handColors?: HandColors;
933
+ /** Realistic 3-D piano styling: white/black key gradients, rounded key fronts,
934
+ * a felt rail + cast shadow at the top, and a soft glow on lit keys. Default
935
+ * false (the flat two-tone look — unchanged for existing consumers). */
936
+ realistic?: boolean;
933
937
  }
934
938
  declare const keyboardFactory: LayerFactory<KeyboardProps>;
935
939
 
@@ -946,6 +950,10 @@ interface FallingNotesProps {
946
950
  speed?: number;
947
951
  /** Glow flash at the hit-line while a note sounds. Default true. */
948
952
  hitGlow?: boolean;
953
+ /** Glowy/translucent block styling: rounded corners, a vertical gradient that
954
+ * fades toward the top, a soft bloom (shadow) in the note colour, and a bright
955
+ * leading-edge cap. Default false (flat opaque rectangles — unchanged). */
956
+ glow?: boolean;
949
957
  /** Standalone-only: keyboard range when `keyboard:false`. Default "auto". */
950
958
  range?: '88' | 'auto';
951
959
  /** Standalone-only: hit-line (world y). Default safeBox.bottom - 220. */
@@ -761,7 +761,7 @@ var BLACK_PC = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
761
761
  function whiteIndexAtOrBelow(midi) {
762
762
  const oct = Math.floor(midi / 12);
763
763
  const pc = midi - oct * 12;
764
- const WHITES_BELOW = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6];
764
+ const WHITES_BELOW = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
765
765
  return oct * 7 + WHITES_BELOW[pc];
766
766
  }
767
767
  function isBlackKey(midi) {
@@ -884,15 +884,34 @@ function getKeyboardLayout(ctx) {
884
884
  }
885
885
 
886
886
  // src/scene/layers/keyboard.ts
887
+ function roundedPath(c, x, y, w, h, radii) {
888
+ c.beginPath();
889
+ const rr = c.roundRect;
890
+ if (typeof rr === "function") {
891
+ rr.call(c, x, y, w, h, radii);
892
+ return;
893
+ }
894
+ c.rect(x, y, w, h);
895
+ }
896
+ function shade(hex, f) {
897
+ const n = parseInt(hex.slice(1), 16);
898
+ const t = f < 0 ? 0 : 255, p = Math.abs(f);
899
+ const r = Math.round((n >> 16 & 255) + (t - (n >> 16 & 255)) * p);
900
+ const g = Math.round((n >> 8 & 255) + (t - (n >> 8 & 255)) * p);
901
+ const b = Math.round((n & 255) + (t - (n & 255)) * p);
902
+ return `rgb(${r},${g},${b})`;
903
+ }
887
904
  function keyboardLayer() {
888
905
  let layout = null;
889
906
  let colorBy = "hand";
890
907
  let hands = { R: "#7b2436", L: "#c8a55b" };
908
+ let realistic = false;
891
909
  return {
892
910
  key: "keyboard",
893
911
  init(ctx, props) {
894
912
  colorBy = props.colorBy ?? "hand";
895
913
  hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
914
+ realistic = props.realistic ?? false;
896
915
  const sb = ctx.safeBox;
897
916
  const height = props.height ?? 220;
898
917
  const bottomY = props.bottomY ?? sb.bottom;
@@ -915,6 +934,10 @@ function keyboardLayer() {
915
934
  for (const n of ctx.score?.notes ?? []) {
916
935
  if (tMs >= n.onsetMs && tMs < n.onsetMs + n.durMs) lit.set(n.pitchMidi, n.hand);
917
936
  }
937
+ if (realistic) {
938
+ drawRealisticKeyboard(c, L, lit, colorBy, hands);
939
+ return;
940
+ }
918
941
  c.save();
919
942
  for (const m of whiteKeys(L)) {
920
943
  const r = keyRect(L, m);
@@ -938,6 +961,78 @@ function keyboardLayer() {
938
961
  }
939
962
  };
940
963
  }
964
+ function drawRealisticKeyboard(c, L, lit, colorBy, hands) {
965
+ {
966
+ const wr = keyRect(L, whiteKeys(L)[0] ?? 0);
967
+ const wRad = Math.max(3, Math.min(8, wr.w * 0.18));
968
+ c.save();
969
+ for (const m of whiteKeys(L)) {
970
+ const r = keyRect(L, m);
971
+ const onHand = lit.get(m);
972
+ const x = r.x + 0.75, w = r.w - 1.5;
973
+ roundedPath(c, x, r.y, w, r.h, [2, 2, wRad, wRad]);
974
+ if (onHand) {
975
+ const base = noteColor(m, onHand, colorBy, hands);
976
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
977
+ g.addColorStop(0, shade(base, 0.18));
978
+ g.addColorStop(1, base);
979
+ c.fillStyle = g;
980
+ } else {
981
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
982
+ g.addColorStop(0, "#e9e4da");
983
+ g.addColorStop(0.12, "#fbfaf7");
984
+ g.addColorStop(1, "#ffffff");
985
+ c.fillStyle = g;
986
+ }
987
+ c.fill();
988
+ c.strokeStyle = "#cdc6ba";
989
+ c.lineWidth = 1;
990
+ c.stroke();
991
+ }
992
+ const railH = Math.max(4, L.height * 0.03);
993
+ c.fillStyle = "#7b2436";
994
+ c.fillRect(L.x, L.top - railH, L.w, railH);
995
+ const sh = c.createLinearGradient(0, L.top, 0, L.top + L.height * 0.14);
996
+ sh.addColorStop(0, "rgba(0,0,0,0.22)");
997
+ sh.addColorStop(1, "rgba(0,0,0,0)");
998
+ c.fillStyle = sh;
999
+ c.fillRect(L.x, L.top, L.w, L.height * 0.14);
1000
+ for (const m of blackKeys(L)) {
1001
+ const r = keyRect(L, m);
1002
+ const bRad = Math.max(2, r.w * 0.22);
1003
+ const onHand = lit.get(m);
1004
+ roundedPath(c, r.x, r.y, r.w, r.h, [1, 1, bRad, bRad]);
1005
+ if (onHand) {
1006
+ const base = noteColor(m, onHand, colorBy, hands);
1007
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
1008
+ g.addColorStop(0, shade(base, -0.25));
1009
+ g.addColorStop(1, base);
1010
+ c.fillStyle = g;
1011
+ } else {
1012
+ const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
1013
+ g.addColorStop(0, "#15110e");
1014
+ g.addColorStop(0.82, "#2c2622");
1015
+ g.addColorStop(1, "#453d36");
1016
+ c.fillStyle = g;
1017
+ }
1018
+ c.fill();
1019
+ c.fillStyle = onHand ? "rgba(255,255,255,0.28)" : "rgba(255,255,255,0.10)";
1020
+ 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]);
1021
+ c.fill();
1022
+ }
1023
+ for (const [m, hand] of lit) {
1024
+ const r = keyRect(L, m);
1025
+ c.save();
1026
+ c.shadowColor = noteColor(m, hand, colorBy, hands);
1027
+ c.shadowBlur = 22;
1028
+ c.globalAlpha = 0.55;
1029
+ c.fillStyle = noteColor(m, hand, colorBy, hands);
1030
+ c.fillRect(r.x + r.w * 0.2, r.y + r.h * 0.5, r.w * 0.6, r.h * 0.3);
1031
+ c.restore();
1032
+ }
1033
+ c.restore();
1034
+ }
1035
+ }
941
1036
  var keyboardFactory = {
942
1037
  key: "keyboard",
943
1038
  create: keyboardLayer,
@@ -953,17 +1048,68 @@ var keyboardFactory = {
953
1048
  errs.push("keyboard.height must be a positive number");
954
1049
  if (p.bottomY != null && typeof p.bottomY !== "number")
955
1050
  errs.push("keyboard.bottomY must be a number");
1051
+ if (p.realistic != null && typeof p.realistic !== "boolean")
1052
+ errs.push("keyboard.realistic must be a boolean");
956
1053
  return errs;
957
1054
  }
958
1055
  };
959
1056
 
960
1057
  // src/scene/layers/fallingNotes.ts
961
1058
  var DEFAULT_LEAD_MS = 2e3;
1059
+ function rgba(color, a) {
1060
+ if (/^#[0-9a-fA-F]{6}$/.test(color)) {
1061
+ const n = parseInt(color.slice(1), 16);
1062
+ return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
1063
+ }
1064
+ return color;
1065
+ }
1066
+ function roundedRect(c, x, y, w, h, r) {
1067
+ c.beginPath();
1068
+ const rr = c.roundRect;
1069
+ if (typeof rr === "function") {
1070
+ rr.call(c, x, y, w, h, r);
1071
+ return;
1072
+ }
1073
+ c.rect(x, y, w, h);
1074
+ }
1075
+ function drawGlowBlock(c, x, y, w, hRaw, color) {
1076
+ const h = Math.max(2, hRaw);
1077
+ const r = Math.min(w * 0.42, 8);
1078
+ const bx = x + 1, bw = Math.max(1, w - 2);
1079
+ c.save();
1080
+ c.shadowColor = rgba(color, 1);
1081
+ c.shadowBlur = 22;
1082
+ c.fillStyle = rgba(color, 0.5);
1083
+ roundedRect(c, bx, y, bw, h, r);
1084
+ c.fill();
1085
+ c.fill();
1086
+ c.shadowBlur = 0;
1087
+ const g = c.createLinearGradient(0, y, 0, y + h);
1088
+ g.addColorStop(0, rgba(color, 0.06));
1089
+ g.addColorStop(0.5, rgba(color, 0.38));
1090
+ g.addColorStop(1, rgba(color, 0.78));
1091
+ c.fillStyle = g;
1092
+ roundedRect(c, bx, y, bw, h, r);
1093
+ c.fill();
1094
+ const hl = c.createLinearGradient(bx, 0, bx + bw, 0);
1095
+ hl.addColorStop(0, "rgba(255,255,255,0)");
1096
+ hl.addColorStop(0.5, "rgba(255,255,255,0.22)");
1097
+ hl.addColorStop(1, "rgba(255,255,255,0)");
1098
+ c.fillStyle = hl;
1099
+ roundedRect(c, bx + bw * 0.18, y, bw * 0.46, h, Math.min(r, bw * 0.2));
1100
+ c.fill();
1101
+ const capH = Math.min(7, h);
1102
+ c.fillStyle = rgba(color, 0.98);
1103
+ roundedRect(c, bx, y + h - capH, bw, capH, Math.min(r, capH / 2));
1104
+ c.fill();
1105
+ c.restore();
1106
+ }
962
1107
  function fallingNotesLayer() {
963
1108
  let paired = true;
964
1109
  let colorBy = "hand";
965
1110
  let hands = { R: "#7b2436", L: "#c8a55b" };
966
1111
  let hitGlow = true;
1112
+ let glow = false;
967
1113
  let ownLayout = null;
968
1114
  let topY = 0;
969
1115
  let leadMsProp;
@@ -987,6 +1133,7 @@ function fallingNotesLayer() {
987
1133
  colorBy = props.colorBy ?? "hand";
988
1134
  hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
989
1135
  hitGlow = props.hitGlow ?? true;
1136
+ glow = props.glow ?? false;
990
1137
  leadMsProp = props.leadMs;
991
1138
  speedProp = props.speed;
992
1139
  const sb = ctx.safeBox;
@@ -1024,11 +1171,15 @@ function fallingNotesLayer() {
1024
1171
  const drawTop = Math.max(topY, topEdge);
1025
1172
  const drawBottom = Math.min(hit, bottomY);
1026
1173
  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();
1174
+ if (glow) {
1175
+ drawGlowBlock(c, cx - w / 2, drawTop, w, drawBottom - drawTop, fill);
1176
+ } else {
1177
+ c.save();
1178
+ c.fillStyle = fill;
1179
+ c.globalAlpha = 0.92;
1180
+ c.fillRect(cx - w / 2, drawTop, w, Math.max(1, drawBottom - drawTop));
1181
+ c.restore();
1182
+ }
1032
1183
  }
1033
1184
  if (hitGlow) {
1034
1185
  for (const n of ctx.score?.notes ?? []) {
@@ -1066,6 +1217,8 @@ var fallingNotesFactory = {
1066
1217
  errs.push("falling-notes.speed must be a positive number");
1067
1218
  if (p.hitGlow != null && typeof p.hitGlow !== "boolean")
1068
1219
  errs.push("falling-notes.hitGlow must be a boolean");
1220
+ if (p.glow != null && typeof p.glow !== "boolean")
1221
+ errs.push("falling-notes.glow must be a boolean");
1069
1222
  if (p.range != null && p.range !== "88" && p.range !== "auto")
1070
1223
  errs.push('falling-notes.range must be "88" | "auto"');
1071
1224
  return errs;