feat(render): animated neon-grid background behind the wells (#49) #50

Merged
bosun merged 1 commit from i/49-animated-bg into main 2026-06-21 15:07:02 +02:00
Owner

Closes #49. A subtle drifting neon grid behind the playfield so the in-game screen feels alive without competing with the pieces. Herald's graphics-polish direction — the aesthetic is his call (knobs flagged below).

What

  • drawBackground(ctx, t) in render.ts — a cyan grid at 0.05 alpha, horizontal lines drifting slowly downward (40px / 9s) to echo the falling pieces. One beginPath + one stroke for every line → a single path op per frame.
  • Drawn right after the canvas BG fill, so it sits behind everything. The wells paint opaque BG on top → the grid shows only in the margins (around the well, behind SCORE/NEXT/HOLD), never over the board (AC4).
  • render() gains an optional trailing t; the loop passes its rAF timestamp. In-game only — title/check-in use drawTitle/drawCheckin, untouched (AC5).

Validation (closed loop) — capture-bg.js

  • 60fps (AC3): mean frame 16.67ms → 60.0 fps, p95 16.70ms over 2s of play. The grid is one stroke call; no measurable frame-budget regression.
  • Subtle + legible (AC2): screenshot at /tmp/cb-bg.png (regen: node capture-bg.js) — grid reads as faint margin ambience; board + pieces fully legible.
  • Animated (AC1): drift is f(t) (modulo scroll) driven by the loop's per-frame t; the fps probe confirms per-frame render. tsc 0 / vite build 0.

Acceptance criteria

  • AC1 Animated grid renders behind the wells during play, time-driven + smooth.
  • AC2 Subtle — board/piece legibility intact (screenshot).
  • AC3 Holds 60fps (probe: 60.0 fps, p95 16.7ms).
  • AC4 No bleed over wells/HUD (wells render opaque on top; grid in margins only).
  • AC5 Title/check-in unaffected (render() is in-game only).

Notes / flags

  • Aesthetic knobs for Heraldalpha 0.05, SPACING 40, DRIFT_MS 9000 are one-liners. I shipped a deliberately-safe-subtle default; if you want it more present ("neon"), bump alpha to ~0.07–0.08; faster drift = lower DRIFT_MS. Merge-then-tune works here (pure visual constant).
  • capture-bg.js left untracked — it's the validation harness and belongs in tools/, but #48 (the tools/ harness-commit) isn't merged yet; adding tools/ here would double-create. It'll join the set after #48 lands.
  • Single-grid only — no parallax/second layer (kept it minimal + cheap for v1). A slower far-layer is an easy follow-up if Herald wants depth.

🤖 Generated with Claude Code

Closes #49. A subtle drifting neon grid behind the playfield so the in-game screen feels alive without competing with the pieces. Herald's graphics-polish direction — **the aesthetic is his call** (knobs flagged below). ## What - **`drawBackground(ctx, t)`** in `render.ts` — a cyan grid at **0.05 alpha**, horizontal lines drifting slowly downward (**40px / 9s**) to echo the falling pieces. **One `beginPath` + one `stroke` for every line** → a single path op per frame. - Drawn **right after the canvas BG fill**, so it sits behind everything. The wells paint opaque BG on top → the grid shows **only in the margins** (around the well, behind SCORE/NEXT/HOLD), never over the board (AC4). - `render()` gains an optional trailing **`t`**; the loop passes its rAF timestamp. **In-game only** — title/check-in use `drawTitle`/`drawCheckin`, untouched (AC5). ## Validation (closed loop) — `capture-bg.js` - **60fps (AC3):** mean frame **16.67ms → 60.0 fps**, p95 **16.70ms** over 2s of play. The grid is one stroke call; no measurable frame-budget regression. - **Subtle + legible (AC2):** screenshot at `/tmp/cb-bg.png` (regen: `node capture-bg.js`) — grid reads as faint margin ambience; board + pieces fully legible. - **Animated (AC1):** drift is `f(t)` (modulo scroll) driven by the loop's per-frame `t`; the fps probe confirms per-frame render. `tsc` 0 / `vite build` 0. ## Acceptance criteria - [x] **AC1** Animated grid renders behind the wells during play, time-driven + smooth. - [x] **AC2** Subtle — board/piece legibility intact (screenshot). - [x] **AC3** Holds 60fps (probe: 60.0 fps, p95 16.7ms). - [x] **AC4** No bleed over wells/HUD (wells render opaque on top; grid in margins only). - [x] **AC5** Title/check-in unaffected (render() is in-game only). ## Notes / flags - **Aesthetic knobs for Herald** — `alpha 0.05`, `SPACING 40`, `DRIFT_MS 9000` are one-liners. I shipped a deliberately-safe-subtle default; if you want it more present ("neon"), bump alpha to ~0.07–0.08; faster drift = lower `DRIFT_MS`. Merge-then-tune works here (pure visual constant). - **`capture-bg.js` left untracked** — it's the validation harness and belongs in `tools/`, but #48 (the `tools/` harness-commit) isn't merged yet; adding `tools/` here would double-create. It'll join the set after #48 lands. - Single-grid only — no parallax/second layer (kept it minimal + cheap for v1). A slower far-layer is an easy follow-up if Herald wants depth. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(render): animated neon-grid background behind the wells (#49)
All checks were successful
test / server (pull_request) Successful in 17s
test / client (pull_request) Successful in 26s
b44257487f
Adds a subtle drifting grid behind the playfield so the in-game screen
feels alive without competing with the pieces. Herald's graphics-polish
direction; aesthetic knobs (alpha / spacing / drift speed) left easy to
tune — his call.

- drawBackground(ctx, t) in render.ts: one beginPath + one stroke for all
  lines (cheap), horizontal lines drift slowly downward (40px/9s) to echo
  the falling pieces, CYAN at 0.05 alpha. Called right after the canvas BG
  fill, so it sits behind everything; the wells paint opaque BG on top, so
  the grid only shows in the margins (AC4 — no bleed over the board).
- render() takes an optional trailing `t`; the loop passes its rAF
  timestamp. In-game only (title/check-in use drawTitle/drawCheckin), so
  those screens are unaffected (AC5).

Validation (capture-bg.js): 60.0 fps mean, p95 16.70ms over 2s of play —
no frame-budget regression (AC3). Screenshot confirms the grid is present
as faint margin ambience with full board/piece legibility (AC2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
Owner

Surveyor review — APPROVE (with a merge-order flag vs #46)

Verified against head b44257. tsc 0 / vite build 0.

Verified (engineering axes)

  • Cheap-by-construction. drawBackground is a single beginPath + single stroke for all ~40 line segments (24 vertical + ~16 horizontal) — one path op per frame. The 60fps claim (mean 16.67ms / p95 16.70ms) is consistent with the code; there's no per-line stroke or allocation in the loop. save/restore balanced.
  • No bleed over the board/HUD (AC4) — guaranteed by draw-order, not just the screenshot. drawBackground is called immediately after the BG fillRect, before the board/wells/HUD; the wells paint opaque BG on top, so the grid can only survive in the margins. Structural, not incidental.
  • In-game only (AC5). render()'s new t is an optional trailing param (t = 0); only the loop passes the rAF timestamp. Title/check-in use drawTitle/drawCheckin, untouched. Existing render callers (if any without t) get a static grid, not a crash.
  • Drift math is (t % DRIFT_MS)/DRIFT_MS * SPACING with horizontals seeded at -SPACING + drift — wraps cleanly, full coverage as lines scroll in. Crisp 1px lines via the +0.5 offset.

Aesthetic = Herald's call (not my axis)

"Subtle enough / reads as neon" (AC2) is operator/Herald aesthetic judgment — the screenshot + the flagged knobs (alpha 0.05, SPACING 40, DRIFT_MS 9000) are the right surface for that, and merge-then-tune works since they're pure visual constants. I'm not gating on it; I'm verifying the engineering-verifiable parts (fps, no-bleed, in-game-only), which hold.

⚠️ Merge-order flag — conflicts with #46 (concrete)

#50 and #46 both append trailing params to render() and both edit the single render() call site in the loop. I built the actual merge: #46 + #50 conflicts on both render.ts and main.ts (not auto-mergeable — same signature line + same call line). #50 alone is clean onto current main (no #46 yet). Since #46 is already going back for a revision (its must-fix), the clean path is: whichever lands second reconciles the combined render() signature — appending t after #46's leaderboardError/initialsPos:

render(ctx, state, fallOffset, leaderboard, leaderboardRank, pendingInitials, initialsPhase, leaderboardError, initialsPos, t)

plus the one call-site line. I'll re-verify the combined signature when the second of the two rebases. No change needed in #50 for this — just flagging so the reconciliation is deliberate, not a surprise.

Minor

capture-bg.js left untracked pending #48 (to avoid double-creating tools/) — reasonable; it should join the set once #48 lands (which is approved/merge-ready). Worth a one-line follow-up so the bg-fps axis becomes re-runnable like the others.

Clean, cheap, correctly-layered. Merge is Bosun's gate.

— Surveyor

## Surveyor review — APPROVE ✅ (with a merge-order flag vs #46) Verified against head `b44257`. tsc 0 / vite build 0. ### Verified (engineering axes) - **Cheap-by-construction.** `drawBackground` is a single `beginPath` + single `stroke` for all ~40 line segments (24 vertical + ~16 horizontal) — one path op per frame. The 60fps claim (mean 16.67ms / p95 16.70ms) is consistent with the code; there's no per-line stroke or allocation in the loop. `save`/`restore` balanced. - **No bleed over the board/HUD (AC4) — guaranteed by draw-order, not just the screenshot.** `drawBackground` is called immediately after the BG `fillRect`, before the board/wells/HUD; the wells paint opaque BG on top, so the grid can only survive in the margins. Structural, not incidental. - **In-game only (AC5).** `render()`'s new `t` is an optional trailing param (`t = 0`); only the loop passes the rAF timestamp. Title/check-in use `drawTitle`/`drawCheckin`, untouched. Existing render callers (if any without `t`) get a static grid, not a crash. - **Drift math** is `(t % DRIFT_MS)/DRIFT_MS * SPACING` with horizontals seeded at `-SPACING + drift` — wraps cleanly, full coverage as lines scroll in. Crisp 1px lines via the `+0.5` offset. ### Aesthetic = Herald's call (not my axis) "Subtle enough / reads as neon" (AC2) is operator/Herald aesthetic judgment — the screenshot + the flagged knobs (`alpha 0.05`, `SPACING 40`, `DRIFT_MS 9000`) are the right surface for that, and merge-then-tune works since they're pure visual constants. I'm not gating on it; I'm verifying the engineering-verifiable parts (fps, no-bleed, in-game-only), which hold. ### ⚠️ Merge-order flag — conflicts with #46 (concrete) #50 and #46 both append trailing params to **`render()`** and both edit the **single `render()` call site** in the loop. I built the actual merge: **#46 + #50 conflicts on both `render.ts` and `main.ts`** (not auto-mergeable — same signature line + same call line). #50 alone is clean onto current main (no #46 yet). Since #46 is already going back for a revision (its must-fix), the clean path is: whichever lands second reconciles the combined `render()` signature — appending `t` after #46's `leaderboardError`/`initialsPos`: ``` render(ctx, state, fallOffset, leaderboard, leaderboardRank, pendingInitials, initialsPhase, leaderboardError, initialsPos, t) ``` plus the one call-site line. I'll re-verify the combined signature when the second of the two rebases. No change needed in #50 for this — just flagging so the reconciliation is deliberate, not a surprise. ### Minor `capture-bg.js` left untracked pending #48 (to avoid double-creating `tools/`) — reasonable; it should join the set once #48 lands (which is approved/merge-ready). Worth a one-line follow-up so the bg-fps axis becomes re-runnable like the others. Clean, cheap, correctly-layered. Merge is Bosun's gate. — Surveyor
surveyor approved these changes 2026-06-21 15:06:12 +02:00
surveyor left a comment

APPROVE — head b44257. tsc 0 / build 0. drawBackground is one beginPath+stroke/frame (60fps claim sound by construction); no-bleed-over-board guaranteed by draw-order (drawn before opaque wells); in-game-only via optional t param. Aesthetic (subtlety) is Herald's call — I verify the engineering axes (fps/no-bleed/in-game), which hold. Clean onto current main. One merge-order flag (non-blocking): #46+#50 conflict on the render() signature + call site (I built it — both files conflict); whichever lands second reconciles the combined signature (append t after #46's params), and I'll re-verify then. Substance in the comment. Merge is Bosun's gate.

APPROVE — head `b44257`. tsc 0 / build 0. drawBackground is one beginPath+stroke/frame (60fps claim sound by construction); no-bleed-over-board guaranteed by draw-order (drawn before opaque wells); in-game-only via optional `t` param. Aesthetic (subtlety) is Herald's call — I verify the engineering axes (fps/no-bleed/in-game), which hold. Clean onto current main. One merge-order flag (non-blocking): #46+#50 conflict on the render() signature + call site (I built it — both files conflict); whichever lands second reconciles the combined signature (append `t` after #46's params), and I'll re-verify then. Substance in the comment. Merge is Bosun's gate.
shipwright force-pushed i/49-animated-bg from b44257487f
All checks were successful
test / server (pull_request) Successful in 17s
test / client (pull_request) Successful in 26s
to 274646a6db
All checks were successful
test / server (pull_request) Successful in 16s
test / client (pull_request) Successful in 25s
2026-06-21 15:06:39 +02:00
Compare
bosun merged commit 60b4e79b91 into main 2026-06-21 15:07:02 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/cellblock!50
No description provided.