@real-music-packages/web-core 0.22.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 +130 -1
- package/dist/scene/index.js +672 -1
- 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,
|
|
@@ -3003,6 +3006,657 @@ var sectionMinimapFactory = {
|
|
|
3003
3006
|
}
|
|
3004
3007
|
};
|
|
3005
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
|
+
|
|
3006
3660
|
// src/scene/registry.ts
|
|
3007
3661
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
3008
3662
|
function registerLayer(factory) {
|
|
@@ -3035,6 +3689,12 @@ registerLayer(mcqCardFactory);
|
|
|
3035
3689
|
registerLayer(circleOfFifthsFactory);
|
|
3036
3690
|
registerLayer(pitchContourFactory);
|
|
3037
3691
|
registerLayer(sectionMinimapFactory);
|
|
3692
|
+
registerLayer(beatPulseFactory);
|
|
3693
|
+
registerLayer(chordRibbonFactory);
|
|
3694
|
+
registerLayer(progressRingFactory);
|
|
3695
|
+
registerLayer(radialSpectrumFactory);
|
|
3696
|
+
registerLayer(karaokeCaptionFactory);
|
|
3697
|
+
registerLayer(intervalArcsFactory);
|
|
3038
3698
|
|
|
3039
3699
|
// src/scene/audioLayers.ts
|
|
3040
3700
|
function countInSchedule(opts) {
|
|
@@ -3187,7 +3847,11 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3187
3847
|
"reveal",
|
|
3188
3848
|
"portrait",
|
|
3189
3849
|
"safe-guides",
|
|
3190
|
-
"mcq-card"
|
|
3850
|
+
"mcq-card",
|
|
3851
|
+
"beat-pulse",
|
|
3852
|
+
"chord-ribbon",
|
|
3853
|
+
"progress-ring",
|
|
3854
|
+
"karaoke-caption"
|
|
3191
3855
|
]);
|
|
3192
3856
|
function defaultBufferFactory() {
|
|
3193
3857
|
const g = globalThis;
|
|
@@ -3786,12 +4450,14 @@ export {
|
|
|
3786
4450
|
ballX,
|
|
3787
4451
|
beatGrid,
|
|
3788
4452
|
beatPhase,
|
|
4453
|
+
beatPulseFactory,
|
|
3789
4454
|
blackKeys,
|
|
3790
4455
|
bpmOf,
|
|
3791
4456
|
brandingFactory,
|
|
3792
4457
|
buildScene,
|
|
3793
4458
|
cameraForFollow,
|
|
3794
4459
|
cameraTransform,
|
|
4460
|
+
chordRibbonFactory,
|
|
3795
4461
|
circleOfFifthsDemoSpec,
|
|
3796
4462
|
circleOfFifthsFactory,
|
|
3797
4463
|
clamp,
|
|
@@ -3840,8 +4506,10 @@ export {
|
|
|
3840
4506
|
hookFactory,
|
|
3841
4507
|
identityCamera,
|
|
3842
4508
|
inRange,
|
|
4509
|
+
intervalArcsFactory,
|
|
3843
4510
|
invLerp,
|
|
3844
4511
|
isBlackKey,
|
|
4512
|
+
karaokeCaptionFactory,
|
|
3845
4513
|
kenBurns,
|
|
3846
4514
|
keyCenterX,
|
|
3847
4515
|
keyColumnWidth,
|
|
@@ -3876,9 +4544,12 @@ export {
|
|
|
3876
4544
|
playheadLine,
|
|
3877
4545
|
portraitFactory,
|
|
3878
4546
|
progress01,
|
|
4547
|
+
progressRingFactory,
|
|
3879
4548
|
projectPoint,
|
|
3880
4549
|
promoCardsDemoSpec,
|
|
4550
|
+
pulseAt,
|
|
3881
4551
|
quizPhase,
|
|
4552
|
+
radialSpectrumFactory,
|
|
3882
4553
|
rayEndpoints,
|
|
3883
4554
|
rayPointAt,
|
|
3884
4555
|
recordSceneSpec,
|