Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat: top bar flush-to-top, wave-from-needle, press feedback, shift outline

Batch of notepat UX fixes from live-test feedback:

1. Top bar flush to top
Status text (notepat.com / MIDI / vol / FPS / time / battery) now
sits at y=3 instead of y=22. topBarH reduced 54→34 so the strip
hugs the top edge tightly and gives the pad grid + wave strip back
18-20 px of vertical space. QR code stays at (2,2) scale=1 (25×25
px) — it lives under the top-row text now.

2. Waveform emerges from the NEEDLE, not the right edge
drawStrip now renders only the LEFT HALF (pixels 0..needle_off) of
the strip. Newest sample lands immediately left of the needle, old
samples extend leftward into the past. Right of the needle stays
empty (background only). This matches the user's mental model of
"the needle is where audio is being generated from" — new tones
visibly emit from the red needle rather than appearing at the far
right edge of the screen.

3. Needle press feedback
When a note is actively playing or was pressed within the last
~18 frames, the JS overlay draws a second needle in GREEN on top
of the C-rendered red needle, with:
- ±1 px horizontal shake (phase-locked to frame so it reads as
vibration, not noise)
- 15 Hz blink (alternates 255/140 brightness every 2 frames)
- Alpha fade over ~25 frames after release so a quick tap still
flashes cleanly
Lets the player see "yes, my keypress is generating the audio
you see writing to the strip."

4. Shift = GLOBAL alternate-mode, not just a "harp→pluck" rename
Removed the per-button label swap that flipped "harp" → "pluck"
while Shift was held. Instead, a pulsing amber outline is drawn
around the entire screen (top/bottom/left/right 2-px bars) while
shiftHeld is true. This makes the alternate-sound mode obvious
regardless of which instrument is loaded, matches the existing
per-kit Shift behavior (velocity boost, harp short-pluck, etc.),
and leaves room to add more per-instrument alternates (whistle
→ breathy blow, sine → octave-doubled, etc.) without having to
rename individual wave buttons for each.

Known deferred follow-ups:
- Per-instrument Shift alternate sounds (whistle→blow etc.)
- 'z' / side-key mapping on upper-octave drum kit
- Auxiliary-key legend overlay
- Dropping the 'noise' voice from the sampler
- Tiny aux-pad UI showing hotkeys + tones for both octaves
These are tracked; rolling them into subsequent commits.

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

