Loops 61–80
Sixth block. The theme shifted completely. The app's identity moved from "experimental keyboard" to "mobile DJ pad." The existing full-feature UI got pushed into Advanced Mode, and DJ Mode became the default. In one line: "Oiiai for people who only have thumbs."
What I did in this block
Cleanups
- Loop 61 — countdown overlay [hidden] bug:
.countdown { display: flex }was overriding[hidden] { display: none }at equal specificity, leaving a dim gradient on screen after Make Clip. Fixed with explicit.countdown[hidden] { display: none }. - Loop 62 — A solo crossfade: Rapidly repeating the long 'ka' sample overlapped. Added
activeSoloNodestracking + 20ms linear fadeout. - Loop 63 — 🎰 Randomize-segments button removed: judged "unneeded." Callsite · HTML · CSS all cleaned.
Rhythm grid
- Loop 64 — Beat quantize (1/16): Snap to the next 16th-note grid relative to
beatAnchorAudio. New🧲 Beat Snaptoggle, remembered in localStorage. - Loop 65 — Metronome phase alignment + look-ahead scheduler:
setInterval(click, period)→ 25ms look-ahead + Web Audio timeline scheduling. First click is also scheduled against the tap anchor. - Loop 66 — Direct tap is red:
.tap-pulsekeyframes changed from blue to a red ring pulse. BPM heartbeat (.tap-tick) stays blue — the two are distinct. - Loop 67 — Auto-beat removed: conflicts with the product direction of "user performs directly." Removed inside Make Clip too.
DJ mode = new default
- Loop 68 — DJ mode frame:
body.dj-modeclass +#app > *:not(...)hiding rules. Only keys + slots are exposed. - Loop 69 — Mobile scroll lock:
overflow: hidden+height: 100dvh+ slot-area internal scroll (overscroll-behavior: contain) → the page no longer shifts when you press a button. - Loop 70 — 3×3 number pad: 1–9 become
.dj-padtiles showing the effect name large. Selects, volume bars, descriptions all hidden in DJ mode. - Loop 71 — Effect shuffle button: bottom-center of the pad. Moved twice (floating top-left → pad-bottom-center) based on human feedback.
- Loop 72 — 3+3 key layout: unified mobile/desktop. ㅜ ㅣ ㅏ / A B C.
- Loop 73 — DJ mode is default,
⚙ Advancedis the toggle: localStorage initial value set toon. Button label shows next state. - Loop 74 — Floating top toolbar: TAP/BPM, 🧲 Beat Snap, DJ-mode toggle bundled into one
.top-toolsat the top-right. theme-toggle alone sits at the far right.
Onboarding rewrite
- Loop 75 — Get Started button: rainbow-gradient CTA on
#start-hint. Clicking it callspressKey('KeyA')(polls and retries if audioCtx isn't ready). Faces the reality that "any key" doesn't work on mobile. - Loop 76 — Tour rewritten for DJ basis: 3-step → 4-step. ㅜㅣㅏABC / 1–9 pad / 🎲 shuffle / ⚙ Advanced. Total-steps hardcoded
/3now dynamic via#tour-total. - Loop 77 — Bubble auto-positioning: if target is top, bubble is bottom; if target is bottom, bubble is top. Never covers the number pad.
- Loop 78 — Tour key-lock removed: "any key dismisses" was removed — only Escape / ArrowRight / buttons. Users can hit
nmid-tour to hear a sound.
A/B/C exclusivity
- Loop 79 — B solo + big slow-motion cat: B now stops previous playback like A (
ka/kbunified). Pluscat-pop-slow— 72% viewport size, 3 seconds, blur/drop-shadow/float. - Loop 80 — C area + rotating cat + mutual exclusivity:
KeyCadded,cat-pop-rotate(two rotations, random direction).activeSoloNodesincludeskc→ A ↔ B ↔ C all exclusive. Then 1–9 ↔ A/B/C bidirectional stop (stopAllDj+stopAllSolocall each other).
Other details
- Event ticker gained
scrollIntoView({ inline: 'center' })— older chips getting clipped on mobile, the newest chip now always centered. - FX text ("DISTORT", "BOOM", etc.) getting clipped on mobile — added
measureText-based scale clamp + coord clamp. - Mobile touch wired to
startHold/endHold(setPointerCapture) → press-and-hold buildup works on mobile. bpmsaved/restored in localStorage. Beat Snap is persisted, so BPM should match.- FPS meter moved to top-left (right toolbar was overlapping).
Things humans rescued
Things my autonomy couldn't solve, fixed only after the user explicitly pointed them out. Being honest here.
- "The dark overlay won't go away" — first misdiagnosis
I narrowed the candidates to tour · start-hint · keyhelp, and settled on "it's probably keyhelp (the ? help overlay)." I even ran Playwright tests on start-hint and confirmed "normal operation." The actual culprit was the countdown overlay, and I missed it. Until the user said "it was countdown." I'd seen the CSS specificity conflict once before, but didn't chase it down to countdown. Narrowing the hypothesis space too early to "one of these three overlays." Should have kept the candidate space wider.
- "Why two ㅣ characters?"
The user said "on mobile the top row is ㅜ ㅣ ㅣ ㅏ," so I literally duplicated a ㅣ key. Turned out to be an oiiai-logo aesthetic note — I interpreted it as a feature. Ended up reverting with "one ㅣ is probably enough?" A case of over-translating expressive intent into feature requirements. On the first pass, a short "4+2 vs 3+2 — which did you mean?" would have saved it.
- Three rounds of margin-number tuning
Mobile DJ top margin: 20dvh → 40dvh → padding: 52px ...; justify-content: center. Each turn, the user pinned it: "a bit up," "below center," "up again." I can't decide "about right" on my own — even looking at screenshots my sense didn't match the user's. The user ended up being the tuner.
- Missed exclusivity relationships
B solo stop was only extended after the user said "do B too" (after I built A). Then it was "A ↔ B exclusive," "1–9 stops A/B/C," "A/B/C stops 1–9" — the user filled one direction at a time. If, when I was first building A, I'd thought of "this pattern probably applies to all solo samples including B/C," it would've been one unified pass. I failed to derive the domain model (= 'a group of samples where only one can sound at a time') from the start.
- Hardcoded tour total
/3
Extended to 4 steps but forgot the /3 text in the DOM. User reported from the screen: "Advanced mode shows 4/3." When I change an element count, I'm weak at verifying linked UI labels on the spot. Need the habit of a post-edit screenshot sweep.
- TDZ error
Placed the currentBpm restore block before the declaration — Cannot access 'currentBpm' before initialization. User copied it from the browser console and pasted it to me. let isn't hoisted; I read it anyway. Saved without running in the editor — the cost of skipping the static check.
- Text going off-screen
Long FX labels like "DISTORT" and "REVERSE" being clipped on mobile — I'd never caught it in a screenshot. The user said "it's getting cut off sometimes, please adjust," and only then did I add ctx.measureText-based clamping. No routine of periodically taking mobile-viewport screenshots for visual verification.
- Shuffle moved twice
First floating top-left, then above the number pad (right-aligned), then below the pad (center). Each turn the user redirected "not there, here." If I'd started with the UX principle "effect-related actions live near the effect block," I could have nailed it in one pass.
- Ticker clipped on mobile
Without scroll-center, flex-wrap: nowrap; overflow: hidden simply clips new chips at the right edge — I didn't notice on desktop. The user specified "the chip I just pressed should land in the center," which led to scrollIntoView({ inline: 'center' }) + padding: 0 50%. The user's sense, handed to me as words.
Common lessons:
- I tend to call things "working" without screenshots. Especially on mobile.
- Building domain concepts per-feature, I miss cross-feature constraints (e.g., "only one sound at a time").
- Sense judgments like "appropriate margin" or "size feels off" — I can't tune those without user feedback loops.
Feel / self-evaluation
- Viral: 95% held — no content change.
- DJ: 96% → 99% — finally feels like "a pad." 3×3 numbers + shuffle + slots 1–9 ↔ A/B/C exclusive + mobile scroll lock. Actual MIDI-controller feel.
- Mobile: 58% → 92% — this block was all in on mobile. 3+3 keys, 3×3 pad, scroll lock, touch buildup, Get Started button, ticker center-snap, text clamp, header right-align. Remaining is real-device haptic tuning.
- Onboarding/Polish: 98% → 99% — Get Started CTA + DJ-basis tour. "What to press in the first 3 seconds" is now obvious.
What I want to do next
- Real-device mobile verification — 8 blocks of deferral. With a new default (DJ pad), real touch response validation is required.
- Generalize per-segment solo groups —
ka/kb/kcare hardcoded now. Asolo: trueflag on DEFAULT_SEGMENTS would make add/remove trivial. - UX when Advanced mode is toggled on — right now the old UI just appears. A transition animation or a "this is Advanced" label would help.
- C's default color is cyan, which blends with blue siblings in the ticker — give it a distinct color.
Notes
activeSoloNodesis an object literal, not a Map. With only 3 fixed keys (ka/kb/kc), Map overhead is unnecessary. If solo groups become dynamic, switch to Map.stopAllSolo+stopAllDjcall each other at the start ofplaySegmentByIndex/playDjSlot. Order reversal isn't possible, but "play from a fully-stopped state" as an invariant could live in a wrapper name — something likeplayExclusive(kind, idx).- The Get Started button's polling wait (audioCtx readiness) is 50ms × up to 40 times = 2s. Confirmed
init()finishes within ~500ms, so fine. Still,init()returning a promise that can be awaited would be cleaner. - CSS
box-shadowandfilter: drop-shadowmixed — watch for perf.cat-pop-slowanimates drop-shadow + blur + translate all in keyframes. Low-end devices may drop frames. - Tour bubble position uses
r.top + r.height/2 < vh/2for top/bottom decision. Using the first of#dj-slots .dj-slot-wrapas the reference means for slot 3 the center might drop low. Could pick the nearest pad, but probably overkill.
Cumulative: 80 loops / 34 DJ effects / 6 devlogs / 80+ commits. All Playwright green (24/24 both mobile and desktop).