Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat(notepat): animated stochastic graphic notation inside drum pads

User: "can the pads have graphics inside representing the mechanism
of stochasticness animated like graphic notation within each pad
while the perc kit is selected".

Each drum's voice stack is now plotted as a compact stochastic-style
score inside its pad whenever percussion is active on that side.

Data: PERCUSSION_NOTATION maps each drum letter to a list of
[waveType, frequency, nominalVolume] triples matching the voices in
playPercussion(). Wave types are 1-char codes:
s = sine (sky blue)
t = triangle (mint)
q = square (amber)
w = sawtooth (orange)
n = noise (pale grey)

Render (in drawGrid):
- When drumActive, walk PERCUSSION_NOTATION[letter] and draw each
voice inside the pad's interior plot area (between the top key
label and the bottom drum label).
- Tonal voices: horizontal line at Y = log-mapped frequency with ±1px
per-frame jitter so it wobbles, thickness ∝ nominal volume.
- Noise voices: vol×8 randomly-scattered 1px dots in a ±5px band
around the log-frequency target, re-scattered each frame.
- Colors come from NOTATION_WAVE_RGB; alpha brighter when the pad is
active (key pressed).

Randomness uses a tiny LCG seeded by (frame + key char code) so every
pad animates independently — you see different jitter patterns across
the 12 pads instead of a lockstep flicker. Log-frequency Y mapping
covers 30 Hz to 10 kHz so the kick's 44/54/90/120/180 Hz stack reads
as a dense bottom band while the hi-hats read as a thin top band.

The bottom drum label (BAS/SNR/CLP/...) is now drawn AFTER the
notation glyph so it sits on top of the graphic. In non-percussion
mode the pads still show note+octave as before — nothing changes for
melodic play.

