@real-music-packages/web-core 0.24.1 → 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 +103 -1
- package/dist/scene/index.js +497 -2
- 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,
|
|
@@ -4074,6 +4076,485 @@ var scaleHighlightFactory = {
|
|
|
4074
4076
|
}
|
|
4075
4077
|
};
|
|
4076
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"');
|
|
4554
|
+
return errs;
|
|
4555
|
+
}
|
|
4556
|
+
};
|
|
4557
|
+
|
|
4077
4558
|
// src/scene/registry.ts
|
|
4078
4559
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
4079
4560
|
function registerLayer(factory) {
|
|
@@ -4116,6 +4597,11 @@ registerLayer(kineticTextFactory);
|
|
|
4116
4597
|
registerLayer(countdownFactory);
|
|
4117
4598
|
registerLayer(waveformFactory);
|
|
4118
4599
|
registerLayer(scaleHighlightFactory);
|
|
4600
|
+
registerLayer(particleBurstFactory);
|
|
4601
|
+
registerLayer(tensionGraphFactory);
|
|
4602
|
+
registerLayer(textureFactory);
|
|
4603
|
+
registerLayer(statCounterFactory);
|
|
4604
|
+
registerLayer(endCardFactory);
|
|
4119
4605
|
|
|
4120
4606
|
// src/scene/audioLayers.ts
|
|
4121
4607
|
function countInSchedule(opts) {
|
|
@@ -4274,7 +4760,11 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
4274
4760
|
"progress-ring",
|
|
4275
4761
|
"karaoke-caption",
|
|
4276
4762
|
"kinetic-text",
|
|
4277
|
-
"countdown"
|
|
4763
|
+
"countdown",
|
|
4764
|
+
"particle-burst",
|
|
4765
|
+
"texture",
|
|
4766
|
+
"stat-counter",
|
|
4767
|
+
"end-card"
|
|
4278
4768
|
]);
|
|
4279
4769
|
function defaultBufferFactory() {
|
|
4280
4770
|
const g = globalThis;
|
|
@@ -4912,6 +5402,7 @@ export {
|
|
|
4912
5402
|
easeIn,
|
|
4913
5403
|
easeInOut,
|
|
4914
5404
|
easeOut,
|
|
5405
|
+
endCardFactory,
|
|
4915
5406
|
fallingKeyboardDemoScore,
|
|
4916
5407
|
fallingKeyboardDemoSpec,
|
|
4917
5408
|
fallingNotesFactory,
|
|
@@ -4963,6 +5454,7 @@ export {
|
|
|
4963
5454
|
noteSetXRange,
|
|
4964
5455
|
parseKey,
|
|
4965
5456
|
parseTimeSig,
|
|
5457
|
+
particleBurstFactory,
|
|
4966
5458
|
pcToSlot,
|
|
4967
5459
|
pitchAt,
|
|
4968
5460
|
pitchContourFactory,
|
|
@@ -5003,7 +5495,10 @@ export {
|
|
|
5003
5495
|
staffAnchor,
|
|
5004
5496
|
staffKeyboardRayFactory,
|
|
5005
5497
|
staffRayDemoSpec,
|
|
5498
|
+
statCounterFactory,
|
|
5006
5499
|
systemBox,
|
|
5500
|
+
tensionGraphFactory,
|
|
5501
|
+
textureFactory,
|
|
5007
5502
|
transitionProgress,
|
|
5008
5503
|
validateChordTrack,
|
|
5009
5504
|
validateQuiz,
|