@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.
- package/dist/scene/index.d.ts +8 -0
- package/dist/scene/index.js +159 -6
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scene/index.d.ts
CHANGED
|
@@ -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. */
|
package/dist/scene/index.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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;
|