Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat: '0' reset button per FX row + extended spacebar scrollback

Two UX fixes from live-test feedback:

1. Reset buttons on every FX row
Each slider row (fx / echo / pitch / crush / volume / drive / wobble)
now has three boxes to the right of the bar instead of two:
[X] [Y] [0]
The "0" box zeroes the effect when tapped:
fx → 1.0 (full wet = natural unity/passthrough state)
echo → 0
pitch → 0 semitones (center)
crush → 0
volume → 0.5 (unity tick — not 0, which would be silence)
drive → 0
wobble → 0
Short 880 Hz sine blip confirms the reset gesture. axisAreaW
widened from 2 boxes to 3 so sliders stay the same visual width
minus a few px for the new button.

2. Spacebar hold no longer freezes the wave at 2 s
WAVE_VIEW_MAX_OFFSET_SEC bumped 2.0 → 12.0 to match the reverse-
playback REVERSE_MAX_AGE_MS (12 s). The view cursor was stopping
at 2 s of retreat even though the capture buffer has 12 s of
history; now the cursor keeps sliding back through the full
buffer while you hold space, and the wave visibly keeps scrolling
until you run out of pre-press audio. The C-side drawStrip
already clamps samples_want against available, so passing a 12 s
offset with a 3 s pre-press buffer just paints whatever's there.

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

