@real-music-packages/web-core 0.21.0 → 0.23.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/README.md +8 -2
- package/dist/chunk-N56UTMWA.js +24 -0
- package/dist/chunk-N56UTMWA.js.map +1 -0
- package/dist/index.js +4 -19
- package/dist/index.js.map +1 -1
- package/dist/scene/index.d.ts +138 -10
- package/dist/scene/index.js +679 -25
- package/dist/scene/index.js.map +1 -1
- package/package.json +2 -1
package/dist/scene/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
intervalBySemitones
|
|
3
|
+
} from "../chunk-N56UTMWA.js";
|
|
1
4
|
import {
|
|
2
5
|
ctaScene,
|
|
3
6
|
drawSafeGuides,
|
|
@@ -818,25 +821,18 @@ var notationFactory = {
|
|
|
818
821
|
};
|
|
819
822
|
|
|
820
823
|
// src/scene/layers/scrollCursor.ts
|
|
821
|
-
function followLayoutFor(ctx,
|
|
824
|
+
function followLayoutFor(ctx, progress012, scrollMode) {
|
|
822
825
|
const eng = getNotationEngraving(ctx);
|
|
823
826
|
if (!eng) return null;
|
|
824
827
|
const nBars = measureCount(eng.rendered);
|
|
825
828
|
if (nBars <= 0) return eng.base;
|
|
826
|
-
const focusBox = scrollMode === "vstack" ? vstackFollowBox(eng.rendered,
|
|
829
|
+
const focusBox = scrollMode === "vstack" ? vstackFollowBox(eng.rendered, progress012) : followBoxAt(eng.rendered, followWindowStart(eng.rendered, progress012));
|
|
827
830
|
return notationLayout(eng.rendered, ctx.W, ctx.H, eng.bandTop, eng.bandHeight, { focusBox });
|
|
828
831
|
}
|
|
829
832
|
function scrollCursorLayer() {
|
|
830
|
-
let mode = "audio";
|
|
831
833
|
let scrollMode = "hstack";
|
|
832
|
-
let musicMs = 0;
|
|
833
834
|
let openingZoomMs = 900;
|
|
834
835
|
let color;
|
|
835
|
-
function camProgress(tMs) {
|
|
836
|
-
const phMs = tMs - openingZoomMs;
|
|
837
|
-
if (phMs < 0 || musicMs <= 0) return 0;
|
|
838
|
-
return Math.min(1, phMs / musicMs);
|
|
839
|
-
}
|
|
840
836
|
function onsetsFor(ctx) {
|
|
841
837
|
const notes = ctx.score?.notes;
|
|
842
838
|
return notes && notes.length ? distinctOnsets(notes) : [];
|
|
@@ -859,9 +855,7 @@ function scrollCursorLayer() {
|
|
|
859
855
|
return (lo + segFrac) / (n - 1);
|
|
860
856
|
}
|
|
861
857
|
function followProgress(ctx, tMs) {
|
|
862
|
-
|
|
863
|
-
const onsets = onsetsFor(ctx);
|
|
864
|
-
return onsets.length ? notesProgress(onsets, tMs) : camProgress(tMs);
|
|
858
|
+
return notesProgress(onsetsFor(ctx), tMs);
|
|
865
859
|
}
|
|
866
860
|
function layoutAt(ctx, tMs) {
|
|
867
861
|
const eng = getNotationEngraving(ctx);
|
|
@@ -870,9 +864,7 @@ function scrollCursorLayer() {
|
|
|
870
864
|
return {
|
|
871
865
|
key: "scroll-cursor",
|
|
872
866
|
init(ctx, props) {
|
|
873
|
-
mode = props.mode ?? "audio";
|
|
874
867
|
scrollMode = props.scrollMode ?? "hstack";
|
|
875
|
-
musicMs = props.musicMs ?? 0;
|
|
876
868
|
openingZoomMs = props.openingZoomMs ?? 900;
|
|
877
869
|
color = props.color;
|
|
878
870
|
setFollowLayoutProvider(ctx, layoutAt);
|
|
@@ -881,16 +873,12 @@ function scrollCursorLayer() {
|
|
|
881
873
|
const eng = getNotationEngraving(ctx);
|
|
882
874
|
if (!eng) return;
|
|
883
875
|
const layout = layoutAt(ctx, tMs);
|
|
876
|
+
const onsets = onsetsFor(ctx);
|
|
884
877
|
let line;
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if (scrollMode === "vstack") {
|
|
888
|
-
line = vstackAudioPlayheadLine(layout, onsets, tMs, measureCount(eng.rendered));
|
|
889
|
-
} else {
|
|
890
|
-
line = audioPlayheadLine(layout, onsets, tMs);
|
|
891
|
-
}
|
|
878
|
+
if (scrollMode === "vstack") {
|
|
879
|
+
line = vstackAudioPlayheadLine(layout, onsets, tMs, measureCount(eng.rendered));
|
|
892
880
|
} else {
|
|
893
|
-
line =
|
|
881
|
+
line = audioPlayheadLine(layout, onsets, tMs);
|
|
894
882
|
}
|
|
895
883
|
if (!line) return;
|
|
896
884
|
const c = ctx.ctx2d;
|
|
@@ -917,8 +905,6 @@ var scrollCursorFactory = {
|
|
|
917
905
|
errs.push('scroll-cursor.mode must be "audio" | "linear"');
|
|
918
906
|
if (p.scrollMode != null && p.scrollMode !== "hstack" && p.scrollMode !== "vstack")
|
|
919
907
|
errs.push('scroll-cursor.scrollMode must be "hstack" | "vstack"');
|
|
920
|
-
if (p.mode === "linear" && (typeof p.musicMs !== "number" || !(p.musicMs > 0)))
|
|
921
|
-
errs.push('scroll-cursor.musicMs must be a positive number for mode:"linear"');
|
|
922
908
|
if (p.musicMs != null && (typeof p.musicMs !== "number" || !(p.musicMs > 0)))
|
|
923
909
|
errs.push("scroll-cursor.musicMs must be a positive number");
|
|
924
910
|
if (p.followBars != null && (typeof p.followBars !== "number" || p.followBars < 1))
|
|
@@ -3020,6 +3006,657 @@ var sectionMinimapFactory = {
|
|
|
3020
3006
|
}
|
|
3021
3007
|
};
|
|
3022
3008
|
|
|
3009
|
+
// src/scene/layers/beatPulse.ts
|
|
3010
|
+
var DEFAULTS = {
|
|
3011
|
+
styles: ["bloom"],
|
|
3012
|
+
intensity: 0.42,
|
|
3013
|
+
attackMs: 0,
|
|
3014
|
+
decayMs: 170,
|
|
3015
|
+
blend: "source-over"
|
|
3016
|
+
};
|
|
3017
|
+
function envAt(tMs, onsetMs, attackMs, decayMs) {
|
|
3018
|
+
const dt = tMs - onsetMs;
|
|
3019
|
+
if (dt < 0) return 0;
|
|
3020
|
+
if (dt < attackMs) return attackMs <= 0 ? 1 : dt / attackMs;
|
|
3021
|
+
return Math.exp(-(dt - attackMs) / decayMs);
|
|
3022
|
+
}
|
|
3023
|
+
function pulseAt(onsetsMs, tMs, attackMs = DEFAULTS.attackMs, decayMs = DEFAULTS.decayMs) {
|
|
3024
|
+
let p = 0;
|
|
3025
|
+
for (let i = onsetsMs.length - 1; i >= 0; i--) {
|
|
3026
|
+
const on = onsetsMs[i];
|
|
3027
|
+
if (on > tMs) continue;
|
|
3028
|
+
const e = envAt(tMs, on, attackMs, decayMs);
|
|
3029
|
+
if (e > p) p = e;
|
|
3030
|
+
if (tMs - on > attackMs + decayMs * 6) break;
|
|
3031
|
+
}
|
|
3032
|
+
return p;
|
|
3033
|
+
}
|
|
3034
|
+
function rgba2(hex, a) {
|
|
3035
|
+
const h = hex.replace("#", "");
|
|
3036
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3037
|
+
const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
|
|
3038
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
3039
|
+
}
|
|
3040
|
+
function beatPulseLayer() {
|
|
3041
|
+
let onsets = [];
|
|
3042
|
+
let styles = DEFAULTS.styles;
|
|
3043
|
+
let intensity = DEFAULTS.intensity;
|
|
3044
|
+
let attackMs = DEFAULTS.attackMs;
|
|
3045
|
+
let decayMs = DEFAULTS.decayMs;
|
|
3046
|
+
let color;
|
|
3047
|
+
let blend = DEFAULTS.blend;
|
|
3048
|
+
return {
|
|
3049
|
+
key: "beat-pulse",
|
|
3050
|
+
init(ctx, props) {
|
|
3051
|
+
onsets = props.onsetsMs ?? (ctx.score ? distinctOnsets(ctx.score.notes) : []);
|
|
3052
|
+
onsets = [...onsets].sort((a, b) => a - b);
|
|
3053
|
+
styles = props.styles ?? DEFAULTS.styles;
|
|
3054
|
+
intensity = props.intensity ?? DEFAULTS.intensity;
|
|
3055
|
+
attackMs = props.attackMs ?? DEFAULTS.attackMs;
|
|
3056
|
+
decayMs = props.decayMs ?? DEFAULTS.decayMs;
|
|
3057
|
+
color = props.color;
|
|
3058
|
+
blend = props.blend ?? DEFAULTS.blend;
|
|
3059
|
+
},
|
|
3060
|
+
draw(ctx, tMs) {
|
|
3061
|
+
if (!onsets.length) return;
|
|
3062
|
+
const p = pulseAt(onsets, tMs, attackMs, decayMs);
|
|
3063
|
+
if (p <= 4e-3) return;
|
|
3064
|
+
const c = ctx.ctx2d;
|
|
3065
|
+
const W = ctx.W, H = ctx.H;
|
|
3066
|
+
const col = color ?? ctx.theme.accent;
|
|
3067
|
+
const a = intensity * p;
|
|
3068
|
+
c.save();
|
|
3069
|
+
c.globalCompositeOperation = blend;
|
|
3070
|
+
if (styles.includes("bloom")) {
|
|
3071
|
+
const cx = W / 2, cy = H * 0.46;
|
|
3072
|
+
const rad = Math.max(W, H) * (0.3 + 0.1 * p);
|
|
3073
|
+
const g = c.createRadialGradient(cx, cy, 0, cx, cy, rad);
|
|
3074
|
+
g.addColorStop(0, rgba2(col, a * 0.55));
|
|
3075
|
+
g.addColorStop(0.55, rgba2(col, a * 0.18));
|
|
3076
|
+
g.addColorStop(1, rgba2(col, 0));
|
|
3077
|
+
c.fillStyle = g;
|
|
3078
|
+
c.fillRect(0, 0, W, H);
|
|
3079
|
+
}
|
|
3080
|
+
if (styles.includes("vignette")) {
|
|
3081
|
+
const cx = W / 2, cy = H / 2;
|
|
3082
|
+
const inner = Math.min(W, H) * 0.42;
|
|
3083
|
+
const outer = Math.max(W, H) * 0.72;
|
|
3084
|
+
const g = c.createRadialGradient(cx, cy, inner, cx, cy, outer);
|
|
3085
|
+
g.addColorStop(0, rgba2(col, 0));
|
|
3086
|
+
g.addColorStop(1, rgba2(col, a * 0.6));
|
|
3087
|
+
c.fillStyle = g;
|
|
3088
|
+
c.fillRect(0, 0, W, H);
|
|
3089
|
+
}
|
|
3090
|
+
c.restore();
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
var beatPulseFactory = {
|
|
3095
|
+
key: "beat-pulse",
|
|
3096
|
+
create: beatPulseLayer,
|
|
3097
|
+
validateProps(props) {
|
|
3098
|
+
if (props == null || typeof props !== "object") return ["beat-pulse: props must be an object"];
|
|
3099
|
+
const p = props;
|
|
3100
|
+
const errs = [];
|
|
3101
|
+
if (p.onsetsMs != null && (!Array.isArray(p.onsetsMs) || p.onsetsMs.some((x) => typeof x !== "number")))
|
|
3102
|
+
errs.push("beat-pulse.onsetsMs must be a number[]");
|
|
3103
|
+
if (p.styles != null) {
|
|
3104
|
+
if (!Array.isArray(p.styles)) errs.push("beat-pulse.styles must be an array");
|
|
3105
|
+
else if (p.styles.some((s) => s !== "bloom" && s !== "vignette"))
|
|
3106
|
+
errs.push('beat-pulse.styles items must be "bloom" | "vignette"');
|
|
3107
|
+
}
|
|
3108
|
+
if (p.intensity != null && (typeof p.intensity !== "number" || p.intensity < 0 || p.intensity > 1))
|
|
3109
|
+
errs.push("beat-pulse.intensity must be in [0,1]");
|
|
3110
|
+
if (p.attackMs != null && (typeof p.attackMs !== "number" || p.attackMs < 0))
|
|
3111
|
+
errs.push("beat-pulse.attackMs must be a number >= 0");
|
|
3112
|
+
if (p.decayMs != null && (typeof p.decayMs !== "number" || p.decayMs <= 0))
|
|
3113
|
+
errs.push("beat-pulse.decayMs must be a positive number");
|
|
3114
|
+
if (p.color != null && typeof p.color !== "string") errs.push("beat-pulse.color must be a string");
|
|
3115
|
+
if (p.blend != null && p.blend !== "source-over" && p.blend !== "lighter")
|
|
3116
|
+
errs.push('beat-pulse.blend must be "source-over" | "lighter"');
|
|
3117
|
+
return errs;
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
|
|
3121
|
+
// src/scene/layers/chordRibbon.ts
|
|
3122
|
+
var DEFAULTS2 = { pxPerMs: 0.06, playheadFrac: 0.32, height: 84, showFunction: true };
|
|
3123
|
+
function rgba3(hex, a) {
|
|
3124
|
+
const h = hex.replace("#", "");
|
|
3125
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3126
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3127
|
+
}
|
|
3128
|
+
function roundRectPath(c, x, y, w, h, r) {
|
|
3129
|
+
if (typeof c.roundRect === "function") {
|
|
3130
|
+
c.beginPath();
|
|
3131
|
+
c.roundRect(x, y, w, h, r);
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
3135
|
+
c.beginPath();
|
|
3136
|
+
c.moveTo(x + rr, y);
|
|
3137
|
+
c.arcTo(x + w, y, x + w, y + h, rr);
|
|
3138
|
+
c.arcTo(x + w, y + h, x, y + h, rr);
|
|
3139
|
+
c.arcTo(x, y + h, x, y, rr);
|
|
3140
|
+
c.arcTo(x, y, x + w, y, rr);
|
|
3141
|
+
c.closePath();
|
|
3142
|
+
}
|
|
3143
|
+
function chordRibbonLayer() {
|
|
3144
|
+
let track = [];
|
|
3145
|
+
let pxPerMs = DEFAULTS2.pxPerMs;
|
|
3146
|
+
let playheadFrac = DEFAULTS2.playheadFrac;
|
|
3147
|
+
let yProp;
|
|
3148
|
+
let height = DEFAULTS2.height;
|
|
3149
|
+
let showFunction = DEFAULTS2.showFunction;
|
|
3150
|
+
let colors = DEFAULT_FUNCTION_COLORS;
|
|
3151
|
+
return {
|
|
3152
|
+
key: "chord-ribbon",
|
|
3153
|
+
init(_ctx, props) {
|
|
3154
|
+
track = (props.chordTrack ?? []).slice().sort((a, b) => a.startMs - b.startMs);
|
|
3155
|
+
pxPerMs = props.pxPerMs ?? DEFAULTS2.pxPerMs;
|
|
3156
|
+
playheadFrac = props.playheadFrac ?? DEFAULTS2.playheadFrac;
|
|
3157
|
+
yProp = props.y;
|
|
3158
|
+
height = props.height ?? DEFAULTS2.height;
|
|
3159
|
+
showFunction = props.showFunction ?? DEFAULTS2.showFunction;
|
|
3160
|
+
colors = props.colors ?? DEFAULT_FUNCTION_COLORS;
|
|
3161
|
+
},
|
|
3162
|
+
draw(ctx, tMs) {
|
|
3163
|
+
if (!track.length) return;
|
|
3164
|
+
const c = ctx.ctx2d;
|
|
3165
|
+
const sb = ctx.safeBox;
|
|
3166
|
+
const left = sb.left;
|
|
3167
|
+
const right = sb.right;
|
|
3168
|
+
const span = right - left;
|
|
3169
|
+
const playX = left + span * playheadFrac;
|
|
3170
|
+
const y = yProp ?? sb.bottom - 260;
|
|
3171
|
+
const h = height;
|
|
3172
|
+
const pad = 8;
|
|
3173
|
+
c.save();
|
|
3174
|
+
c.beginPath();
|
|
3175
|
+
c.rect(left, y - pad, span, h + pad * 2);
|
|
3176
|
+
c.clip();
|
|
3177
|
+
for (const s of track) {
|
|
3178
|
+
const x0 = playX + (s.startMs - tMs) * pxPerMs;
|
|
3179
|
+
const w = Math.max(36, (s.endMs - s.startMs) * pxPerMs - 6);
|
|
3180
|
+
if (x0 + w < left || x0 > right) continue;
|
|
3181
|
+
const active = tMs >= s.startMs && tMs < s.endMs;
|
|
3182
|
+
const played = tMs >= s.endMs;
|
|
3183
|
+
const fnCol = showFunction ? functionColor(s.fn, colors) : ctx.theme.accent;
|
|
3184
|
+
const chipAlpha = active ? 0.95 : played ? 0.28 : 0.55;
|
|
3185
|
+
roundRectPath(c, x0, y, w, h, 14);
|
|
3186
|
+
c.fillStyle = rgba3(fnCol, chipAlpha);
|
|
3187
|
+
c.fill();
|
|
3188
|
+
if (active) {
|
|
3189
|
+
c.lineWidth = 3;
|
|
3190
|
+
c.strokeStyle = rgba3(ctx.theme.paper ?? "#ffffff", 0.9);
|
|
3191
|
+
c.stroke();
|
|
3192
|
+
}
|
|
3193
|
+
const label = s.label ?? "";
|
|
3194
|
+
if (label) {
|
|
3195
|
+
c.fillStyle = rgba3(ctx.theme.paper ?? "#ffffff", active ? 1 : 0.85);
|
|
3196
|
+
c.font = `${active ? 700 : 600} ${Math.round(h * 0.42)}px ${ctx.theme.fontDisplay}`;
|
|
3197
|
+
c.textAlign = "center";
|
|
3198
|
+
c.textBaseline = "middle";
|
|
3199
|
+
c.fillText(label, x0 + w / 2, y + h / 2);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
c.strokeStyle = rgba3(ctx.theme.gold ?? "#d4af37", 0.95);
|
|
3203
|
+
c.lineWidth = 3;
|
|
3204
|
+
c.beginPath();
|
|
3205
|
+
c.moveTo(playX, y - pad);
|
|
3206
|
+
c.lineTo(playX, y + h + pad);
|
|
3207
|
+
c.stroke();
|
|
3208
|
+
c.restore();
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
var chordRibbonFactory = {
|
|
3213
|
+
key: "chord-ribbon",
|
|
3214
|
+
create: chordRibbonLayer,
|
|
3215
|
+
validateProps(props) {
|
|
3216
|
+
if (props == null || typeof props !== "object") return ["chord-ribbon: props must be an object"];
|
|
3217
|
+
const p = props;
|
|
3218
|
+
const errs = validateChordTrack(p.chordTrack);
|
|
3219
|
+
if (p.pxPerMs != null && (typeof p.pxPerMs !== "number" || p.pxPerMs <= 0))
|
|
3220
|
+
errs.push("chord-ribbon.pxPerMs must be a positive number");
|
|
3221
|
+
if (p.playheadFrac != null && (typeof p.playheadFrac !== "number" || p.playheadFrac < 0 || p.playheadFrac > 1))
|
|
3222
|
+
errs.push("chord-ribbon.playheadFrac must be in [0,1]");
|
|
3223
|
+
if (p.y != null && typeof p.y !== "number") errs.push("chord-ribbon.y must be a number");
|
|
3224
|
+
if (p.height != null && (typeof p.height !== "number" || p.height <= 0))
|
|
3225
|
+
errs.push("chord-ribbon.height must be a positive number");
|
|
3226
|
+
if (p.showFunction != null && typeof p.showFunction !== "boolean")
|
|
3227
|
+
errs.push("chord-ribbon.showFunction must be a boolean");
|
|
3228
|
+
return errs;
|
|
3229
|
+
}
|
|
3230
|
+
};
|
|
3231
|
+
|
|
3232
|
+
// src/scene/layers/progressRing.ts
|
|
3233
|
+
var DEFAULTS3 = { radius: 46, thickness: 9, showCountdown: false };
|
|
3234
|
+
function rgba4(hex, a) {
|
|
3235
|
+
const h = hex.replace("#", "");
|
|
3236
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3237
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3238
|
+
}
|
|
3239
|
+
function progressRingLayer() {
|
|
3240
|
+
let totalMsProp;
|
|
3241
|
+
let radius = DEFAULTS3.radius;
|
|
3242
|
+
let thickness = DEFAULTS3.thickness;
|
|
3243
|
+
let cxProp;
|
|
3244
|
+
let cyProp;
|
|
3245
|
+
let color;
|
|
3246
|
+
let trackColor;
|
|
3247
|
+
let showCountdown = DEFAULTS3.showCountdown;
|
|
3248
|
+
return {
|
|
3249
|
+
key: "progress-ring",
|
|
3250
|
+
init(ctx, props) {
|
|
3251
|
+
totalMsProp = props.totalMs ?? ctx.score?.durationMs;
|
|
3252
|
+
radius = props.radius ?? DEFAULTS3.radius;
|
|
3253
|
+
thickness = props.thickness ?? DEFAULTS3.thickness;
|
|
3254
|
+
cxProp = props.cx;
|
|
3255
|
+
cyProp = props.cy;
|
|
3256
|
+
color = props.color;
|
|
3257
|
+
trackColor = props.trackColor;
|
|
3258
|
+
showCountdown = props.showCountdown ?? DEFAULTS3.showCountdown;
|
|
3259
|
+
},
|
|
3260
|
+
draw(ctx, tMs) {
|
|
3261
|
+
const total = totalMsProp;
|
|
3262
|
+
if (!total || total <= 0) return;
|
|
3263
|
+
const c = ctx.ctx2d;
|
|
3264
|
+
const sb = ctx.safeBox;
|
|
3265
|
+
const cx = cxProp ?? sb.right - radius - 16;
|
|
3266
|
+
const cy = cyProp ?? sb.top + radius + 16;
|
|
3267
|
+
const p = Math.min(1, Math.max(0, tMs / total));
|
|
3268
|
+
const start = -Math.PI / 2;
|
|
3269
|
+
const end = start + p * Math.PI * 2;
|
|
3270
|
+
const fill = color ?? ctx.theme.gold;
|
|
3271
|
+
const track = trackColor ?? rgba4(ctx.theme.paper, 0.22);
|
|
3272
|
+
c.save();
|
|
3273
|
+
c.lineCap = "round";
|
|
3274
|
+
c.beginPath();
|
|
3275
|
+
c.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
3276
|
+
c.strokeStyle = track;
|
|
3277
|
+
c.lineWidth = thickness;
|
|
3278
|
+
c.stroke();
|
|
3279
|
+
if (p > 0) {
|
|
3280
|
+
c.beginPath();
|
|
3281
|
+
c.arc(cx, cy, radius, start, end);
|
|
3282
|
+
c.strokeStyle = fill;
|
|
3283
|
+
c.lineWidth = thickness;
|
|
3284
|
+
c.stroke();
|
|
3285
|
+
}
|
|
3286
|
+
if (showCountdown) {
|
|
3287
|
+
const remain = Math.max(0, Math.ceil((total - tMs) / 1e3));
|
|
3288
|
+
c.fillStyle = ctx.theme.paper;
|
|
3289
|
+
c.font = `700 ${Math.round(radius * 0.7)}px ${ctx.theme.fontDisplay}`;
|
|
3290
|
+
c.textAlign = "center";
|
|
3291
|
+
c.textBaseline = "middle";
|
|
3292
|
+
c.fillText(String(remain), cx, cy + 1);
|
|
3293
|
+
}
|
|
3294
|
+
c.restore();
|
|
3295
|
+
}
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
var progressRingFactory = {
|
|
3299
|
+
key: "progress-ring",
|
|
3300
|
+
create: progressRingLayer,
|
|
3301
|
+
validateProps(props) {
|
|
3302
|
+
if (props == null || typeof props !== "object") return ["progress-ring: props must be an object"];
|
|
3303
|
+
const p = props;
|
|
3304
|
+
const errs = [];
|
|
3305
|
+
if (p.totalMs != null && (typeof p.totalMs !== "number" || p.totalMs <= 0))
|
|
3306
|
+
errs.push("progress-ring.totalMs must be a positive number");
|
|
3307
|
+
if (p.radius != null && (typeof p.radius !== "number" || p.radius <= 0))
|
|
3308
|
+
errs.push("progress-ring.radius must be a positive number");
|
|
3309
|
+
if (p.thickness != null && (typeof p.thickness !== "number" || p.thickness <= 0))
|
|
3310
|
+
errs.push("progress-ring.thickness must be a positive number");
|
|
3311
|
+
if (p.cx != null && typeof p.cx !== "number") errs.push("progress-ring.cx must be a number");
|
|
3312
|
+
if (p.cy != null && typeof p.cy !== "number") errs.push("progress-ring.cy must be a number");
|
|
3313
|
+
if (p.color != null && typeof p.color !== "string") errs.push("progress-ring.color must be a string");
|
|
3314
|
+
if (p.trackColor != null && typeof p.trackColor !== "string") errs.push("progress-ring.trackColor must be a string");
|
|
3315
|
+
if (p.showCountdown != null && typeof p.showCountdown !== "boolean")
|
|
3316
|
+
errs.push("progress-ring.showCountdown must be a boolean");
|
|
3317
|
+
return errs;
|
|
3318
|
+
}
|
|
3319
|
+
};
|
|
3320
|
+
|
|
3321
|
+
// src/scene/layers/radialSpectrum.ts
|
|
3322
|
+
var SPEC_BARS2 = 64;
|
|
3323
|
+
function clamp013(x) {
|
|
3324
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
3325
|
+
}
|
|
3326
|
+
function lerpHex2(a, b, f) {
|
|
3327
|
+
const pa = parseInt(a.slice(1), 16);
|
|
3328
|
+
const pb = parseInt(b.slice(1), 16);
|
|
3329
|
+
const r = Math.round((pa >> 16 & 255) + ((pb >> 16 & 255) - (pa >> 16 & 255)) * f);
|
|
3330
|
+
const g = Math.round((pa >> 8 & 255) + ((pb >> 8 & 255) - (pa >> 8 & 255)) * f);
|
|
3331
|
+
const bl = Math.round((pa & 255) + ((pb & 255) - (pa & 255)) * f);
|
|
3332
|
+
return `rgb(${r},${g},${bl})`;
|
|
3333
|
+
}
|
|
3334
|
+
function syntheticMagnitude2(tSec, b, n) {
|
|
3335
|
+
const phase = tSec * 5 + b * 0.45;
|
|
3336
|
+
let m = 0.22 + 0.16 * Math.sin(phase) + 0.1 * Math.sin(phase * 1.7 + 1.2);
|
|
3337
|
+
m *= 0.5 + 0.5 * Math.sin(b / (n - 1) * Math.PI);
|
|
3338
|
+
return m;
|
|
3339
|
+
}
|
|
3340
|
+
function binByteFreq2(freq, n) {
|
|
3341
|
+
let peak = 0;
|
|
3342
|
+
for (let i = 0; i < freq.length; i++) if (freq[i] > peak) peak = freq[i];
|
|
3343
|
+
if (peak <= 4) return null;
|
|
3344
|
+
const lo = 2, hi = 440;
|
|
3345
|
+
const out = [];
|
|
3346
|
+
for (let b = 0; b < n; b++) {
|
|
3347
|
+
const f0 = lo * Math.pow(hi / lo, b / n);
|
|
3348
|
+
const f1 = lo * Math.pow(hi / lo, (b + 1) / n);
|
|
3349
|
+
const i0 = Math.floor(f0);
|
|
3350
|
+
const i1 = Math.max(i0 + 1, Math.floor(f1));
|
|
3351
|
+
let sum = 0, cnt = 0;
|
|
3352
|
+
for (let i = i0; i < i1 && i < freq.length; i++) {
|
|
3353
|
+
sum += freq[i];
|
|
3354
|
+
cnt++;
|
|
3355
|
+
}
|
|
3356
|
+
let m = cnt ? sum / cnt / 255 : 0;
|
|
3357
|
+
m = Math.pow(m, 0.78);
|
|
3358
|
+
out.push(m);
|
|
3359
|
+
}
|
|
3360
|
+
return out;
|
|
3361
|
+
}
|
|
3362
|
+
function radialSpectrumLayer() {
|
|
3363
|
+
let n = SPEC_BARS2;
|
|
3364
|
+
let cxFrac = 0.5, cyFrac = 0.42, innerFrac = 0.2, maxLenFrac = 0.13;
|
|
3365
|
+
let colorLow, colorHigh;
|
|
3366
|
+
let levelsFn;
|
|
3367
|
+
function magnitudesAt(ctx, tMs) {
|
|
3368
|
+
const fromProp = levelsFn?.(tMs, n);
|
|
3369
|
+
const src = fromProp ?? resolveFromCtx(ctx, tMs);
|
|
3370
|
+
if (src && src.length) {
|
|
3371
|
+
const out = new Array(n);
|
|
3372
|
+
for (let b = 0; b < n; b++) out[b] = clamp013(src[Math.min(src.length - 1, b)] ?? 0);
|
|
3373
|
+
return out;
|
|
3374
|
+
}
|
|
3375
|
+
const tSec = tMs / 1e3;
|
|
3376
|
+
return Array.from({ length: n }, (_, b) => clamp013(syntheticMagnitude2(tSec, b, n)));
|
|
3377
|
+
}
|
|
3378
|
+
function resolveFromCtx(ctx, tMs) {
|
|
3379
|
+
const sp = ctx.spectrum;
|
|
3380
|
+
if (!sp) return null;
|
|
3381
|
+
const lv = sp.levels?.(tMs, n);
|
|
3382
|
+
if (lv && lv.length) return Array.from(lv);
|
|
3383
|
+
const bf = sp.byteFreq?.(tMs);
|
|
3384
|
+
if (bf && bf.length) return binByteFreq2(bf, n);
|
|
3385
|
+
return null;
|
|
3386
|
+
}
|
|
3387
|
+
return {
|
|
3388
|
+
key: "radial-spectrum",
|
|
3389
|
+
init(_ctx, props) {
|
|
3390
|
+
n = props.bars ?? SPEC_BARS2;
|
|
3391
|
+
cxFrac = props.cxFrac ?? 0.5;
|
|
3392
|
+
cyFrac = props.cyFrac ?? 0.42;
|
|
3393
|
+
innerFrac = props.innerFrac ?? 0.2;
|
|
3394
|
+
maxLenFrac = props.maxLenFrac ?? 0.13;
|
|
3395
|
+
colorLow = props.colorLow;
|
|
3396
|
+
colorHigh = props.colorHigh;
|
|
3397
|
+
levelsFn = props.levelsFn;
|
|
3398
|
+
},
|
|
3399
|
+
draw(ctx, tMs) {
|
|
3400
|
+
const c = ctx.ctx2d;
|
|
3401
|
+
const W = ctx.W, H = ctx.H;
|
|
3402
|
+
const cx = W * cxFrac, cy = H * cyFrac;
|
|
3403
|
+
const unit = Math.min(W, H);
|
|
3404
|
+
const inner = unit * innerFrac;
|
|
3405
|
+
const maxLen = unit * maxLenFrac;
|
|
3406
|
+
const lo = colorLow ?? ctx.theme.accent;
|
|
3407
|
+
const hi = colorHigh ?? ctx.theme.gold;
|
|
3408
|
+
const mags = magnitudesAt(ctx, tMs);
|
|
3409
|
+
const barW = Math.max(3, 2 * Math.PI * inner / n * 0.6);
|
|
3410
|
+
c.save();
|
|
3411
|
+
c.lineCap = "round";
|
|
3412
|
+
for (let b = 0; b < n; b++) {
|
|
3413
|
+
const m = mags[b];
|
|
3414
|
+
const ang = b / n * Math.PI * 2 - Math.PI / 2;
|
|
3415
|
+
const len = Math.max(barW * 0.5, m * maxLen);
|
|
3416
|
+
const x0 = cx + Math.cos(ang) * inner;
|
|
3417
|
+
const y0 = cy + Math.sin(ang) * inner;
|
|
3418
|
+
const x1 = cx + Math.cos(ang) * (inner + len);
|
|
3419
|
+
const y1 = cy + Math.sin(ang) * (inner + len);
|
|
3420
|
+
c.strokeStyle = lerpHex2(lo, hi, m);
|
|
3421
|
+
c.lineWidth = barW;
|
|
3422
|
+
c.beginPath();
|
|
3423
|
+
c.moveTo(x0, y0);
|
|
3424
|
+
c.lineTo(x1, y1);
|
|
3425
|
+
c.stroke();
|
|
3426
|
+
}
|
|
3427
|
+
c.restore();
|
|
3428
|
+
}
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
var radialSpectrumFactory = {
|
|
3432
|
+
key: "radial-spectrum",
|
|
3433
|
+
create: radialSpectrumLayer,
|
|
3434
|
+
validateProps(props) {
|
|
3435
|
+
if (props == null || typeof props !== "object") return ["radial-spectrum: props must be an object"];
|
|
3436
|
+
const p = props;
|
|
3437
|
+
const errs = [];
|
|
3438
|
+
if (p.bars != null && (typeof p.bars !== "number" || p.bars < 6)) errs.push("radial-spectrum.bars must be a number >= 6");
|
|
3439
|
+
for (const k of ["cxFrac", "cyFrac", "innerFrac", "maxLenFrac"]) {
|
|
3440
|
+
if (p[k] != null && (typeof p[k] !== "number" || p[k] < 0 || p[k] > 1))
|
|
3441
|
+
errs.push(`radial-spectrum.${k} must be in [0,1]`);
|
|
3442
|
+
}
|
|
3443
|
+
if (p.colorLow != null && typeof p.colorLow !== "string") errs.push("radial-spectrum.colorLow must be a string");
|
|
3444
|
+
if (p.colorHigh != null && typeof p.colorHigh !== "string") errs.push("radial-spectrum.colorHigh must be a string");
|
|
3445
|
+
if (p.levelsFn != null && typeof p.levelsFn !== "function") errs.push("radial-spectrum.levelsFn must be a function");
|
|
3446
|
+
return errs;
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
|
|
3450
|
+
// src/scene/layers/karaokeCaption.ts
|
|
3451
|
+
var DEFAULTS4 = { centerFrac: 0.5, fontPx: 64, activeHoldMs: 320 };
|
|
3452
|
+
function rgba5(hex, a) {
|
|
3453
|
+
const h = hex.replace("#", "");
|
|
3454
|
+
const n = h.length === 3 ? parseInt(h.split("").map((ch) => ch + ch).join(""), 16) : parseInt(h, 16);
|
|
3455
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3456
|
+
}
|
|
3457
|
+
function karaokeCaptionLayer() {
|
|
3458
|
+
let words = [];
|
|
3459
|
+
let centerFrac = DEFAULTS4.centerFrac;
|
|
3460
|
+
let fontPx = DEFAULTS4.fontPx;
|
|
3461
|
+
let sungColor;
|
|
3462
|
+
let upcomingColor;
|
|
3463
|
+
let activeColor;
|
|
3464
|
+
let activeHoldMs = DEFAULTS4.activeHoldMs;
|
|
3465
|
+
return {
|
|
3466
|
+
key: "karaoke-caption",
|
|
3467
|
+
init(_ctx, props) {
|
|
3468
|
+
words = (props.words ?? []).slice().sort((a, b) => a.atMs - b.atMs);
|
|
3469
|
+
centerFrac = props.centerFrac ?? DEFAULTS4.centerFrac;
|
|
3470
|
+
fontPx = props.fontPx ?? DEFAULTS4.fontPx;
|
|
3471
|
+
sungColor = props.sungColor;
|
|
3472
|
+
upcomingColor = props.upcomingColor;
|
|
3473
|
+
activeColor = props.activeColor;
|
|
3474
|
+
activeHoldMs = props.activeHoldMs ?? DEFAULTS4.activeHoldMs;
|
|
3475
|
+
},
|
|
3476
|
+
draw(ctx, tMs) {
|
|
3477
|
+
if (!words.length) return;
|
|
3478
|
+
const c = ctx.ctx2d;
|
|
3479
|
+
const sb = ctx.safeBox;
|
|
3480
|
+
c.save();
|
|
3481
|
+
c.font = `700 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
3482
|
+
c.textBaseline = "middle";
|
|
3483
|
+
c.textAlign = "left";
|
|
3484
|
+
const spaceW = c.measureText(" ").width;
|
|
3485
|
+
const maxW = sb.w;
|
|
3486
|
+
const lines = [];
|
|
3487
|
+
let cur = { items: [], width: 0 };
|
|
3488
|
+
for (const w of words) {
|
|
3489
|
+
const ww = c.measureText(w.text).width;
|
|
3490
|
+
const add = cur.items.length ? spaceW + ww : ww;
|
|
3491
|
+
if (cur.items.length && cur.width + add > maxW) {
|
|
3492
|
+
lines.push(cur);
|
|
3493
|
+
cur = { items: [], width: 0 };
|
|
3494
|
+
}
|
|
3495
|
+
cur.items.push({ w, width: ww });
|
|
3496
|
+
cur.width += cur.items.length === 1 ? ww : spaceW + ww;
|
|
3497
|
+
}
|
|
3498
|
+
if (cur.items.length) lines.push(cur);
|
|
3499
|
+
const lineH = fontPx * 1.3;
|
|
3500
|
+
const totalH = lines.length * lineH;
|
|
3501
|
+
let y = H_center(ctx, centerFrac) - totalH / 2 + lineH / 2;
|
|
3502
|
+
const sung = sungColor ?? ctx.theme.ink;
|
|
3503
|
+
const upcoming = upcomingColor ?? rgba5(ctx.theme.ink, 0.28);
|
|
3504
|
+
const active = activeColor ?? ctx.theme.accent;
|
|
3505
|
+
for (const line of lines) {
|
|
3506
|
+
let x = sb.left + (maxW - line.width) / 2;
|
|
3507
|
+
for (const it of line.items) {
|
|
3508
|
+
const lit = tMs >= it.w.atMs;
|
|
3509
|
+
const fresh = lit && tMs - it.w.atMs < activeHoldMs;
|
|
3510
|
+
c.fillStyle = fresh ? active : lit ? sung : upcoming;
|
|
3511
|
+
c.fillText(it.w.text, x, y);
|
|
3512
|
+
x += it.width + spaceW;
|
|
3513
|
+
}
|
|
3514
|
+
y += lineH;
|
|
3515
|
+
}
|
|
3516
|
+
c.restore();
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
function H_center(ctx, frac) {
|
|
3521
|
+
return ctx.H * frac;
|
|
3522
|
+
}
|
|
3523
|
+
var karaokeCaptionFactory = {
|
|
3524
|
+
key: "karaoke-caption",
|
|
3525
|
+
create: karaokeCaptionLayer,
|
|
3526
|
+
validateProps(props) {
|
|
3527
|
+
if (props == null || typeof props !== "object") return ["karaoke-caption: props must be an object"];
|
|
3528
|
+
const p = props;
|
|
3529
|
+
const errs = [];
|
|
3530
|
+
if (!Array.isArray(p.words)) {
|
|
3531
|
+
errs.push("karaoke-caption.words must be an array of {text, atMs}");
|
|
3532
|
+
} else {
|
|
3533
|
+
p.words.forEach((raw, i) => {
|
|
3534
|
+
const w = raw;
|
|
3535
|
+
if (typeof w?.text !== "string" || typeof w?.atMs !== "number")
|
|
3536
|
+
errs.push(`karaoke-caption.words[${i}] must be {text:string, atMs:number}`);
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1))
|
|
3540
|
+
errs.push("karaoke-caption.centerFrac must be in [0,1]");
|
|
3541
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0))
|
|
3542
|
+
errs.push("karaoke-caption.fontPx must be a positive number");
|
|
3543
|
+
if (p.activeHoldMs != null && (typeof p.activeHoldMs !== "number" || p.activeHoldMs < 0))
|
|
3544
|
+
errs.push("karaoke-caption.activeHoldMs must be a number >= 0");
|
|
3545
|
+
for (const k of ["sungColor", "upcomingColor", "activeColor"]) {
|
|
3546
|
+
if (p[k] != null && typeof p[k] !== "string") errs.push(`karaoke-caption.${k} must be a string`);
|
|
3547
|
+
}
|
|
3548
|
+
return errs;
|
|
3549
|
+
}
|
|
3550
|
+
};
|
|
3551
|
+
|
|
3552
|
+
// src/scene/layers/intervalArcs.ts
|
|
3553
|
+
function intervalLabel(deltaSemis) {
|
|
3554
|
+
const semi = Math.abs(deltaSemis);
|
|
3555
|
+
const within = semi % 12;
|
|
3556
|
+
if (semi > 0 && within === 0) return "P8";
|
|
3557
|
+
return intervalBySemitones(within)?.label ?? `${within}`;
|
|
3558
|
+
}
|
|
3559
|
+
function intervalArcsLayer() {
|
|
3560
|
+
let topProp;
|
|
3561
|
+
let heightProp;
|
|
3562
|
+
let upColor;
|
|
3563
|
+
let downColor;
|
|
3564
|
+
let showLabels = true;
|
|
3565
|
+
let fontPx = 30;
|
|
3566
|
+
let dotRadius = 9;
|
|
3567
|
+
let points = [];
|
|
3568
|
+
return {
|
|
3569
|
+
key: "interval-arcs",
|
|
3570
|
+
init(ctx, props) {
|
|
3571
|
+
topProp = props.top;
|
|
3572
|
+
heightProp = props.height;
|
|
3573
|
+
upColor = props.upColor;
|
|
3574
|
+
downColor = props.downColor;
|
|
3575
|
+
showLabels = props.showLabels ?? true;
|
|
3576
|
+
fontPx = props.fontPx ?? 30;
|
|
3577
|
+
dotRadius = props.dotRadius ?? 9;
|
|
3578
|
+
points = contourPoints(ctx.score?.notes ?? []);
|
|
3579
|
+
},
|
|
3580
|
+
draw(ctx, tMs) {
|
|
3581
|
+
if (points.length < 2) return;
|
|
3582
|
+
const c = ctx.ctx2d;
|
|
3583
|
+
const sb = ctx.safeBox;
|
|
3584
|
+
const top = topProp ?? sb.top + sb.h * 0.18;
|
|
3585
|
+
const height = heightProp ?? sb.h * 0.34;
|
|
3586
|
+
const range = pitchRange(points);
|
|
3587
|
+
const plot = {
|
|
3588
|
+
left: sb.left,
|
|
3589
|
+
right: sb.right,
|
|
3590
|
+
top,
|
|
3591
|
+
bottom: top + height,
|
|
3592
|
+
minPitch: range.min,
|
|
3593
|
+
maxPitch: range.max,
|
|
3594
|
+
durationMs: ctx.score?.durationMs ?? points[points.length - 1].tMs
|
|
3595
|
+
};
|
|
3596
|
+
const up = upColor ?? ctx.theme.accent;
|
|
3597
|
+
const down = downColor ?? ctx.theme.sepia;
|
|
3598
|
+
c.save();
|
|
3599
|
+
c.lineCap = "round";
|
|
3600
|
+
c.font = `700 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
3601
|
+
c.textAlign = "center";
|
|
3602
|
+
c.textBaseline = "bottom";
|
|
3603
|
+
for (let i = 1; i < points.length; i++) {
|
|
3604
|
+
const b = points[i];
|
|
3605
|
+
if (b.tMs > tMs) break;
|
|
3606
|
+
const a = points[i - 1];
|
|
3607
|
+
const pa = projectPoint(plot, a.tMs, a.pitchMidi);
|
|
3608
|
+
const pb = projectPoint(plot, b.tMs, b.pitchMidi);
|
|
3609
|
+
const delta = b.pitchMidi - a.pitchMidi;
|
|
3610
|
+
const ascending = delta >= 0;
|
|
3611
|
+
const col = ascending ? up : down;
|
|
3612
|
+
const midX = (pa.x + pb.x) / 2;
|
|
3613
|
+
const bow = Math.min(70, Math.max(28, Math.abs(pb.x - pa.x) * 0.4));
|
|
3614
|
+
const ctrlY = Math.min(pa.y, pb.y) - bow;
|
|
3615
|
+
c.strokeStyle = col;
|
|
3616
|
+
c.lineWidth = 4;
|
|
3617
|
+
c.globalAlpha = 0.9;
|
|
3618
|
+
c.beginPath();
|
|
3619
|
+
c.moveTo(pa.x, pa.y);
|
|
3620
|
+
c.quadraticCurveTo(midX, ctrlY, pb.x, pb.y);
|
|
3621
|
+
c.stroke();
|
|
3622
|
+
if (showLabels && delta !== 0) {
|
|
3623
|
+
c.globalAlpha = 1;
|
|
3624
|
+
c.fillStyle = col;
|
|
3625
|
+
c.fillText(intervalLabel(delta), midX, ctrlY - 2);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
c.globalAlpha = 1;
|
|
3629
|
+
c.fillStyle = ctx.theme.ink;
|
|
3630
|
+
for (const p of points) {
|
|
3631
|
+
if (p.tMs > tMs) break;
|
|
3632
|
+
const pt = projectPoint(plot, p.tMs, p.pitchMidi);
|
|
3633
|
+
c.beginPath();
|
|
3634
|
+
c.arc(pt.x, pt.y, dotRadius, 0, Math.PI * 2);
|
|
3635
|
+
c.fill();
|
|
3636
|
+
}
|
|
3637
|
+
c.restore();
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
var intervalArcsFactory = {
|
|
3642
|
+
key: "interval-arcs",
|
|
3643
|
+
create: intervalArcsLayer,
|
|
3644
|
+
validateProps(props) {
|
|
3645
|
+
if (props == null || typeof props !== "object") return ["interval-arcs: props must be an object"];
|
|
3646
|
+
const p = props;
|
|
3647
|
+
const errs = [];
|
|
3648
|
+
if (p.top != null && typeof p.top !== "number") errs.push("interval-arcs.top must be a number");
|
|
3649
|
+
if (p.height != null && (typeof p.height !== "number" || p.height <= 0))
|
|
3650
|
+
errs.push("interval-arcs.height must be a positive number");
|
|
3651
|
+
if (p.upColor != null && typeof p.upColor !== "string") errs.push("interval-arcs.upColor must be a string");
|
|
3652
|
+
if (p.downColor != null && typeof p.downColor !== "string") errs.push("interval-arcs.downColor must be a string");
|
|
3653
|
+
if (p.showLabels != null && typeof p.showLabels !== "boolean") errs.push("interval-arcs.showLabels must be a boolean");
|
|
3654
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("interval-arcs.fontPx must be a positive number");
|
|
3655
|
+
if (p.dotRadius != null && (typeof p.dotRadius !== "number" || p.dotRadius <= 0)) errs.push("interval-arcs.dotRadius must be a positive number");
|
|
3656
|
+
return errs;
|
|
3657
|
+
}
|
|
3658
|
+
};
|
|
3659
|
+
|
|
3023
3660
|
// src/scene/registry.ts
|
|
3024
3661
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
3025
3662
|
function registerLayer(factory) {
|
|
@@ -3052,6 +3689,12 @@ registerLayer(mcqCardFactory);
|
|
|
3052
3689
|
registerLayer(circleOfFifthsFactory);
|
|
3053
3690
|
registerLayer(pitchContourFactory);
|
|
3054
3691
|
registerLayer(sectionMinimapFactory);
|
|
3692
|
+
registerLayer(beatPulseFactory);
|
|
3693
|
+
registerLayer(chordRibbonFactory);
|
|
3694
|
+
registerLayer(progressRingFactory);
|
|
3695
|
+
registerLayer(radialSpectrumFactory);
|
|
3696
|
+
registerLayer(karaokeCaptionFactory);
|
|
3697
|
+
registerLayer(intervalArcsFactory);
|
|
3055
3698
|
|
|
3056
3699
|
// src/scene/audioLayers.ts
|
|
3057
3700
|
function countInSchedule(opts) {
|
|
@@ -3204,7 +3847,11 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3204
3847
|
"reveal",
|
|
3205
3848
|
"portrait",
|
|
3206
3849
|
"safe-guides",
|
|
3207
|
-
"mcq-card"
|
|
3850
|
+
"mcq-card",
|
|
3851
|
+
"beat-pulse",
|
|
3852
|
+
"chord-ribbon",
|
|
3853
|
+
"progress-ring",
|
|
3854
|
+
"karaoke-caption"
|
|
3208
3855
|
]);
|
|
3209
3856
|
function defaultBufferFactory() {
|
|
3210
3857
|
const g = globalThis;
|
|
@@ -3803,12 +4450,14 @@ export {
|
|
|
3803
4450
|
ballX,
|
|
3804
4451
|
beatGrid,
|
|
3805
4452
|
beatPhase,
|
|
4453
|
+
beatPulseFactory,
|
|
3806
4454
|
blackKeys,
|
|
3807
4455
|
bpmOf,
|
|
3808
4456
|
brandingFactory,
|
|
3809
4457
|
buildScene,
|
|
3810
4458
|
cameraForFollow,
|
|
3811
4459
|
cameraTransform,
|
|
4460
|
+
chordRibbonFactory,
|
|
3812
4461
|
circleOfFifthsDemoSpec,
|
|
3813
4462
|
circleOfFifthsFactory,
|
|
3814
4463
|
clamp,
|
|
@@ -3857,8 +4506,10 @@ export {
|
|
|
3857
4506
|
hookFactory,
|
|
3858
4507
|
identityCamera,
|
|
3859
4508
|
inRange,
|
|
4509
|
+
intervalArcsFactory,
|
|
3860
4510
|
invLerp,
|
|
3861
4511
|
isBlackKey,
|
|
4512
|
+
karaokeCaptionFactory,
|
|
3862
4513
|
kenBurns,
|
|
3863
4514
|
keyCenterX,
|
|
3864
4515
|
keyColumnWidth,
|
|
@@ -3893,9 +4544,12 @@ export {
|
|
|
3893
4544
|
playheadLine,
|
|
3894
4545
|
portraitFactory,
|
|
3895
4546
|
progress01,
|
|
4547
|
+
progressRingFactory,
|
|
3896
4548
|
projectPoint,
|
|
3897
4549
|
promoCardsDemoSpec,
|
|
4550
|
+
pulseAt,
|
|
3898
4551
|
quizPhase,
|
|
4552
|
+
radialSpectrumFactory,
|
|
3899
4553
|
rayEndpoints,
|
|
3900
4554
|
rayPointAt,
|
|
3901
4555
|
recordSceneSpec,
|