Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(percussion): drums pulse full-screen background in drum colors

Tones have always painted the whole background from their note colors
while held — a kick drum, being a one-shot, never got the same visual
response. Drums now get a transient flash layer that merges with the
tone layer in paint(), so every hit pulses the screen in its kit color.

Design:
- PERCUSSION_COLORS (existing drum-pad palette) is reused as the flash
palette: kick = deep orange, snare = tan, clap = pale yellow, hats =
cyan/mint, ride = silver-blue, crash = lavender, splash = pink,
cowbell = brass, block = wood brown, tambourine = sandy, etc.
- New `drumFlashes` queue (module-level) holds up to 8 concurrent
fading entries. Each entry = { color, frame, life }. Life = 14
frames (~230ms at 60fps) so rolls overlap into a color wash.
- `flashDrum(letter)` pushes a new entry with max alpha; paint() prunes
expired entries and blends them into the bg compute.

Alpha curve per entry: hold full 1.0 for the first 2 frames (so the
hit reads as a punchy blink), then linear decay to 0 over the rest of
the life. This matches how real drum transients feel.

Blend in paint():
- Tone layer and drum layer are computed separately, then weighted-
summed (drums get 1.2× weight so a single hit still reads even
against multiple held tones).
- If any tone is held, the combined color snaps instantly (matches
previous "blink" behavior for tones).
- If drum-only (no held tones), the color smooth-blends at 0.55/frame
so rapid hits stack nicely and single hits fade out over ~230ms.
- With neither tone nor drum, background fades to dark/light theme.

Wired into all 4 drum trigger paths:
- Keyboard act() drum branch (notepat.mjs:~1450)
- Touch-tap drum pad (notepat.mjs:~1820)
- Drag-rollover drum pad (notepat.mjs:~1975)
- Reverse-playback replay (playReverseEvent, notepat.mjs:~467)

