Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat: smooth release, teal wave, aligned needle, status chips

Five polish passes from live-test feedback:

1. Release animates instead of snapping
waveViewOffsetSec now eases out over ~15-20 frames when space is
released (0.18 factor + 0.25 frame bias), so the needle visibly
sweeps back to live instead of jumping in a single frame. sim()
handles the lerp; the key-up path just clears spacePressStartMs +
un-pauses the capture ring. Re-engages cleanly if space is
pressed again mid-catch-up (elapsed-since-press overrides).

2. Waveform strip palette: cool teal → cyan-bright
drawStrip's warm red/orange ramp swapped for
r = 40 + peak × 180
g = 160 + peak × 90
b = 200 + peak × 55
Deep teal at low peaks, cyan-white at high. Keeps the red/green
needles clearly legible on top and reads less "alarm-y" against
the strip's dark-purple background.

3. Green press-needle aligned to red needle X exactly
Removed the ±1 px shake offset that was splitting the overlay
into a second vibrating bar. Green now draws at the same needle_x
as the C-rendered red, so it's literally "same line, different
color" — the underlying red shows through during the dim half of
the 15 Hz blink, giving a subtle red↔green pulse rather than two
separate bars.

4. Status bar chips
USB MIDI / UDP MIDI / FPS moved from the left-side statusWrite
stream to colored-background chips on the right side, between
brightness and the rest of the right-hand group:
… [brt] [vol] [FPS:60] [UDP ok] [USB on] [time] [bat]
Each chip has a tinted fill (green when active, amber when
connecting, gray when disabled) + matching text color, a bottom
accent for tactility, and a tiny gap between chips. The left-
status area is now just auto-update messages + last-key readout,
so key-press info no longer bumps into state indicators.

