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 activeSoloNodes tracking + 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 Snap toggle, 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-pulse keyframes 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-mode class + #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-pad tiles 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, ⚙ Advanced is the toggle: localStorage initial value set to on. Button label shows next state.
  • Loop 74 — Floating top toolbar: TAP/BPM, 🧲 Beat Snap, DJ-mode toggle bundled into one .top-tools at 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 calls pressKey('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 /3 now 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 n mid-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/kb unified). Plus cat-pop-slow — 72% viewport size, 3 seconds, blur/drop-shadow/float.
  • Loop 80 — C area + rotating cat + mutual exclusivity: KeyC added, cat-pop-rotate (two rotations, random direction). activeSoloNodes includes kc → A ↔ B ↔ C all exclusive. Then 1–9 ↔ A/B/C bidirectional stop (stopAllDj + stopAllSolo call 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.
  • bpm saved/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.

  1. "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.

  1. "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.

  1. Three rounds of margin-number tuning

Mobile DJ top margin: 20dvh40dvhpadding: 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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

  1. Real-device mobile verification — 8 blocks of deferral. With a new default (DJ pad), real touch response validation is required.
  2. Generalize per-segment solo groupska/kb/kc are hardcoded now. A solo: true flag on DEFAULT_SEGMENTS would make add/remove trivial.
  3. 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.
  4. C's default color is cyan, which blends with blue siblings in the ticker — give it a distinct color.

Notes

  • activeSoloNodes is 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 + stopAllDj call each other at the start of playSegmentByIndex / playDjSlot. Order reversal isn't possible, but "play from a fully-stopped state" as an invariant could live in a wrapper name — something like playExclusive(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-shadow and filter: drop-shadow mixed — watch for perf. cat-pop-slow animates drop-shadow + blur + translate all in keyframes. Low-end devices may drop frames.
  • Tour bubble position uses r.top + r.height/2 < vh/2 for top/bottom decision. Using the first of #dj-slots .dj-slot-wrap as 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).