This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Unified Design Studio — a single web app consolidating 8 standalone generative art/design tools into one cohesive experience. Tools share UI components, theming, and utilities but each has its own canvas renderer and settings.
Detailed specs live in docs/PLAN.md. Treat it as the source of truth for architecture decisions, tool specs, and implementation phases.
- Vite + React 19 + TypeScript (strict mode)
- Tailwind CSS v4 + shadcn/ui for UI controls
- react-router-dom v7 for path-based routing (
/blocks,/topo, etc.) - p5.js (instance mode) for 7 tools, Canvas 2D for dither
- mp4-muxer for video export, react-colorful for color pickers
- vitest for utility tests
npm run dev # Vite dev server
npm run build # Production build
npm run preview # Preview production build
npm run lint # Lint
npx tsc -b # Type check (uses project references; `tsc --noEmit` checks nothing)
npx vitest run # Run all tests
npx vitest run src/lib/color.test.ts # Single test file- ToolSwitcher (48px left rail) — vertical icon nav, stores last-used tool in localStorage
- Canvas Area (flex: 1 center) — mounts p5/Canvas2D per tool
- Sidebar (280px right) — scrollable controls with pinned footer for action buttons, varies per tool
- Each tool is a lazy-loaded route (
/topo,/blocks, etc.) /redirects to last-used tool from localStorage- Tool registry in
src/tools/registry.tsmaps tool IDs to lazy components
Each tool lives in src/tools/<name>/ with:
index.tsx— React component (sidebar controls + canvas mount)sketch.ts(orengine.ts) — rendering logictypes.ts— settings type (must usetype, notinterface— interfaces don't satisfyuseSettings'sRecord<string, unknown>constraint)
useSettings(key, defaults)— localStorage persistence with 200ms debounced writes, merges with defaults on loaduseP5(containerRef, sketchFn, settings, options)— p5.js instance lifecycle, settingsRef stays in sync, callsredraw()unlessanimated: true
color.ts— hex/rgb conversion, lerp, gradient samplingmath.ts— seeded random, mapRange, constraintexture.ts— grain and canvas texture overlaysexport.ts— PNG/SVG/MP4 export helpers
Reusable control components: slider-control, select-control, color-control, switch-control, section (collapsible), gradient-editor, palette-editor, button-row.
Sidebar footer pattern: Action buttons (Randomize/Reset/Download) must always be visible. Pass them as <Sidebar footer={<ButtonRow>...</ButtonRow>}>. Settings scroll above; buttons stay pinned at the bottom.
Dark theme. bg-canvas: #000, bg-sidebar: #0a0a0a, text-primary: #ededed.
- No shadows, no gradients on UI, no cards — hierarchy through font size and color only
- All borders:
1px solid,border-radius: 6px - All transitions:
150ms - Slider: 2px track
#333, 14px white circular thumb - Scrollbar: 4px wide,
#333thumb, transparent track - Numeric values:
tabular-nums, mono font
All p5 tools use instance mode. Every p5 global must be prefixed with p. (e.g., p.background(), p.createCanvas(), p.PI). Do NOT use global mode. DOM helpers (createDiv, createSlider, etc.) are replaced by React — never use them.
Ambiguous names: Use p.random() for p5 random, Math.random() for JS random. Use p.map() for p5 mapping, Array.prototype.map() for array mapping. Prefer Math.* for non-p5 math.
p5.js v2 gotchas:
curveVertex()was renamed tosplineVertex()- Type defs are incomplete — cast
p.drawingContexttoCanvasRenderingContext2D, accessp.canvasvia(p as any).canvas - Constructor is async (uses
requestAnimationFrame) —useP5handles StrictMode cleanup viahitCriticalError - When using canvas 2D API directly (e.g.
getImageData), multiply dimensions byp.pixelDensity() - Cache expensive computations (elevation fields, contour extraction) and only recompute when the inputs that affect them change — visual-only settings should skip recomputation
- Performance in hot rendering loops: For tools that draw many line segments (organic, lines, gradients), use direct canvas 2D API (
ctx.strokeStyle,ctx.lineWidth,ctx.beginPath/moveTo/lineTo/stroke) instead ofp.stroke(),p.strokeWeight(),p.line(). p5 methods have significant per-call overhead. Pre-parse color stops (hex→RGB) once per frame, not per segment. Usergb(r,g,b)strings instead ofp5.Colorobjects to avoid allocation in tight loops.
- TypeScript strict mode is the primary safety net
- vitest for
src/lib/utilities only (~20-30 tests) - No UI or E2E tests — visual tools are verified by inspection
- Per-tool verification checklist is in
docs/PLAN.md
Original implementations live in ~/Development/art/ (7 tools) and ~/Development/dither/ (1 tool). Reference these when porting. The 8 tools are: topo, blocks, organic, dither, gradients, plotter, ascii, lines.