+77 -22
+77 -22
fedac/native/pieces/notepat.mjs
··· 137 137 let bgColor = [0, 0, 0]; 138 138 let bgTarget = dark ? [20, 20, 25] : [240, 238, 232]; 139 139 140 + // === DRUM BACKGROUND FLASH === 141 + // Tones paint the full-screen background from their note colors while held 142 + // (see the activeKeys loop in paint()). Drums are one-shots and don't live 143 + // in `sounds[key]`, so they need their own transient flash state. Each drum 144 + // hit pushes an entry here; paint() sums+decays them into the bg color so 145 + // a kick pulses orange, a ride pulses silver-blue, a crash pulses lavender, 146 + // etc. Rolls overlap because we keep up to 8 concurrent fading entries. 147 + const DRUM_FLASH_LIFE = 14; // ~230 ms at 60 fps 148 + let drumFlashes = []; // [{ color: [r,g,b], frame, life }] 149 + 140 150 // Cached sound API ref (for leave) 141 151 let soundAPI = null; 142 152 let systemAPI = null; ··· 454 464 if (!sound) return; 455 465 if (ev.kind === "drum") { 456 466 playPercussion(sound, ev.letter, (ev.vel || 1) * 1.5, ev.pan || 0, ev.pitch || 1); 467 + flashDrum(ev.letter); 457 468 } else { 458 469 const playFreq = (ev.freq || 440) * (ev.pitch || 1); 459 470 sound?.synth?.({ ··· 599 610 return Math.max(-0.9, Math.min(0.9, gridBias + drumOffset * 0.7)); 600 611 } 601 612 613 + // Push a drum flash entry so paint() will pulse the background in that 614 + // drum's characteristic color. Called from every drum trigger site 615 + // (keyboard act(), touch-tap, drag-rollover, reverse-playback replay). 616 + function flashDrum(letter) { 617 + const color = PERCUSSION_COLORS[letter] || [200, 200, 200]; 618 + drumFlashes.push({ color, frame, life: DRUM_FLASH_LIFE }); 619 + if (drumFlashes.length > 8) drumFlashes.shift(); 620 + } 621 + 602 622 // Fire a drum hit from short auto-stopping synth voices. No cleanup 603 623 // needed on key-up because every voice uses a finite duration. 604 624 // ··· 1484 1504 } else { 1485 1505 playPercussion(sound, letter, drumVol, drumPan, drumPitch); 1486 1506 } 1507 + flashDrum(letter); 1487 1508 recordPlayback({ kind: "drum", letter, octave: noteOctave, vel: drumVol, pan: drumPan, pitch: drumPitch }); 1488 1509 trail[key] = { note: letter, octave: noteOctave, brightness: velocity }; 1489 1510 return; ··· 1817 1838 } else { 1818 1839 playPercussion(sound, hitNote.letter, 1.8, touchDrumPan, touchDrumPitch); 1819 1840 } 1841 + flashDrum(hitNote.letter); 1820 1842 recordPlayback({ kind: "drum", letter: hitNote.letter, octave: hitNote.octave, vel: 1.8, pan: touchDrumPan, pitch: touchDrumPitch }); 1821 1843 trail[hitNote.key] = { note: hitNote.letter, octave: hitNote.octave, brightness: 1.0 }; 1822 1844 touchNotes[pid] = { key: hitNote.key }; ··· 1968 1990 } else { 1969 1991 playPercussion(sound, hitNote.letter, 1.8, rollDrumPan, rollDrumPitch); 1970 1992 } 1993 + flashDrum(hitNote.letter); 1971 1994 recordPlayback({ kind: "drum", letter: hitNote.letter, octave: hitNote.octave, vel: 1.8, pan: rollDrumPan, pitch: rollDrumPitch }); 1972 1995 trail[hitNote.key] = { note: hitNote.letter, octave: hitNote.octave, brightness: 1.0 }; 1973 1996 touchNotes[pid] = { key: hitNote.key }; ··· 2140 2163 const PAD_SHARP = dark ? [18, 18, 20] : [235, 232, 228]; 2141 2164 const PAD_OUTLINE = dark ? [50, 50, 55] : [210, 205, 200]; 2142 2165 2143 - // Compute background color from active notes — full color flash 2166 + // Compute background color from active notes (held) + drum flashes (decaying) 2167 + // — full-screen color response so drums have the same visual presence as tones. 2144 2168 const activeKeys = Object.keys(sounds); 2145 - if (activeKeys.length > 0) { 2146 - let r = 0, g = 0, b = 0; 2147 - for (const key of activeKeys) { 2148 - const noteName = KEY_TO_NOTE[key]; 2149 - if (noteName) { 2150 - const [letter] = parseNote(noteName); 2151 - const nc = noteColor(letter); 2152 - r += nc[0]; g += nc[1]; b += nc[2]; 2153 - } 2169 + 2170 + // Tone layer: sum of colors for every held note 2171 + let toneR = 0, toneG = 0, toneB = 0, toneWeight = 0; 2172 + for (const key of activeKeys) { 2173 + const noteName = KEY_TO_NOTE[key]; 2174 + if (noteName) { 2175 + const [letter] = parseNote(noteName); 2176 + const nc = noteColor(letter); 2177 + toneR += nc[0]; toneG += nc[1]; toneB += nc[2]; toneWeight++; 2154 2178 } 2155 - const n = activeKeys.length; 2156 - // Saturate: boost toward max channel, clamp to 255 2157 - const avg = [r / n, g / n, b / n]; 2158 - const mx = Math.max(avg[0], avg[1], avg[2], 1); 2159 - const sat = 255 / mx; // scale so brightest channel hits 255 2179 + } 2180 + 2181 + // Drum layer: prune expired entries, then sum with per-entry alpha decay. 2182 + // First 2 frames hold full intensity for a punchy hit, then linear fade. 2183 + if (drumFlashes.length > 0) { 2184 + drumFlashes = drumFlashes.filter(f => (frame - f.frame) < f.life); 2185 + } 2186 + let drumR = 0, drumG = 0, drumB = 0, drumWeight = 0; 2187 + for (const f of drumFlashes) { 2188 + const age = frame - f.frame; 2189 + let alpha; 2190 + if (age < 2) alpha = 1.0; 2191 + else alpha = Math.max(0, 1 - (age - 2) / (f.life - 2)); 2192 + drumR += f.color[0] * alpha; 2193 + drumG += f.color[1] * alpha; 2194 + drumB += f.color[2] * alpha; 2195 + drumWeight += alpha; 2196 + } 2197 + 2198 + if (toneWeight > 0 || drumWeight > 0) { 2199 + // Tones count as full weight each; drums' contribution is their summed alpha. 2200 + // Drums get a 1.2× weight multiplier so a single hit still reads on screen 2201 + // even if multiple tones are held simultaneously. 2202 + const totalWeight = toneWeight + drumWeight * 1.2; 2203 + const r = (toneR + drumR * 1.2) / totalWeight; 2204 + const g = (toneG + drumG * 1.2) / totalWeight; 2205 + const b = (toneB + drumB * 1.2) / totalWeight; 2206 + const mx = Math.max(r, g, b, 1); 2207 + const sat = 255 / mx; 2160 2208 bgTarget = [ 2161 - Math.min(255, Math.floor(avg[0] * sat)), 2162 - Math.min(255, Math.floor(avg[1] * sat)), 2163 - Math.min(255, Math.floor(avg[2] * sat)), 2209 + Math.min(255, Math.floor(r * sat)), 2210 + Math.min(255, Math.floor(g * sat)), 2211 + Math.min(255, Math.floor(b * sat)), 2164 2212 ]; 2165 - // Snap on instantly (blink) 2166 - bgColor[0] = bgTarget[0]; 2167 - bgColor[1] = bgTarget[1]; 2168 - bgColor[2] = bgTarget[2]; 2213 + if (toneWeight > 0) { 2214 + // Held tone(s) — snap instantly so the blink reads 2215 + bgColor[0] = bgTarget[0]; 2216 + bgColor[1] = bgTarget[1]; 2217 + bgColor[2] = bgTarget[2]; 2218 + } else { 2219 + // Drum-only — fast blend so rapid hits still read but single hits fade 2220 + bgColor[0] += (bgTarget[0] - bgColor[0]) * 0.55; 2221 + bgColor[1] += (bgTarget[1] - bgColor[1]) * 0.55; 2222 + bgColor[2] += (bgTarget[2] - bgColor[2]) * 0.55; 2223 + } 2169 2224 } else { 2170 2225 bgTarget = dark ? [0, 0, 0] : [255, 255, 255]; 2171 2226 // Fade smoothly