5. Volume + brightness bars thicker + bordered
Bumped height from 3 → 6 px and added a 1-px top/bottom border
so the max-range track is unambiguous — you can see how far
the slider can drag at a glance. Fill color got a small palette
tune (cooler for volume, warmer amber for brightness).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+145 -85
+138 -82
fedac/native/pieces/notepat.mjs
··· 2741 2741 if (key === "space") { 2742 2742 spaceHeld = false; 2743 2743 stopReversePlayback(sound); 2744 - // Unfreeze the output-history ring and snap the visual cursor back 2745 - // to the live edge. The ring's write_pos resumes from exactly where 2746 - // it paused — so the wave picks up recording at the press-moment, 2747 - // and the needle jumps to "now" in one frame (matches the user's 2748 - // mental model of "release = snap back to present"). 2744 + // Unfreeze the output-history ring so live audio resumes being 2745 + // captured. sim() will animate waveViewOffsetSec back to 0 over 2746 + // ~20 frames (ease-out lerp) so the needle VISIBLY catches up to 2747 + // the present — which reads as a smooth "returning to live" sweep 2748 + // rather than a snap. The ring's write_pos didn't advance during 2749 + // the hold, so the catch-up target (offset=0) is still exactly the 2750 + // moment of press; recording picks up from there. 2749 2751 sound?.speaker?.setCapturePaused?.(false); 2750 - waveViewOffsetSec = 0; 2751 - waveDriftSpeed = 1.0; 2752 2752 spacePressStartMs = 0; 2753 2753 return; 2754 2754 } ··· 3585 3585 statusWrite("reboot?", 100, 220, 100, 220); 3586 3586 } 3587 3587 3588 - const usbMidiText = usbMidiStatusText(usbMidiStatus); 3589 - if (usbMidiStatus?.active) { 3590 - statusWrite(usbMidiText, 100, 220, 140, 220); 3591 - } else if (usbMidiStatus?.enabled) { 3592 - statusWrite(usbMidiText, 255, 190, 80, 220); 3593 - } else { 3594 - statusWrite(usbMidiText, FG_DIM, FG_DIM, FG_DIM, 200); 3595 - } 3596 - 3597 3588 // Last key pressed — fades from bright to dim over ~60 frames (1s) so 3598 3589 // rapid-fire input flashes are visible while idle state stays subtle. 3590 + // (USB MIDI / UDP MIDI / FPS moved to colored chips on the right side 3591 + // so the left-of-notepat-label area is dedicated to key-press + auto- 3592 + // update transient messages.) 3599 3593 if (lastKeyPressed) { 3600 3594 const age = frame - lastKeyFrame; 3601 3595 const fadeA = age < 60 ? Math.max(100, 255 - Math.floor(age * 2.5)) : 100; 3602 3596 statusWrite("key:" + lastKeyPressed, 180, 220, 255, fadeA); 3603 - } 3604 - 3605 - // UDP MIDI — sibling indicator to USB MIDI. Color encodes state: 3606 - // disabled → FG_DIM (flat "OFF") 3607 - // enabled + down → amber (255,190,80) "..." 3608 - // enabled + up → dim green (100,220,140) "ON" 3609 - // actively sending→ bright green, pulsing with recency 3610 - // The pulse decays over ~1.5s after each note so rapid play visibly 3611 - // lights the indicator vs just sitting on "connected". 3612 - const relayText = udpMidiRelayStatusText(system); 3613 - if (relayText) { 3614 - if (!udpMidiBroadcast) { 3615 - statusWrite(relayText, FG_DIM, FG_DIM, FG_DIM, 200); 3616 - } else if (!system?.udp?.connected) { 3617 - statusWrite(relayText, 255, 190, 80, 220); 3618 - } else { 3619 - const recency = udpMidiSendRecency(); 3620 - // dim green (100,220,140) → bright green (160,255,190) as recency rises 3621 - const r = Math.round(100 + recency * 60); 3622 - const g = Math.round(220 + recency * 35); 3623 - const b = Math.round(140 + recency * 50); 3624 - statusWrite(relayText, r, g, b, 220); 3625 - } 3626 3597 } 3627 3598 3628 3599 // Metronome indicator (pendulum) in status bar — shown when enabled ··· 4220 4191 rx -= 4; 4221 4192 } 4222 4193 4223 - // Volume bar 4194 + // Volume bar — taller (6 px) with high-contrast BG track so you can 4195 + // see the full drag range at a glance, filled portion shows current. 4224 4196 const sysVol = sound?.speaker?.systemVolume ?? 100; 4225 4197 { 4226 - const volW = 20, volH = 3; 4198 + const volW = 20, volH = 6; 4199 + const volBarY = barY - 1; 4227 4200 rx -= volW; 4228 4201 const volBarX = rx; 4229 - ink(dark ? 45 : 220, dark ? 45 : 220, dark ? 50 : 225); 4230 - box(rx, barY + 2, volW, volH, true); 4202 + // Max-range track (dark cool-gray in dark mode, cool-light in light) 4203 + ink(dark ? 55 : 205, dark ? 55 : 205, dark ? 70 : 215, 255); 4204 + box(rx, volBarY, volW, volH, true); 4231 4205 const fillV = Math.floor(sysVol * volW / 100); 4232 - if (fillV > 0) { ink(dark ? 150 : 80, dark ? 150 : 80, dark ? 150 : 80); box(rx, barY + 2, fillV, volH, true); } 4206 + if (fillV > 0) { 4207 + ink(dark ? 180 : 60, dark ? 200 : 80, dark ? 220 : 140, 255); 4208 + box(rx, volBarY, fillV, volH, true); 4209 + } 4210 + // 1-px border frame 4211 + ink(dark ? 90 : 160, dark ? 90 : 160, dark ? 100 : 170, 200); 4212 + box(rx, volBarY, volW, 1, true); 4213 + box(rx, volBarY + volH - 1, volW, 1, true); 4233 4214 rx -= 2; 4234 4215 ink(FG_MUTED, FG_MUTED, FG_MUTED); 4235 4216 rx -= 3 * CH; 4236 4217 write("vol", { x: rx, y: barY, size: 1 }); 4237 - // Store hit zone for mouse interaction (label + bar) 4238 4218 globalThis.__volBar = { x: rx, w: volBarX + volW - rx, barX: volBarX, barW: volW }; 4239 4219 } 4240 4220 4241 - // Brightness bar 4221 + // Brightness bar — same prominence as volume 4242 4222 const sysBrt = system?.brightness ?? -1; 4243 4223 if (sysBrt >= 0) { 4244 4224 rx -= 4; 4245 - const brtW = 16, brtH = 3; 4225 + const brtW = 16, brtH = 6; 4226 + const brtBarY = barY - 1; 4246 4227 rx -= brtW; 4247 4228 const brtBarX = rx; 4248 - ink(dark ? 45 : 220, dark ? 45 : 220, dark ? 50 : 225); 4249 - box(rx, barY + 2, brtW, brtH, true); 4229 + ink(dark ? 55 : 205, dark ? 55 : 205, dark ? 70 : 215, 255); 4230 + box(rx, brtBarY, brtW, brtH, true); 4250 4231 const fillB = Math.floor(sysBrt * brtW / 100); 4251 - if (fillB > 0) { ink(dark ? 180 : 60, dark ? 160 : 60, dark ? 80 : 30); box(rx, barY + 2, fillB, brtH, true); } 4232 + if (fillB > 0) { 4233 + ink(dark ? 220 : 120, dark ? 200 : 90, dark ? 100 : 40, 255); 4234 + box(rx, brtBarY, fillB, brtH, true); 4235 + } 4236 + ink(dark ? 90 : 160, dark ? 90 : 160, dark ? 100 : 170, 200); 4237 + box(rx, brtBarY, brtW, 1, true); 4238 + box(rx, brtBarY + brtH - 1, brtW, 1, true); 4252 4239 rx -= 2; 4253 4240 ink(FG_MUTED, FG_MUTED, FG_MUTED); 4254 4241 rx -= 3 * CH; ··· 4256 4243 globalThis.__brtBar = { x: rx, w: brtBarX + brtW - rx, barX: brtBarX, barW: brtW }; 4257 4244 } 4258 4245 4259 - // FPS counter (left of brightness) 4246 + // Status chips — USB MIDI / UDP MIDI / FPS grouped as small colored 4247 + // strips to the left of the brightness bar. Each chip has a tinted 4248 + // background + short monospace label so their states read at a glance 4249 + // without mixing into the key-press / auto-update message stream on 4250 + // the left. Layout (right → left, starting from current `rx`): 4251 + // 4252 + // … [brt] [vol] [FPS:60] [UDP·MIDI] [USB·MIDI] [time] [bat] 4253 + // 4254 + // Chip helper — draws a filled rectangle with `fg` text centered in 4255 + // it, advances `rx` leftward. `bg`/`fg` are 4-tuples {r,g,b,a}. 4256 + const chipH = topBarH - 4; 4257 + const chipY = 2; 4258 + const chipPad = 3; 4259 + const drawStatusChip = (text, bg, fg) => { 4260 + const chipW = text.length * CH + chipPad * 2; 4261 + rx -= 3; // gap between chips 4262 + rx -= chipW; 4263 + ink(bg[0], bg[1], bg[2], bg[3]); 4264 + box(rx, chipY, chipW, chipH, true); 4265 + // Thin bottom accent for tactility 4266 + ink(Math.max(0, bg[0] - 40), Math.max(0, bg[1] - 40), Math.max(0, bg[2] - 40), bg[3]); 4267 + box(rx, chipY + chipH - 1, chipW, 1, true); 4268 + ink(fg[0], fg[1], fg[2], fg[3]); 4269 + write(text, { x: rx + chipPad, y: barY, size: 1, font: "matrix" }); 4270 + }; 4271 + 4272 + // FPS chip — color graded green (≥55), amber (≥30), red (<30) 4260 4273 if (fpsDisplay > 0) { 4261 - rx -= 4; 4262 - const fpsStr = fpsDisplay + ""; 4263 - const fpsColor = fpsDisplay >= 55 ? (dark ? 80 : 180) : (fpsDisplay >= 30 ? (dark ? 200 : 180) : 255); 4264 - const fpsG = fpsDisplay >= 55 ? (dark ? 180 : 180) : (fpsDisplay >= 30 ? (dark ? 160 : 120) : 60); 4265 - ink(fpsColor, fpsG, dark ? 80 : 80); 4266 - rx -= fpsStr.length * CH; 4267 - write(fpsStr, { x: rx, y: barY, size: 1 }); 4274 + const fpsStr = "fps" + fpsDisplay; 4275 + const fpsBg = fpsDisplay >= 55 4276 + ? [30, 80, 50, 200] 4277 + : fpsDisplay >= 30 4278 + ? [100, 70, 25, 200] 4279 + : [110, 30, 30, 220]; 4280 + const fpsFg = fpsDisplay >= 55 4281 + ? [160, 255, 190, 255] 4282 + : fpsDisplay >= 30 4283 + ? [255, 220, 140, 255] 4284 + : [255, 180, 180, 255]; 4285 + drawStatusChip(fpsStr, fpsBg, fpsFg); 4286 + } 4287 + 4288 + // UDP MIDI chip — state colors 4289 + // off: dim gray (relay disabled in config.json) 4290 + // connecting: amber 4291 + // connected: dim green 4292 + // sending: bright green (pulses with recency of last sent note) 4293 + { 4294 + const label = udpMidiBroadcast 4295 + ? (!system?.udp?.connected ? "udp..." : "udp ok") 4296 + : "udp off"; 4297 + let bg, fg; 4298 + if (!udpMidiBroadcast) { 4299 + bg = [40, 40, 50, 200]; fg = [140, 140, 150, 220]; 4300 + } else if (!system?.udp?.connected) { 4301 + bg = [100, 70, 25, 210]; fg = [255, 220, 140, 255]; 4302 + } else { 4303 + const recency = udpMidiSendRecency(); 4304 + bg = [Math.round(30 + recency * 70), Math.round(80 + recency * 90), Math.round(50 + recency * 40), 210]; 4305 + fg = [Math.round(160 + recency * 60), Math.round(255), Math.round(190 + recency * 40), 255]; 4306 + } 4307 + drawStatusChip(label, bg, fg); 4308 + } 4309 + 4310 + // USB MIDI chip 4311 + { 4312 + const label = usbMidiStatus?.active 4313 + ? "usb on" 4314 + : (usbMidiStatus?.enabled ? "usb..." : "usb off"); 4315 + let bg, fg; 4316 + if (usbMidiStatus?.active) { 4317 + bg = [30, 80, 50, 210]; fg = [160, 255, 190, 255]; 4318 + } else if (usbMidiStatus?.enabled) { 4319 + bg = [100, 70, 25, 210]; fg = [255, 220, 140, 255]; 4320 + } else { 4321 + bg = [40, 40, 50, 200]; fg = [140, 140, 150, 220]; 4322 + } 4323 + drawStatusChip(label, bg, fg); 4268 4324 } 4269 4325 4270 4326 // === NOTEPAT SCREEN: pads, waveform, echo slider === ··· 4314 4370 sound.speaker.drawStrip(rsX, recordStripTop, rsW, recordStripH, 4315 4371 recordStripSeconds, 0.5, waveViewOffsetSec); 4316 4372 4317 - // Overlay: needle shakes + blinks GREEN when a note is actively 4318 - // being played. Recently-pressed (within ~18 frames) or currently- 4319 - // active notes trigger the effect. Without this the red/orange 4320 - // needle looks inert even while tones are sounding; the green 4321 - // blink gives immediate "the playhead is emitting audio" feedback. 4373 + // Overlay: needle blinks GREEN when a note is actively being played. 4374 + // Drawn at the EXACT same X as the C-rendered red needle — the color 4375 + // change is the only difference, no shake/offset, so it reads as 4376 + // "same line, different state" instead of two separate bars. The 4377 + // underlying red needle shows through during the dim half of the 4378 + // blink cycle, giving a subtle red↔green pulse without any double 4379 + // vision. 4322 4380 const framesSincePress = frame - lastKeyFrame; 4323 4381 const noteActive = activeCount > 0 || framesSincePress < 18; 4324 4382 if (noteActive) { 4325 4383 const needleX = rsX + Math.floor(rsW * 0.5); 4326 - // Pseudo-random shake in pixel space — ±1 px horizontal jitter 4327 - // phase-locked to the frame so it reads as vibration, not noise. 4328 - const shake = (frame & 1) ? 1 : -1; 4329 4384 // Blink intensity pulses at ~15 Hz (every 4 frames at 60 fps). 4330 4385 const blink = (frame & 3) < 2 ? 255 : 140; 4331 - // Fade the overlay out as the press ages, so tapping a short 4332 - // note still gives a quick flash but doesn't linger. 4333 4386 const ageFade = framesSincePress < 6 4334 4387 ? 255 4335 4388 : Math.max(120, 255 - (framesSincePress - 6) * 10); 4336 4389 const a = activeCount > 0 ? 255 : ageFade; 4337 4390 ink(90, blink, 130, a); 4338 - line(needleX + shake, recordStripTop, needleX + shake, recordStripTop + recordStripH); 4391 + line(needleX, recordStripTop, needleX, recordStripTop + recordStripH); 4339 4392 } 4340 4393 } 4341 4394 ··· 5816 5869 // 5817 5870 // spaceHeld: drive offset directly from elapsed-since-press. The 5818 5871 // reverse-replay voice is reading the captured buffer 5819 - // backwards at 1× rate (since Date.now() returns wall 5820 - // clock and the buffer is mono @ the history rate), so 5821 - // setting offset = elapsed guarantees the visual needle 5822 - // sits on exactly the sample being played. Cap at MAX so 5823 - // the display stops retreating once we're at the end of 5824 - // the visible window (audio still ping-pongs beyond). 5872 + // backwards at 1× rate, so offset = elapsed guarantees 5873 + // the visual needle sits on exactly the sample being 5874 + // played. Cap at MAX. 5825 5875 // 5826 - // released: snap the cursor back to 0 immediately — the press- 5827 - // release handler already does this + re-arms the capture 5828 - // ring. No smooth catch-up, because the user explicitly 5829 - // wants "release = back to the present where reversing 5830 - // started" (capture was paused during hold, so live 5831 - // write_pos IS the moment of press). 5876 + // released: animate the cursor back to live over ~20 frames instead 5877 + // of snapping. Feels like the needle "catches up" to the 5878 + // present. The capture ring was paused during hold so 5879 + // live write_pos still marks the press-moment, meaning 5880 + // offset=0 really is "where reversing started" — the 5881 + // animation just makes the visual transition legible. 5882 + const dtSec = 1 / 60; 5832 5883 if (spaceHeld && spacePressStartMs > 0) { 5833 5884 const elapsedSec = (Date.now() - spacePressStartMs) / 1000; 5834 5885 waveViewOffsetSec = Math.min(WAVE_VIEW_MAX_OFFSET_SEC, elapsedSec); 5835 5886 waveDriftSpeed = -1.0; // cosmetic (only needle-color uses it) 5887 + } else if (waveViewOffsetSec > 0) { 5888 + // Ease-out lerp toward 0. At 60 fps with 0.18 factor the offset 5889 + // halves every ~4 frames — settles visually in ~15-20 frames. 5890 + waveViewOffsetSec = Math.max(0, waveViewOffsetSec - waveViewOffsetSec * 0.18 - dtSec * 0.25); 5891 + if (waveViewOffsetSec < 0.005) waveViewOffsetSec = 0; 5892 + waveDriftSpeed = 1.0; 5836 5893 } else { 5837 5894 waveDriftSpeed = 1.0; 5838 - // waveViewOffsetSec is reset to 0 in the space-release handler. 5839 5895 } 5840 5896 // Update dark/light mode via global theme (every ~5 seconds) 5841 5897 if (frame % 300 === 0) {
+7 -3
fedac/native/src/js-bindings.c
··· 1734 1734 if (peak > 1.0f) peak = 1.0f; \ 1735 1735 int bar_h = (int)(peak * (float)amp + 0.5f); \ 1736 1736 if (bar_h < 1) bar_h = 1; \ 1737 - int r = 120 + (int)(peak * 140.0f + 0.5f); if (r > 255) r = 255; \ 1738 - int gc = 120 + (int)(peak * 80.0f + 0.5f); if (gc > 255) gc = 255; \ 1739 - int b_ = 90 + (int)((1.0f - peak) * 120.0f + 0.5f); if (b_ > 255) b_ = 255; \ 1737 + /* Cool → bright palette: deep teal at low peaks, cyan- \ 1738 + * white at high peaks. Reads clearly against the strip's \ 1739 + * dark-purple background and keeps the warm red/green \ 1740 + * needles fully legible on top. */ \ 1741 + int r = 40 + (int)(peak * 180.0f + 0.5f); if (r > 255) r = 255; \ 1742 + int gc = 160 + (int)(peak * 90.0f + 0.5f); if (gc > 255) gc = 255; \ 1743 + int b_ = 200 + (int)(peak * 55.0f + 0.5f); if (b_ > 255) b_ = 255; \ 1740 1744 graph_ink(g, (ACColor){(uint8_t)r, (uint8_t)gc, (uint8_t)b_, 220}); \ 1741 1745 int dx = x + (draw_x_off) + col; \ 1742 1746 graph_line(g, dx, midY - bar_h, dx, midY + bar_h); \