@real-music-packages/web-core 0.24.1 → 0.26.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 +516 -6
- 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,
|
|
@@ -3811,6 +3813,7 @@ function countdownLayer() {
|
|
|
3811
3813
|
let fontPx = DEFAULTS6.fontPx;
|
|
3812
3814
|
let centerFrac = DEFAULTS6.centerFrac;
|
|
3813
3815
|
let ring = DEFAULTS6.ring;
|
|
3816
|
+
let ringRadiusProp;
|
|
3814
3817
|
return {
|
|
3815
3818
|
key: "countdown",
|
|
3816
3819
|
init(_ctx, props) {
|
|
@@ -3821,6 +3824,7 @@ function countdownLayer() {
|
|
|
3821
3824
|
fontPx = props.fontPx ?? DEFAULTS6.fontPx;
|
|
3822
3825
|
centerFrac = props.centerFrac ?? DEFAULTS6.centerFrac;
|
|
3823
3826
|
ring = props.ring ?? DEFAULTS6.ring;
|
|
3827
|
+
ringRadiusProp = props.ringRadius;
|
|
3824
3828
|
},
|
|
3825
3829
|
draw(ctx, tMs) {
|
|
3826
3830
|
const elapsed = tMs - startMs;
|
|
@@ -3839,7 +3843,7 @@ function countdownLayer() {
|
|
|
3839
3843
|
const fade = 1 - Math.max(0, (tickFrac - 0.7) / 0.3) * 0.85;
|
|
3840
3844
|
c.save();
|
|
3841
3845
|
if (ring) {
|
|
3842
|
-
const r = fontPx * 0.62;
|
|
3846
|
+
const r = ringRadiusProp ?? fontPx * 0.62;
|
|
3843
3847
|
c.lineWidth = 10;
|
|
3844
3848
|
c.lineCap = "round";
|
|
3845
3849
|
c.strokeStyle = rgba6(col, 0.18 * fade);
|
|
@@ -3875,6 +3879,7 @@ var countdownFactory = {
|
|
|
3875
3879
|
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("countdown.fontPx must be a positive number");
|
|
3876
3880
|
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("countdown.centerFrac must be in [0,1]");
|
|
3877
3881
|
if (p.ring != null && typeof p.ring !== "boolean") errs.push("countdown.ring must be a boolean");
|
|
3882
|
+
if (p.ringRadius != null && (typeof p.ringRadius !== "number" || p.ringRadius <= 0)) errs.push("countdown.ringRadius must be a positive number");
|
|
3878
3883
|
return errs;
|
|
3879
3884
|
}
|
|
3880
3885
|
};
|
|
@@ -4028,14 +4033,26 @@ function scaleHighlightLayer() {
|
|
|
4028
4033
|
const layout = getKeyboardLayout(ctx);
|
|
4029
4034
|
if (layout) {
|
|
4030
4035
|
c.save();
|
|
4031
|
-
c.globalAlpha = alpha;
|
|
4032
4036
|
for (let m = layout.lowMidi; m <= layout.highMidi; m++) {
|
|
4033
4037
|
if (!inRange(layout, m)) continue;
|
|
4034
4038
|
const pc = pitchClass(m);
|
|
4035
4039
|
if (!inScale.has(pc)) continue;
|
|
4036
4040
|
const r = keyRect(layout, m);
|
|
4037
|
-
|
|
4038
|
-
|
|
4041
|
+
const isTonic = pc === root;
|
|
4042
|
+
const cx = r.x + r.w / 2;
|
|
4043
|
+
const pipR = (r.black ? r.w * 0.5 : r.w * 0.32) * (isTonic ? 1.25 : 1);
|
|
4044
|
+
const cy = r.y + r.h - pipR - (r.black ? 8 : 16);
|
|
4045
|
+
c.globalAlpha = Math.min(1, alpha + 0.4);
|
|
4046
|
+
c.fillStyle = isTonic ? tc : sc;
|
|
4047
|
+
c.beginPath();
|
|
4048
|
+
c.arc(cx, cy, pipR, 0, Math.PI * 2);
|
|
4049
|
+
c.fill();
|
|
4050
|
+
if (isTonic) {
|
|
4051
|
+
c.fillStyle = "#ffffff";
|
|
4052
|
+
c.beginPath();
|
|
4053
|
+
c.arc(cx, cy, pipR * 0.42, 0, Math.PI * 2);
|
|
4054
|
+
c.fill();
|
|
4055
|
+
}
|
|
4039
4056
|
}
|
|
4040
4057
|
c.restore();
|
|
4041
4058
|
}
|
|
@@ -4074,6 +4091,485 @@ var scaleHighlightFactory = {
|
|
|
4074
4091
|
}
|
|
4075
4092
|
};
|
|
4076
4093
|
|
|
4094
|
+
// src/scene/layers/particleBurst.ts
|
|
4095
|
+
var DEFAULTS7 = { count: 16, lifeMs: 750, speed: 520, gravity: 900, originYFrac: 0.6, size: 9 };
|
|
4096
|
+
function seeded(a, b) {
|
|
4097
|
+
const s = Math.sin(a * 127.1 + b * 311.7) * 43758.5453;
|
|
4098
|
+
return s - Math.floor(s);
|
|
4099
|
+
}
|
|
4100
|
+
function rgba7(hex, al) {
|
|
4101
|
+
const h = hex.replace("#", "");
|
|
4102
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
4103
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${al})`;
|
|
4104
|
+
}
|
|
4105
|
+
function particleBurstLayer() {
|
|
4106
|
+
let sources = [];
|
|
4107
|
+
let count = DEFAULTS7.count;
|
|
4108
|
+
let lifeMs = DEFAULTS7.lifeMs;
|
|
4109
|
+
let speed = DEFAULTS7.speed;
|
|
4110
|
+
let gravity = DEFAULTS7.gravity;
|
|
4111
|
+
let originYFrac = DEFAULTS7.originYFrac;
|
|
4112
|
+
let color;
|
|
4113
|
+
let size = DEFAULTS7.size;
|
|
4114
|
+
return {
|
|
4115
|
+
key: "particle-burst",
|
|
4116
|
+
init(ctx, props) {
|
|
4117
|
+
count = props.count ?? DEFAULTS7.count;
|
|
4118
|
+
lifeMs = props.lifeMs ?? DEFAULTS7.lifeMs;
|
|
4119
|
+
speed = props.speed ?? DEFAULTS7.speed;
|
|
4120
|
+
gravity = props.gravity ?? DEFAULTS7.gravity;
|
|
4121
|
+
originYFrac = props.originYFrac ?? DEFAULTS7.originYFrac;
|
|
4122
|
+
color = props.color;
|
|
4123
|
+
size = props.size ?? DEFAULTS7.size;
|
|
4124
|
+
if (props.onsetsMs) {
|
|
4125
|
+
sources = props.onsetsMs.map((tMs) => ({ tMs, x01: 0.5 }));
|
|
4126
|
+
} else if (ctx.score?.notes?.length) {
|
|
4127
|
+
const notes = ctx.score.notes;
|
|
4128
|
+
let lo = Infinity, hi = -Infinity;
|
|
4129
|
+
for (const nn of notes) {
|
|
4130
|
+
if (nn.pitchMidi < lo) lo = nn.pitchMidi;
|
|
4131
|
+
if (nn.pitchMidi > hi) hi = nn.pitchMidi;
|
|
4132
|
+
}
|
|
4133
|
+
const span = Math.max(1, hi - lo);
|
|
4134
|
+
const byOnset = /* @__PURE__ */ new Map();
|
|
4135
|
+
for (const nn of notes) if (!byOnset.has(nn.onsetMs)) byOnset.set(nn.onsetMs, nn.pitchMidi);
|
|
4136
|
+
sources = distinctOnsets(notes).map((tMs) => ({ tMs, x01: 0.15 + 0.7 * (((byOnset.get(tMs) ?? lo) - lo) / span) }));
|
|
4137
|
+
} else {
|
|
4138
|
+
sources = [];
|
|
4139
|
+
}
|
|
4140
|
+
sources.sort((a, b) => a.tMs - b.tMs);
|
|
4141
|
+
},
|
|
4142
|
+
draw(ctx, tMs) {
|
|
4143
|
+
if (!sources.length) return;
|
|
4144
|
+
const c = ctx.ctx2d;
|
|
4145
|
+
const W = ctx.W, H = ctx.H;
|
|
4146
|
+
const oy = H * originYFrac;
|
|
4147
|
+
const col = color ?? ctx.theme.gold;
|
|
4148
|
+
c.save();
|
|
4149
|
+
c.globalCompositeOperation = "lighter";
|
|
4150
|
+
for (let si = 0; si < sources.length; si++) {
|
|
4151
|
+
const src = sources[si];
|
|
4152
|
+
const age = tMs - src.tMs;
|
|
4153
|
+
if (age < 0) break;
|
|
4154
|
+
if (age >= lifeMs) continue;
|
|
4155
|
+
const ageSec = age / 1e3;
|
|
4156
|
+
const lifeFrac = age / lifeMs;
|
|
4157
|
+
const ox = W * src.x01;
|
|
4158
|
+
for (let i = 0; i < count; i++) {
|
|
4159
|
+
const a = seeded(si * 31 + i, 7);
|
|
4160
|
+
const sp = speed * (0.5 + seeded(si, i * 17));
|
|
4161
|
+
const ang = -Math.PI / 2 + (a - 0.5) * Math.PI * 1.2;
|
|
4162
|
+
const vx = Math.cos(ang) * sp;
|
|
4163
|
+
const vy = Math.sin(ang) * sp;
|
|
4164
|
+
const x = ox + vx * ageSec;
|
|
4165
|
+
const y = oy + vy * ageSec + 0.5 * gravity * ageSec * ageSec;
|
|
4166
|
+
const alpha = (1 - lifeFrac) * 0.9;
|
|
4167
|
+
const r = size * (1 - lifeFrac * 0.6);
|
|
4168
|
+
if (r <= 0) continue;
|
|
4169
|
+
c.fillStyle = rgba7(col, alpha);
|
|
4170
|
+
c.beginPath();
|
|
4171
|
+
c.arc(x, y, r, 0, Math.PI * 2);
|
|
4172
|
+
c.fill();
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
c.restore();
|
|
4176
|
+
}
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
var particleBurstFactory = {
|
|
4180
|
+
key: "particle-burst",
|
|
4181
|
+
create: particleBurstLayer,
|
|
4182
|
+
validateProps(props) {
|
|
4183
|
+
if (props == null || typeof props !== "object") return ["particle-burst: props must be an object"];
|
|
4184
|
+
const p = props;
|
|
4185
|
+
const errs = [];
|
|
4186
|
+
if (p.onsetsMs != null && (!Array.isArray(p.onsetsMs) || p.onsetsMs.some((x) => typeof x !== "number"))) errs.push("particle-burst.onsetsMs must be a number[]");
|
|
4187
|
+
for (const k of ["count", "lifeMs", "speed", "gravity", "size"]) {
|
|
4188
|
+
if (p[k] != null && (typeof p[k] !== "number" || p[k] <= 0)) errs.push(`particle-burst.${k} must be a positive number`);
|
|
4189
|
+
}
|
|
4190
|
+
if (p.originYFrac != null && (typeof p.originYFrac !== "number" || p.originYFrac < 0 || p.originYFrac > 1)) errs.push("particle-burst.originYFrac must be in [0,1]");
|
|
4191
|
+
if (p.color != null && typeof p.color !== "string") errs.push("particle-burst.color must be a string");
|
|
4192
|
+
return errs;
|
|
4193
|
+
}
|
|
4194
|
+
};
|
|
4195
|
+
|
|
4196
|
+
// src/scene/layers/tensionGraph.ts
|
|
4197
|
+
function stabilityTension(midi, key) {
|
|
4198
|
+
const deg = getScaleDegree(midi, key);
|
|
4199
|
+
if (deg == null) return 1;
|
|
4200
|
+
const s = getStability(deg);
|
|
4201
|
+
return s === "stable" ? 0.15 : s === "lessStable" ? 0.5 : s === "unstable" ? 0.85 : 0.5;
|
|
4202
|
+
}
|
|
4203
|
+
function rgba8(hex, a) {
|
|
4204
|
+
const h = hex.replace("#", "");
|
|
4205
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
4206
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
4207
|
+
}
|
|
4208
|
+
function tensionGraphLayer() {
|
|
4209
|
+
let series = [];
|
|
4210
|
+
let durationMs = 1;
|
|
4211
|
+
let topProp;
|
|
4212
|
+
let heightProp;
|
|
4213
|
+
let color;
|
|
4214
|
+
let fill = true;
|
|
4215
|
+
let width = 5;
|
|
4216
|
+
let dot = true;
|
|
4217
|
+
return {
|
|
4218
|
+
key: "tension-graph",
|
|
4219
|
+
init(ctx, props) {
|
|
4220
|
+
topProp = props.top;
|
|
4221
|
+
heightProp = props.height;
|
|
4222
|
+
color = props.color;
|
|
4223
|
+
fill = props.fill ?? true;
|
|
4224
|
+
width = props.width ?? 5;
|
|
4225
|
+
dot = props.dot ?? true;
|
|
4226
|
+
if (props.series?.length) {
|
|
4227
|
+
series = props.series.slice().sort((a, b) => a.tMs - b.tMs);
|
|
4228
|
+
} else if (ctx.score?.notes?.length) {
|
|
4229
|
+
const pts = contourPoints(ctx.score.notes);
|
|
4230
|
+
if (props.key) {
|
|
4231
|
+
series = pts.map((p) => ({ tMs: p.tMs, value: stabilityTension(p.pitchMidi, props.key) }));
|
|
4232
|
+
} else {
|
|
4233
|
+
const r = pitchRange(pts);
|
|
4234
|
+
const span = Math.max(1, r.max - r.min);
|
|
4235
|
+
series = pts.map((p) => ({ tMs: p.tMs, value: (p.pitchMidi - r.min) / span }));
|
|
4236
|
+
}
|
|
4237
|
+
} else {
|
|
4238
|
+
series = [];
|
|
4239
|
+
}
|
|
4240
|
+
durationMs = ctx.score?.durationMs ?? (series.length ? series[series.length - 1].tMs : 1);
|
|
4241
|
+
},
|
|
4242
|
+
draw(ctx, tMs) {
|
|
4243
|
+
if (series.length < 2) return;
|
|
4244
|
+
const c = ctx.ctx2d;
|
|
4245
|
+
const sb = ctx.safeBox;
|
|
4246
|
+
const top = topProp ?? sb.top + sb.h * 0.18;
|
|
4247
|
+
const height = heightProp ?? sb.h * 0.3;
|
|
4248
|
+
const bottom = top + height;
|
|
4249
|
+
const col = color ?? ctx.theme.accent;
|
|
4250
|
+
const x = (ms) => sb.left + (sb.right - sb.left) * Math.min(1, Math.max(0, ms / durationMs));
|
|
4251
|
+
const y = (v) => bottom - Math.min(1, Math.max(0, v)) * height;
|
|
4252
|
+
const playX = x(tMs);
|
|
4253
|
+
c.save();
|
|
4254
|
+
c.lineJoin = "round";
|
|
4255
|
+
c.lineCap = "round";
|
|
4256
|
+
c.globalAlpha = 0.2;
|
|
4257
|
+
c.strokeStyle = ctx.theme.sepia;
|
|
4258
|
+
c.lineWidth = width;
|
|
4259
|
+
c.beginPath();
|
|
4260
|
+
series.forEach((p, i) => i === 0 ? c.moveTo(x(p.tMs), y(p.value)) : c.lineTo(x(p.tMs), y(p.value)));
|
|
4261
|
+
c.stroke();
|
|
4262
|
+
const revealed = [];
|
|
4263
|
+
for (let i = 0; i < series.length; i++) {
|
|
4264
|
+
const px = x(series[i].tMs);
|
|
4265
|
+
if (px <= playX) {
|
|
4266
|
+
revealed.push({ x: px, y: y(series[i].value) });
|
|
4267
|
+
} else {
|
|
4268
|
+
if (i > 0) {
|
|
4269
|
+
const a = series[i - 1], b = series[i];
|
|
4270
|
+
const f = (playX - x(a.tMs)) / Math.max(1, x(b.tMs) - x(a.tMs));
|
|
4271
|
+
revealed.push({ x: playX, y: y(a.value + (b.value - a.value) * f) });
|
|
4272
|
+
}
|
|
4273
|
+
break;
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
if (revealed.length) {
|
|
4277
|
+
c.globalAlpha = 1;
|
|
4278
|
+
if (fill) {
|
|
4279
|
+
c.fillStyle = rgba8(col, 0.22);
|
|
4280
|
+
c.beginPath();
|
|
4281
|
+
c.moveTo(revealed[0].x, bottom);
|
|
4282
|
+
for (const p of revealed) c.lineTo(p.x, p.y);
|
|
4283
|
+
c.lineTo(revealed[revealed.length - 1].x, bottom);
|
|
4284
|
+
c.closePath();
|
|
4285
|
+
c.fill();
|
|
4286
|
+
}
|
|
4287
|
+
c.strokeStyle = col;
|
|
4288
|
+
c.lineWidth = width;
|
|
4289
|
+
c.beginPath();
|
|
4290
|
+
revealed.forEach((p, i) => i === 0 ? c.moveTo(p.x, p.y) : c.lineTo(p.x, p.y));
|
|
4291
|
+
c.stroke();
|
|
4292
|
+
if (dot) {
|
|
4293
|
+
const last = revealed[revealed.length - 1];
|
|
4294
|
+
c.fillStyle = col;
|
|
4295
|
+
c.beginPath();
|
|
4296
|
+
c.arc(last.x, last.y, width * 2, 0, Math.PI * 2);
|
|
4297
|
+
c.fill();
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
c.restore();
|
|
4301
|
+
}
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
var tensionGraphFactory = {
|
|
4305
|
+
key: "tension-graph",
|
|
4306
|
+
create: tensionGraphLayer,
|
|
4307
|
+
validateProps(props) {
|
|
4308
|
+
if (props == null || typeof props !== "object") return ["tension-graph: props must be an object"];
|
|
4309
|
+
const p = props;
|
|
4310
|
+
const errs = [];
|
|
4311
|
+
if (p.series != null) {
|
|
4312
|
+
if (!Array.isArray(p.series)) errs.push("tension-graph.series must be an array of {tMs, value}");
|
|
4313
|
+
else p.series.forEach((raw, i) => {
|
|
4314
|
+
const s = raw;
|
|
4315
|
+
if (typeof s?.tMs !== "number" || typeof s?.value !== "number") errs.push(`tension-graph.series[${i}] must be {tMs:number, value:number}`);
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
if (p.key != null && typeof p.key !== "string") errs.push("tension-graph.key must be a string");
|
|
4319
|
+
if (p.top != null && typeof p.top !== "number") errs.push("tension-graph.top must be a number");
|
|
4320
|
+
if (p.height != null && (typeof p.height !== "number" || p.height <= 0)) errs.push("tension-graph.height must be a positive number");
|
|
4321
|
+
if (p.color != null && typeof p.color !== "string") errs.push("tension-graph.color must be a string");
|
|
4322
|
+
if (p.fill != null && typeof p.fill !== "boolean") errs.push("tension-graph.fill must be a boolean");
|
|
4323
|
+
if (p.width != null && (typeof p.width !== "number" || p.width <= 0)) errs.push("tension-graph.width must be a positive number");
|
|
4324
|
+
if (p.dot != null && typeof p.dot !== "boolean") errs.push("tension-graph.dot must be a boolean");
|
|
4325
|
+
return errs;
|
|
4326
|
+
}
|
|
4327
|
+
};
|
|
4328
|
+
|
|
4329
|
+
// src/scene/layers/texture.ts
|
|
4330
|
+
var DEFAULTS8 = { grain: 0.05, grainDensity: 700, vignette: 0.35, letterboxFrac: 0 };
|
|
4331
|
+
function lcg(seed) {
|
|
4332
|
+
let s = seed | 0 || 1;
|
|
4333
|
+
return () => {
|
|
4334
|
+
s = s * 1664525 + 1013904223 & 2147483647;
|
|
4335
|
+
return s / 2147483647;
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
function textureLayer() {
|
|
4339
|
+
let grain = DEFAULTS8.grain;
|
|
4340
|
+
let grainDensity = DEFAULTS8.grainDensity;
|
|
4341
|
+
let vignette = DEFAULTS8.vignette;
|
|
4342
|
+
let letterboxFrac = DEFAULTS8.letterboxFrac;
|
|
4343
|
+
return {
|
|
4344
|
+
key: "texture",
|
|
4345
|
+
init(_ctx, props) {
|
|
4346
|
+
grain = props.grain ?? DEFAULTS8.grain;
|
|
4347
|
+
grainDensity = props.grainDensity ?? DEFAULTS8.grainDensity;
|
|
4348
|
+
vignette = props.vignette ?? DEFAULTS8.vignette;
|
|
4349
|
+
letterboxFrac = props.letterboxFrac ?? DEFAULTS8.letterboxFrac;
|
|
4350
|
+
},
|
|
4351
|
+
draw(ctx, tMs) {
|
|
4352
|
+
const c = ctx.ctx2d;
|
|
4353
|
+
const W = ctx.W, H = ctx.H;
|
|
4354
|
+
c.save();
|
|
4355
|
+
if (vignette > 0) {
|
|
4356
|
+
const g = c.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.35, W / 2, H / 2, Math.max(W, H) * 0.72);
|
|
4357
|
+
g.addColorStop(0, "rgba(0,0,0,0)");
|
|
4358
|
+
g.addColorStop(1, `rgba(0,0,0,${vignette})`);
|
|
4359
|
+
c.fillStyle = g;
|
|
4360
|
+
c.fillRect(0, 0, W, H);
|
|
4361
|
+
}
|
|
4362
|
+
if (grain > 0 && grainDensity > 0) {
|
|
4363
|
+
const frame = Math.floor(tMs / 1e3 * (ctx.fps || 30));
|
|
4364
|
+
const rnd = lcg(frame * 2654435761);
|
|
4365
|
+
for (let i = 0; i < grainDensity; i++) {
|
|
4366
|
+
const x = rnd() * W;
|
|
4367
|
+
const y = rnd() * H;
|
|
4368
|
+
const dark = rnd() < 0.5;
|
|
4369
|
+
c.fillStyle = dark ? `rgba(0,0,0,${grain})` : `rgba(255,255,255,${grain})`;
|
|
4370
|
+
c.fillRect(x, y, 2, 2);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
if (letterboxFrac > 0) {
|
|
4374
|
+
const bh = H * letterboxFrac;
|
|
4375
|
+
c.fillStyle = "#000";
|
|
4376
|
+
c.fillRect(0, 0, W, bh);
|
|
4377
|
+
c.fillRect(0, H - bh, W, bh);
|
|
4378
|
+
}
|
|
4379
|
+
c.restore();
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4382
|
+
}
|
|
4383
|
+
var textureFactory = {
|
|
4384
|
+
key: "texture",
|
|
4385
|
+
create: textureLayer,
|
|
4386
|
+
validateProps(props) {
|
|
4387
|
+
if (props == null || typeof props !== "object") return ["texture: props must be an object"];
|
|
4388
|
+
const p = props;
|
|
4389
|
+
const errs = [];
|
|
4390
|
+
if (p.grain != null && (typeof p.grain !== "number" || p.grain < 0 || p.grain > 1)) errs.push("texture.grain must be in [0,1]");
|
|
4391
|
+
if (p.grainDensity != null && (typeof p.grainDensity !== "number" || p.grainDensity < 0)) errs.push("texture.grainDensity must be a number >= 0");
|
|
4392
|
+
if (p.vignette != null && (typeof p.vignette !== "number" || p.vignette < 0 || p.vignette > 1)) errs.push("texture.vignette must be in [0,1]");
|
|
4393
|
+
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]");
|
|
4394
|
+
return errs;
|
|
4395
|
+
}
|
|
4396
|
+
};
|
|
4397
|
+
|
|
4398
|
+
// src/scene/layers/statCounter.ts
|
|
4399
|
+
var DEFAULTS9 = { startMs: 0, durationMs: 1200, fontPx: 220, centerFrac: 0.46, group: true };
|
|
4400
|
+
function easeOut2(t) {
|
|
4401
|
+
return 1 - Math.pow(1 - t, 3);
|
|
4402
|
+
}
|
|
4403
|
+
function clamp015(x) {
|
|
4404
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
4405
|
+
}
|
|
4406
|
+
function statCounterLayer() {
|
|
4407
|
+
let value = 0;
|
|
4408
|
+
let startMs = DEFAULTS9.startMs;
|
|
4409
|
+
let durationMs = DEFAULTS9.durationMs;
|
|
4410
|
+
let prefix = "";
|
|
4411
|
+
let suffix = "";
|
|
4412
|
+
let label = "";
|
|
4413
|
+
let fontPx = DEFAULTS9.fontPx;
|
|
4414
|
+
let centerFrac = DEFAULTS9.centerFrac;
|
|
4415
|
+
let color;
|
|
4416
|
+
let group = DEFAULTS9.group;
|
|
4417
|
+
return {
|
|
4418
|
+
key: "stat-counter",
|
|
4419
|
+
init(_ctx, props) {
|
|
4420
|
+
value = props.value ?? 0;
|
|
4421
|
+
startMs = props.startMs ?? DEFAULTS9.startMs;
|
|
4422
|
+
durationMs = props.durationMs ?? DEFAULTS9.durationMs;
|
|
4423
|
+
prefix = props.prefix ?? "";
|
|
4424
|
+
suffix = props.suffix ?? "";
|
|
4425
|
+
label = props.label ?? "";
|
|
4426
|
+
fontPx = props.fontPx ?? DEFAULTS9.fontPx;
|
|
4427
|
+
centerFrac = props.centerFrac ?? DEFAULTS9.centerFrac;
|
|
4428
|
+
color = props.color;
|
|
4429
|
+
group = props.group ?? DEFAULTS9.group;
|
|
4430
|
+
},
|
|
4431
|
+
draw(ctx, tMs) {
|
|
4432
|
+
const c = ctx.ctx2d;
|
|
4433
|
+
const p = clamp015((tMs - startMs) / Math.max(1, durationMs));
|
|
4434
|
+
const shown = Math.round(easeOut2(p) * value);
|
|
4435
|
+
const numStr = group ? shown.toLocaleString("en-US") : String(shown);
|
|
4436
|
+
const cx = ctx.W / 2;
|
|
4437
|
+
const cy = ctx.H * centerFrac;
|
|
4438
|
+
const col = color ?? ctx.theme.accent;
|
|
4439
|
+
c.save();
|
|
4440
|
+
c.textAlign = "center";
|
|
4441
|
+
c.textBaseline = "middle";
|
|
4442
|
+
c.fillStyle = col;
|
|
4443
|
+
c.font = `800 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
4444
|
+
c.fillText(`${prefix}${numStr}${suffix}`, cx, cy);
|
|
4445
|
+
if (label) {
|
|
4446
|
+
c.fillStyle = ctx.theme.ink;
|
|
4447
|
+
c.font = `600 ${Math.round(fontPx * 0.26)}px ${ctx.theme.fontDisplay}`;
|
|
4448
|
+
c.fillText(label, cx, cy + fontPx * 0.62);
|
|
4449
|
+
}
|
|
4450
|
+
c.restore();
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
}
|
|
4454
|
+
var statCounterFactory = {
|
|
4455
|
+
key: "stat-counter",
|
|
4456
|
+
create: statCounterLayer,
|
|
4457
|
+
validateProps(props) {
|
|
4458
|
+
if (props == null || typeof props !== "object") return ["stat-counter: props must be an object"];
|
|
4459
|
+
const p = props;
|
|
4460
|
+
const errs = [];
|
|
4461
|
+
if (typeof p.value !== "number") errs.push("stat-counter.value must be a number");
|
|
4462
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("stat-counter.startMs must be a number");
|
|
4463
|
+
if (p.durationMs != null && (typeof p.durationMs !== "number" || p.durationMs <= 0)) errs.push("stat-counter.durationMs must be a positive number");
|
|
4464
|
+
for (const k of ["prefix", "suffix", "label"]) if (p[k] != null && typeof p[k] !== "string") errs.push(`stat-counter.${k} must be a string`);
|
|
4465
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("stat-counter.fontPx must be a positive number");
|
|
4466
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("stat-counter.centerFrac must be in [0,1]");
|
|
4467
|
+
if (p.color != null && typeof p.color !== "string") errs.push("stat-counter.color must be a string");
|
|
4468
|
+
if (p.group != null && typeof p.group !== "boolean") errs.push("stat-counter.group must be a boolean");
|
|
4469
|
+
return errs;
|
|
4470
|
+
}
|
|
4471
|
+
};
|
|
4472
|
+
|
|
4473
|
+
// src/scene/layers/endCard.ts
|
|
4474
|
+
var DEFAULTS10 = { text: "Follow for daily", startMs: 0, inMs: 400, centerFrac: 0.5, fontPx: 72, arrow: true, arrowDir: "up" };
|
|
4475
|
+
function clamp016(x) {
|
|
4476
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
4477
|
+
}
|
|
4478
|
+
function easeOut3(t) {
|
|
4479
|
+
return 1 - Math.pow(1 - t, 3);
|
|
4480
|
+
}
|
|
4481
|
+
function endCardLayer() {
|
|
4482
|
+
let text = DEFAULTS10.text;
|
|
4483
|
+
let handle = "";
|
|
4484
|
+
let startMs = DEFAULTS10.startMs;
|
|
4485
|
+
let inMs = DEFAULTS10.inMs;
|
|
4486
|
+
let color;
|
|
4487
|
+
let centerFrac = DEFAULTS10.centerFrac;
|
|
4488
|
+
let fontPx = DEFAULTS10.fontPx;
|
|
4489
|
+
let arrow = DEFAULTS10.arrow;
|
|
4490
|
+
let arrowDir = DEFAULTS10.arrowDir;
|
|
4491
|
+
return {
|
|
4492
|
+
key: "end-card",
|
|
4493
|
+
init(_ctx, props) {
|
|
4494
|
+
text = props.text ?? DEFAULTS10.text;
|
|
4495
|
+
handle = props.handle ?? "";
|
|
4496
|
+
startMs = props.startMs ?? DEFAULTS10.startMs;
|
|
4497
|
+
inMs = props.inMs ?? DEFAULTS10.inMs;
|
|
4498
|
+
color = props.color;
|
|
4499
|
+
centerFrac = props.centerFrac ?? DEFAULTS10.centerFrac;
|
|
4500
|
+
fontPx = props.fontPx ?? DEFAULTS10.fontPx;
|
|
4501
|
+
arrow = props.arrow ?? DEFAULTS10.arrow;
|
|
4502
|
+
arrowDir = props.arrowDir ?? DEFAULTS10.arrowDir;
|
|
4503
|
+
},
|
|
4504
|
+
draw(ctx, tMs) {
|
|
4505
|
+
if (tMs < startMs) return;
|
|
4506
|
+
const c = ctx.ctx2d;
|
|
4507
|
+
const cx = ctx.W / 2;
|
|
4508
|
+
const cy = ctx.H * centerFrac;
|
|
4509
|
+
const col = color ?? ctx.theme.accent;
|
|
4510
|
+
const p = clamp016((tMs - startMs) / Math.max(1, inMs));
|
|
4511
|
+
const scale = 0.7 + 0.3 * easeOut3(p);
|
|
4512
|
+
const dir = arrowDir === "down" ? 1 : -1;
|
|
4513
|
+
const bounce = Math.sin(tMs / 260) * 14;
|
|
4514
|
+
c.save();
|
|
4515
|
+
c.globalAlpha = p;
|
|
4516
|
+
c.translate(cx, cy);
|
|
4517
|
+
c.scale(scale, scale);
|
|
4518
|
+
c.textAlign = "center";
|
|
4519
|
+
c.textBaseline = "middle";
|
|
4520
|
+
c.fillStyle = col;
|
|
4521
|
+
c.font = `800 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
4522
|
+
c.fillText(text, 0, 0);
|
|
4523
|
+
if (handle) {
|
|
4524
|
+
c.fillStyle = ctx.theme.ink;
|
|
4525
|
+
c.font = `600 ${Math.round(fontPx * 0.5)}px ${ctx.theme.fontDisplay}`;
|
|
4526
|
+
c.fillText(handle, 0, fontPx * 0.9);
|
|
4527
|
+
}
|
|
4528
|
+
c.restore();
|
|
4529
|
+
if (arrow) {
|
|
4530
|
+
const ax = cx;
|
|
4531
|
+
const ay = (arrowDir === "down" ? cy + fontPx * 1.6 : cy - fontPx * 1.6) + bounce * dir;
|
|
4532
|
+
const s = fontPx * 0.4;
|
|
4533
|
+
c.save();
|
|
4534
|
+
c.globalAlpha = p;
|
|
4535
|
+
c.strokeStyle = col;
|
|
4536
|
+
c.lineWidth = 10;
|
|
4537
|
+
c.lineCap = "round";
|
|
4538
|
+
c.lineJoin = "round";
|
|
4539
|
+
c.beginPath();
|
|
4540
|
+
if (arrowDir === "down") {
|
|
4541
|
+
c.moveTo(ax - s, ay - s * 0.6);
|
|
4542
|
+
c.lineTo(ax, ay + s * 0.6);
|
|
4543
|
+
c.lineTo(ax + s, ay - s * 0.6);
|
|
4544
|
+
} else {
|
|
4545
|
+
c.moveTo(ax - s, ay + s * 0.6);
|
|
4546
|
+
c.lineTo(ax, ay - s * 0.6);
|
|
4547
|
+
c.lineTo(ax + s, ay + s * 0.6);
|
|
4548
|
+
}
|
|
4549
|
+
c.stroke();
|
|
4550
|
+
c.restore();
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4555
|
+
var endCardFactory = {
|
|
4556
|
+
key: "end-card",
|
|
4557
|
+
create: endCardLayer,
|
|
4558
|
+
validateProps(props) {
|
|
4559
|
+
if (props == null || typeof props !== "object") return ["end-card: props must be an object"];
|
|
4560
|
+
const p = props;
|
|
4561
|
+
const errs = [];
|
|
4562
|
+
for (const k of ["text", "handle", "color"]) if (p[k] != null && typeof p[k] !== "string") errs.push(`end-card.${k} must be a string`);
|
|
4563
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("end-card.startMs must be a number");
|
|
4564
|
+
if (p.inMs != null && (typeof p.inMs !== "number" || p.inMs <= 0)) errs.push("end-card.inMs must be a positive number");
|
|
4565
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("end-card.centerFrac must be in [0,1]");
|
|
4566
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("end-card.fontPx must be a positive number");
|
|
4567
|
+
if (p.arrow != null && typeof p.arrow !== "boolean") errs.push("end-card.arrow must be a boolean");
|
|
4568
|
+
if (p.arrowDir != null && p.arrowDir !== "up" && p.arrowDir !== "down") errs.push('end-card.arrowDir must be "up" | "down"');
|
|
4569
|
+
return errs;
|
|
4570
|
+
}
|
|
4571
|
+
};
|
|
4572
|
+
|
|
4077
4573
|
// src/scene/registry.ts
|
|
4078
4574
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
4079
4575
|
function registerLayer(factory) {
|
|
@@ -4116,6 +4612,11 @@ registerLayer(kineticTextFactory);
|
|
|
4116
4612
|
registerLayer(countdownFactory);
|
|
4117
4613
|
registerLayer(waveformFactory);
|
|
4118
4614
|
registerLayer(scaleHighlightFactory);
|
|
4615
|
+
registerLayer(particleBurstFactory);
|
|
4616
|
+
registerLayer(tensionGraphFactory);
|
|
4617
|
+
registerLayer(textureFactory);
|
|
4618
|
+
registerLayer(statCounterFactory);
|
|
4619
|
+
registerLayer(endCardFactory);
|
|
4119
4620
|
|
|
4120
4621
|
// src/scene/audioLayers.ts
|
|
4121
4622
|
function countInSchedule(opts) {
|
|
@@ -4274,7 +4775,11 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
4274
4775
|
"progress-ring",
|
|
4275
4776
|
"karaoke-caption",
|
|
4276
4777
|
"kinetic-text",
|
|
4277
|
-
"countdown"
|
|
4778
|
+
"countdown",
|
|
4779
|
+
"particle-burst",
|
|
4780
|
+
"texture",
|
|
4781
|
+
"stat-counter",
|
|
4782
|
+
"end-card"
|
|
4278
4783
|
]);
|
|
4279
4784
|
function defaultBufferFactory() {
|
|
4280
4785
|
const g = globalThis;
|
|
@@ -4912,6 +5417,7 @@ export {
|
|
|
4912
5417
|
easeIn,
|
|
4913
5418
|
easeInOut,
|
|
4914
5419
|
easeOut,
|
|
5420
|
+
endCardFactory,
|
|
4915
5421
|
fallingKeyboardDemoScore,
|
|
4916
5422
|
fallingKeyboardDemoSpec,
|
|
4917
5423
|
fallingNotesFactory,
|
|
@@ -4963,6 +5469,7 @@ export {
|
|
|
4963
5469
|
noteSetXRange,
|
|
4964
5470
|
parseKey,
|
|
4965
5471
|
parseTimeSig,
|
|
5472
|
+
particleBurstFactory,
|
|
4966
5473
|
pcToSlot,
|
|
4967
5474
|
pitchAt,
|
|
4968
5475
|
pitchContourFactory,
|
|
@@ -5003,7 +5510,10 @@ export {
|
|
|
5003
5510
|
staffAnchor,
|
|
5004
5511
|
staffKeyboardRayFactory,
|
|
5005
5512
|
staffRayDemoSpec,
|
|
5513
|
+
statCounterFactory,
|
|
5006
5514
|
systemBox,
|
|
5515
|
+
tensionGraphFactory,
|
|
5516
|
+
textureFactory,
|
|
5007
5517
|
transitionProgress,
|
|
5008
5518
|
validateChordTrack,
|
|
5009
5519
|
validateQuiz,
|