+65 -28
+58 -11
fedac/native/pieces/notepat.mjs
··· 3424 3424 } 3425 3425 3426 3426 // === STATUS BAR === 3427 - // Bar is tall enough to fit a 50×50 QR code (21-module version-1 QR + 3428 - // 2-module quiet-zone margin at scale=2) in the top-left corner, with 3429 - // the text row sitting vertically centered below the QR's midline so 3430 - // readability at arm's length works on a phone-camera scan too. 3431 - const topBarH = 54; 3432 - const barY = 22; // center of text row — leaves 2 px top/bottom padding for status text at size=1 3427 + // Status text (notepat.com / midi / vol / fps / time / battery) sits 3428 + // flush with the top of the screen at barY=3 so the whole row reads 3429 + // as one continuous strip of readouts along the top edge. The QR 3430 + // fills the space underneath (25×25 at scale=1, anchored at (2,2)). 3431 + // topBarH stays tall enough (34) to fully contain the QR; everything 3432 + // downstream (pads, waveform strip) anchors to `topBarH` so shrinking 3433 + // the bar automatically gives the rest of the UI more room. 3434 + const topBarH = 34; 3435 + const barY = 3; // top-flush text row (matrix font at size=1 is ~7 px tall) 3433 3436 3434 3437 ink(BAR_BG[0], BAR_BG[1], BAR_BG[2]); 3435 3438 box(0, 0, w, topBarH, true); ··· 4274 4277 const rsW = w - margin * 2; 4275 4278 sound.speaker.drawStrip(rsX, recordStripTop, rsW, recordStripH, 4276 4279 recordStripSeconds, 0.5, waveViewOffsetSec); 4280 + 4281 + // Overlay: needle shakes + blinks GREEN when a note is actively 4282 + // being played. Recently-pressed (within ~18 frames) or currently- 4283 + // active notes trigger the effect. Without this the red/orange 4284 + // needle looks inert even while tones are sounding; the green 4285 + // blink gives immediate "the playhead is emitting audio" feedback. 4286 + const framesSincePress = frame - lastKeyFrame; 4287 + const noteActive = activeCount > 0 || framesSincePress < 18; 4288 + if (noteActive) { 4289 + const needleX = rsX + Math.floor(rsW * 0.5); 4290 + // Pseudo-random shake in pixel space — ±1 px horizontal jitter 4291 + // phase-locked to the frame so it reads as vibration, not noise. 4292 + const shake = (frame & 1) ? 1 : -1; 4293 + // Blink intensity pulses at ~15 Hz (every 4 frames at 60 fps). 4294 + const blink = (frame & 3) < 2 ? 255 : 140; 4295 + // Fade the overlay out as the press ages, so tapping a short 4296 + // note still gives a quick flash but doesn't linger. 4297 + const ageFade = framesSincePress < 6 4298 + ? 255 4299 + : Math.max(120, 255 - (framesSincePress - 6) * 10); 4300 + const a = activeCount > 0 ? 255 : ageFade; 4301 + ink(90, blink, 130, a); 4302 + line(needleX + shake, recordStripTop, needleX + shake, recordStripTop + recordStripH); 4303 + } 4277 4304 } 4278 4305 4279 4306 // Waveform visualizer bars only in lanes above pad grids (not full-screen). ··· 5043 5070 ink(dark ? 40 : 190, dark ? 40 : 190, dark ? 45 : 195); 5044 5071 box(bx, waveRowY, 1, waveRowH, true); 5045 5072 } 5046 - // Label — switch "harp" → "pluck" while Shift is held to surface 5047 - // the alternate short-pluck mode. The active button also gets a 5048 - // subtle bracket marker so it's obvious from the keyboard. 5049 - let label = waveLabels[i]; 5050 - if (wavetypes[i] === "harp" && shiftHeld) label = "pluck"; 5073 + // Label — stays constant regardless of Shift state. The Shift 5074 + // "alternate sound" mode is now indicated globally via the 5075 + // screen-edge outline (drawn at the end of paint), so per-button 5076 + // label renaming would be redundant and feel local to one 5077 + // instrument. Individual kits still apply their own Shift 5078 + // variation (harp → short pluck, whistle → messy blow, etc.) 5079 + // downstream of this visual label. 5080 + const label = waveLabels[i]; 5051 5081 const lx = bx + Math.floor((btnW2 - label.length * 6) / 2); 5052 5082 write(label, { x: lx, y: waveRowY + 3, size: 1, font: "font_1" }); 5053 5083 } ··· 5655 5685 // Footer hint — always visible, always inside the panel 5656 5686 ink(dark ? 100 : 140, dark ? 100 : 140, dark ? 125 : 160); 5657 5687 write("meta to close", { x: px + padX, y: py + panelH - padY - 2, size: 1, font: "font_1" }); 5688 + } 5689 + 5690 + // Shift = GLOBAL alternate-sound mode. While held, draw an outline 5691 + // around the whole screen so the active state is obvious at a glance 5692 + // regardless of what instrument/kit is loaded. The alternate sound 5693 + // itself is emitted downstream by the kit-specific code (e.g. harp → 5694 + // short pluck, whistle → messy blow, all instruments → accent boost). 5695 + // Color pulses faintly between two amber tones so it feels alive 5696 + // without being visually noisy. 5697 + if (shiftHeld) { 5698 + const pulse = (frame & 7) < 4 ? 220 : 180; 5699 + ink(255, pulse, 60, 220); 5700 + const W = screen.width, H = screen.height; 5701 + box(0, 0, W, 2, true); // top 5702 + box(0, H - 2, W, 2, true); // bottom 5703 + box(0, 0, 2, H, true); // left 5704 + box(W - 2, 0, 2, H, true); // right 5658 5705 } 5659 5706 } 5660 5707
+7 -17
fedac/native/src/js-bindings.c
··· 1648 1648 if (needle_off >= w) needle_off = w - 1; 1649 1649 int needle_x = x + needle_off; 1650 1650 1651 - // Strip always draws the past `seconds` of audio ending AT the cursor, 1652 - // spread across the FULL width with newest at the right edge. The 1653 - // needle at needle_frac is a visual "playhead at center" marker only. 1654 - // 1655 - // We deliberately do NOT render a post-cursor (right-of-needle) 1656 - // region: during reverse-replay the audio being heard is captured 1657 - // back through the speaker output ring, and drawing those samples on 1658 - // the right creates a confusing "double layer" of the original wave 1659 - // layered over its own reverse-played echo. Keeping the render area 1660 - // single-sourced keeps the visual unambiguous — the left-drift / 1661 - // right-drift behavior (driven by viewOffsetSec lerp in JS) is the 1662 - // only thing that changes on space press/release. 1663 - int left_w = w; 1664 - int right_w = 0; 1665 - (void)right_w; 1651 + // Strip draws past `seconds` of audio ending AT the cursor, BUT only 1652 + // in the LEFT half (pixels 0 .. needle_off). This way the newest 1653 + // sample sits immediately left of the needle — the wave visibly 1654 + // "emerges" from the playhead and flows leftward into the past. 1655 + // Right of the needle stays empty (no double-layer from reverse- 1656 + // replay echo; no confusion about where new audio comes from). 1657 + int left_w = needle_off > 0 ? needle_off : w; 1666 1658 double left_seconds = seconds; 1667 - double right_seconds = 0.0; 1668 - (void)right_seconds; 1669 1659 1670 1660 pthread_mutex_lock(&audio->lock); 1671 1661 unsigned int rate = audio->output_history_rate ? audio->output_history_rate