@real-music-packages/web-core 0.22.0 → 0.24.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/audio.js +5 -3
- package/dist/audio.js.map +1 -1
- package/dist/chunk-25RUSM2X.js +31 -0
- package/dist/chunk-25RUSM2X.js.map +1 -0
- package/dist/{chunk-LLMDQM4C.js → chunk-5ZK4HVY4.js} +4 -28
- package/dist/chunk-5ZK4HVY4.js.map +1 -0
- package/dist/chunk-N56UTMWA.js +24 -0
- package/dist/chunk-N56UTMWA.js.map +1 -0
- package/dist/index.js +12 -25
- package/dist/index.js.map +1 -1
- package/dist/scene/index.d.ts +216 -1
- package/dist/scene/index.js +1096 -1
- package/dist/scene/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-LLMDQM4C.js.map +0 -1
package/dist/scene/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
intervalBySemitones
|
|
3
|
+
} from "../chunk-N56UTMWA.js";
|
|
4
|
+
import {
|
|
5
|
+
noteNameToIndex,
|
|
6
|
+
pitchClass
|
|
7
|
+
} from "../chunk-25RUSM2X.js";
|
|
1
8
|
import {
|
|
2
9
|
ctaScene,
|
|
3
10
|
drawSafeGuides,
|
|
@@ -3003,6 +3010,1066 @@ var sectionMinimapFactory = {
|
|
|
3003
3010
|
}
|
|
3004
3011
|
};
|
|
3005
3012
|
|
|
3013
|
+
// src/scene/layers/beatPulse.ts
|
|
3014
|
+
var DEFAULTS = {
|
|
3015
|
+
styles: ["bloom"],
|
|
3016
|
+
intensity: 0.42,
|
|
3017
|
+
attackMs: 0,
|
|
3018
|
+
decayMs: 170,
|
|
3019
|
+
blend: "source-over"
|
|
3020
|
+
};
|
|
3021
|
+
function envAt(tMs, onsetMs, attackMs, decayMs) {
|
|
3022
|
+
const dt = tMs - onsetMs;
|
|
3023
|
+
if (dt < 0) return 0;
|
|
3024
|
+
if (dt < attackMs) return attackMs <= 0 ? 1 : dt / attackMs;
|
|
3025
|
+
return Math.exp(-(dt - attackMs) / decayMs);
|
|
3026
|
+
}
|
|
3027
|
+
function pulseAt(onsetsMs, tMs, attackMs = DEFAULTS.attackMs, decayMs = DEFAULTS.decayMs) {
|
|
3028
|
+
let p = 0;
|
|
3029
|
+
for (let i = onsetsMs.length - 1; i >= 0; i--) {
|
|
3030
|
+
const on = onsetsMs[i];
|
|
3031
|
+
if (on > tMs) continue;
|
|
3032
|
+
const e = envAt(tMs, on, attackMs, decayMs);
|
|
3033
|
+
if (e > p) p = e;
|
|
3034
|
+
if (tMs - on > attackMs + decayMs * 6) break;
|
|
3035
|
+
}
|
|
3036
|
+
return p;
|
|
3037
|
+
}
|
|
3038
|
+
function rgba2(hex, a) {
|
|
3039
|
+
const h = hex.replace("#", "");
|
|
3040
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3041
|
+
const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
|
|
3042
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
3043
|
+
}
|
|
3044
|
+
function beatPulseLayer() {
|
|
3045
|
+
let onsets = [];
|
|
3046
|
+
let styles = DEFAULTS.styles;
|
|
3047
|
+
let intensity = DEFAULTS.intensity;
|
|
3048
|
+
let attackMs = DEFAULTS.attackMs;
|
|
3049
|
+
let decayMs = DEFAULTS.decayMs;
|
|
3050
|
+
let color;
|
|
3051
|
+
let blend = DEFAULTS.blend;
|
|
3052
|
+
return {
|
|
3053
|
+
key: "beat-pulse",
|
|
3054
|
+
init(ctx, props) {
|
|
3055
|
+
onsets = props.onsetsMs ?? (ctx.score ? distinctOnsets(ctx.score.notes) : []);
|
|
3056
|
+
onsets = [...onsets].sort((a, b) => a - b);
|
|
3057
|
+
styles = props.styles ?? DEFAULTS.styles;
|
|
3058
|
+
intensity = props.intensity ?? DEFAULTS.intensity;
|
|
3059
|
+
attackMs = props.attackMs ?? DEFAULTS.attackMs;
|
|
3060
|
+
decayMs = props.decayMs ?? DEFAULTS.decayMs;
|
|
3061
|
+
color = props.color;
|
|
3062
|
+
blend = props.blend ?? DEFAULTS.blend;
|
|
3063
|
+
},
|
|
3064
|
+
draw(ctx, tMs) {
|
|
3065
|
+
if (!onsets.length) return;
|
|
3066
|
+
const p = pulseAt(onsets, tMs, attackMs, decayMs);
|
|
3067
|
+
if (p <= 4e-3) return;
|
|
3068
|
+
const c = ctx.ctx2d;
|
|
3069
|
+
const W = ctx.W, H = ctx.H;
|
|
3070
|
+
const col = color ?? ctx.theme.accent;
|
|
3071
|
+
const a = intensity * p;
|
|
3072
|
+
c.save();
|
|
3073
|
+
c.globalCompositeOperation = blend;
|
|
3074
|
+
if (styles.includes("bloom")) {
|
|
3075
|
+
const cx = W / 2, cy = H * 0.46;
|
|
3076
|
+
const rad = Math.max(W, H) * (0.3 + 0.1 * p);
|
|
3077
|
+
const g = c.createRadialGradient(cx, cy, 0, cx, cy, rad);
|
|
3078
|
+
g.addColorStop(0, rgba2(col, a * 0.55));
|
|
3079
|
+
g.addColorStop(0.55, rgba2(col, a * 0.18));
|
|
3080
|
+
g.addColorStop(1, rgba2(col, 0));
|
|
3081
|
+
c.fillStyle = g;
|
|
3082
|
+
c.fillRect(0, 0, W, H);
|
|
3083
|
+
}
|
|
3084
|
+
if (styles.includes("vignette")) {
|
|
3085
|
+
const cx = W / 2, cy = H / 2;
|
|
3086
|
+
const inner = Math.min(W, H) * 0.42;
|
|
3087
|
+
const outer = Math.max(W, H) * 0.72;
|
|
3088
|
+
const g = c.createRadialGradient(cx, cy, inner, cx, cy, outer);
|
|
3089
|
+
g.addColorStop(0, rgba2(col, 0));
|
|
3090
|
+
g.addColorStop(1, rgba2(col, a * 0.6));
|
|
3091
|
+
c.fillStyle = g;
|
|
3092
|
+
c.fillRect(0, 0, W, H);
|
|
3093
|
+
}
|
|
3094
|
+
c.restore();
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
var beatPulseFactory = {
|
|
3099
|
+
key: "beat-pulse",
|
|
3100
|
+
create: beatPulseLayer,
|
|
3101
|
+
validateProps(props) {
|
|
3102
|
+
if (props == null || typeof props !== "object") return ["beat-pulse: props must be an object"];
|
|
3103
|
+
const p = props;
|
|
3104
|
+
const errs = [];
|
|
3105
|
+
if (p.onsetsMs != null && (!Array.isArray(p.onsetsMs) || p.onsetsMs.some((x) => typeof x !== "number")))
|
|
3106
|
+
errs.push("beat-pulse.onsetsMs must be a number[]");
|
|
3107
|
+
if (p.styles != null) {
|
|
3108
|
+
if (!Array.isArray(p.styles)) errs.push("beat-pulse.styles must be an array");
|
|
3109
|
+
else if (p.styles.some((s) => s !== "bloom" && s !== "vignette"))
|
|
3110
|
+
errs.push('beat-pulse.styles items must be "bloom" | "vignette"');
|
|
3111
|
+
}
|
|
3112
|
+
if (p.intensity != null && (typeof p.intensity !== "number" || p.intensity < 0 || p.intensity > 1))
|
|
3113
|
+
errs.push("beat-pulse.intensity must be in [0,1]");
|
|
3114
|
+
if (p.attackMs != null && (typeof p.attackMs !== "number" || p.attackMs < 0))
|
|
3115
|
+
errs.push("beat-pulse.attackMs must be a number >= 0");
|
|
3116
|
+
if (p.decayMs != null && (typeof p.decayMs !== "number" || p.decayMs <= 0))
|
|
3117
|
+
errs.push("beat-pulse.decayMs must be a positive number");
|
|
3118
|
+
if (p.color != null && typeof p.color !== "string") errs.push("beat-pulse.color must be a string");
|
|
3119
|
+
if (p.blend != null && p.blend !== "source-over" && p.blend !== "lighter")
|
|
3120
|
+
errs.push('beat-pulse.blend must be "source-over" | "lighter"');
|
|
3121
|
+
return errs;
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
|
|
3125
|
+
// src/scene/layers/chordRibbon.ts
|
|
3126
|
+
var DEFAULTS2 = { pxPerMs: 0.06, playheadFrac: 0.32, height: 84, showFunction: true };
|
|
3127
|
+
function rgba3(hex, a) {
|
|
3128
|
+
const h = hex.replace("#", "");
|
|
3129
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3130
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3131
|
+
}
|
|
3132
|
+
function roundRectPath(c, x, y, w, h, r) {
|
|
3133
|
+
if (typeof c.roundRect === "function") {
|
|
3134
|
+
c.beginPath();
|
|
3135
|
+
c.roundRect(x, y, w, h, r);
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
3139
|
+
c.beginPath();
|
|
3140
|
+
c.moveTo(x + rr, y);
|
|
3141
|
+
c.arcTo(x + w, y, x + w, y + h, rr);
|
|
3142
|
+
c.arcTo(x + w, y + h, x, y + h, rr);
|
|
3143
|
+
c.arcTo(x, y + h, x, y, rr);
|
|
3144
|
+
c.arcTo(x, y, x + w, y, rr);
|
|
3145
|
+
c.closePath();
|
|
3146
|
+
}
|
|
3147
|
+
function chordRibbonLayer() {
|
|
3148
|
+
let track = [];
|
|
3149
|
+
let pxPerMs = DEFAULTS2.pxPerMs;
|
|
3150
|
+
let playheadFrac = DEFAULTS2.playheadFrac;
|
|
3151
|
+
let yProp;
|
|
3152
|
+
let height = DEFAULTS2.height;
|
|
3153
|
+
let showFunction = DEFAULTS2.showFunction;
|
|
3154
|
+
let colors = DEFAULT_FUNCTION_COLORS;
|
|
3155
|
+
return {
|
|
3156
|
+
key: "chord-ribbon",
|
|
3157
|
+
init(_ctx, props) {
|
|
3158
|
+
track = (props.chordTrack ?? []).slice().sort((a, b) => a.startMs - b.startMs);
|
|
3159
|
+
pxPerMs = props.pxPerMs ?? DEFAULTS2.pxPerMs;
|
|
3160
|
+
playheadFrac = props.playheadFrac ?? DEFAULTS2.playheadFrac;
|
|
3161
|
+
yProp = props.y;
|
|
3162
|
+
height = props.height ?? DEFAULTS2.height;
|
|
3163
|
+
showFunction = props.showFunction ?? DEFAULTS2.showFunction;
|
|
3164
|
+
colors = props.colors ?? DEFAULT_FUNCTION_COLORS;
|
|
3165
|
+
},
|
|
3166
|
+
draw(ctx, tMs) {
|
|
3167
|
+
if (!track.length) return;
|
|
3168
|
+
const c = ctx.ctx2d;
|
|
3169
|
+
const sb = ctx.safeBox;
|
|
3170
|
+
const left = sb.left;
|
|
3171
|
+
const right = sb.right;
|
|
3172
|
+
const span = right - left;
|
|
3173
|
+
const playX = left + span * playheadFrac;
|
|
3174
|
+
const y = yProp ?? sb.bottom - 260;
|
|
3175
|
+
const h = height;
|
|
3176
|
+
const pad = 8;
|
|
3177
|
+
c.save();
|
|
3178
|
+
c.beginPath();
|
|
3179
|
+
c.rect(left, y - pad, span, h + pad * 2);
|
|
3180
|
+
c.clip();
|
|
3181
|
+
for (const s of track) {
|
|
3182
|
+
const x0 = playX + (s.startMs - tMs) * pxPerMs;
|
|
3183
|
+
const w = Math.max(36, (s.endMs - s.startMs) * pxPerMs - 6);
|
|
3184
|
+
if (x0 + w < left || x0 > right) continue;
|
|
3185
|
+
const active = tMs >= s.startMs && tMs < s.endMs;
|
|
3186
|
+
const played = tMs >= s.endMs;
|
|
3187
|
+
const fnCol = showFunction ? functionColor(s.fn, colors) : ctx.theme.accent;
|
|
3188
|
+
const chipAlpha = active ? 0.95 : played ? 0.28 : 0.55;
|
|
3189
|
+
roundRectPath(c, x0, y, w, h, 14);
|
|
3190
|
+
c.fillStyle = rgba3(fnCol, chipAlpha);
|
|
3191
|
+
c.fill();
|
|
3192
|
+
if (active) {
|
|
3193
|
+
c.lineWidth = 3;
|
|
3194
|
+
c.strokeStyle = rgba3(ctx.theme.paper ?? "#ffffff", 0.9);
|
|
3195
|
+
c.stroke();
|
|
3196
|
+
}
|
|
3197
|
+
const label = s.label ?? "";
|
|
3198
|
+
if (label) {
|
|
3199
|
+
c.fillStyle = rgba3(ctx.theme.paper ?? "#ffffff", active ? 1 : 0.85);
|
|
3200
|
+
c.font = `${active ? 700 : 600} ${Math.round(h * 0.42)}px ${ctx.theme.fontDisplay}`;
|
|
3201
|
+
c.textAlign = "center";
|
|
3202
|
+
c.textBaseline = "middle";
|
|
3203
|
+
c.fillText(label, x0 + w / 2, y + h / 2);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
c.strokeStyle = rgba3(ctx.theme.gold ?? "#d4af37", 0.95);
|
|
3207
|
+
c.lineWidth = 3;
|
|
3208
|
+
c.beginPath();
|
|
3209
|
+
c.moveTo(playX, y - pad);
|
|
3210
|
+
c.lineTo(playX, y + h + pad);
|
|
3211
|
+
c.stroke();
|
|
3212
|
+
c.restore();
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
var chordRibbonFactory = {
|
|
3217
|
+
key: "chord-ribbon",
|
|
3218
|
+
create: chordRibbonLayer,
|
|
3219
|
+
validateProps(props) {
|
|
3220
|
+
if (props == null || typeof props !== "object") return ["chord-ribbon: props must be an object"];
|
|
3221
|
+
const p = props;
|
|
3222
|
+
const errs = validateChordTrack(p.chordTrack);
|
|
3223
|
+
if (p.pxPerMs != null && (typeof p.pxPerMs !== "number" || p.pxPerMs <= 0))
|
|
3224
|
+
errs.push("chord-ribbon.pxPerMs must be a positive number");
|
|
3225
|
+
if (p.playheadFrac != null && (typeof p.playheadFrac !== "number" || p.playheadFrac < 0 || p.playheadFrac > 1))
|
|
3226
|
+
errs.push("chord-ribbon.playheadFrac must be in [0,1]");
|
|
3227
|
+
if (p.y != null && typeof p.y !== "number") errs.push("chord-ribbon.y must be a number");
|
|
3228
|
+
if (p.height != null && (typeof p.height !== "number" || p.height <= 0))
|
|
3229
|
+
errs.push("chord-ribbon.height must be a positive number");
|
|
3230
|
+
if (p.showFunction != null && typeof p.showFunction !== "boolean")
|
|
3231
|
+
errs.push("chord-ribbon.showFunction must be a boolean");
|
|
3232
|
+
return errs;
|
|
3233
|
+
}
|
|
3234
|
+
};
|
|
3235
|
+
|
|
3236
|
+
// src/scene/layers/progressRing.ts
|
|
3237
|
+
var DEFAULTS3 = { radius: 46, thickness: 9, showCountdown: false };
|
|
3238
|
+
function rgba4(hex, a) {
|
|
3239
|
+
const h = hex.replace("#", "");
|
|
3240
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3241
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3242
|
+
}
|
|
3243
|
+
function progressRingLayer() {
|
|
3244
|
+
let totalMsProp;
|
|
3245
|
+
let radius = DEFAULTS3.radius;
|
|
3246
|
+
let thickness = DEFAULTS3.thickness;
|
|
3247
|
+
let cxProp;
|
|
3248
|
+
let cyProp;
|
|
3249
|
+
let color;
|
|
3250
|
+
let trackColor;
|
|
3251
|
+
let showCountdown = DEFAULTS3.showCountdown;
|
|
3252
|
+
return {
|
|
3253
|
+
key: "progress-ring",
|
|
3254
|
+
init(ctx, props) {
|
|
3255
|
+
totalMsProp = props.totalMs ?? ctx.score?.durationMs;
|
|
3256
|
+
radius = props.radius ?? DEFAULTS3.radius;
|
|
3257
|
+
thickness = props.thickness ?? DEFAULTS3.thickness;
|
|
3258
|
+
cxProp = props.cx;
|
|
3259
|
+
cyProp = props.cy;
|
|
3260
|
+
color = props.color;
|
|
3261
|
+
trackColor = props.trackColor;
|
|
3262
|
+
showCountdown = props.showCountdown ?? DEFAULTS3.showCountdown;
|
|
3263
|
+
},
|
|
3264
|
+
draw(ctx, tMs) {
|
|
3265
|
+
const total = totalMsProp;
|
|
3266
|
+
if (!total || total <= 0) return;
|
|
3267
|
+
const c = ctx.ctx2d;
|
|
3268
|
+
const sb = ctx.safeBox;
|
|
3269
|
+
const cx = cxProp ?? sb.right - radius - 16;
|
|
3270
|
+
const cy = cyProp ?? sb.top + radius + 16;
|
|
3271
|
+
const p = Math.min(1, Math.max(0, tMs / total));
|
|
3272
|
+
const start = -Math.PI / 2;
|
|
3273
|
+
const end = start + p * Math.PI * 2;
|
|
3274
|
+
const fill = color ?? ctx.theme.gold;
|
|
3275
|
+
const track = trackColor ?? rgba4(ctx.theme.paper, 0.22);
|
|
3276
|
+
c.save();
|
|
3277
|
+
c.lineCap = "round";
|
|
3278
|
+
c.beginPath();
|
|
3279
|
+
c.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
3280
|
+
c.strokeStyle = track;
|
|
3281
|
+
c.lineWidth = thickness;
|
|
3282
|
+
c.stroke();
|
|
3283
|
+
if (p > 0) {
|
|
3284
|
+
c.beginPath();
|
|
3285
|
+
c.arc(cx, cy, radius, start, end);
|
|
3286
|
+
c.strokeStyle = fill;
|
|
3287
|
+
c.lineWidth = thickness;
|
|
3288
|
+
c.stroke();
|
|
3289
|
+
}
|
|
3290
|
+
if (showCountdown) {
|
|
3291
|
+
const remain = Math.max(0, Math.ceil((total - tMs) / 1e3));
|
|
3292
|
+
c.fillStyle = ctx.theme.paper;
|
|
3293
|
+
c.font = `700 ${Math.round(radius * 0.7)}px ${ctx.theme.fontDisplay}`;
|
|
3294
|
+
c.textAlign = "center";
|
|
3295
|
+
c.textBaseline = "middle";
|
|
3296
|
+
c.fillText(String(remain), cx, cy + 1);
|
|
3297
|
+
}
|
|
3298
|
+
c.restore();
|
|
3299
|
+
}
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
var progressRingFactory = {
|
|
3303
|
+
key: "progress-ring",
|
|
3304
|
+
create: progressRingLayer,
|
|
3305
|
+
validateProps(props) {
|
|
3306
|
+
if (props == null || typeof props !== "object") return ["progress-ring: props must be an object"];
|
|
3307
|
+
const p = props;
|
|
3308
|
+
const errs = [];
|
|
3309
|
+
if (p.totalMs != null && (typeof p.totalMs !== "number" || p.totalMs <= 0))
|
|
3310
|
+
errs.push("progress-ring.totalMs must be a positive number");
|
|
3311
|
+
if (p.radius != null && (typeof p.radius !== "number" || p.radius <= 0))
|
|
3312
|
+
errs.push("progress-ring.radius must be a positive number");
|
|
3313
|
+
if (p.thickness != null && (typeof p.thickness !== "number" || p.thickness <= 0))
|
|
3314
|
+
errs.push("progress-ring.thickness must be a positive number");
|
|
3315
|
+
if (p.cx != null && typeof p.cx !== "number") errs.push("progress-ring.cx must be a number");
|
|
3316
|
+
if (p.cy != null && typeof p.cy !== "number") errs.push("progress-ring.cy must be a number");
|
|
3317
|
+
if (p.color != null && typeof p.color !== "string") errs.push("progress-ring.color must be a string");
|
|
3318
|
+
if (p.trackColor != null && typeof p.trackColor !== "string") errs.push("progress-ring.trackColor must be a string");
|
|
3319
|
+
if (p.showCountdown != null && typeof p.showCountdown !== "boolean")
|
|
3320
|
+
errs.push("progress-ring.showCountdown must be a boolean");
|
|
3321
|
+
return errs;
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
|
|
3325
|
+
// src/scene/layers/radialSpectrum.ts
|
|
3326
|
+
var SPEC_BARS2 = 64;
|
|
3327
|
+
function clamp013(x) {
|
|
3328
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
3329
|
+
}
|
|
3330
|
+
function lerpHex2(a, b, f) {
|
|
3331
|
+
const pa = parseInt(a.slice(1), 16);
|
|
3332
|
+
const pb = parseInt(b.slice(1), 16);
|
|
3333
|
+
const r = Math.round((pa >> 16 & 255) + ((pb >> 16 & 255) - (pa >> 16 & 255)) * f);
|
|
3334
|
+
const g = Math.round((pa >> 8 & 255) + ((pb >> 8 & 255) - (pa >> 8 & 255)) * f);
|
|
3335
|
+
const bl = Math.round((pa & 255) + ((pb & 255) - (pa & 255)) * f);
|
|
3336
|
+
return `rgb(${r},${g},${bl})`;
|
|
3337
|
+
}
|
|
3338
|
+
function syntheticMagnitude2(tSec, b, n) {
|
|
3339
|
+
const phase = tSec * 5 + b * 0.45;
|
|
3340
|
+
let m = 0.22 + 0.16 * Math.sin(phase) + 0.1 * Math.sin(phase * 1.7 + 1.2);
|
|
3341
|
+
m *= 0.5 + 0.5 * Math.sin(b / (n - 1) * Math.PI);
|
|
3342
|
+
return m;
|
|
3343
|
+
}
|
|
3344
|
+
function binByteFreq2(freq, n) {
|
|
3345
|
+
let peak = 0;
|
|
3346
|
+
for (let i = 0; i < freq.length; i++) if (freq[i] > peak) peak = freq[i];
|
|
3347
|
+
if (peak <= 4) return null;
|
|
3348
|
+
const lo = 2, hi = 440;
|
|
3349
|
+
const out = [];
|
|
3350
|
+
for (let b = 0; b < n; b++) {
|
|
3351
|
+
const f0 = lo * Math.pow(hi / lo, b / n);
|
|
3352
|
+
const f1 = lo * Math.pow(hi / lo, (b + 1) / n);
|
|
3353
|
+
const i0 = Math.floor(f0);
|
|
3354
|
+
const i1 = Math.max(i0 + 1, Math.floor(f1));
|
|
3355
|
+
let sum = 0, cnt = 0;
|
|
3356
|
+
for (let i = i0; i < i1 && i < freq.length; i++) {
|
|
3357
|
+
sum += freq[i];
|
|
3358
|
+
cnt++;
|
|
3359
|
+
}
|
|
3360
|
+
let m = cnt ? sum / cnt / 255 : 0;
|
|
3361
|
+
m = Math.pow(m, 0.78);
|
|
3362
|
+
out.push(m);
|
|
3363
|
+
}
|
|
3364
|
+
return out;
|
|
3365
|
+
}
|
|
3366
|
+
function radialSpectrumLayer() {
|
|
3367
|
+
let n = SPEC_BARS2;
|
|
3368
|
+
let cxFrac = 0.5, cyFrac = 0.42, innerFrac = 0.2, maxLenFrac = 0.13;
|
|
3369
|
+
let colorLow, colorHigh;
|
|
3370
|
+
let levelsFn;
|
|
3371
|
+
function magnitudesAt(ctx, tMs) {
|
|
3372
|
+
const fromProp = levelsFn?.(tMs, n);
|
|
3373
|
+
const src = fromProp ?? resolveFromCtx(ctx, tMs);
|
|
3374
|
+
if (src && src.length) {
|
|
3375
|
+
const out = new Array(n);
|
|
3376
|
+
for (let b = 0; b < n; b++) out[b] = clamp013(src[Math.min(src.length - 1, b)] ?? 0);
|
|
3377
|
+
return out;
|
|
3378
|
+
}
|
|
3379
|
+
const tSec = tMs / 1e3;
|
|
3380
|
+
return Array.from({ length: n }, (_, b) => clamp013(syntheticMagnitude2(tSec, b, n)));
|
|
3381
|
+
}
|
|
3382
|
+
function resolveFromCtx(ctx, tMs) {
|
|
3383
|
+
const sp = ctx.spectrum;
|
|
3384
|
+
if (!sp) return null;
|
|
3385
|
+
const lv = sp.levels?.(tMs, n);
|
|
3386
|
+
if (lv && lv.length) return Array.from(lv);
|
|
3387
|
+
const bf = sp.byteFreq?.(tMs);
|
|
3388
|
+
if (bf && bf.length) return binByteFreq2(bf, n);
|
|
3389
|
+
return null;
|
|
3390
|
+
}
|
|
3391
|
+
return {
|
|
3392
|
+
key: "radial-spectrum",
|
|
3393
|
+
init(_ctx, props) {
|
|
3394
|
+
n = props.bars ?? SPEC_BARS2;
|
|
3395
|
+
cxFrac = props.cxFrac ?? 0.5;
|
|
3396
|
+
cyFrac = props.cyFrac ?? 0.42;
|
|
3397
|
+
innerFrac = props.innerFrac ?? 0.2;
|
|
3398
|
+
maxLenFrac = props.maxLenFrac ?? 0.13;
|
|
3399
|
+
colorLow = props.colorLow;
|
|
3400
|
+
colorHigh = props.colorHigh;
|
|
3401
|
+
levelsFn = props.levelsFn;
|
|
3402
|
+
},
|
|
3403
|
+
draw(ctx, tMs) {
|
|
3404
|
+
const c = ctx.ctx2d;
|
|
3405
|
+
const W = ctx.W, H = ctx.H;
|
|
3406
|
+
const cx = W * cxFrac, cy = H * cyFrac;
|
|
3407
|
+
const unit = Math.min(W, H);
|
|
3408
|
+
const inner = unit * innerFrac;
|
|
3409
|
+
const maxLen = unit * maxLenFrac;
|
|
3410
|
+
const lo = colorLow ?? ctx.theme.accent;
|
|
3411
|
+
const hi = colorHigh ?? ctx.theme.gold;
|
|
3412
|
+
const mags = magnitudesAt(ctx, tMs);
|
|
3413
|
+
const barW = Math.max(3, 2 * Math.PI * inner / n * 0.6);
|
|
3414
|
+
c.save();
|
|
3415
|
+
c.lineCap = "round";
|
|
3416
|
+
for (let b = 0; b < n; b++) {
|
|
3417
|
+
const m = mags[b];
|
|
3418
|
+
const ang = b / n * Math.PI * 2 - Math.PI / 2;
|
|
3419
|
+
const len = Math.max(barW * 0.5, m * maxLen);
|
|
3420
|
+
const x0 = cx + Math.cos(ang) * inner;
|
|
3421
|
+
const y0 = cy + Math.sin(ang) * inner;
|
|
3422
|
+
const x1 = cx + Math.cos(ang) * (inner + len);
|
|
3423
|
+
const y1 = cy + Math.sin(ang) * (inner + len);
|
|
3424
|
+
c.strokeStyle = lerpHex2(lo, hi, m);
|
|
3425
|
+
c.lineWidth = barW;
|
|
3426
|
+
c.beginPath();
|
|
3427
|
+
c.moveTo(x0, y0);
|
|
3428
|
+
c.lineTo(x1, y1);
|
|
3429
|
+
c.stroke();
|
|
3430
|
+
}
|
|
3431
|
+
c.restore();
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
var radialSpectrumFactory = {
|
|
3436
|
+
key: "radial-spectrum",
|
|
3437
|
+
create: radialSpectrumLayer,
|
|
3438
|
+
validateProps(props) {
|
|
3439
|
+
if (props == null || typeof props !== "object") return ["radial-spectrum: props must be an object"];
|
|
3440
|
+
const p = props;
|
|
3441
|
+
const errs = [];
|
|
3442
|
+
if (p.bars != null && (typeof p.bars !== "number" || p.bars < 6)) errs.push("radial-spectrum.bars must be a number >= 6");
|
|
3443
|
+
for (const k of ["cxFrac", "cyFrac", "innerFrac", "maxLenFrac"]) {
|
|
3444
|
+
if (p[k] != null && (typeof p[k] !== "number" || p[k] < 0 || p[k] > 1))
|
|
3445
|
+
errs.push(`radial-spectrum.${k} must be in [0,1]`);
|
|
3446
|
+
}
|
|
3447
|
+
if (p.colorLow != null && typeof p.colorLow !== "string") errs.push("radial-spectrum.colorLow must be a string");
|
|
3448
|
+
if (p.colorHigh != null && typeof p.colorHigh !== "string") errs.push("radial-spectrum.colorHigh must be a string");
|
|
3449
|
+
if (p.levelsFn != null && typeof p.levelsFn !== "function") errs.push("radial-spectrum.levelsFn must be a function");
|
|
3450
|
+
return errs;
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
|
|
3454
|
+
// src/scene/layers/karaokeCaption.ts
|
|
3455
|
+
var DEFAULTS4 = { centerFrac: 0.5, fontPx: 64, activeHoldMs: 320 };
|
|
3456
|
+
function rgba5(hex, a) {
|
|
3457
|
+
const h = hex.replace("#", "");
|
|
3458
|
+
const n = h.length === 3 ? parseInt(h.split("").map((ch) => ch + ch).join(""), 16) : parseInt(h, 16);
|
|
3459
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3460
|
+
}
|
|
3461
|
+
function karaokeCaptionLayer() {
|
|
3462
|
+
let words = [];
|
|
3463
|
+
let centerFrac = DEFAULTS4.centerFrac;
|
|
3464
|
+
let fontPx = DEFAULTS4.fontPx;
|
|
3465
|
+
let sungColor;
|
|
3466
|
+
let upcomingColor;
|
|
3467
|
+
let activeColor;
|
|
3468
|
+
let activeHoldMs = DEFAULTS4.activeHoldMs;
|
|
3469
|
+
return {
|
|
3470
|
+
key: "karaoke-caption",
|
|
3471
|
+
init(_ctx, props) {
|
|
3472
|
+
words = (props.words ?? []).slice().sort((a, b) => a.atMs - b.atMs);
|
|
3473
|
+
centerFrac = props.centerFrac ?? DEFAULTS4.centerFrac;
|
|
3474
|
+
fontPx = props.fontPx ?? DEFAULTS4.fontPx;
|
|
3475
|
+
sungColor = props.sungColor;
|
|
3476
|
+
upcomingColor = props.upcomingColor;
|
|
3477
|
+
activeColor = props.activeColor;
|
|
3478
|
+
activeHoldMs = props.activeHoldMs ?? DEFAULTS4.activeHoldMs;
|
|
3479
|
+
},
|
|
3480
|
+
draw(ctx, tMs) {
|
|
3481
|
+
if (!words.length) return;
|
|
3482
|
+
const c = ctx.ctx2d;
|
|
3483
|
+
const sb = ctx.safeBox;
|
|
3484
|
+
c.save();
|
|
3485
|
+
c.font = `700 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
3486
|
+
c.textBaseline = "middle";
|
|
3487
|
+
c.textAlign = "left";
|
|
3488
|
+
const spaceW = c.measureText(" ").width;
|
|
3489
|
+
const maxW = sb.w;
|
|
3490
|
+
const lines = [];
|
|
3491
|
+
let cur = { items: [], width: 0 };
|
|
3492
|
+
for (const w of words) {
|
|
3493
|
+
const ww = c.measureText(w.text).width;
|
|
3494
|
+
const add = cur.items.length ? spaceW + ww : ww;
|
|
3495
|
+
if (cur.items.length && cur.width + add > maxW) {
|
|
3496
|
+
lines.push(cur);
|
|
3497
|
+
cur = { items: [], width: 0 };
|
|
3498
|
+
}
|
|
3499
|
+
cur.items.push({ w, width: ww });
|
|
3500
|
+
cur.width += cur.items.length === 1 ? ww : spaceW + ww;
|
|
3501
|
+
}
|
|
3502
|
+
if (cur.items.length) lines.push(cur);
|
|
3503
|
+
const lineH = fontPx * 1.3;
|
|
3504
|
+
const totalH = lines.length * lineH;
|
|
3505
|
+
let y = H_center(ctx, centerFrac) - totalH / 2 + lineH / 2;
|
|
3506
|
+
const sung = sungColor ?? ctx.theme.ink;
|
|
3507
|
+
const upcoming = upcomingColor ?? rgba5(ctx.theme.ink, 0.28);
|
|
3508
|
+
const active = activeColor ?? ctx.theme.accent;
|
|
3509
|
+
for (const line of lines) {
|
|
3510
|
+
let x = sb.left + (maxW - line.width) / 2;
|
|
3511
|
+
for (const it of line.items) {
|
|
3512
|
+
const lit = tMs >= it.w.atMs;
|
|
3513
|
+
const fresh = lit && tMs - it.w.atMs < activeHoldMs;
|
|
3514
|
+
c.fillStyle = fresh ? active : lit ? sung : upcoming;
|
|
3515
|
+
c.fillText(it.w.text, x, y);
|
|
3516
|
+
x += it.width + spaceW;
|
|
3517
|
+
}
|
|
3518
|
+
y += lineH;
|
|
3519
|
+
}
|
|
3520
|
+
c.restore();
|
|
3521
|
+
}
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
function H_center(ctx, frac) {
|
|
3525
|
+
return ctx.H * frac;
|
|
3526
|
+
}
|
|
3527
|
+
var karaokeCaptionFactory = {
|
|
3528
|
+
key: "karaoke-caption",
|
|
3529
|
+
create: karaokeCaptionLayer,
|
|
3530
|
+
validateProps(props) {
|
|
3531
|
+
if (props == null || typeof props !== "object") return ["karaoke-caption: props must be an object"];
|
|
3532
|
+
const p = props;
|
|
3533
|
+
const errs = [];
|
|
3534
|
+
if (!Array.isArray(p.words)) {
|
|
3535
|
+
errs.push("karaoke-caption.words must be an array of {text, atMs}");
|
|
3536
|
+
} else {
|
|
3537
|
+
p.words.forEach((raw, i) => {
|
|
3538
|
+
const w = raw;
|
|
3539
|
+
if (typeof w?.text !== "string" || typeof w?.atMs !== "number")
|
|
3540
|
+
errs.push(`karaoke-caption.words[${i}] must be {text:string, atMs:number}`);
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1))
|
|
3544
|
+
errs.push("karaoke-caption.centerFrac must be in [0,1]");
|
|
3545
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0))
|
|
3546
|
+
errs.push("karaoke-caption.fontPx must be a positive number");
|
|
3547
|
+
if (p.activeHoldMs != null && (typeof p.activeHoldMs !== "number" || p.activeHoldMs < 0))
|
|
3548
|
+
errs.push("karaoke-caption.activeHoldMs must be a number >= 0");
|
|
3549
|
+
for (const k of ["sungColor", "upcomingColor", "activeColor"]) {
|
|
3550
|
+
if (p[k] != null && typeof p[k] !== "string") errs.push(`karaoke-caption.${k} must be a string`);
|
|
3551
|
+
}
|
|
3552
|
+
return errs;
|
|
3553
|
+
}
|
|
3554
|
+
};
|
|
3555
|
+
|
|
3556
|
+
// src/scene/layers/intervalArcs.ts
|
|
3557
|
+
function intervalLabel(deltaSemis) {
|
|
3558
|
+
const semi = Math.abs(deltaSemis);
|
|
3559
|
+
const within = semi % 12;
|
|
3560
|
+
if (semi > 0 && within === 0) return "P8";
|
|
3561
|
+
return intervalBySemitones(within)?.label ?? `${within}`;
|
|
3562
|
+
}
|
|
3563
|
+
function intervalArcsLayer() {
|
|
3564
|
+
let topProp;
|
|
3565
|
+
let heightProp;
|
|
3566
|
+
let upColor;
|
|
3567
|
+
let downColor;
|
|
3568
|
+
let showLabels = true;
|
|
3569
|
+
let fontPx = 30;
|
|
3570
|
+
let dotRadius = 9;
|
|
3571
|
+
let points = [];
|
|
3572
|
+
return {
|
|
3573
|
+
key: "interval-arcs",
|
|
3574
|
+
init(ctx, props) {
|
|
3575
|
+
topProp = props.top;
|
|
3576
|
+
heightProp = props.height;
|
|
3577
|
+
upColor = props.upColor;
|
|
3578
|
+
downColor = props.downColor;
|
|
3579
|
+
showLabels = props.showLabels ?? true;
|
|
3580
|
+
fontPx = props.fontPx ?? 30;
|
|
3581
|
+
dotRadius = props.dotRadius ?? 9;
|
|
3582
|
+
points = contourPoints(ctx.score?.notes ?? []);
|
|
3583
|
+
},
|
|
3584
|
+
draw(ctx, tMs) {
|
|
3585
|
+
if (points.length < 2) return;
|
|
3586
|
+
const c = ctx.ctx2d;
|
|
3587
|
+
const sb = ctx.safeBox;
|
|
3588
|
+
const top = topProp ?? sb.top + sb.h * 0.18;
|
|
3589
|
+
const height = heightProp ?? sb.h * 0.34;
|
|
3590
|
+
const range = pitchRange(points);
|
|
3591
|
+
const plot = {
|
|
3592
|
+
left: sb.left,
|
|
3593
|
+
right: sb.right,
|
|
3594
|
+
top,
|
|
3595
|
+
bottom: top + height,
|
|
3596
|
+
minPitch: range.min,
|
|
3597
|
+
maxPitch: range.max,
|
|
3598
|
+
durationMs: ctx.score?.durationMs ?? points[points.length - 1].tMs
|
|
3599
|
+
};
|
|
3600
|
+
const up = upColor ?? ctx.theme.accent;
|
|
3601
|
+
const down = downColor ?? ctx.theme.sepia;
|
|
3602
|
+
c.save();
|
|
3603
|
+
c.lineCap = "round";
|
|
3604
|
+
c.font = `700 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
3605
|
+
c.textAlign = "center";
|
|
3606
|
+
c.textBaseline = "bottom";
|
|
3607
|
+
for (let i = 1; i < points.length; i++) {
|
|
3608
|
+
const b = points[i];
|
|
3609
|
+
if (b.tMs > tMs) break;
|
|
3610
|
+
const a = points[i - 1];
|
|
3611
|
+
const pa = projectPoint(plot, a.tMs, a.pitchMidi);
|
|
3612
|
+
const pb = projectPoint(plot, b.tMs, b.pitchMidi);
|
|
3613
|
+
const delta = b.pitchMidi - a.pitchMidi;
|
|
3614
|
+
const ascending = delta >= 0;
|
|
3615
|
+
const col = ascending ? up : down;
|
|
3616
|
+
const midX = (pa.x + pb.x) / 2;
|
|
3617
|
+
const bow = Math.min(70, Math.max(28, Math.abs(pb.x - pa.x) * 0.4));
|
|
3618
|
+
const ctrlY = Math.min(pa.y, pb.y) - bow;
|
|
3619
|
+
c.strokeStyle = col;
|
|
3620
|
+
c.lineWidth = 4;
|
|
3621
|
+
c.globalAlpha = 0.9;
|
|
3622
|
+
c.beginPath();
|
|
3623
|
+
c.moveTo(pa.x, pa.y);
|
|
3624
|
+
c.quadraticCurveTo(midX, ctrlY, pb.x, pb.y);
|
|
3625
|
+
c.stroke();
|
|
3626
|
+
if (showLabels && delta !== 0) {
|
|
3627
|
+
c.globalAlpha = 1;
|
|
3628
|
+
c.fillStyle = col;
|
|
3629
|
+
c.fillText(intervalLabel(delta), midX, ctrlY - 2);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
c.globalAlpha = 1;
|
|
3633
|
+
c.fillStyle = ctx.theme.ink;
|
|
3634
|
+
for (const p of points) {
|
|
3635
|
+
if (p.tMs > tMs) break;
|
|
3636
|
+
const pt = projectPoint(plot, p.tMs, p.pitchMidi);
|
|
3637
|
+
c.beginPath();
|
|
3638
|
+
c.arc(pt.x, pt.y, dotRadius, 0, Math.PI * 2);
|
|
3639
|
+
c.fill();
|
|
3640
|
+
}
|
|
3641
|
+
c.restore();
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
var intervalArcsFactory = {
|
|
3646
|
+
key: "interval-arcs",
|
|
3647
|
+
create: intervalArcsLayer,
|
|
3648
|
+
validateProps(props) {
|
|
3649
|
+
if (props == null || typeof props !== "object") return ["interval-arcs: props must be an object"];
|
|
3650
|
+
const p = props;
|
|
3651
|
+
const errs = [];
|
|
3652
|
+
if (p.top != null && typeof p.top !== "number") errs.push("interval-arcs.top must be a number");
|
|
3653
|
+
if (p.height != null && (typeof p.height !== "number" || p.height <= 0))
|
|
3654
|
+
errs.push("interval-arcs.height must be a positive number");
|
|
3655
|
+
if (p.upColor != null && typeof p.upColor !== "string") errs.push("interval-arcs.upColor must be a string");
|
|
3656
|
+
if (p.downColor != null && typeof p.downColor !== "string") errs.push("interval-arcs.downColor must be a string");
|
|
3657
|
+
if (p.showLabels != null && typeof p.showLabels !== "boolean") errs.push("interval-arcs.showLabels must be a boolean");
|
|
3658
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("interval-arcs.fontPx must be a positive number");
|
|
3659
|
+
if (p.dotRadius != null && (typeof p.dotRadius !== "number" || p.dotRadius <= 0)) errs.push("interval-arcs.dotRadius must be a positive number");
|
|
3660
|
+
return errs;
|
|
3661
|
+
}
|
|
3662
|
+
};
|
|
3663
|
+
|
|
3664
|
+
// src/scene/layers/kineticText.ts
|
|
3665
|
+
var DEFAULTS5 = { mode: "word-pop", startMs: 0, revealMs: 1400, fontPx: 88, centerFrac: 0.42, cursor: true };
|
|
3666
|
+
function clamp014(x) {
|
|
3667
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
3668
|
+
}
|
|
3669
|
+
function wrap2(c, words, maxW) {
|
|
3670
|
+
const lines = [];
|
|
3671
|
+
let cur = [];
|
|
3672
|
+
let curW = 0;
|
|
3673
|
+
const spaceW = c.measureText(" ").width;
|
|
3674
|
+
for (const w of words) {
|
|
3675
|
+
const ww = c.measureText(w).width;
|
|
3676
|
+
const add = cur.length ? spaceW + ww : ww;
|
|
3677
|
+
if (cur.length && curW + add > maxW) {
|
|
3678
|
+
lines.push(cur);
|
|
3679
|
+
cur = [];
|
|
3680
|
+
curW = 0;
|
|
3681
|
+
}
|
|
3682
|
+
cur.push(w);
|
|
3683
|
+
curW += cur.length === 1 ? ww : spaceW + ww;
|
|
3684
|
+
}
|
|
3685
|
+
if (cur.length) lines.push(cur);
|
|
3686
|
+
return lines;
|
|
3687
|
+
}
|
|
3688
|
+
function kineticTextLayer() {
|
|
3689
|
+
let text = "";
|
|
3690
|
+
let mode = DEFAULTS5.mode;
|
|
3691
|
+
let startMs = DEFAULTS5.startMs;
|
|
3692
|
+
let revealMs = DEFAULTS5.revealMs;
|
|
3693
|
+
let fontPx = DEFAULTS5.fontPx;
|
|
3694
|
+
let color;
|
|
3695
|
+
let centerFrac = DEFAULTS5.centerFrac;
|
|
3696
|
+
let cursor = DEFAULTS5.cursor;
|
|
3697
|
+
return {
|
|
3698
|
+
key: "kinetic-text",
|
|
3699
|
+
init(_ctx, props) {
|
|
3700
|
+
text = props.text ?? "";
|
|
3701
|
+
mode = props.mode ?? DEFAULTS5.mode;
|
|
3702
|
+
startMs = props.startMs ?? DEFAULTS5.startMs;
|
|
3703
|
+
revealMs = props.revealMs ?? DEFAULTS5.revealMs;
|
|
3704
|
+
fontPx = props.fontPx ?? DEFAULTS5.fontPx;
|
|
3705
|
+
color = props.color;
|
|
3706
|
+
centerFrac = props.centerFrac ?? DEFAULTS5.centerFrac;
|
|
3707
|
+
cursor = props.cursor ?? DEFAULTS5.cursor;
|
|
3708
|
+
},
|
|
3709
|
+
draw(ctx, tMs) {
|
|
3710
|
+
if (!text) return;
|
|
3711
|
+
const c = ctx.ctx2d;
|
|
3712
|
+
const sb = ctx.safeBox;
|
|
3713
|
+
const col = color ?? ctx.theme.ink;
|
|
3714
|
+
c.save();
|
|
3715
|
+
c.font = `700 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
3716
|
+
c.textBaseline = "middle";
|
|
3717
|
+
const rawLines = text.split("\n");
|
|
3718
|
+
const lines = [];
|
|
3719
|
+
for (const rl of rawLines) lines.push(...wrap2(c, rl.split(/\s+/).filter(Boolean), sb.w));
|
|
3720
|
+
const lineH = fontPx * 1.25;
|
|
3721
|
+
const totalH = lines.length * lineH;
|
|
3722
|
+
const top = ctx.H * centerFrac - totalH / 2 + lineH / 2;
|
|
3723
|
+
const p = clamp014((tMs - startMs) / Math.max(1, revealMs));
|
|
3724
|
+
if (mode === "typewriter") {
|
|
3725
|
+
const full = lines.map((l) => l.join(" "));
|
|
3726
|
+
const totalChars = full.reduce((n, l) => n + l.length, 0);
|
|
3727
|
+
let shown = Math.floor(p * totalChars);
|
|
3728
|
+
c.textAlign = "center";
|
|
3729
|
+
c.fillStyle = col;
|
|
3730
|
+
let y = top;
|
|
3731
|
+
let lastDrawn = { x: 0, y: 0, end: false };
|
|
3732
|
+
for (const line of full) {
|
|
3733
|
+
const take = Math.max(0, Math.min(line.length, shown));
|
|
3734
|
+
const sub = line.slice(0, take);
|
|
3735
|
+
if (sub) c.fillText(sub, ctx.W / 2, y);
|
|
3736
|
+
shown -= line.length;
|
|
3737
|
+
if (take < line.length && take >= 0 && !lastDrawn.end) {
|
|
3738
|
+
lastDrawn = { x: ctx.W / 2 + c.measureText(sub).width / 2, y, end: true };
|
|
3739
|
+
}
|
|
3740
|
+
y += lineH;
|
|
3741
|
+
}
|
|
3742
|
+
if (cursor && p < 1 && Math.floor(tMs / 450) % 2 === 0) {
|
|
3743
|
+
c.fillRect(lastDrawn.x + 6, lastDrawn.y - fontPx * 0.42, 6, fontPx * 0.8);
|
|
3744
|
+
}
|
|
3745
|
+
} else {
|
|
3746
|
+
const allWords = lines.flat();
|
|
3747
|
+
const n = allWords.length || 1;
|
|
3748
|
+
const stagger = revealMs / n;
|
|
3749
|
+
const POP = 280;
|
|
3750
|
+
c.textAlign = "left";
|
|
3751
|
+
const spaceW = c.measureText(" ").width;
|
|
3752
|
+
let wi = 0;
|
|
3753
|
+
let y = top;
|
|
3754
|
+
for (const line of lines) {
|
|
3755
|
+
const lineW = line.reduce((w, word, i) => w + c.measureText(word).width + (i ? spaceW : 0), 0);
|
|
3756
|
+
let x = sb.left + (sb.w - lineW) / 2;
|
|
3757
|
+
for (const word of line) {
|
|
3758
|
+
const ww = c.measureText(word).width;
|
|
3759
|
+
const wp = clamp014((tMs - startMs - wi * stagger) / POP);
|
|
3760
|
+
if (wp > 0) {
|
|
3761
|
+
const scale = 0.7 + 0.3 * wp;
|
|
3762
|
+
c.save();
|
|
3763
|
+
c.globalAlpha = wp;
|
|
3764
|
+
c.translate(x + ww / 2, y);
|
|
3765
|
+
c.scale(scale, scale);
|
|
3766
|
+
c.fillStyle = col;
|
|
3767
|
+
c.fillText(word, -ww / 2, 0);
|
|
3768
|
+
c.restore();
|
|
3769
|
+
}
|
|
3770
|
+
x += ww + spaceW;
|
|
3771
|
+
wi++;
|
|
3772
|
+
}
|
|
3773
|
+
y += lineH;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
c.restore();
|
|
3777
|
+
}
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
var kineticTextFactory = {
|
|
3781
|
+
key: "kinetic-text",
|
|
3782
|
+
create: kineticTextLayer,
|
|
3783
|
+
validateProps(props) {
|
|
3784
|
+
if (props == null || typeof props !== "object") return ["kinetic-text: props must be an object"];
|
|
3785
|
+
const p = props;
|
|
3786
|
+
const errs = [];
|
|
3787
|
+
if (typeof p.text !== "string" || !p.text) errs.push("kinetic-text.text must be a non-empty string");
|
|
3788
|
+
if (p.mode != null && p.mode !== "typewriter" && p.mode !== "word-pop") errs.push('kinetic-text.mode must be "typewriter" | "word-pop"');
|
|
3789
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("kinetic-text.startMs must be a number");
|
|
3790
|
+
if (p.revealMs != null && (typeof p.revealMs !== "number" || p.revealMs <= 0)) errs.push("kinetic-text.revealMs must be a positive number");
|
|
3791
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("kinetic-text.fontPx must be a positive number");
|
|
3792
|
+
if (p.color != null && typeof p.color !== "string") errs.push("kinetic-text.color must be a string");
|
|
3793
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("kinetic-text.centerFrac must be in [0,1]");
|
|
3794
|
+
if (p.cursor != null && typeof p.cursor !== "boolean") errs.push("kinetic-text.cursor must be a boolean");
|
|
3795
|
+
return errs;
|
|
3796
|
+
}
|
|
3797
|
+
};
|
|
3798
|
+
|
|
3799
|
+
// src/scene/layers/countdown.ts
|
|
3800
|
+
var DEFAULTS6 = { from: 3, startMs: 0, durationMs: 3e3, fontPx: 280, centerFrac: 0.46, ring: true };
|
|
3801
|
+
function rgba6(hex, a) {
|
|
3802
|
+
const h = hex.replace("#", "");
|
|
3803
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3804
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3805
|
+
}
|
|
3806
|
+
function countdownLayer() {
|
|
3807
|
+
let from = DEFAULTS6.from;
|
|
3808
|
+
let startMs = DEFAULTS6.startMs;
|
|
3809
|
+
let durationMs = DEFAULTS6.durationMs;
|
|
3810
|
+
let color;
|
|
3811
|
+
let fontPx = DEFAULTS6.fontPx;
|
|
3812
|
+
let centerFrac = DEFAULTS6.centerFrac;
|
|
3813
|
+
let ring = DEFAULTS6.ring;
|
|
3814
|
+
return {
|
|
3815
|
+
key: "countdown",
|
|
3816
|
+
init(_ctx, props) {
|
|
3817
|
+
from = props.from ?? DEFAULTS6.from;
|
|
3818
|
+
startMs = props.startMs ?? DEFAULTS6.startMs;
|
|
3819
|
+
durationMs = props.durationMs ?? DEFAULTS6.durationMs;
|
|
3820
|
+
color = props.color;
|
|
3821
|
+
fontPx = props.fontPx ?? DEFAULTS6.fontPx;
|
|
3822
|
+
centerFrac = props.centerFrac ?? DEFAULTS6.centerFrac;
|
|
3823
|
+
ring = props.ring ?? DEFAULTS6.ring;
|
|
3824
|
+
},
|
|
3825
|
+
draw(ctx, tMs) {
|
|
3826
|
+
const elapsed = tMs - startMs;
|
|
3827
|
+
if (elapsed < 0 || elapsed >= durationMs) return;
|
|
3828
|
+
const perTick = durationMs / from;
|
|
3829
|
+
const idx = Math.floor(elapsed / perTick);
|
|
3830
|
+
const n = from - idx;
|
|
3831
|
+
if (n < 1 || n > from) return;
|
|
3832
|
+
const tickElapsed = elapsed - idx * perTick;
|
|
3833
|
+
const tickFrac = tickElapsed / perTick;
|
|
3834
|
+
const c = ctx.ctx2d;
|
|
3835
|
+
const cx = ctx.W / 2;
|
|
3836
|
+
const cy = ctx.H * centerFrac;
|
|
3837
|
+
const col = color ?? ctx.theme.accent;
|
|
3838
|
+
const pop = 1 + 0.28 * Math.exp(-tickElapsed / 120);
|
|
3839
|
+
const fade = 1 - Math.max(0, (tickFrac - 0.7) / 0.3) * 0.85;
|
|
3840
|
+
c.save();
|
|
3841
|
+
if (ring) {
|
|
3842
|
+
const r = fontPx * 0.62;
|
|
3843
|
+
c.lineWidth = 10;
|
|
3844
|
+
c.lineCap = "round";
|
|
3845
|
+
c.strokeStyle = rgba6(col, 0.18 * fade);
|
|
3846
|
+
c.beginPath();
|
|
3847
|
+
c.arc(cx, cy, r, 0, Math.PI * 2);
|
|
3848
|
+
c.stroke();
|
|
3849
|
+
c.strokeStyle = rgba6(col, 0.9 * fade);
|
|
3850
|
+
c.beginPath();
|
|
3851
|
+
c.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + (1 - tickFrac) * Math.PI * 2);
|
|
3852
|
+
c.stroke();
|
|
3853
|
+
}
|
|
3854
|
+
c.globalAlpha = fade;
|
|
3855
|
+
c.fillStyle = col;
|
|
3856
|
+
c.font = `800 ${Math.round(fontPx * pop)}px ${ctx.theme.fontDisplay}`;
|
|
3857
|
+
c.textAlign = "center";
|
|
3858
|
+
c.textBaseline = "middle";
|
|
3859
|
+
c.fillText(String(n), cx, cy + 2);
|
|
3860
|
+
c.restore();
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
var countdownFactory = {
|
|
3865
|
+
key: "countdown",
|
|
3866
|
+
create: countdownLayer,
|
|
3867
|
+
validateProps(props) {
|
|
3868
|
+
if (props == null || typeof props !== "object") return ["countdown: props must be an object"];
|
|
3869
|
+
const p = props;
|
|
3870
|
+
const errs = [];
|
|
3871
|
+
if (p.from != null && (typeof p.from !== "number" || p.from < 1 || !Number.isInteger(p.from))) errs.push("countdown.from must be an integer >= 1");
|
|
3872
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("countdown.startMs must be a number");
|
|
3873
|
+
if (p.durationMs != null && (typeof p.durationMs !== "number" || p.durationMs <= 0)) errs.push("countdown.durationMs must be a positive number");
|
|
3874
|
+
if (p.color != null && typeof p.color !== "string") errs.push("countdown.color must be a string");
|
|
3875
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("countdown.fontPx must be a positive number");
|
|
3876
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("countdown.centerFrac must be in [0,1]");
|
|
3877
|
+
if (p.ring != null && typeof p.ring !== "boolean") errs.push("countdown.ring must be a boolean");
|
|
3878
|
+
return errs;
|
|
3879
|
+
}
|
|
3880
|
+
};
|
|
3881
|
+
|
|
3882
|
+
// src/scene/layers/waveform.ts
|
|
3883
|
+
var SAMPLES = 128;
|
|
3884
|
+
function clampUnit(x) {
|
|
3885
|
+
return x < -1 ? -1 : x > 1 ? 1 : x;
|
|
3886
|
+
}
|
|
3887
|
+
function syntheticSample(tSec, u) {
|
|
3888
|
+
const phase = u * Math.PI * 2;
|
|
3889
|
+
return 0.55 * Math.sin(phase * 3 + tSec * 6) + 0.28 * Math.sin(phase * 5 + tSec * 9 + 1.1) + 0.14 * Math.sin(phase * 8 + tSec * 4 + 2.3);
|
|
3890
|
+
}
|
|
3891
|
+
function waveformLayer() {
|
|
3892
|
+
let n = SAMPLES;
|
|
3893
|
+
let centerFrac = 0.46;
|
|
3894
|
+
let amplitudeFrac = 0.1;
|
|
3895
|
+
let lineWidth = 5;
|
|
3896
|
+
let color;
|
|
3897
|
+
let samplesFn;
|
|
3898
|
+
function samplesAt(ctx, tMs) {
|
|
3899
|
+
const fromProp = samplesFn?.(tMs, n);
|
|
3900
|
+
if (fromProp && fromProp.length) {
|
|
3901
|
+
return Array.from({ length: n }, (_, i) => clampUnit(fromProp[Math.min(fromProp.length - 1, i)] ?? 0));
|
|
3902
|
+
}
|
|
3903
|
+
const bt = ctx.waveform?.byteTime?.(tMs);
|
|
3904
|
+
if (bt && bt.length) {
|
|
3905
|
+
return Array.from({ length: n }, (_, i) => clampUnit((bt[Math.floor(i / n * bt.length)] - 128) / 128));
|
|
3906
|
+
}
|
|
3907
|
+
const tSec = tMs / 1e3;
|
|
3908
|
+
return Array.from({ length: n }, (_, i) => clampUnit(syntheticSample(tSec, i / (n - 1))));
|
|
3909
|
+
}
|
|
3910
|
+
return {
|
|
3911
|
+
key: "waveform",
|
|
3912
|
+
init(_ctx, props) {
|
|
3913
|
+
n = props.samples ?? SAMPLES;
|
|
3914
|
+
centerFrac = props.centerFrac ?? 0.46;
|
|
3915
|
+
amplitudeFrac = props.amplitudeFrac ?? 0.1;
|
|
3916
|
+
lineWidth = props.lineWidth ?? 5;
|
|
3917
|
+
color = props.color;
|
|
3918
|
+
samplesFn = props.samplesFn;
|
|
3919
|
+
},
|
|
3920
|
+
draw(ctx, tMs) {
|
|
3921
|
+
const c = ctx.ctx2d;
|
|
3922
|
+
const sb = ctx.safeBox;
|
|
3923
|
+
const left = sb.left, right = sb.right;
|
|
3924
|
+
const cy = ctx.H * centerFrac;
|
|
3925
|
+
const amp = ctx.H * amplitudeFrac;
|
|
3926
|
+
const s = samplesAt(ctx, tMs);
|
|
3927
|
+
c.save();
|
|
3928
|
+
c.lineJoin = "round";
|
|
3929
|
+
c.lineCap = "round";
|
|
3930
|
+
c.strokeStyle = color ?? ctx.theme.accent;
|
|
3931
|
+
c.lineWidth = lineWidth;
|
|
3932
|
+
c.beginPath();
|
|
3933
|
+
for (let i = 0; i < n; i++) {
|
|
3934
|
+
const x = left + (right - left) * (i / (n - 1));
|
|
3935
|
+
const y = cy + s[i] * amp;
|
|
3936
|
+
if (i === 0) c.moveTo(x, y);
|
|
3937
|
+
else c.lineTo(x, y);
|
|
3938
|
+
}
|
|
3939
|
+
c.stroke();
|
|
3940
|
+
c.restore();
|
|
3941
|
+
}
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
var waveformFactory = {
|
|
3945
|
+
key: "waveform",
|
|
3946
|
+
create: waveformLayer,
|
|
3947
|
+
validateProps(props) {
|
|
3948
|
+
if (props == null || typeof props !== "object") return ["waveform: props must be an object"];
|
|
3949
|
+
const p = props;
|
|
3950
|
+
const errs = [];
|
|
3951
|
+
if (p.samples != null && (typeof p.samples !== "number" || p.samples < 8)) errs.push("waveform.samples must be a number >= 8");
|
|
3952
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("waveform.centerFrac must be in [0,1]");
|
|
3953
|
+
if (p.amplitudeFrac != null && (typeof p.amplitudeFrac !== "number" || p.amplitudeFrac <= 0)) errs.push("waveform.amplitudeFrac must be a positive number");
|
|
3954
|
+
if (p.lineWidth != null && (typeof p.lineWidth !== "number" || p.lineWidth <= 0)) errs.push("waveform.lineWidth must be a positive number");
|
|
3955
|
+
if (p.color != null && typeof p.color !== "string") errs.push("waveform.color must be a string");
|
|
3956
|
+
if (p.samplesFn != null && typeof p.samplesFn !== "function") errs.push("waveform.samplesFn must be a function");
|
|
3957
|
+
return errs;
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
|
|
3961
|
+
// src/scene/layers/scaleHighlight.ts
|
|
3962
|
+
var SCALE_INTERVALS = {
|
|
3963
|
+
major: [0, 2, 4, 5, 7, 9, 11],
|
|
3964
|
+
ionian: [0, 2, 4, 5, 7, 9, 11],
|
|
3965
|
+
minor: [0, 2, 3, 5, 7, 8, 10],
|
|
3966
|
+
aeolian: [0, 2, 3, 5, 7, 8, 10],
|
|
3967
|
+
dorian: [0, 2, 3, 5, 7, 9, 10],
|
|
3968
|
+
phrygian: [0, 1, 3, 5, 7, 8, 10],
|
|
3969
|
+
lydian: [0, 2, 4, 6, 7, 9, 11],
|
|
3970
|
+
mixolydian: [0, 2, 4, 5, 7, 9, 10],
|
|
3971
|
+
locrian: [0, 1, 3, 5, 6, 8, 10],
|
|
3972
|
+
harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
|
|
3973
|
+
melodicMinor: [0, 2, 3, 5, 7, 9, 11],
|
|
3974
|
+
pentatonicMajor: [0, 2, 4, 7, 9],
|
|
3975
|
+
pentatonicMinor: [0, 3, 5, 7, 10],
|
|
3976
|
+
blues: [0, 3, 5, 6, 7, 10],
|
|
3977
|
+
chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
|
3978
|
+
};
|
|
3979
|
+
var PRETTY = {
|
|
3980
|
+
major: "Major",
|
|
3981
|
+
ionian: "Ionian",
|
|
3982
|
+
minor: "Minor",
|
|
3983
|
+
aeolian: "Aeolian",
|
|
3984
|
+
dorian: "Dorian",
|
|
3985
|
+
phrygian: "Phrygian",
|
|
3986
|
+
lydian: "Lydian",
|
|
3987
|
+
mixolydian: "Mixolydian",
|
|
3988
|
+
locrian: "Locrian",
|
|
3989
|
+
harmonicMinor: "Harmonic Minor",
|
|
3990
|
+
melodicMinor: "Melodic Minor",
|
|
3991
|
+
pentatonicMajor: "Pentatonic",
|
|
3992
|
+
pentatonicMinor: "Minor Pentatonic",
|
|
3993
|
+
blues: "Blues",
|
|
3994
|
+
chromatic: "Chromatic"
|
|
3995
|
+
};
|
|
3996
|
+
function rootPc(root) {
|
|
3997
|
+
if (typeof root === "number") return (root % 12 + 12) % 12;
|
|
3998
|
+
return noteNameToIndex(root);
|
|
3999
|
+
}
|
|
4000
|
+
function scaleHighlightLayer() {
|
|
4001
|
+
let rootProp = "C";
|
|
4002
|
+
let scaleKey = "major";
|
|
4003
|
+
let scaleColor;
|
|
4004
|
+
let tonicColor;
|
|
4005
|
+
let alpha = 0.55;
|
|
4006
|
+
let label = true;
|
|
4007
|
+
let labelTextProp;
|
|
4008
|
+
return {
|
|
4009
|
+
key: "scale-highlight",
|
|
4010
|
+
init(_ctx, props) {
|
|
4011
|
+
rootProp = props.root ?? "C";
|
|
4012
|
+
scaleKey = props.scale ?? "major";
|
|
4013
|
+
scaleColor = props.scaleColor;
|
|
4014
|
+
tonicColor = props.tonicColor;
|
|
4015
|
+
alpha = props.alpha ?? 0.55;
|
|
4016
|
+
label = props.label ?? true;
|
|
4017
|
+
labelTextProp = props.labelText;
|
|
4018
|
+
},
|
|
4019
|
+
draw(ctx, _tMs) {
|
|
4020
|
+
const c = ctx.ctx2d;
|
|
4021
|
+
const intervals = SCALE_INTERVALS[scaleKey] ?? SCALE_INTERVALS.major;
|
|
4022
|
+
const root = rootPc(rootProp);
|
|
4023
|
+
const inScale = new Set(intervals.map((iv) => (root + iv) % 12));
|
|
4024
|
+
const sc = scaleColor ?? ctx.theme.accent;
|
|
4025
|
+
const tc = tonicColor ?? ctx.theme.gold;
|
|
4026
|
+
const layout = getKeyboardLayout(ctx);
|
|
4027
|
+
if (layout) {
|
|
4028
|
+
c.save();
|
|
4029
|
+
c.globalAlpha = alpha;
|
|
4030
|
+
for (let m = layout.lowMidi; m <= layout.highMidi; m++) {
|
|
4031
|
+
if (!inRange(layout, m)) continue;
|
|
4032
|
+
const pc = pitchClass(m);
|
|
4033
|
+
if (!inScale.has(pc)) continue;
|
|
4034
|
+
const r = keyRect(layout, m);
|
|
4035
|
+
c.fillStyle = pc === root ? tc : sc;
|
|
4036
|
+
c.fillRect(r.x, r.y, r.w, r.h);
|
|
4037
|
+
}
|
|
4038
|
+
c.restore();
|
|
4039
|
+
}
|
|
4040
|
+
if (label) {
|
|
4041
|
+
const sb = ctx.safeBox;
|
|
4042
|
+
const name = typeof rootProp === "string" ? rootProp : ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][root];
|
|
4043
|
+
const text = labelTextProp ?? `${name} ${PRETTY[scaleKey] ?? scaleKey}`;
|
|
4044
|
+
c.save();
|
|
4045
|
+
c.fillStyle = ctx.theme.ink;
|
|
4046
|
+
c.font = `700 ${Math.round(ctx.H * 0.03)}px ${ctx.theme.fontDisplay}`;
|
|
4047
|
+
c.textAlign = "center";
|
|
4048
|
+
c.textBaseline = "top";
|
|
4049
|
+
c.fillText(text, ctx.W / 2, sb.top + sb.h * 0.04);
|
|
4050
|
+
c.restore();
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
var scaleHighlightFactory = {
|
|
4056
|
+
key: "scale-highlight",
|
|
4057
|
+
create: scaleHighlightLayer,
|
|
4058
|
+
validateProps(props) {
|
|
4059
|
+
if (props == null || typeof props !== "object") return ["scale-highlight: props must be an object"];
|
|
4060
|
+
const p = props;
|
|
4061
|
+
const errs = [];
|
|
4062
|
+
if (p.root != null && typeof p.root !== "string" && typeof p.root !== "number") errs.push("scale-highlight.root must be a note name or pitch-class number");
|
|
4063
|
+
if (p.scale != null && (typeof p.scale !== "string" || !(p.scale in SCALE_INTERVALS))) errs.push(`scale-highlight.scale must be one of: ${Object.keys(SCALE_INTERVALS).join(", ")}`);
|
|
4064
|
+
if (p.scaleColor != null && typeof p.scaleColor !== "string") errs.push("scale-highlight.scaleColor must be a string");
|
|
4065
|
+
if (p.tonicColor != null && typeof p.tonicColor !== "string") errs.push("scale-highlight.tonicColor must be a string");
|
|
4066
|
+
if (p.alpha != null && (typeof p.alpha !== "number" || p.alpha < 0 || p.alpha > 1)) errs.push("scale-highlight.alpha must be in [0,1]");
|
|
4067
|
+
if (p.label != null && typeof p.label !== "boolean") errs.push("scale-highlight.label must be a boolean");
|
|
4068
|
+
if (p.labelText != null && typeof p.labelText !== "string") errs.push("scale-highlight.labelText must be a string");
|
|
4069
|
+
return errs;
|
|
4070
|
+
}
|
|
4071
|
+
};
|
|
4072
|
+
|
|
3006
4073
|
// src/scene/registry.ts
|
|
3007
4074
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
3008
4075
|
function registerLayer(factory) {
|
|
@@ -3035,6 +4102,16 @@ registerLayer(mcqCardFactory);
|
|
|
3035
4102
|
registerLayer(circleOfFifthsFactory);
|
|
3036
4103
|
registerLayer(pitchContourFactory);
|
|
3037
4104
|
registerLayer(sectionMinimapFactory);
|
|
4105
|
+
registerLayer(beatPulseFactory);
|
|
4106
|
+
registerLayer(chordRibbonFactory);
|
|
4107
|
+
registerLayer(progressRingFactory);
|
|
4108
|
+
registerLayer(radialSpectrumFactory);
|
|
4109
|
+
registerLayer(karaokeCaptionFactory);
|
|
4110
|
+
registerLayer(intervalArcsFactory);
|
|
4111
|
+
registerLayer(kineticTextFactory);
|
|
4112
|
+
registerLayer(countdownFactory);
|
|
4113
|
+
registerLayer(waveformFactory);
|
|
4114
|
+
registerLayer(scaleHighlightFactory);
|
|
3038
4115
|
|
|
3039
4116
|
// src/scene/audioLayers.ts
|
|
3040
4117
|
function countInSchedule(opts) {
|
|
@@ -3187,7 +4264,13 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3187
4264
|
"reveal",
|
|
3188
4265
|
"portrait",
|
|
3189
4266
|
"safe-guides",
|
|
3190
|
-
"mcq-card"
|
|
4267
|
+
"mcq-card",
|
|
4268
|
+
"beat-pulse",
|
|
4269
|
+
"chord-ribbon",
|
|
4270
|
+
"progress-ring",
|
|
4271
|
+
"karaoke-caption",
|
|
4272
|
+
"kinetic-text",
|
|
4273
|
+
"countdown"
|
|
3191
4274
|
]);
|
|
3192
4275
|
function defaultBufferFactory() {
|
|
3193
4276
|
const g = globalThis;
|
|
@@ -3774,6 +4857,7 @@ export {
|
|
|
3774
4857
|
FOLLOW_PAD,
|
|
3775
4858
|
PIANO_HIGH,
|
|
3776
4859
|
PIANO_LOW,
|
|
4860
|
+
SCALE_INTERVALS,
|
|
3777
4861
|
activeChord,
|
|
3778
4862
|
activeCue,
|
|
3779
4863
|
activeSection,
|
|
@@ -3786,12 +4870,14 @@ export {
|
|
|
3786
4870
|
ballX,
|
|
3787
4871
|
beatGrid,
|
|
3788
4872
|
beatPhase,
|
|
4873
|
+
beatPulseFactory,
|
|
3789
4874
|
blackKeys,
|
|
3790
4875
|
bpmOf,
|
|
3791
4876
|
brandingFactory,
|
|
3792
4877
|
buildScene,
|
|
3793
4878
|
cameraForFollow,
|
|
3794
4879
|
cameraTransform,
|
|
4880
|
+
chordRibbonFactory,
|
|
3795
4881
|
circleOfFifthsDemoSpec,
|
|
3796
4882
|
circleOfFifthsFactory,
|
|
3797
4883
|
clamp,
|
|
@@ -3801,6 +4887,7 @@ export {
|
|
|
3801
4887
|
contourPolyline,
|
|
3802
4888
|
countInLeadSec,
|
|
3803
4889
|
countInSchedule,
|
|
4890
|
+
countdownFactory,
|
|
3804
4891
|
countdownRemaining,
|
|
3805
4892
|
countdownSeconds,
|
|
3806
4893
|
countingDegreeDemoSpec,
|
|
@@ -3840,8 +4927,10 @@ export {
|
|
|
3840
4927
|
hookFactory,
|
|
3841
4928
|
identityCamera,
|
|
3842
4929
|
inRange,
|
|
4930
|
+
intervalArcsFactory,
|
|
3843
4931
|
invLerp,
|
|
3844
4932
|
isBlackKey,
|
|
4933
|
+
karaokeCaptionFactory,
|
|
3845
4934
|
kenBurns,
|
|
3846
4935
|
keyCenterX,
|
|
3847
4936
|
keyColumnWidth,
|
|
@@ -3849,6 +4938,7 @@ export {
|
|
|
3849
4938
|
keySlot,
|
|
3850
4939
|
keyboardFactory,
|
|
3851
4940
|
keyboardLayout,
|
|
4941
|
+
kineticTextFactory,
|
|
3852
4942
|
lerp,
|
|
3853
4943
|
lerpBox,
|
|
3854
4944
|
lerpCamera,
|
|
@@ -3876,9 +4966,12 @@ export {
|
|
|
3876
4966
|
playheadLine,
|
|
3877
4967
|
portraitFactory,
|
|
3878
4968
|
progress01,
|
|
4969
|
+
progressRingFactory,
|
|
3879
4970
|
projectPoint,
|
|
3880
4971
|
promoCardsDemoSpec,
|
|
4972
|
+
pulseAt,
|
|
3881
4973
|
quizPhase,
|
|
4974
|
+
radialSpectrumFactory,
|
|
3882
4975
|
rayEndpoints,
|
|
3883
4976
|
rayPointAt,
|
|
3884
4977
|
recordSceneSpec,
|
|
@@ -3891,6 +4984,7 @@ export {
|
|
|
3891
4984
|
revealProgress,
|
|
3892
4985
|
runGate,
|
|
3893
4986
|
safeGuidesFactory,
|
|
4987
|
+
scaleHighlightFactory,
|
|
3894
4988
|
scoreFromMusicXML,
|
|
3895
4989
|
scorePitchSpan,
|
|
3896
4990
|
scrollCursorFactory,
|
|
@@ -3913,6 +5007,7 @@ export {
|
|
|
3913
5007
|
visualTimelineMs,
|
|
3914
5008
|
vstackAudioPlayheadLine,
|
|
3915
5009
|
vstackFollowBox,
|
|
5010
|
+
waveformFactory,
|
|
3916
5011
|
whiteKeys,
|
|
3917
5012
|
worldToViewport
|
|
3918
5013
|
};
|