@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.
- package/dist/scene/index.d.ts +24 -2
- package/dist/scene/index.js +161 -7
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scene/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
721
|
-
*
|
|
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. */
|
package/dist/scene/index.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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;
|