+96 -4
+96 -4
fedac/native/pieces/notepat.mjs
··· 429 429 "a#": [230, 210, 170], // tambourine — sandy 430 430 }; 431 431 432 + // Graphic-notation signatures — compact descriptions of each drum's internal 433 + // voices so drawGrid() can render animated glyphs inside each percussion pad. 434 + // Each element is either a tonal "line" (horizontal bar whose Y = log(freq)) 435 + // or "noise" (random dots scattered around the frequency band). `v` is the 436 + // nominal volume, used for line thickness / dot density. 437 + // t: "s"=sine "t"=triangle "q"=square "w"=sawtooth "n"=noise 438 + // Drawn with per-frame random jitter so the viewer sees the stochastic 439 + // mechanism in action while the pad is idle. 440 + const PERCUSSION_NOTATION = { 441 + c: [["t",1800,1.0],["n",4000,0.55],["q",180,1.2],["w",90,1.0],["s",44,1.9],["s",54,1.3],["q",120,0.85]], 442 + d: [["n",2200,0.55],["t",220,0.4],["q",180,0.2]], 443 + e: [["q",2500,0.9],["n",6000,0.6],["n",1600,0.75],["n",1700,0.6],["n",1500,0.5],["n",1800,0.45]], 444 + f: [["n",3200,0.45],["q",1800,0.22],["t",2400,0.18]], 445 + g: [["n",7000,0.35],["n",5000,0.2]], 446 + a: [["n",6500,0.3],["n",4800,0.18]], 447 + b: [["n",4200,0.28],["q",3100,0.1],["q",4600,0.08]], 448 + "c#": [["n",3500,0.42],["n",6500,0.28],["q",4200,0.08]], 449 + "d#": [["n",5500,0.38],["n",8500,0.25]], 450 + "f#": [["q",810,0.22],["q",540,0.18]], 451 + "g#": [["t",900,0.35],["q",1800,0.14]], 452 + "a#": [["n",7000,0.3],["n",4500,0.18],["q",6500,0.1]], 453 + }; 454 + // Wave type → accent color for the notation glyph 455 + const NOTATION_WAVE_RGB = { 456 + s: [120, 200, 255], // sine — sky blue 457 + t: [120, 255, 180], // triangle — mint 458 + q: [255, 220, 120], // square — amber 459 + w: [255, 140, 80], // sawtooth — orange 460 + n: [220, 220, 230], // noise — pale grey 461 + }; 462 + 432 463 // Return drum name if the given (letter, grid offset) is a live drum pad, else null. 433 464 // offset 0 = left grid, 1 = right grid. Low/high extras (z, x, ;, ', ]) stay melodic. 434 465 function percussionDrumFor(letter, offset) { ··· 2752 2783 const label = key ? key.toUpperCase() : ""; 2753 2784 write(label, { x: x + 2, y: y + 2, size: 1, font: "font_1" }); 2754 2785 2755 - if (btnH > 12) { 2786 + // Stochastic graphic notation inside drum pads (percussion mode only). 2787 + // Each drum's voices are plotted as horizontal lines (tonal) or noise 2788 + // dots (noise-based) at log-frequency Y positions. Every frame, the 2789 + // positions jitter by ±1 px and noise dots are re-scattered, so the 2790 + // viewer sees the randomness mechanism animating even when idle. 2791 + if (drumActive) { 2792 + const sig = PERCUSSION_NOTATION[letter]; 2793 + if (sig) { 2794 + // Usable plot area inside the pad (leave room for label at top 2795 + // and bottom-label area). 2796 + const pxMin = x + 3; 2797 + const pxMax = x + btnW - 4; 2798 + const pyMin = y + 11; // below top label 2799 + const pyMax = y + btnH - 13; // above bottom label 2800 + if (pxMax > pxMin && pyMax > pyMin + 4) { 2801 + const plotW = pxMax - pxMin; 2802 + const plotH = pyMax - pyMin; 2803 + const logLo = Math.log(30); 2804 + const logHi = Math.log(10000); 2805 + // Use a key-stable random seed so the jitter across pads 2806 + // differs without being globally synchronized. 2807 + const seed = (frame * 9301 + (key ? key.charCodeAt(0) : 0) * 49297) % 233280; 2808 + const prng = (n) => { 2809 + // Tiny LCG derived from seed + n 2810 + const s = (seed + n * 1103515245 + 12345) & 0x7fffffff; 2811 + return (s / 2147483648); 2812 + }; 2813 + for (let i = 0; i < sig.length; i++) { 2814 + const [t, f, vol] = sig[i]; 2815 + const logF = Math.log(Math.max(30, Math.min(10000, f))); 2816 + const yNorm = 1 - (logF - logLo) / (logHi - logLo); 2817 + const py = pyMin + Math.round(yNorm * plotH); 2818 + const rgb = NOTATION_WAVE_RGB[t] || [180, 180, 200]; 2819 + const alpha = isActive ? 220 : 130; 2820 + if (t === "n") { 2821 + // Scatter ~vol*8 random dots around py 2822 + const count = Math.max(2, Math.round(vol * 8)); 2823 + for (let d = 0; d < count; d++) { 2824 + const dx = pxMin + Math.floor(prng(i * 31 + d) * plotW); 2825 + const dy = py + Math.floor((prng(i * 31 + d + 17) - 0.5) * 5); 2826 + if (dx >= pxMin && dx <= pxMax && dy >= pyMin && dy <= pyMax) { 2827 + ink(rgb[0], rgb[1], rgb[2], alpha); 2828 + box(dx, dy, 1, 1, true); 2829 + } 2830 + } 2831 + } else { 2832 + // Horizontal line (tonal voice); thickness ∝ volume 2833 + const jitterY = Math.floor((prng(i) - 0.5) * 2); 2834 + const lineY = Math.max(pyMin, Math.min(pyMax - 1, py + jitterY)); 2835 + const thickness = Math.max(1, Math.min(2, Math.round(vol * 0.9))); 2836 + ink(rgb[0], rgb[1], rgb[2], alpha); 2837 + box(pxMin, lineY, plotW, thickness, true); 2838 + } 2839 + } 2840 + } 2841 + } 2842 + } else if (btnH > 12) { 2756 2843 if (isActive) ink(255, 255, 255, 180); 2757 2844 else { const sl = dark ? (sharp ? 80 : 120) : (sharp ? 160 : 110); ink(sl, sl, sl); } 2758 - const bottomLabel = drumActive 2759 - ? (PERCUSSION_LABELS[letter] || (letter + noteOctave)) 2760 - : (letter + noteOctave); 2845 + write(letter + noteOctave, { x: x + 2, y: y + btnH - 12, size: 1, font: "font_1" }); 2846 + } 2847 + 2848 + // In drum mode the bottom label is the drum name, not the note. 2849 + if (drumActive && btnH > 12) { 2850 + if (isActive) ink(255, 255, 255, 210); 2851 + else { const sl = dark ? (sharp ? 100 : 150) : (sharp ? 120 : 80); ink(sl, sl, sl); } 2852 + const bottomLabel = PERCUSSION_LABELS[letter] || (letter + noteOctave); 2761 2853 write(bottomLabel, { x: x + 2, y: y + btnH - 12, size: 1, font: "font_1" }); 2762 2854 } 2763 2855