@real-music-packages/web-core 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio.js +4 -4
- package/dist/chunk-BYZBLH25.js +15 -0
- package/dist/chunk-BYZBLH25.js.map +1 -0
- package/dist/{chunk-5ZK4HVY4.js → chunk-GORQ5YMR.js} +29 -17
- package/dist/chunk-GORQ5YMR.js.map +1 -0
- package/dist/index.js +8 -8
- package/dist/scene/index.d.ts +106 -1
- package/dist/scene/index.js +503 -4
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-25RUSM2X.js +0 -31
- package/dist/chunk-25RUSM2X.js.map +0 -1
- package/dist/chunk-5ZK4HVY4.js.map +0 -1
package/dist/scene/index.js
CHANGED
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
intervalBySemitones
|
|
3
3
|
} from "../chunk-N56UTMWA.js";
|
|
4
4
|
import {
|
|
5
|
+
getScaleDegree,
|
|
6
|
+
getStability,
|
|
5
7
|
noteNameToIndex,
|
|
6
8
|
pitchClass
|
|
7
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-GORQ5YMR.js";
|
|
8
10
|
import {
|
|
9
11
|
ctaScene,
|
|
10
12
|
drawSafeGuides,
|
|
@@ -4005,6 +4007,7 @@ function scaleHighlightLayer() {
|
|
|
4005
4007
|
let alpha = 0.55;
|
|
4006
4008
|
let label = true;
|
|
4007
4009
|
let labelTextProp;
|
|
4010
|
+
let labelCenterFrac;
|
|
4008
4011
|
return {
|
|
4009
4012
|
key: "scale-highlight",
|
|
4010
4013
|
init(_ctx, props) {
|
|
@@ -4015,6 +4018,7 @@ function scaleHighlightLayer() {
|
|
|
4015
4018
|
alpha = props.alpha ?? 0.55;
|
|
4016
4019
|
label = props.label ?? true;
|
|
4017
4020
|
labelTextProp = props.labelText;
|
|
4021
|
+
labelCenterFrac = props.labelCenterFrac;
|
|
4018
4022
|
},
|
|
4019
4023
|
draw(ctx, _tMs) {
|
|
4020
4024
|
const c = ctx.ctx2d;
|
|
@@ -4045,8 +4049,9 @@ function scaleHighlightLayer() {
|
|
|
4045
4049
|
c.fillStyle = ctx.theme.ink;
|
|
4046
4050
|
c.font = `700 ${Math.round(ctx.H * 0.03)}px ${ctx.theme.fontDisplay}`;
|
|
4047
4051
|
c.textAlign = "center";
|
|
4048
|
-
c.textBaseline = "top";
|
|
4049
|
-
|
|
4052
|
+
c.textBaseline = labelCenterFrac != null ? "middle" : "top";
|
|
4053
|
+
const ly = labelCenterFrac != null ? ctx.H * labelCenterFrac : sb.top + sb.h * 0.04;
|
|
4054
|
+
c.fillText(text, ctx.W / 2, ly);
|
|
4050
4055
|
c.restore();
|
|
4051
4056
|
}
|
|
4052
4057
|
}
|
|
@@ -4066,6 +4071,486 @@ var scaleHighlightFactory = {
|
|
|
4066
4071
|
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
4072
|
if (p.label != null && typeof p.label !== "boolean") errs.push("scale-highlight.label must be a boolean");
|
|
4068
4073
|
if (p.labelText != null && typeof p.labelText !== "string") errs.push("scale-highlight.labelText must be a string");
|
|
4074
|
+
if (p.labelCenterFrac != null && (typeof p.labelCenterFrac !== "number" || p.labelCenterFrac < 0 || p.labelCenterFrac > 1)) errs.push("scale-highlight.labelCenterFrac must be in [0,1]");
|
|
4075
|
+
return errs;
|
|
4076
|
+
}
|
|
4077
|
+
};
|
|
4078
|
+
|
|
4079
|
+
// src/scene/layers/particleBurst.ts
|
|
4080
|
+
var DEFAULTS7 = { count: 16, lifeMs: 750, speed: 520, gravity: 900, originYFrac: 0.6, size: 9 };
|
|
4081
|
+
function seeded(a, b) {
|
|
4082
|
+
const s = Math.sin(a * 127.1 + b * 311.7) * 43758.5453;
|
|
4083
|
+
return s - Math.floor(s);
|
|
4084
|
+
}
|
|
4085
|
+
function rgba7(hex, al) {
|
|
4086
|
+
const h = hex.replace("#", "");
|
|
4087
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
4088
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${al})`;
|
|
4089
|
+
}
|
|
4090
|
+
function particleBurstLayer() {
|
|
4091
|
+
let sources = [];
|
|
4092
|
+
let count = DEFAULTS7.count;
|
|
4093
|
+
let lifeMs = DEFAULTS7.lifeMs;
|
|
4094
|
+
let speed = DEFAULTS7.speed;
|
|
4095
|
+
let gravity = DEFAULTS7.gravity;
|
|
4096
|
+
let originYFrac = DEFAULTS7.originYFrac;
|
|
4097
|
+
let color;
|
|
4098
|
+
let size = DEFAULTS7.size;
|
|
4099
|
+
return {
|
|
4100
|
+
key: "particle-burst",
|
|
4101
|
+
init(ctx, props) {
|
|
4102
|
+
count = props.count ?? DEFAULTS7.count;
|
|
4103
|
+
lifeMs = props.lifeMs ?? DEFAULTS7.lifeMs;
|
|
4104
|
+
speed = props.speed ?? DEFAULTS7.speed;
|
|
4105
|
+
gravity = props.gravity ?? DEFAULTS7.gravity;
|
|
4106
|
+
originYFrac = props.originYFrac ?? DEFAULTS7.originYFrac;
|
|
4107
|
+
color = props.color;
|
|
4108
|
+
size = props.size ?? DEFAULTS7.size;
|
|
4109
|
+
if (props.onsetsMs) {
|
|
4110
|
+
sources = props.onsetsMs.map((tMs) => ({ tMs, x01: 0.5 }));
|
|
4111
|
+
} else if (ctx.score?.notes?.length) {
|
|
4112
|
+
const notes = ctx.score.notes;
|
|
4113
|
+
let lo = Infinity, hi = -Infinity;
|
|
4114
|
+
for (const nn of notes) {
|
|
4115
|
+
if (nn.pitchMidi < lo) lo = nn.pitchMidi;
|
|
4116
|
+
if (nn.pitchMidi > hi) hi = nn.pitchMidi;
|
|
4117
|
+
}
|
|
4118
|
+
const span = Math.max(1, hi - lo);
|
|
4119
|
+
const byOnset = /* @__PURE__ */ new Map();
|
|
4120
|
+
for (const nn of notes) if (!byOnset.has(nn.onsetMs)) byOnset.set(nn.onsetMs, nn.pitchMidi);
|
|
4121
|
+
sources = distinctOnsets(notes).map((tMs) => ({ tMs, x01: 0.15 + 0.7 * (((byOnset.get(tMs) ?? lo) - lo) / span) }));
|
|
4122
|
+
} else {
|
|
4123
|
+
sources = [];
|
|
4124
|
+
}
|
|
4125
|
+
sources.sort((a, b) => a.tMs - b.tMs);
|
|
4126
|
+
},
|
|
4127
|
+
draw(ctx, tMs) {
|
|
4128
|
+
if (!sources.length) return;
|
|
4129
|
+
const c = ctx.ctx2d;
|
|
4130
|
+
const W = ctx.W, H = ctx.H;
|
|
4131
|
+
const oy = H * originYFrac;
|
|
4132
|
+
const col = color ?? ctx.theme.gold;
|
|
4133
|
+
c.save();
|
|
4134
|
+
c.globalCompositeOperation = "lighter";
|
|
4135
|
+
for (let si = 0; si < sources.length; si++) {
|
|
4136
|
+
const src = sources[si];
|
|
4137
|
+
const age = tMs - src.tMs;
|
|
4138
|
+
if (age < 0) break;
|
|
4139
|
+
if (age >= lifeMs) continue;
|
|
4140
|
+
const ageSec = age / 1e3;
|
|
4141
|
+
const lifeFrac = age / lifeMs;
|
|
4142
|
+
const ox = W * src.x01;
|
|
4143
|
+
for (let i = 0; i < count; i++) {
|
|
4144
|
+
const a = seeded(si * 31 + i, 7);
|
|
4145
|
+
const sp = speed * (0.5 + seeded(si, i * 17));
|
|
4146
|
+
const ang = -Math.PI / 2 + (a - 0.5) * Math.PI * 1.2;
|
|
4147
|
+
const vx = Math.cos(ang) * sp;
|
|
4148
|
+
const vy = Math.sin(ang) * sp;
|
|
4149
|
+
const x = ox + vx * ageSec;
|
|
4150
|
+
const y = oy + vy * ageSec + 0.5 * gravity * ageSec * ageSec;
|
|
4151
|
+
const alpha = (1 - lifeFrac) * 0.9;
|
|
4152
|
+
const r = size * (1 - lifeFrac * 0.6);
|
|
4153
|
+
if (r <= 0) continue;
|
|
4154
|
+
c.fillStyle = rgba7(col, alpha);
|
|
4155
|
+
c.beginPath();
|
|
4156
|
+
c.arc(x, y, r, 0, Math.PI * 2);
|
|
4157
|
+
c.fill();
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
c.restore();
|
|
4161
|
+
}
|
|
4162
|
+
};
|
|
4163
|
+
}
|
|
4164
|
+
var particleBurstFactory = {
|
|
4165
|
+
key: "particle-burst",
|
|
4166
|
+
create: particleBurstLayer,
|
|
4167
|
+
validateProps(props) {
|
|
4168
|
+
if (props == null || typeof props !== "object") return ["particle-burst: props must be an object"];
|
|
4169
|
+
const p = props;
|
|
4170
|
+
const errs = [];
|
|
4171
|
+
if (p.onsetsMs != null && (!Array.isArray(p.onsetsMs) || p.onsetsMs.some((x) => typeof x !== "number"))) errs.push("particle-burst.onsetsMs must be a number[]");
|
|
4172
|
+
for (const k of ["count", "lifeMs", "speed", "gravity", "size"]) {
|
|
4173
|
+
if (p[k] != null && (typeof p[k] !== "number" || p[k] <= 0)) errs.push(`particle-burst.${k} must be a positive number`);
|
|
4174
|
+
}
|
|
4175
|
+
if (p.originYFrac != null && (typeof p.originYFrac !== "number" || p.originYFrac < 0 || p.originYFrac > 1)) errs.push("particle-burst.originYFrac must be in [0,1]");
|
|
4176
|
+
if (p.color != null && typeof p.color !== "string") errs.push("particle-burst.color must be a string");
|
|
4177
|
+
return errs;
|
|
4178
|
+
}
|
|
4179
|
+
};
|
|
4180
|
+
|
|
4181
|
+
// src/scene/layers/tensionGraph.ts
|
|
4182
|
+
function stabilityTension(midi, key) {
|
|
4183
|
+
const deg = getScaleDegree(midi, key);
|
|
4184
|
+
if (deg == null) return 1;
|
|
4185
|
+
const s = getStability(deg);
|
|
4186
|
+
return s === "stable" ? 0.15 : s === "lessStable" ? 0.5 : s === "unstable" ? 0.85 : 0.5;
|
|
4187
|
+
}
|
|
4188
|
+
function rgba8(hex, a) {
|
|
4189
|
+
const h = hex.replace("#", "");
|
|
4190
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
4191
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
4192
|
+
}
|
|
4193
|
+
function tensionGraphLayer() {
|
|
4194
|
+
let series = [];
|
|
4195
|
+
let durationMs = 1;
|
|
4196
|
+
let topProp;
|
|
4197
|
+
let heightProp;
|
|
4198
|
+
let color;
|
|
4199
|
+
let fill = true;
|
|
4200
|
+
let width = 5;
|
|
4201
|
+
let dot = true;
|
|
4202
|
+
return {
|
|
4203
|
+
key: "tension-graph",
|
|
4204
|
+
init(ctx, props) {
|
|
4205
|
+
topProp = props.top;
|
|
4206
|
+
heightProp = props.height;
|
|
4207
|
+
color = props.color;
|
|
4208
|
+
fill = props.fill ?? true;
|
|
4209
|
+
width = props.width ?? 5;
|
|
4210
|
+
dot = props.dot ?? true;
|
|
4211
|
+
if (props.series?.length) {
|
|
4212
|
+
series = props.series.slice().sort((a, b) => a.tMs - b.tMs);
|
|
4213
|
+
} else if (ctx.score?.notes?.length) {
|
|
4214
|
+
const pts = contourPoints(ctx.score.notes);
|
|
4215
|
+
if (props.key) {
|
|
4216
|
+
series = pts.map((p) => ({ tMs: p.tMs, value: stabilityTension(p.pitchMidi, props.key) }));
|
|
4217
|
+
} else {
|
|
4218
|
+
const r = pitchRange(pts);
|
|
4219
|
+
const span = Math.max(1, r.max - r.min);
|
|
4220
|
+
series = pts.map((p) => ({ tMs: p.tMs, value: (p.pitchMidi - r.min) / span }));
|
|
4221
|
+
}
|
|
4222
|
+
} else {
|
|
4223
|
+
series = [];
|
|
4224
|
+
}
|
|
4225
|
+
durationMs = ctx.score?.durationMs ?? (series.length ? series[series.length - 1].tMs : 1);
|
|
4226
|
+
},
|
|
4227
|
+
draw(ctx, tMs) {
|
|
4228
|
+
if (series.length < 2) return;
|
|
4229
|
+
const c = ctx.ctx2d;
|
|
4230
|
+
const sb = ctx.safeBox;
|
|
4231
|
+
const top = topProp ?? sb.top + sb.h * 0.18;
|
|
4232
|
+
const height = heightProp ?? sb.h * 0.3;
|
|
4233
|
+
const bottom = top + height;
|
|
4234
|
+
const col = color ?? ctx.theme.accent;
|
|
4235
|
+
const x = (ms) => sb.left + (sb.right - sb.left) * Math.min(1, Math.max(0, ms / durationMs));
|
|
4236
|
+
const y = (v) => bottom - Math.min(1, Math.max(0, v)) * height;
|
|
4237
|
+
const playX = x(tMs);
|
|
4238
|
+
c.save();
|
|
4239
|
+
c.lineJoin = "round";
|
|
4240
|
+
c.lineCap = "round";
|
|
4241
|
+
c.globalAlpha = 0.2;
|
|
4242
|
+
c.strokeStyle = ctx.theme.sepia;
|
|
4243
|
+
c.lineWidth = width;
|
|
4244
|
+
c.beginPath();
|
|
4245
|
+
series.forEach((p, i) => i === 0 ? c.moveTo(x(p.tMs), y(p.value)) : c.lineTo(x(p.tMs), y(p.value)));
|
|
4246
|
+
c.stroke();
|
|
4247
|
+
const revealed = [];
|
|
4248
|
+
for (let i = 0; i < series.length; i++) {
|
|
4249
|
+
const px = x(series[i].tMs);
|
|
4250
|
+
if (px <= playX) {
|
|
4251
|
+
revealed.push({ x: px, y: y(series[i].value) });
|
|
4252
|
+
} else {
|
|
4253
|
+
if (i > 0) {
|
|
4254
|
+
const a = series[i - 1], b = series[i];
|
|
4255
|
+
const f = (playX - x(a.tMs)) / Math.max(1, x(b.tMs) - x(a.tMs));
|
|
4256
|
+
revealed.push({ x: playX, y: y(a.value + (b.value - a.value) * f) });
|
|
4257
|
+
}
|
|
4258
|
+
break;
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
if (revealed.length) {
|
|
4262
|
+
c.globalAlpha = 1;
|
|
4263
|
+
if (fill) {
|
|
4264
|
+
c.fillStyle = rgba8(col, 0.22);
|
|
4265
|
+
c.beginPath();
|
|
4266
|
+
c.moveTo(revealed[0].x, bottom);
|
|
4267
|
+
for (const p of revealed) c.lineTo(p.x, p.y);
|
|
4268
|
+
c.lineTo(revealed[revealed.length - 1].x, bottom);
|
|
4269
|
+
c.closePath();
|
|
4270
|
+
c.fill();
|
|
4271
|
+
}
|
|
4272
|
+
c.strokeStyle = col;
|
|
4273
|
+
c.lineWidth = width;
|
|
4274
|
+
c.beginPath();
|
|
4275
|
+
revealed.forEach((p, i) => i === 0 ? c.moveTo(p.x, p.y) : c.lineTo(p.x, p.y));
|
|
4276
|
+
c.stroke();
|
|
4277
|
+
if (dot) {
|
|
4278
|
+
const last = revealed[revealed.length - 1];
|
|
4279
|
+
c.fillStyle = col;
|
|
4280
|
+
c.beginPath();
|
|
4281
|
+
c.arc(last.x, last.y, width * 2, 0, Math.PI * 2);
|
|
4282
|
+
c.fill();
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
c.restore();
|
|
4286
|
+
}
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
var tensionGraphFactory = {
|
|
4290
|
+
key: "tension-graph",
|
|
4291
|
+
create: tensionGraphLayer,
|
|
4292
|
+
validateProps(props) {
|
|
4293
|
+
if (props == null || typeof props !== "object") return ["tension-graph: props must be an object"];
|
|
4294
|
+
const p = props;
|
|
4295
|
+
const errs = [];
|
|
4296
|
+
if (p.series != null) {
|
|
4297
|
+
if (!Array.isArray(p.series)) errs.push("tension-graph.series must be an array of {tMs, value}");
|
|
4298
|
+
else p.series.forEach((raw, i) => {
|
|
4299
|
+
const s = raw;
|
|
4300
|
+
if (typeof s?.tMs !== "number" || typeof s?.value !== "number") errs.push(`tension-graph.series[${i}] must be {tMs:number, value:number}`);
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
if (p.key != null && typeof p.key !== "string") errs.push("tension-graph.key must be a string");
|
|
4304
|
+
if (p.top != null && typeof p.top !== "number") errs.push("tension-graph.top must be a number");
|
|
4305
|
+
if (p.height != null && (typeof p.height !== "number" || p.height <= 0)) errs.push("tension-graph.height must be a positive number");
|
|
4306
|
+
if (p.color != null && typeof p.color !== "string") errs.push("tension-graph.color must be a string");
|
|
4307
|
+
if (p.fill != null && typeof p.fill !== "boolean") errs.push("tension-graph.fill must be a boolean");
|
|
4308
|
+
if (p.width != null && (typeof p.width !== "number" || p.width <= 0)) errs.push("tension-graph.width must be a positive number");
|
|
4309
|
+
if (p.dot != null && typeof p.dot !== "boolean") errs.push("tension-graph.dot must be a boolean");
|
|
4310
|
+
return errs;
|
|
4311
|
+
}
|
|
4312
|
+
};
|
|
4313
|
+
|
|
4314
|
+
// src/scene/layers/texture.ts
|
|
4315
|
+
var DEFAULTS8 = { grain: 0.05, grainDensity: 700, vignette: 0.35, letterboxFrac: 0 };
|
|
4316
|
+
function lcg(seed) {
|
|
4317
|
+
let s = seed | 0 || 1;
|
|
4318
|
+
return () => {
|
|
4319
|
+
s = s * 1664525 + 1013904223 & 2147483647;
|
|
4320
|
+
return s / 2147483647;
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
function textureLayer() {
|
|
4324
|
+
let grain = DEFAULTS8.grain;
|
|
4325
|
+
let grainDensity = DEFAULTS8.grainDensity;
|
|
4326
|
+
let vignette = DEFAULTS8.vignette;
|
|
4327
|
+
let letterboxFrac = DEFAULTS8.letterboxFrac;
|
|
4328
|
+
return {
|
|
4329
|
+
key: "texture",
|
|
4330
|
+
init(_ctx, props) {
|
|
4331
|
+
grain = props.grain ?? DEFAULTS8.grain;
|
|
4332
|
+
grainDensity = props.grainDensity ?? DEFAULTS8.grainDensity;
|
|
4333
|
+
vignette = props.vignette ?? DEFAULTS8.vignette;
|
|
4334
|
+
letterboxFrac = props.letterboxFrac ?? DEFAULTS8.letterboxFrac;
|
|
4335
|
+
},
|
|
4336
|
+
draw(ctx, tMs) {
|
|
4337
|
+
const c = ctx.ctx2d;
|
|
4338
|
+
const W = ctx.W, H = ctx.H;
|
|
4339
|
+
c.save();
|
|
4340
|
+
if (vignette > 0) {
|
|
4341
|
+
const g = c.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.35, W / 2, H / 2, Math.max(W, H) * 0.72);
|
|
4342
|
+
g.addColorStop(0, "rgba(0,0,0,0)");
|
|
4343
|
+
g.addColorStop(1, `rgba(0,0,0,${vignette})`);
|
|
4344
|
+
c.fillStyle = g;
|
|
4345
|
+
c.fillRect(0, 0, W, H);
|
|
4346
|
+
}
|
|
4347
|
+
if (grain > 0 && grainDensity > 0) {
|
|
4348
|
+
const frame = Math.floor(tMs / 1e3 * (ctx.fps || 30));
|
|
4349
|
+
const rnd = lcg(frame * 2654435761);
|
|
4350
|
+
for (let i = 0; i < grainDensity; i++) {
|
|
4351
|
+
const x = rnd() * W;
|
|
4352
|
+
const y = rnd() * H;
|
|
4353
|
+
const dark = rnd() < 0.5;
|
|
4354
|
+
c.fillStyle = dark ? `rgba(0,0,0,${grain})` : `rgba(255,255,255,${grain})`;
|
|
4355
|
+
c.fillRect(x, y, 2, 2);
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
if (letterboxFrac > 0) {
|
|
4359
|
+
const bh = H * letterboxFrac;
|
|
4360
|
+
c.fillStyle = "#000";
|
|
4361
|
+
c.fillRect(0, 0, W, bh);
|
|
4362
|
+
c.fillRect(0, H - bh, W, bh);
|
|
4363
|
+
}
|
|
4364
|
+
c.restore();
|
|
4365
|
+
}
|
|
4366
|
+
};
|
|
4367
|
+
}
|
|
4368
|
+
var textureFactory = {
|
|
4369
|
+
key: "texture",
|
|
4370
|
+
create: textureLayer,
|
|
4371
|
+
validateProps(props) {
|
|
4372
|
+
if (props == null || typeof props !== "object") return ["texture: props must be an object"];
|
|
4373
|
+
const p = props;
|
|
4374
|
+
const errs = [];
|
|
4375
|
+
if (p.grain != null && (typeof p.grain !== "number" || p.grain < 0 || p.grain > 1)) errs.push("texture.grain must be in [0,1]");
|
|
4376
|
+
if (p.grainDensity != null && (typeof p.grainDensity !== "number" || p.grainDensity < 0)) errs.push("texture.grainDensity must be a number >= 0");
|
|
4377
|
+
if (p.vignette != null && (typeof p.vignette !== "number" || p.vignette < 0 || p.vignette > 1)) errs.push("texture.vignette must be in [0,1]");
|
|
4378
|
+
if (p.letterboxFrac != null && (typeof p.letterboxFrac !== "number" || p.letterboxFrac < 0 || p.letterboxFrac > 0.45)) errs.push("texture.letterboxFrac must be in [0,0.45]");
|
|
4379
|
+
return errs;
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4382
|
+
|
|
4383
|
+
// src/scene/layers/statCounter.ts
|
|
4384
|
+
var DEFAULTS9 = { startMs: 0, durationMs: 1200, fontPx: 220, centerFrac: 0.46, group: true };
|
|
4385
|
+
function easeOut2(t) {
|
|
4386
|
+
return 1 - Math.pow(1 - t, 3);
|
|
4387
|
+
}
|
|
4388
|
+
function clamp015(x) {
|
|
4389
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
4390
|
+
}
|
|
4391
|
+
function statCounterLayer() {
|
|
4392
|
+
let value = 0;
|
|
4393
|
+
let startMs = DEFAULTS9.startMs;
|
|
4394
|
+
let durationMs = DEFAULTS9.durationMs;
|
|
4395
|
+
let prefix = "";
|
|
4396
|
+
let suffix = "";
|
|
4397
|
+
let label = "";
|
|
4398
|
+
let fontPx = DEFAULTS9.fontPx;
|
|
4399
|
+
let centerFrac = DEFAULTS9.centerFrac;
|
|
4400
|
+
let color;
|
|
4401
|
+
let group = DEFAULTS9.group;
|
|
4402
|
+
return {
|
|
4403
|
+
key: "stat-counter",
|
|
4404
|
+
init(_ctx, props) {
|
|
4405
|
+
value = props.value ?? 0;
|
|
4406
|
+
startMs = props.startMs ?? DEFAULTS9.startMs;
|
|
4407
|
+
durationMs = props.durationMs ?? DEFAULTS9.durationMs;
|
|
4408
|
+
prefix = props.prefix ?? "";
|
|
4409
|
+
suffix = props.suffix ?? "";
|
|
4410
|
+
label = props.label ?? "";
|
|
4411
|
+
fontPx = props.fontPx ?? DEFAULTS9.fontPx;
|
|
4412
|
+
centerFrac = props.centerFrac ?? DEFAULTS9.centerFrac;
|
|
4413
|
+
color = props.color;
|
|
4414
|
+
group = props.group ?? DEFAULTS9.group;
|
|
4415
|
+
},
|
|
4416
|
+
draw(ctx, tMs) {
|
|
4417
|
+
const c = ctx.ctx2d;
|
|
4418
|
+
const p = clamp015((tMs - startMs) / Math.max(1, durationMs));
|
|
4419
|
+
const shown = Math.round(easeOut2(p) * value);
|
|
4420
|
+
const numStr = group ? shown.toLocaleString("en-US") : String(shown);
|
|
4421
|
+
const cx = ctx.W / 2;
|
|
4422
|
+
const cy = ctx.H * centerFrac;
|
|
4423
|
+
const col = color ?? ctx.theme.accent;
|
|
4424
|
+
c.save();
|
|
4425
|
+
c.textAlign = "center";
|
|
4426
|
+
c.textBaseline = "middle";
|
|
4427
|
+
c.fillStyle = col;
|
|
4428
|
+
c.font = `800 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
4429
|
+
c.fillText(`${prefix}${numStr}${suffix}`, cx, cy);
|
|
4430
|
+
if (label) {
|
|
4431
|
+
c.fillStyle = ctx.theme.ink;
|
|
4432
|
+
c.font = `600 ${Math.round(fontPx * 0.26)}px ${ctx.theme.fontDisplay}`;
|
|
4433
|
+
c.fillText(label, cx, cy + fontPx * 0.62);
|
|
4434
|
+
}
|
|
4435
|
+
c.restore();
|
|
4436
|
+
}
|
|
4437
|
+
};
|
|
4438
|
+
}
|
|
4439
|
+
var statCounterFactory = {
|
|
4440
|
+
key: "stat-counter",
|
|
4441
|
+
create: statCounterLayer,
|
|
4442
|
+
validateProps(props) {
|
|
4443
|
+
if (props == null || typeof props !== "object") return ["stat-counter: props must be an object"];
|
|
4444
|
+
const p = props;
|
|
4445
|
+
const errs = [];
|
|
4446
|
+
if (typeof p.value !== "number") errs.push("stat-counter.value must be a number");
|
|
4447
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("stat-counter.startMs must be a number");
|
|
4448
|
+
if (p.durationMs != null && (typeof p.durationMs !== "number" || p.durationMs <= 0)) errs.push("stat-counter.durationMs must be a positive number");
|
|
4449
|
+
for (const k of ["prefix", "suffix", "label"]) if (p[k] != null && typeof p[k] !== "string") errs.push(`stat-counter.${k} must be a string`);
|
|
4450
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("stat-counter.fontPx must be a positive number");
|
|
4451
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("stat-counter.centerFrac must be in [0,1]");
|
|
4452
|
+
if (p.color != null && typeof p.color !== "string") errs.push("stat-counter.color must be a string");
|
|
4453
|
+
if (p.group != null && typeof p.group !== "boolean") errs.push("stat-counter.group must be a boolean");
|
|
4454
|
+
return errs;
|
|
4455
|
+
}
|
|
4456
|
+
};
|
|
4457
|
+
|
|
4458
|
+
// src/scene/layers/endCard.ts
|
|
4459
|
+
var DEFAULTS10 = { text: "Follow for daily", startMs: 0, inMs: 400, centerFrac: 0.5, fontPx: 72, arrow: true, arrowDir: "up" };
|
|
4460
|
+
function clamp016(x) {
|
|
4461
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
4462
|
+
}
|
|
4463
|
+
function easeOut3(t) {
|
|
4464
|
+
return 1 - Math.pow(1 - t, 3);
|
|
4465
|
+
}
|
|
4466
|
+
function endCardLayer() {
|
|
4467
|
+
let text = DEFAULTS10.text;
|
|
4468
|
+
let handle = "";
|
|
4469
|
+
let startMs = DEFAULTS10.startMs;
|
|
4470
|
+
let inMs = DEFAULTS10.inMs;
|
|
4471
|
+
let color;
|
|
4472
|
+
let centerFrac = DEFAULTS10.centerFrac;
|
|
4473
|
+
let fontPx = DEFAULTS10.fontPx;
|
|
4474
|
+
let arrow = DEFAULTS10.arrow;
|
|
4475
|
+
let arrowDir = DEFAULTS10.arrowDir;
|
|
4476
|
+
return {
|
|
4477
|
+
key: "end-card",
|
|
4478
|
+
init(_ctx, props) {
|
|
4479
|
+
text = props.text ?? DEFAULTS10.text;
|
|
4480
|
+
handle = props.handle ?? "";
|
|
4481
|
+
startMs = props.startMs ?? DEFAULTS10.startMs;
|
|
4482
|
+
inMs = props.inMs ?? DEFAULTS10.inMs;
|
|
4483
|
+
color = props.color;
|
|
4484
|
+
centerFrac = props.centerFrac ?? DEFAULTS10.centerFrac;
|
|
4485
|
+
fontPx = props.fontPx ?? DEFAULTS10.fontPx;
|
|
4486
|
+
arrow = props.arrow ?? DEFAULTS10.arrow;
|
|
4487
|
+
arrowDir = props.arrowDir ?? DEFAULTS10.arrowDir;
|
|
4488
|
+
},
|
|
4489
|
+
draw(ctx, tMs) {
|
|
4490
|
+
if (tMs < startMs) return;
|
|
4491
|
+
const c = ctx.ctx2d;
|
|
4492
|
+
const cx = ctx.W / 2;
|
|
4493
|
+
const cy = ctx.H * centerFrac;
|
|
4494
|
+
const col = color ?? ctx.theme.accent;
|
|
4495
|
+
const p = clamp016((tMs - startMs) / Math.max(1, inMs));
|
|
4496
|
+
const scale = 0.7 + 0.3 * easeOut3(p);
|
|
4497
|
+
const dir = arrowDir === "down" ? 1 : -1;
|
|
4498
|
+
const bounce = Math.sin(tMs / 260) * 14;
|
|
4499
|
+
c.save();
|
|
4500
|
+
c.globalAlpha = p;
|
|
4501
|
+
c.translate(cx, cy);
|
|
4502
|
+
c.scale(scale, scale);
|
|
4503
|
+
c.textAlign = "center";
|
|
4504
|
+
c.textBaseline = "middle";
|
|
4505
|
+
c.fillStyle = col;
|
|
4506
|
+
c.font = `800 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
4507
|
+
c.fillText(text, 0, 0);
|
|
4508
|
+
if (handle) {
|
|
4509
|
+
c.fillStyle = ctx.theme.ink;
|
|
4510
|
+
c.font = `600 ${Math.round(fontPx * 0.5)}px ${ctx.theme.fontDisplay}`;
|
|
4511
|
+
c.fillText(handle, 0, fontPx * 0.9);
|
|
4512
|
+
}
|
|
4513
|
+
c.restore();
|
|
4514
|
+
if (arrow) {
|
|
4515
|
+
const ax = cx;
|
|
4516
|
+
const ay = (arrowDir === "down" ? cy + fontPx * 1.6 : cy - fontPx * 1.6) + bounce * dir;
|
|
4517
|
+
const s = fontPx * 0.4;
|
|
4518
|
+
c.save();
|
|
4519
|
+
c.globalAlpha = p;
|
|
4520
|
+
c.strokeStyle = col;
|
|
4521
|
+
c.lineWidth = 10;
|
|
4522
|
+
c.lineCap = "round";
|
|
4523
|
+
c.lineJoin = "round";
|
|
4524
|
+
c.beginPath();
|
|
4525
|
+
if (arrowDir === "down") {
|
|
4526
|
+
c.moveTo(ax - s, ay - s * 0.6);
|
|
4527
|
+
c.lineTo(ax, ay + s * 0.6);
|
|
4528
|
+
c.lineTo(ax + s, ay - s * 0.6);
|
|
4529
|
+
} else {
|
|
4530
|
+
c.moveTo(ax - s, ay + s * 0.6);
|
|
4531
|
+
c.lineTo(ax, ay - s * 0.6);
|
|
4532
|
+
c.lineTo(ax + s, ay + s * 0.6);
|
|
4533
|
+
}
|
|
4534
|
+
c.stroke();
|
|
4535
|
+
c.restore();
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
var endCardFactory = {
|
|
4541
|
+
key: "end-card",
|
|
4542
|
+
create: endCardLayer,
|
|
4543
|
+
validateProps(props) {
|
|
4544
|
+
if (props == null || typeof props !== "object") return ["end-card: props must be an object"];
|
|
4545
|
+
const p = props;
|
|
4546
|
+
const errs = [];
|
|
4547
|
+
for (const k of ["text", "handle", "color"]) if (p[k] != null && typeof p[k] !== "string") errs.push(`end-card.${k} must be a string`);
|
|
4548
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("end-card.startMs must be a number");
|
|
4549
|
+
if (p.inMs != null && (typeof p.inMs !== "number" || p.inMs <= 0)) errs.push("end-card.inMs must be a positive number");
|
|
4550
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("end-card.centerFrac must be in [0,1]");
|
|
4551
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("end-card.fontPx must be a positive number");
|
|
4552
|
+
if (p.arrow != null && typeof p.arrow !== "boolean") errs.push("end-card.arrow must be a boolean");
|
|
4553
|
+
if (p.arrowDir != null && p.arrowDir !== "up" && p.arrowDir !== "down") errs.push('end-card.arrowDir must be "up" | "down"');
|
|
4069
4554
|
return errs;
|
|
4070
4555
|
}
|
|
4071
4556
|
};
|
|
@@ -4112,6 +4597,11 @@ registerLayer(kineticTextFactory);
|
|
|
4112
4597
|
registerLayer(countdownFactory);
|
|
4113
4598
|
registerLayer(waveformFactory);
|
|
4114
4599
|
registerLayer(scaleHighlightFactory);
|
|
4600
|
+
registerLayer(particleBurstFactory);
|
|
4601
|
+
registerLayer(tensionGraphFactory);
|
|
4602
|
+
registerLayer(textureFactory);
|
|
4603
|
+
registerLayer(statCounterFactory);
|
|
4604
|
+
registerLayer(endCardFactory);
|
|
4115
4605
|
|
|
4116
4606
|
// src/scene/audioLayers.ts
|
|
4117
4607
|
function countInSchedule(opts) {
|
|
@@ -4270,7 +4760,11 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
4270
4760
|
"progress-ring",
|
|
4271
4761
|
"karaoke-caption",
|
|
4272
4762
|
"kinetic-text",
|
|
4273
|
-
"countdown"
|
|
4763
|
+
"countdown",
|
|
4764
|
+
"particle-burst",
|
|
4765
|
+
"texture",
|
|
4766
|
+
"stat-counter",
|
|
4767
|
+
"end-card"
|
|
4274
4768
|
]);
|
|
4275
4769
|
function defaultBufferFactory() {
|
|
4276
4770
|
const g = globalThis;
|
|
@@ -4908,6 +5402,7 @@ export {
|
|
|
4908
5402
|
easeIn,
|
|
4909
5403
|
easeInOut,
|
|
4910
5404
|
easeOut,
|
|
5405
|
+
endCardFactory,
|
|
4911
5406
|
fallingKeyboardDemoScore,
|
|
4912
5407
|
fallingKeyboardDemoSpec,
|
|
4913
5408
|
fallingNotesFactory,
|
|
@@ -4959,6 +5454,7 @@ export {
|
|
|
4959
5454
|
noteSetXRange,
|
|
4960
5455
|
parseKey,
|
|
4961
5456
|
parseTimeSig,
|
|
5457
|
+
particleBurstFactory,
|
|
4962
5458
|
pcToSlot,
|
|
4963
5459
|
pitchAt,
|
|
4964
5460
|
pitchContourFactory,
|
|
@@ -4999,7 +5495,10 @@ export {
|
|
|
4999
5495
|
staffAnchor,
|
|
5000
5496
|
staffKeyboardRayFactory,
|
|
5001
5497
|
staffRayDemoSpec,
|
|
5498
|
+
statCounterFactory,
|
|
5002
5499
|
systemBox,
|
|
5500
|
+
tensionGraphFactory,
|
|
5501
|
+
textureFactory,
|
|
5003
5502
|
transitionProgress,
|
|
5004
5503
|
validateChordTrack,
|
|
5005
5504
|
validateQuiz,
|