# AGENTS.md Conventions and operational notes for AI agents (and humans) working in this repo. Read this before editing. Keep it updated when conventions change. ## Project A portfolio website using a **hybrid architecture**: a WebGPU `` renders the generative-art background, and a DOM overlay on top of it owns every UI element (nav, titles, body text, button). The canvas is the only GPU surface; all interactive UI is standard HTML/CSS. Pointer position is still captured at the window level and fed to the shader as a uniform for background reactivity. ### North-star goals (from the kickoff decisions) - Tech: **Vite + TypeScript**, `@webgpu/types` for ambient types. - Rendering: a **single fullscreen-triangle fragment shader** draws the procedural generative-art background only. All UI (text, nav, button) is **DOM overlay**. - Aesthetic: **abstract generative art** (domain-warped fbm flow fields, noise). - Sections: **Hero / About / Projects / Contact**, driven by `scene` state in the app loop. Scene changes update the DOM overlay content and shift the shader's `palette`. - Deliver style: **solid foundation first, extend later.** ## Commands ```bash npm install npm run dev # vite dev server, opens browser npm run build # tsc --noEmit (typecheck) + vite build npm run typecheck # tsc --noEmit — run before considering work done npm run preview # serve the built dist/ ``` **Every task that changes code MUST end with `npm run typecheck` (and `npm run build` if you want a full check).** Do not commit code that fails typecheck. ## Stack / tooling notes - TypeScript `strict`, plus `noUnusedLocals` / `noUnusedParameters`. Remove unused params/vars or prefix with `_` rather than leaving them. - WGSL shaders live in `src/engine/shaders/*.wgsl` and are imported with Vite's `?raw` suffix (`import src from './x.wgsl?raw'`). Keep all GPU code in `.wgsl` files — do not inline large shader strings in `.ts`. - `@webgpu/types` provides global `GPUDevice`, etc. Do not import them; they are ambient via `types` in `tsconfig.json`. - Target a modern WebGPU browser (recent Chrome/Edge/ChromeOS/Safari preview). `index.html` ships a `#fallback` notice shown when `navigator.gpu` is missing. - `opentype.js` is a runtime dep (font parsing for the Slug text generator); `@types/opentype.js` is a dev dep. **The Slug text engine is dormant** — see the "Text rendering" section. The deps are kept so the dormant files still typecheck; they are not wired into the render loop. ## Architecture ``` src/ ├── main.ts # boot: create App, fall back on no WebGPU ├── app.ts # rAF loop, input→uniforms, scene state, DOM overlay wiring ├── engine/ │ ├── renderer.ts # device/context/pipeline, single procedural pass │ ├── shaders/ │ │ └── fullscreen.wgsl # procedural background only, one fragment shader │ └── text/ # DORMANT Slug text engine (kept, not wired in) │ ├── slug-generator.ts # CPU: opentype.js → curve/band texture data (Slug port) │ ├── text-layout.ts # CPU: text string → instance buffer data per glyph │ ├── text-renderer.ts # GPU: instanced quad pipeline + textures + bind groups │ └── text.wgsl # WGSL Slug shader (ray-traces quadratic Béziers per frag) ├── ui/ │ ├── input.ts # window-level pointer position capture (mouse uniform) │ └── overlay.ts # DOM overlay: scene toggling, resume/project HTML builders └── data/ ├── resume.ts # ResumeData type + RESUME object (name, contact, work, edu, skills) └── projects/ ├── index.ts # ProjectPage type + PROJECTS registry (add new projects here) ├── radiance-caching.ts ├── point-cloud.ts ├── evol-engine.ts ├── auv-simulation.ts └── improv-gfx.ts ``` ### Layout: DOM-owned UI layout is owned by **CSS** in `index.html` (flexbox + viewport units). There is no shared pixel-space layout contract between the DOM and the shader anymore: the shader draws only the fullscreen background, so it needs only `resolution`, `time`, `scene`, and `mouse` — no per-element bounds. When you add a new UI element: add it to the DOM overlay in `index.html`, style it with CSS, and wire any click/hover in `overlay.ts`. You do not need to touch `renderer.ts`, `fullscreen.wgsl`, or uniforms unless the background itself must react to the new element. ### Hybrid DOM-overlay principle The **DOM overlay** (`#overlay` in `index.html`, `z-index: 1` over the canvas) owns all visible UI: the nav bar, scene title/subtitle/body, and the cycle button. The canvas underneath draws the generative background only. The overlay is `pointer-events: none` with interactive children set to `pointer-events: auto`, so pointermove still reaches the window for the shader's mouse uniform while clicks land on the right element. Do not add UI shapes inside the fragment shader — if you need a new visual element, prefer a DOM element with CSS. ### Data layer Content lives in `src/data/` as typed TypeScript objects. No runtime parsing, no fetches — data is imported directly and rendered into HTML at first visit. **Resume** (`src/data/resume.ts`): - Exports `ResumeData` interface and `RESUME` object. - Fields: `name`, `contact[]`, `location`, `summary`, `work[]`, `education[]`, `skills[]`. - `overlay.ts` has `buildResumeHTML(data)` that generates the full resume DOM from the typed object. To update resume content, edit `resume.ts`. **Projects** (`src/data/projects/`): - Each project is its own `.ts` file exporting a `ProjectPage`: `{ id, name, tagline, date, tech[], content }` where `content` is free-form HTML. - `index.ts` exports the `ProjectPage` type and the `PROJECTS` registry array. - **To add a project:** create `src/data/projects/my-project.ts`, import it in `index.ts`, add to the `PROJECTS` array. It auto-appears on the Projects page. - **To remove a project:** remove its import and array entry from `index.ts`. - Clicking a project card opens a detail view; a back button returns to the list. Navigation is handled by event delegation on `#projects-section` in `overlay.ts`. - The list resets on every nav entry (if you're viewing a detail and switch tabs, coming back shows the list again). **To add a new data-driven section**, follow the Projects/Resume pattern: 1. Create `src/data/whatever.ts` with a typed interface + data export. 2. Add a `#whatever-section` div to `index.html` with the same CSS pattern (opacity transition, `.visible` class with fade-in animation). 3. In `overlay.ts`: query the div, add a `whateverBuilt` flag, build the HTML from data on first visit, toggle `.visible`/`.hidden` classes in `setScene()`. 4. Extend `colorBase`/`speedBase` arrays in `app.ts` and update cycle modulo. ### Coordinate conventions - Layout space = **drawing-buffer pixels** (CSS pixels × `dpr`, capped at 2). - Shader convention: origin **bottom-left**, **y up**. `app.ts` flips input y: `mouse.y = res.y - clientY * dpr`. - `Renderer` keeps `resolution` in actual device pixels. ## WGSL / shader conventions - One oversized triangle, 3 hardcoded vertices in the vertex shader; no vertex buffers. - Uniforms are a single `var u: Uniforms` at group 0, binding 0. - Update `UNIFORM_FLOATS` in `renderer.ts` whenever you add a uniform field, and keep the field order in TS `render(state)` identical to the WGSL struct declaration. - Keep the shader readable: section it with `// ---------- name ----------` comments. ## SDF reference helpers already in the shader - `sdRoundBox(p, halfExtent, radius)` — rounded rect (kept for future background motifs) - `hash`, `noise`, `fbm` — value-noise fbm for the generative background - `palette(s)` — hue shift keyed off `scene` To add more shapes, add `sd*` helpers next to the existing ones. ## Currently rendered - **Background (shader):** domain-warped fbm flow field, palette indexed by `scene`, vignette + grain. `scene` cycles 0→1→2→3→4 on each button click or nav click; each scene shifts `palette`. `colorBase` and `speedBase` arrays in `app.ts` are indexed by scene and must be extended when adding a new scene. - **Nav bar (DOM):** top-center row of 5 `