Paint the stands once
- The opaque menu no longer has the whole court rendered behind it at full resolution — minutes of battery burn nobody could see. - Stands, ads, rot tint, court, lines and the run-off are one cached blit, rebuilt only when the palette actually moves; the clap ripple stays live. - RES derives from devicePixelRatio (capped at 4), so a 2x phone stops painting double the pixels it can show. - One shared noise buffer for every synthesized sound — applause alone was allocating ~200 fresh buffers per cheer. - The touch-steer path stops re-measuring a fixed canvas rect per move event. Skipped as not-quick: the z-sort closure churn and swap-remove splices — real but small, and each is a behavior-preserving refactor for another day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1f0d05a61
commit
e07196f247
4 files changed
+173
-129
No files matched your search
@@ -147,11 +147,22 @@ export function say(text, delay = 0) { // fallback when a recorded umpire line i
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function noiseBuf(ac, len) {
|
||||
const b = ac.createBuffer(1, ac.sampleRate * len, ac.sampleRate), d = b.getChannelData(0);
|
||||
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
||||
return b;
|
||||
// ONE noise buffer, shared. Every swish/pock/thud/splat/clap used to fill a fresh Float32 buffer
|
||||
// (thousands of Math.random() calls, ~30 KB) per sound — during a horde the gun did that a dozen
|
||||
// times a second, and applause() did it ~200 times per cheer. Callers play a random slice instead
|
||||
// (src.start's offset + duration), so repeats never phase-align and the node still stops on time.
|
||||
const NOISE_LEN = 1.5;
|
||||
let noise = null, noiseAc = null;
|
||||
function noiseBuf(ac) {
|
||||
if (noiseAc !== ac) { // a fresh context invalidates the old buffer
|
||||
noise = ac.createBuffer(1, ac.sampleRate * NOISE_LEN, ac.sampleRate);
|
||||
const d = noise.getChannelData(0);
|
||||
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
||||
noiseAc = ac;
|
||||
}
|
||||
return noise;
|
||||
}
|
||||
const nOff = len => Math.random() * (NOISE_LEN - len); // a random slice into the shared buffer
|
||||
|
||||
// Filtered noise sweep. Default = the racket cutting air (rising, short) — the sound of a whiff.
|
||||
// The sweep is parameterised because a DESCENDING, longer one is a panel swooshing in and settling:
|
||||
@@ -159,13 +170,13 @@ function noiseBuf(ac, len) {
|
||||
export function swish(vol = 0.3, f0 = 1600, f1 = 3200, dur = 0.16) {
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime;
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, dur);
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(f0, t);
|
||||
bp.frequency.exponentialRampToValueAtTime(f1, t + dur * 0.75); bp.Q.value = 1.5;
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(0.001, t); g.gain.exponentialRampToValueAtTime(vol, t + dur * 0.3);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start();
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t, nOff(dur), dur);
|
||||
} catch (e) {}
|
||||
}
|
||||
export const whoosh = (vol = 0.34) => swish(vol, 4200, 700, 0.3); // a panel flying in and settling
|
||||
@@ -173,11 +184,11 @@ export const whoosh = (vol = 0.34) => swish(vol, 4200, 700, 0.3); // a panel fly
|
||||
export function pock(vol = 0.8, freq = 900) { // racket contact / bounce thock
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime;
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.06);
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = freq; bp.Q.value = 1.2;
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.07);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start();
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t, nOff(0.06), 0.06);
|
||||
const o = ac.createOscillator(), og = ac.createGain(); // low thump body
|
||||
o.frequency.setValueAtTime(180, t); o.frequency.exponentialRampToValueAtTime(70, t + 0.08);
|
||||
og.gain.setValueAtTime(vol * 0.5, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.09);
|
||||
@@ -197,13 +208,13 @@ export function thud(vol = 0.6, size = 1) {
|
||||
const dur = 0.15 * k;
|
||||
// the slap. The band FALLS, where pock's sits still — a collapsing resonance is the wet part,
|
||||
// and it is the same trick splat() uses, just shorter and much less of it.
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, dur);
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 3.4;
|
||||
bp.frequency.setValueAtTime(780 / k, t);
|
||||
bp.frequency.exponentialRampToValueAtTime(190 / k, t + dur);
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol * 0.8, t); g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t, nOff(dur), dur);
|
||||
// the weight behind it — THIS is what actually carries the size difference
|
||||
const o = ac.createOscillator(), og = ac.createGain();
|
||||
o.frequency.setValueAtTime(150 / k, t); o.frequency.exponentialRampToValueAtTime(52 / k, t + 0.11 * k);
|
||||
@@ -211,12 +222,12 @@ export function thud(vol = 0.6, size = 1) {
|
||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.15 * k);
|
||||
// one short bright tick just behind the slap: something gave. Quiet on purpose — this is a hit,
|
||||
// not a burst, and at splat()'s spatter level every rally would sound like a kill.
|
||||
const d = 0.03 * k, sp = ac.createBufferSource(); sp.buffer = noiseBuf(ac, 0.07 * k);
|
||||
const d = 0.03 * k, sp = ac.createBufferSource(); sp.buffer = noiseBuf(ac);
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 1100;
|
||||
const spg = ac.createGain();
|
||||
spg.gain.setValueAtTime(vol * 0.18, t + d);
|
||||
spg.gain.exponentialRampToValueAtTime(0.001, t + d + 0.07 * k);
|
||||
sp.connect(hp).connect(spg).connect(ac.destination); sp.start(t + d);
|
||||
sp.connect(hp).connect(spg).connect(ac.destination); sp.start(t + d, nOff(0.07 * k), 0.07 * k);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -231,26 +242,26 @@ export function splat(vol = 0.7, size = 1) {
|
||||
const dur = 0.22 * k;
|
||||
// The wetness lives HERE. The old filter was a plain lowpass with no resonance, which is a dry
|
||||
// hiss with the top taken off; a RESONANT band collapsing down the spectrum is a squelch.
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, dur);
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 3.2;
|
||||
bp.frequency.setValueAtTime(1100 / k, t);
|
||||
bp.frequency.exponentialRampToValueAtTime(130 / k, t + dur * 0.85);
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t, nOff(dur), dur);
|
||||
const o = ac.createOscillator(), og = ac.createGain(); // body thud, heavier on a heavier body
|
||||
o.frequency.setValueAtTime(120 / k, t); o.frequency.exponentialRampToValueAtTime(45 / k, t + 0.15 * k);
|
||||
og.gain.setValueAtTime(vol * 0.7, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.16 * k);
|
||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.18 * k);
|
||||
// ...and what came out of it lands a moment later. This second, brighter burst is most of what
|
||||
// reads as "wet" — one clean hit is a thud, a thud followed by spatter is a body.
|
||||
const d = 0.06 * k, sp = ac.createBufferSource(); sp.buffer = noiseBuf(ac, 0.18 * k);
|
||||
const d = 0.06 * k, sp = ac.createBufferSource(); sp.buffer = noiseBuf(ac);
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 900;
|
||||
const spg = ac.createGain();
|
||||
spg.gain.setValueAtTime(0.001, t + d);
|
||||
spg.gain.exponentialRampToValueAtTime(vol * 0.3, t + d + 0.02);
|
||||
spg.gain.exponentialRampToValueAtTime(0.001, t + d + 0.18 * k);
|
||||
sp.connect(hp).connect(spg).connect(ac.destination); sp.start(t + d);
|
||||
sp.connect(hp).connect(spg).connect(ac.destination); sp.start(t + d, nOff(0.18 * k), 0.18 * k);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -261,12 +272,12 @@ export function boom(vol = 0.9) { // bomb ball — sub-bass thump under a filter
|
||||
o.frequency.setValueAtTime(80, t); o.frequency.exponentialRampToValueAtTime(30, t + 0.35);
|
||||
og.gain.setValueAtTime(vol, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
|
||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.42);
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.4); // the blast
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac); // the blast
|
||||
const lp = ac.createBiquadFilter(); lp.type = 'lowpass';
|
||||
lp.frequency.setValueAtTime(2200, t); lp.frequency.exponentialRampToValueAtTime(200, t + 0.35);
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol * 0.8, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
|
||||
src.connect(lp).connect(g).connect(ac.destination); src.start(t);
|
||||
src.connect(lp).connect(g).connect(ac.destination); src.start(t, nOff(0.4), 0.4);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -276,11 +287,11 @@ export function boom(vol = 0.9) { // bomb ball — sub-bass thump under a filter
|
||||
export function zap(vol = 0.5) {
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime, dur = 0.42;
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.09); // the crack: air leaving
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac); // the crack: air leaving
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 2200;
|
||||
const ng = ac.createGain();
|
||||
ng.gain.setValueAtTime(vol * 0.85, t); ng.gain.exponentialRampToValueAtTime(0.001, t + 0.09);
|
||||
src.connect(hp).connect(ng).connect(ac.destination); src.start(t);
|
||||
src.connect(hp).connect(ng).connect(ac.destination); src.start(t, nOff(0.09), 0.09);
|
||||
// the body: two detuned saws DIVING through a resonant lowpass. The direction is the whole
|
||||
// reading — a rising sweep is a charge-up, and this thing never charges, it discharges.
|
||||
const lp = ac.createBiquadFilter(); lp.type = 'lowpass'; lp.Q.value = 12;
|
||||
@@ -344,13 +355,13 @@ export function applause(seconds = 2.4, vol = 0.5) { // ponytail: synthesized cl
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = t0 + Math.random() * seconds;
|
||||
const env = Math.min(1, 4 * (1 - Math.abs(2 * (t - t0) / seconds - 1))); // fade in/out
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.025);
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass';
|
||||
bp.frequency.value = 1000 + Math.random() * 1600; bp.Q.value = 2.5;
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol * env * (0.3 + Math.random() * 0.7), t);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, t + 0.03);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t, nOff(0.025), 0.025);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
Reference in new issue
Block a user