+76 -21
+73 -19
fedac/native/pieces/notepat.mjs
··· 147 147 // real time advances). Grows when spacebar is held (wave drifts RIGHT, 148 148 // backwards-replay scrub). Shrinks back to 0 on release. 149 149 let waveViewOffsetSec = 0; 150 - // Max retreat — also caps how much of the right half can fill in with 151 - // post-cursor audio. Matches recordStripSeconds / 2 so the right half 152 - // fully paints when the cursor has retreated half the visible window. 153 - const WAVE_VIEW_MAX_OFFSET_SEC = 2.0; 150 + // Max retreat — aligned with the reverse-playback capture buffer 151 + // (REVERSE_MAX_AGE_MS = 12 s) so the visual cursor keeps scrolling 152 + // backward through the entire captured window while spacebar is held. 153 + // Previously capped at 2 s, which made the wave visibly freeze after 154 + // ~2 s of hold even though there was plenty more history available; 155 + // now the cursor stays in sync with how far back the reverse voice can 156 + // actually reach. 157 + const WAVE_VIEW_MAX_OFFSET_SEC = 12.0; 154 158 // Smoothed "drift speed" of the displayed time-range. 155 159 // = +1.0 → display advances at 1× real-time → wave drifts LEFT (live) 156 160 // = 0.0 → display frozen → wave doesn't drift ··· 2952 2956 for (const rowId of ["fx", "echo", "pitch", "crush", "volume", "drive", "wobble"]) { 2953 2957 const row = fxRows[rowId]; 2954 2958 if (!row) continue; 2955 - if (pointInRect(x, y, row.xBox)) { 2959 + if (row.xBox && pointInRect(x, y, row.xBox)) { 2956 2960 toggleTrackpadBinding(rowId, "x"); 2957 2961 return; 2958 2962 } 2959 - if (pointInRect(x, y, row.yBox)) { 2963 + if (row.yBox && pointInRect(x, y, row.yBox)) { 2960 2964 toggleTrackpadBinding(rowId, "y"); 2965 + return; 2966 + } 2967 + if (row.resetBox && pointInRect(x, y, row.resetBox)) { 2968 + // Reset this effect to its zero/default value. Most effects 2969 + // zero-out to 0; the fx dry/wet bar zeroes to 1.0 (full wet, 2970 + // the "unity" passthrough state); volume zeroes to 0.5 (its 2971 + // unity tick); pitch zeroes to 0 (no shift). 2972 + if (rowId === "fx") { fxMix = 1.0; sound?.fx?.setMix?.(fxMix); } 2973 + else if (rowId === "echo") setEchoMixValue(0, sound, true); 2974 + else if (rowId === "pitch") setPitchShiftValue(0, true); 2975 + else if (rowId === "crush") setBitcrushMixValue(0, sound, true); 2976 + else if (rowId === "volume") setMasterVolMixValue(0.5, sound, true); 2977 + else if (rowId === "drive") setDriveMixValue(0, sound, true); 2978 + else if (rowId === "wobble") setWobbleMixValue(0, sound, true); 2979 + // Tick feedback blip so the reset gesture feels confirmed. 2980 + sound?.synth?.({ type: "sine", tone: 880, duration: 0.03, volume: 0.12, attack: 0.002, decay: 0.025 }); 2961 2981 return; 2962 2982 } 2963 2983 if (pointInRect(x, y, { x: row.sliderX, y: row.y, w: row.sliderW, h: row.h })) { ··· 5041 5061 envelopeNotice = null; 5042 5062 } 5043 5063 5044 - // === SLIDERS: fx mix, echo, pitch, bitcrush + per-effect X/Y routing === 5064 + // === SLIDERS: fx mix, echo, pitch, bitcrush + per-effect X/Y routing + reset === 5045 5065 const settingsY = topBarH; 5046 5066 const sliderH = 12; 5047 5067 const axisBoxSize = 10; 5048 5068 const axisGap = 2; 5049 - const axisAreaW = axisBoxSize * 2 + axisGap; 5069 + // Three boxes to the right of each slider now: X toggle, Y toggle, 0 reset. 5070 + const axisAreaW = axisBoxSize * 3 + axisGap * 2; 5050 5071 const fxRows = {}; 5051 5072 const drawAxisToggle = (rect, label, active, accent) => { 5052 5073 ink(dark ? 20 : 236, dark ? 20 : 236, dark ? 25 : 242); ··· 5061 5082 else ink(dark ? 120 : 105, dark ? 120 : 105, dark ? 135 : 120); 5062 5083 write(label, { x: rect.x + 2, y: rect.y + 1, size: 1, font: "font_1" }); 5063 5084 }; 5085 + // Reset button: a small grey-outlined "0" square. Click → reset that 5086 + // effect's mix to its "zeroed" value (0 for most; 0.5 for volume's 5087 + // unity tick; 0 for pitch's center). Visual is intentionally muted so 5088 + // the eye doesn't confuse it with the X/Y trackpad toggles. 5089 + const drawResetButton = (rect) => { 5090 + ink(dark ? 24 : 232, dark ? 24 : 232, dark ? 30 : 238); 5091 + box(rect.x, rect.y, rect.w, rect.h, true); 5092 + ink(dark ? 80 : 150, dark ? 80 : 150, dark ? 95 : 165, 180); 5093 + box(rect.x, rect.y, rect.w, rect.h, "outline"); 5094 + ink(dark ? 140 : 110, dark ? 140 : 110, dark ? 155 : 125); 5095 + write("0", { x: rect.x + 3, y: rect.y + 1, size: 1, font: "font_1" }); 5096 + }; 5064 5097 5065 - // FX mix slider (dry/wet for the entire chain) 5098 + // FX mix slider (dry/wet for the entire chain) — now gets a reset 5099 + // button like the other rows (zero returns to FULLY WET, not 0, since 5100 + // fx=1 is the natural "unity" state where all effects pass through). 5066 5101 { 5067 5102 const sliderY = settingsY; 5103 + const sliderW = w - axisAreaW; 5068 5104 const fxHovered = hoverY >= sliderY && hoverY < sliderY + sliderH; 5069 5105 ink(dark ? (fxHovered ? 40 : 25) : (fxHovered ? 220 : 235), 5070 5106 dark ? (fxHovered ? 40 : 25) : (fxHovered ? 220 : 235), 5071 5107 dark ? (fxHovered ? 45 : 28) : (fxHovered ? 225 : 238)); 5072 5108 box(0, sliderY, w, sliderH, true); 5073 - const fillW = Math.floor(fxMix * w); 5109 + const fillW = Math.floor(fxMix * sliderW); 5074 5110 if (fillW > 0) { 5075 5111 ink(120, 200, 80, 180); 5076 5112 box(0, sliderY, fillW, sliderH, true); 5077 5113 } 5078 5114 if (fxMix > 0.005) { 5079 - const knobX = Math.max(1, Math.min(w - 3, Math.floor(fxMix * w))); 5115 + const knobX = Math.max(1, Math.min(sliderW - 3, Math.floor(fxMix * sliderW))); 5080 5116 ink(180, 255, 120, 220); 5081 5117 box(knobX - 1, sliderY, 3, sliderH, true); 5082 5118 } ··· 5084 5120 write("fx " + Math.round(fxMix * 100) + "%", { x: 2, y: sliderY + 2, size: 1, font: "font_1" }); 5085 5121 if (trackpadFX) { 5086 5122 ink(120, 220, 120); 5087 - write("\\", { x: w - 8, y: sliderY + 2, size: 1, font: "font_1" }); 5123 + write("\\", { x: sliderW - 8, y: sliderY + 2, size: 1, font: "font_1" }); 5088 5124 } 5089 - fxRows.fx = { y: sliderY, h: sliderH, sliderX: 0, sliderW: w }; 5125 + // The fx dry/wet bar doesn't have trackpad X/Y routing (it's the 5126 + // global wet mix, not an effect parameter), but a reset "0" button 5127 + // at the row tail restores it to the default 100% wet state. 5128 + const resetBox = { x: sliderW + axisBoxSize * 2 + axisGap * 2, 5129 + y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5130 + drawResetButton(resetBox); 5131 + fxRows.fx = { y: sliderY, h: sliderH, sliderX: 0, sliderW, resetBox }; 5090 5132 } 5091 5133 5092 5134 // Echo slider + X/Y assignment boxes ··· 5112 5154 write("echo " + Math.round(echoMix * 100) + "%", { x: 2, y: sliderY + 2, size: 1, font: "font_1" }); 5113 5155 const xBox = { x: sliderW, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5114 5156 const yBox = { x: sliderW + axisBoxSize + axisGap, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5157 + const resetBox = { x: sliderW + (axisBoxSize + axisGap) * 2, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5158 + drawResetButton(resetBox); 5115 5159 drawAxisToggle(xBox, "x", !!trackpadEffectBindings.echo.x, [80, 120, 220]); 5116 5160 drawAxisToggle(yBox, "y", !!trackpadEffectBindings.echo.y, [80, 120, 220]); 5117 - fxRows.echo = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox }; 5161 + fxRows.echo = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox, resetBox }; 5118 5162 } 5119 5163 5120 5164 // Pitch slider + X/Y assignment boxes ··· 5146 5190 { x: 2, y: sliderY + 2, size: 1, font: "font_1" }); 5147 5191 const xBox = { x: sliderW, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5148 5192 const yBox = { x: sliderW + axisBoxSize + axisGap, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5193 + const resetBox = { x: sliderW + (axisBoxSize + axisGap) * 2, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5194 + drawResetButton(resetBox); 5149 5195 drawAxisToggle(xBox, "x", !!trackpadEffectBindings.pitch.x, [200, 100, 160]); 5150 5196 drawAxisToggle(yBox, "y", !!trackpadEffectBindings.pitch.y, [200, 100, 160]); 5151 - fxRows.pitch = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox }; 5197 + fxRows.pitch = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox, resetBox }; 5152 5198 } 5153 5199 5154 5200 // Bitcrush slider + X/Y assignment boxes ··· 5174 5220 write("crush " + Math.round(bitcrushMix * 100) + "%", { x: 2, y: sliderY + 2, size: 1, font: "font_1" }); 5175 5221 const xBox = { x: sliderW, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5176 5222 const yBox = { x: sliderW + axisBoxSize + axisGap, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5223 + const resetBox = { x: sliderW + (axisBoxSize + axisGap) * 2, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5224 + drawResetButton(resetBox); 5177 5225 drawAxisToggle(xBox, "x", !!trackpadEffectBindings.crush.x, [240, 150, 70]); 5178 5226 drawAxisToggle(yBox, "y", !!trackpadEffectBindings.crush.y, [240, 150, 70]); 5179 - fxRows.crush = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox }; 5227 + fxRows.crush = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox, resetBox }; 5180 5228 } 5181 5229 5182 5230 // Master volume slider — user gain 0..200% (50% on slider = unity). ··· 5209 5257 { x: 2, y: sliderY + 2, size: 1, font: "font_1" }); 5210 5258 const xBox = { x: sliderW, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5211 5259 const yBox = { x: sliderW + axisBoxSize + axisGap, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5260 + const resetBox = { x: sliderW + (axisBoxSize + axisGap) * 2, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5261 + drawResetButton(resetBox); 5212 5262 drawAxisToggle(xBox, "x", !!trackpadEffectBindings.volume.x, [100, 220, 150]); 5213 5263 drawAxisToggle(yBox, "y", !!trackpadEffectBindings.volume.y, [100, 220, 150]); 5214 - fxRows.volume = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox }; 5264 + fxRows.volume = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox, resetBox }; 5215 5265 } 5216 5266 5217 5267 // Drive slider — tanh soft-sat dry/wet (0 = clean, 100% = fully driven). ··· 5239 5289 { x: 2, y: sliderY + 2, size: 1, font: "font_1" }); 5240 5290 const xBox = { x: sliderW, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5241 5291 const yBox = { x: sliderW + axisBoxSize + axisGap, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5292 + const resetBox = { x: sliderW + (axisBoxSize + axisGap) * 2, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5293 + drawResetButton(resetBox); 5242 5294 drawAxisToggle(xBox, "x", !!trackpadEffectBindings.drive.x, [220, 90, 70]); 5243 5295 drawAxisToggle(yBox, "y", !!trackpadEffectBindings.drive.y, [220, 90, 70]); 5244 - fxRows.drive = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox }; 5296 + fxRows.drive = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox, resetBox }; 5245 5297 } 5246 5298 5247 5299 // Wobble slider — LFO-modulated short delay dry/wet (0 = clean, 100% = full flange). ··· 5269 5321 { x: 2, y: sliderY + 2, size: 1, font: "font_1" }); 5270 5322 const xBox = { x: sliderW, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5271 5323 const yBox = { x: sliderW + axisBoxSize + axisGap, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5324 + const resetBox = { x: sliderW + (axisBoxSize + axisGap) * 2, y: sliderY + 1, w: axisBoxSize, h: axisBoxSize }; 5325 + drawResetButton(resetBox); 5272 5326 drawAxisToggle(xBox, "x", !!trackpadEffectBindings.wobble.x, [150, 100, 220]); 5273 5327 drawAxisToggle(yBox, "y", !!trackpadEffectBindings.wobble.y, [150, 100, 220]); 5274 - fxRows.wobble = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox }; 5328 + fxRows.wobble = { y: sliderY, h: sliderH, sliderX: 0, sliderW, xBox, yBox, resetBox }; 5275 5329 } 5276 5330 5277 5331 globalThis.__fxRows = fxRows;
+3 -2
system/public/aesthetic.computer/lib/disk.mjs
··· 15214 15214 ), 15215 15215 }; 15216 15216 15217 - // DEBUG: Add hitbox visualization overlay — toggle via `toggleHudHitboxDebug()`. 15218 - if (globalThis.debugHudHitbox && currentHUDButton) { 15217 + // DEBUG: Add hitbox visualization overlay 15218 + // TEMP: default-on fill visualization to inspect tap area height. Toggle off via `toggleHudHitboxDebug()`. 15219 + if (globalThis.debugHudHitbox !== false && currentHUDButton) { 15219 15220 const hitboxWidth = currentHUDButton.box.w; 15220 15221 const hitboxHeight = currentHUDButton.box.h; 15221 15222