Visual Iteration — let the AI see what it builds
Code review cannot tell you whether a button is misaligned, a texture looks
plastic, a character's hat is floating off its head, or a hero section feels
cramped. You have to look. This skill is the disciplined loop for doing that:
render a single deterministic frame, view it, compare it to the target, change
the code, render again — until the pixels match the intent.
It works for two cases:
- Web / UI design — layout, spacing, typography, color, responsive behavior.
- 3D / game / canvas — character look, mesh/texture quality, object placement,
lighting, camera framing (Three.js, react-three-fiber, raw WebGL,
<canvas>).
The loop
- Define the target. Either reference image(s) (a mockup, a competitor, a
real photo) or a written brief ("warm, editorial, lots of whitespace; the CTA
must dominate"). Be explicit about what "correct" means before rendering.
- Render ONE frame deterministically (see
scripts/visual-capture.mjs).
Determinism is everything — same input must produce a pixel-comparable output
every run, or you can't tell whether your edit helped.
- Look at it. Read the PNG into context. Describe what you actually see —
not what you intended. Name specific defects with their location.
- Compare to the target. If you have a reference image, view both and list
concrete differences (position, scale, color, density, mood). If you have a
brief, score the frame against each requirement.
- Make ONE focused change, then re-render the same station/viewport and
compare before/after. Loop until it matches. Stop when defects are gone, not
when you're tired.
Rendering deterministically
The #1 mistake is comparing two frames that differ for reasons other than your
edit. Pin everything that isn't under test:
- Fixed viewport / window size (e.g. 1600×900). Never rely on the default.
- Fixed camera for 3D — capture from named "stations" (a fixed
position+target+FOV passed via URL query), so every run of a station is
comparable. Don't free-fly the camera between captures.
- Freeze time / animation — pin the clock, freeze the sim at a fixed t,
fix any RNG seed, disable network/live data (
?nonet), hide cursors/HUD.
- Wait for "ready" — expose a
window.__captureReady flag your app sets when
fonts, textures, avatars, and async data have all settled, then waitFor it
plus a short settle delay. Screenshotting too early is the #2 mistake.
- Clip to the region under test — screenshot just the canvas / the component,
not the whole chrome, so diffs aren't dominated by irrelevant UI.
⚠️ GPU caveat (critical for 3D / post-processing)
Headless Chrome on Linux/CI often falls back to SwiftShader (software WebGL,
~2 fps) whose post-processing output differs from a real GPU — bloom, SSAO,
tone-mapping, and antialiasing will not match what users see. Never
vision-tune a 3D scene against a software-rendered frame. Render against a real
GPU:
- On WSL/Windows, drive the Windows-side
chrome.exe --headless=new (it sees the
discrete GPU via D3D11) over CDP, instead of Linux-side headless. The capture
script supports --chrome <path> + CDP for exactly this.
- Confirm the renderer per frame via
WEBGL_debug_renderer_info and record it in
the manifest — if it says "SwiftShader", your post-FX critique is invalid.
For plain web/UI (no WebGL post-processing), the bundled Playwright chromium is
fine.
Comparing to references
- Reference images: load the reference and your render together and diff them
verbally — "logo is ~40px too low and too warm; reference is cooler and
tighter." Pixel-diff tools help for regressions but the AI's own visual
judgment is what catches design problems.
- Reference video / style words: sample a frame or two from the video, or
turn the brief into a checklist, and grade each render against it.
- Never claim it looks right without having looked — read the actual output
frame first. (See the
verification-before-completion discipline.)
Quick start
# Web page (bundled chromium, clip to a selector):
node scripts/visual-capture.mjs --url http://localhost:3000 \
--selector "main" --out ./captures/home --settle 1500
# 3D scene at fixed camera stations, real GPU over CDP (WSL example):
node scripts/visual-capture.mjs --url http://localhost:4173 \
--chrome "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" \
--selector ".scene-canvas canvas" --ready __captureReady \
--stations "cam=0,7,33,0,4,-6,55|cam=5.5,2.4,10.5,1.5,1.8,4.5,45" \
--out ./captures/scene --settle 3500
Then read each PNG, compare to your target, edit, and re-run the same command.
The output is pixel-comparable across runs, so before/after tells you the truth.
Requirements
- Node 18+ and
playwright-core (or playwright) available.
- The thing under test served locally (dev server or
build && preview).
- For real-GPU 3D: a Chrome/Chromium binary with GPU access.
1---2name: visual-iteration3description: Visual Iteration — let the AI see what it builds4---56# Visual Iteration — let the AI see what it builds78Code review cannot tell you whether a button is misaligned, a texture looks9plastic, a character's hat is floating off its head, or a hero section feels10cramped. **You have to look.** This skill is the disciplined loop for doing that:11render a single deterministic frame, view it, compare it to the target, change12the code, render again — until the pixels match the intent.1314It works for two cases:15- **Web / UI design** — layout, spacing, typography, color, responsive behavior.16- **3D / game / canvas** — character look, mesh/texture quality, object placement,17 lighting, camera framing (Three.js, react-three-fiber, raw WebGL, `<canvas>`).1819## The loop20211. **Define the target.** Either reference image(s) (a mockup, a competitor, a22 real photo) or a written brief ("warm, editorial, lots of whitespace; the CTA23 must dominate"). Be explicit about what "correct" means *before* rendering.242. **Render ONE frame deterministically** (see `scripts/visual-capture.mjs`).25 Determinism is everything — same input must produce a pixel-comparable output26 every run, or you can't tell whether your edit helped.273. **Look at it.** Read the PNG into context. Describe what you actually see —28 not what you intended. Name specific defects with their location.294. **Compare to the target.** If you have a reference image, view both and list30 concrete differences (position, scale, color, density, mood). If you have a31 brief, score the frame against each requirement.325. **Make ONE focused change**, then re-render the *same* station/viewport and33 compare before/after. Loop until it matches. Stop when defects are gone, not34 when you're tired.3536## Rendering deterministically3738The #1 mistake is comparing two frames that differ for reasons other than your39edit. Pin everything that isn't under test:4041- **Fixed viewport / window size** (e.g. 1600×900). Never rely on the default.42- **Fixed camera** for 3D — capture from named "stations" (a fixed43 position+target+FOV passed via URL query), so every run of a station is44 comparable. Don't free-fly the camera between captures.45- **Freeze time / animation** — pin the clock, freeze the sim at a fixed t,46 fix any RNG seed, disable network/live data (`?nonet`), hide cursors/HUD.47- **Wait for "ready"** — expose a `window.__captureReady` flag your app sets when48 fonts, textures, avatars, and async data have all settled, then `waitFor` it49 plus a short settle delay. Screenshotting too early is the #2 mistake.50- **Clip to the region under test** — screenshot just the canvas / the component,51 not the whole chrome, so diffs aren't dominated by irrelevant UI.5253## ⚠️ GPU caveat (critical for 3D / post-processing)5455Headless Chrome on Linux/CI often falls back to **SwiftShader** (software WebGL,56~2 fps) whose **post-processing output differs from a real GPU** — bloom, SSAO,57tone-mapping, and antialiasing will not match what users see. **Never58vision-tune a 3D scene against a software-rendered frame.** Render against a real59GPU:6061- On WSL/Windows, drive the Windows-side `chrome.exe --headless=new` (it sees the62 discrete GPU via D3D11) over CDP, instead of Linux-side headless. The capture63 script supports `--chrome <path>` + CDP for exactly this.64- Confirm the renderer per frame via `WEBGL_debug_renderer_info` and record it in65 the manifest — if it says "SwiftShader", your post-FX critique is invalid.6667For plain web/UI (no WebGL post-processing), the bundled Playwright chromium is68fine.6970## Comparing to references7172- **Reference images:** load the reference and your render together and diff them73 *verbally* — "logo is ~40px too low and too warm; reference is cooler and74 tighter." Pixel-diff tools help for regressions but the AI's own visual75 judgment is what catches design problems.76- **Reference video / style words:** sample a frame or two from the video, or77 turn the brief into a checklist, and grade each render against it.78- **Never claim it looks right without having looked** — read the actual output79 frame first. (See the `verification-before-completion` discipline.)8081## Quick start8283```bash84# Web page (bundled chromium, clip to a selector):85node scripts/visual-capture.mjs --url http://localhost:3000 \86 --selector "main" --out ./captures/home --settle 15008788# 3D scene at fixed camera stations, real GPU over CDP (WSL example):89node scripts/visual-capture.mjs --url http://localhost:4173 \90 --chrome "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" \91 --selector ".scene-canvas canvas" --ready __captureReady \92 --stations "cam=0,7,33,0,4,-6,55|cam=5.5,2.4,10.5,1.5,1.8,4.5,45" \93 --out ./captures/scene --settle 350094```9596Then **read each PNG**, compare to your target, edit, and re-run the same command.97The output is pixel-comparable across runs, so before/after tells you the truth.9899## Requirements100101- Node 18+ and `playwright-core` (or `playwright`) available.102- The thing under test served locally (dev server or `build && preview`).103- For real-GPU 3D: a Chrome/Chromium binary with GPU access.