Imported from thejustinwalsh/three-flatland (
AGENTS.md). Install upstream withnpx skills add thejustinwalsh/three-flatland. Copyright stays with the author.
three-flatland
Package manager: pnpm. Task runner: Nx. Use pnpm workspaces + Nx exclusively — never
npmoryarn. Install withpnpm install; run scripts withpnpm <script>; run tasks withpnpm nx <target>/pnpm nx run-many/pnpm nx affected. The nx-generated section below mentionsnpm exec nx …as an alternative — in this repo, always use pnpm.
Build & Test
pnpm installis an exclusive workspace mutation. Wait for it to exit successfully before starting Nx, dev, build, test, or review commands in that worktree. Never overlap an install with workspace tasks: pnpm rewritesnode_modulesin place, so a concurrent Nx process can lose its own executor mid-run.- Run parallel validation through one Nx invocation per worktree. Do not launch independent Nx processes that build the same projects concurrently; their declared outputs are shared directories.
pnpm dev— docs (port 4000) + examples MPA (port 5174) behind microfrontends proxy at http://localhost:5173pnpm --filter=example-react-tilemap dev— run a single examplepnpm sync:pack— sync example/mini package.json deps with the workspace catalog after editingpnpm-workspace.yamlpnpm sync:react— regenerate React subpath wrappers after touchingpackages/three-flatland/src/*/index.ts
Code Style
- No semicolons, single quotes, trailing commas, 120-column width (oxfmt)
typekeyword required for type-only imports (consistent-type-imports+verbatimModuleSyntax)- Unused vars must be prefixed with
_
Architecture
- Author TSL (Three Shader Language) and construct
WebGPURendererexclusively — never instantiate legacyWebGLRendereror hand-write GLSL. Three's internal WebGL 2 node backend remains a supported compatibility path. - R3F examples import from
@react-three/fiber/webgpu, not@react-three/fiber - Three.js users:
import from 'three-flatland'— R3F users:import from 'three-flatland/react'(all packages follow this/reactsubpath pattern, incl.@three-flatland/devtools/react) - Shared versions in
pnpm-workspace.yamlcatalog;pnpm.overridesmaps@three-flatland/*toworkspace:*
Package routing map (what to recommend to consumers)
Same content ships in the starter templates' AGENTS.md — keep the two in sync when editing either.
| Package | Reach for it when |
|---|---|
three-flatland |
Default entry. Sprites, animation, tilemaps, materials, lights, events, everyday loaders. |
@three-flatland/nodes |
You want a specific 2D shader effect (retro/CRT, blur, distortion, color, upscale) without writing TSL by hand. |
@three-flatland/presets |
You want lit sprites working immediately. Thin — two symbols (DefaultLightEffect, NormalMapProvider). |
@three-flatland/normals |
Dynamic lighting on flat 2D art without hand-authoring normal maps. |
@three-flatland/atlas |
Loose sprite PNGs that should become one draw-call-friendly atlas, optionally polygon-trimmed for overdraw. |
@three-flatland/alphamap |
Pixel-perfect pointer hit testing on transparent sprites (hitTestMode: "alpha") instead of bounding-box hits. |
@three-flatland/image |
PNG/WebP/AVIF/KTX2 encode and decode, plus Ktx2Loader. Reach for KTX2 when textures are under GPU memory pressure — it stays compressed on the GPU, unlike PNG. |
@three-flatland/bake |
Authoring a new baker, or you just need the flatland-bake binary. |
@three-flatland/devtools |
Live inspection of scene/material/sprite state. Seven required peers — heaviest install in the ecosystem. |
@three-flatland/skia |
A general immediate-mode 2D canvas in the scene: arbitrary paths, boolean ops, filters, gradients, images. |
@three-flatland/slug |
Text that must stay sharp at any zoom or perspective, or thousands of glyphs in one draw call. |
Never recommend installing these — they are unpublished: @three-flatland/schemas, @three-flatland/io.
Two calibration notes:
private: trueis not the signal — the distribution channel is.tools/vscodeis correctly private (VS Code extensions ship to a marketplace, not npm) while being fully public, distributed asthree-flatland.fl-tools. Ask "where does this ship?", not "what does the flag say?"- Version numbers are not maturity signals.
@three-flatland/presetssits at0.1.0-alpha.7because of a changesetslinkedgroup, and@three-flatland/schemasat1.0.0despite never having been released.
Examples
- Examples always exist in pairs — Three.js + React. Create both or neither.
examples/three/= plain Three.js,examples/react/= React Three Fiber- R3F classes must be registered with
extend()before use in JSX - All Three.js objects used as R3F JSX elements need: optional constructor params, property setters, array-compatible setters
Planning
- All planning, PRDs, milestones, and specs live in /planning, ensure all planning docs live under this directory.
- Save superpowers specs to planning/superpowers/specs.
- Save superpowers plans to planning/superpowers/plans.
Architectural references (.library/)
.library/threejs/— three.js + TSL reference patterns.library/react-three-fiber/— R3F idioms and helpers.library/three-flatland/loader-architecture.md— must-read before designing or refactoring any loader (texture, atlas, tilemap, font, normal map, image format). Defines package layering, the no-registry rule, three-tier surface (everyday/direct/preload), and cross-package dependency policy. Companion:planning/bake/loader-pattern.md(baked → runtime fallback shape).
Workflow
- Use Conventional Commits.
feat:,fix:,docs:,chore:,build:,test:,refactor:,ci:, with an optional scope —fix(slug): …. The type drives the release bump, so it is not cosmetic. - Every change to a package needs a changeset. Write it yourself.
pnpm changeset(interactive) or hand-author a descriptive file in.changeset/, inspect the package bump and release note against the diff, and commit it with the change. The CI generator is intentionally DISABLED: do not re-enable.github/workflows/changeset.ymlto satisfy a missing changeset. Its script and prompt may be used locally as drafting aids, but the agent owns the final file and commit. A package change without one ships unreleased. Withinpackages/, a package is release-visible iffprivate !== trueand it is not in.changeset/config.json'signorelist; packages outsidepackages/(skills,minis,tools) always need one by hand. Bump mapping and alpha pre-release mode: .changeset/README.md. - New public package? Bootstrap npm BEFORE its first release. CI publishes via OIDC trusted publishing, which can update but never CREATE an npm package — an unbootstrapped package E404s the entire publish run (this killed four releases:
image,create-three-flatland,atlas,alphamap). One-time, by hand, from an up-to-datemain:pnpm --filter <name> build && pnpm --filter <name> publish --access public --no-git-checks, then add the GitHub Actions trusted publisher for it on npmjs.com. Also register the package in.changeset/pre.jsoninitialVersionswhile pre-release mode is active. The release workflow's preflight fails fast with these instructions if a package is missing. - Bootstrap with the package's REAL prerelease version — never a
0.0.0placeholder. Changesets in pre mode publishes to thelatestdist-tag only while a package's registry history is all-prerelease (getReleaseTag); a single regular release on record — a0.0.0placeholder counts — permanently flips that package's publishes to thealphatag, solatest(what npmjs.com and barenpm installserve) goes stale. This bitimageandcreate-three-flatland. If it happens: unpublish the placeholder within npm's 72-hour window, ornpm dist-tag addafter every release. The release workflow warns on this drift post-publish. - Stage by exact path —
git add tools/io/src/foo.ts, nevergit add -A/git add ./git commit -a. This branch routinely has WIP modifications across many files; bundling them is a recoverable but disruptive mistake.
Engineering discipline (iron law)
- Tech debt is fixed at the point of discovery — fold it in, every turn. When you hit a bug, a broken build step, or tech debt while working — even if it's outside your immediate task — you fix it in the same change. Do not dodge it, do not merely note it, do not defer-by-default. The only exception: you can prove another active workstream already owns the fix (a named branch/PR/issue) — then cross-reference that work so the connection isn't lost, and move on. "I'll leave it for later" is not an option.
Editor tools (tools/)
The VSCode extension and its supporting packages live under tools/. Each has its own agent-facing reference:
| Package | Doc | Read this when |
|---|---|---|
tools/vscode |
tools/vscode/AGENTS.md |
adding/modifying a VSCode tool (host + webview) |
tools/design-system |
tools/design-system/AGENTS.md |
building any tool UI — primitive inventory, StyleX token rules, Lit gotchas |
tools/preview |
tools/preview/AGENTS.md |
reusing canvas / animation / drag primitives |
tools/bridge |
tools/bridge/AGENTS.md |
host ↔ webview messaging — ClientBridge vs HostBridge semantics |
tools/io |
tools/io/AGENTS.md |
adding pure data helpers (image decode, atlas types/builders/packing/merge) |
tools/codelens-service |
tools/codelens-service/AGENTS.md |
the ZzFX CodeLens's Rust sidecar (workspace scan/parse) + its TS client — wire protocol, framing fatality policy, varRef.defRange contract |
tools/audio-play |
tools/audio-play/AGENTS.md |
the inline (no-panel) audio sidecar — real AudioContext via node-web-audio-api, the macOS code-signing prototype-gate findings, newline-JSON protocol |
When dispatching a sub-agent for tool work, include the relevant tools/<pkg>/AGENTS.md paths in the prompt — they encode hard-won API contracts (e.g. ClientBridge.on() returns an unsubscribe function, NOT a dispose() method) that aren't obvious from the source.
Skills
When authoring styles with @stylexjs/stylex — creating styles with stylex.create, applying with stylex.props, defining tokens with defineVars/defineConsts, building themes, or migrating CSS — invoke the stylex skill. It captures the Do/Don't rules and the authoring + installation references. The per-package AGENTS.md files only call out the project-specific gotchas (subpath token imports, tools' design-system primitive inventory) and lean on the skill for everything else.
Constraints
- Performance is critical — minimize draw calls, batch sprites via SpriteGroup, watch frame budgets
- All custom Three.js classes must work with R3F's no-arg construction + property-setting pattern
Do NOT
- Use GLSL or
onBeforeCompile— all shaders use TSL node materials - Use
WebGLRenderTarget— use renderer-agnosticRenderTarget - Use Web Awesome (
@awesome.me/webawesome) — examples use Tweakpane (@three-flatland/devtools/react) now - Add
declare global { namespace JSX }— useThreeElementsinterface augmentation viathree-flatland/react
Design Context
Applies to the docs/ site and the packages/starlight-theme/ workspace plugin. Originally captured 2026-05-06; revised 2026-05-06 (PR #33 mid-flight) when the Materia/Linear-minimalist substrate proved too pastel and too low-contrast. Refresh by re-running /impeccable:teach-impeccable if the brand direction shifts again.
Users
Developers building 2D scenes with Three.js or React Three Fiber — indie game devs, generative-art / interactive-visualization makers, and devs exploring WebGPU + TSL. Context when using the docs: evaluating whether the library fits a perf-sensitive use case, learning TSL idioms, copying examples into their own projects. Their job is to ship high-performance 2D scenes (sprites, tilemaps, effects) with confidence — not to consume marketing.
Brand Personality
Crafted, Expressive, Performant — and unapologetically colorful.
Voice: confident-technical and welcoming-collaborative. Existing copy lands the tone — "we're exploring," "your feedback shapes what we build" — keep it. Not corporate, not flippant. Aside callouts read like a teammate sharing notes; preserve that register.
Visual register: technicolor on near-black. A docs site for a graphics library should itself feel like a graphics demo — saturated accents, jewel-toned highlights, color used as taxonomy, not just decoration. The library renders sprites and shaders; the site renders confidence and curiosity through chroma.
Emotional goal: confidence in the tool's capability (it's serious infrastructure), and curiosity about what's possible (the expressive ceiling rewards exploration).
Naming
- Visual / wordmark: flatland — set in the pixelated
Silkscreentypeface in the header alongside the geometric FL icon mark. This is what users see in the browser tab, the header, and brand assets. - Package / npm / SEO: three-flatland — the npm package name, README headers, install commands, and any place a developer types or links to the package. Stays unchanged for discoverability against the Three.js / React Three Fiber ecosystem.
- The mismatch is intentional: short distinctive brand for humans, descriptive package name for search and crates registries.
Aesthetic Direction
Ground floor: high-contrast vibrant minimalism — restrained layout, generous whitespace, but every accent and affordance lands in jewel-toned color. Density is still earned through clarity, but accents do not desaturate to read "grown-up." Light and dark are both first-class citizens, both auditable to WCAG AA, both saturated.
Ceiling: Rauno-/Vlad-leaning crafted moments — subtle motion, bespoke micro-interactions, distinctive details that reward attention. Performance is part of the aesthetic.
Substrate:
- Palette: technicolor gem-named taxonomy inspired by bearded-theme/black — gold, ruby, emerald, diamond, amethyst, pink, salmon, turquoize sit alongside the conventional blue, green, orange, red, yellow, purple primitives. Every gem name is a token; components opt into them via
color="gem"props or scope-driven assignment (sidebar sections, card grids, asides). Backgrounds sit at near-black#111418with gem-tinted soft variants (gold-soft,ruby-soft, etc.) for surface differentiation. Light mode keeps the same gem names with deeper saturation for contrast on paper-toned backgrounds. - Color taxonomy: the design system does NOT stop at
primary / secondary / tertiary. Color carries meaning at every level:- Section identity — sidebar groups each pick a gem. Hover and active states inherit that gem's hue.
- Card accent —
<FeatureCard color="emerald">(etc.). Card grids cycle gems by default; explicitcoloroverrides. - Links — distinct token (
--link) different from foreground;--link-hovershifts hue intentionally. - Asides — note (diamond-blue), tip (amethyst-purple), success (emerald-green), warning (gold-orange), danger (ruby-salmon).
- Code-block accents — language-token colors lean on the gem palette.
- Typography:
- Wordmark / site title:
Silkscreen(pixel font) — the "flatland" mark only. - Page titles, section headings:
Public Sans600/700 — display weight, tight tracking. - Navigation, sidebar, UI labels:
Inter400/500/600 — humanist UI sans. - Body prose:
JetBrains Mono— yes, prose-as-mono. Reads as "engineering log." - Code blocks:
Commit Mono(with JetBrains Mono fallback) — programming ligatures, contextual alternates. - All four bundled locally via Fontsource. Site-wide font-families MUST be set explicitly on
body,header,nav,aside, etc. — Tailwind v4'stheme.fontFamilyonly generates utility classes, it does not auto-apply at the body level.
- Wordmark / site title:
- Texture (subtle): a barely-perceptible grain/noise overlay on the near-black background — the kind of thing readers don't consciously notice but can feel the absence of. The ghost hack you don't know is hitting you in the feels. Implementation: an SVG fractal-noise filter or a tiny tiled noise PNG at very low opacity (≤ 4%), additive on dark mode, multiplicative or skipped on light. NEVER raise opacity to where it reads "textured" — if a user notices it, it's wrong. Reference: the way Vercel's, Linear's, and Rauno's surfaces feel "deep" without obvious patterns.
- Motion (purposeful): the substrate embraces animation as a craft layer — with deliberate asymmetry. Ambient layers stay quiet so interactive moments can land hard.
- Ambient — texture grain (above): sub-perceptual, you-don't-see-it.
- Ambient — reveal on scroll — sections, cards, and figures fade-rise into view as they enter the viewport. CSS scroll-driven animations (
animation-timeline: view()) where supported, IntersectionObserver fallback for older browsers. Stagger grids bynth-child. Translate ≤ 16px, opacity 0→1, 240–360ms. Restrained. - Interaction — pointer-tracking light — soft radial-gradient highlight follows the cursor on cards, buttons, and key affordances. CSS custom properties
--mx/--myupdated viapointermove; gradient renders throughradial-gradient(at var(--mx) var(--my), …). The light hue tracks the surface's gem accent so the glint reads as "lit by the local color." Peak luminance ≤ 15% over base. - Interaction — physically-lit foil sheen (the headline): truly dynamically reactive surfaces with real lighting math, not CSS gradient cosplay. Implementation primitives:
- SVG filter pipeline per gem material —
<feImage>sources a baked normal map texture (subtle bump for the foil grain),<feDiffuseLighting>+<feSpecularLighting>with a<fePointLight>whose position tracks the pointer (set via JS-driven attribute updates). The diffuse term colors the surface, the specular term creates the sheen highlight, both react to cursor-derived light direction.<feTurbulence>+ small<feDisplacementMap>adds the holo-fleck/sparkle layer. - Per-gem materials — each gem has its own tuned filter (
#mat-gold,#mat-ruby,#mat-emerald,#mat-diamond,#mat-amethyst, …) with material-appropriate parameters: gold = high specular intensity, narrow lobe, warm diffuse, mild roughness; emerald = saturated green diffuse, prismatic chromatic-aberration sheen via per-channel offset; ruby = deep saturated diffuse, glossy specular; diamond = broad specular with cool blue-white, simulated dispersion via RGB-offset on the spec layer; amethyst = soft violet diffuse + sharp purple spec. Materials feel different — gold has weight, diamond has sparkle, emerald has depth. - Pointer-light coupling — JS handler maps
pointermoveto light position (x/y/zon thefePointLight) and to a tilt CSS transform on the surface. Light position carries a slight perspective offset so the sheen sweeps across the surface as the cursor moves. - Opt-in per surface — utility class
.holo+ attributedata-gem="gold|ruby|emerald|…"selects the material. Brand mark, landing hero, key CTAs, sidebar active item earn the spend. Not ambient. - Reduced motion — collapses to a static rendering of the same filter (light position pinned), so the surface still reads as "lit" without animation.
- SVG filter pipeline per gem material —
- Implementation rule: convincing output is the bar. The effect must sell living, breathing 3D — surfaces with ambient idle motion + cursor-driven dynamic light + material weight that differs per gem. Three layers required:
- 3D depth feel —
perspective, layered conic/linear gradients with parallax (background layers translate less than foreground when surface tilts),transform: rotate3ddriven by pointer position. The surface bends toward the viewer's cursor. - Ambient motion = perlin-noise-driven light position. The light source is always moving, even when no cursor is present, via 2D Perlin/simplex noise sampled per frame. The noise has a low spatial + temporal frequency so the wander reads organic, not mechanical — no
@keyframesoscillation, just continuous noise drift of the light xy. ≤ 8% surface dimensions, ~0.05–0.1 Hz temporal scale. The surface breathes because the light breathes. - Dynamic light — pointer position sets the center the noise wanders around, with ~80–120ms inertia ease. Cursor steers; noise jitters. When the cursor is idle the noise center stays put but the light keeps drifting around it; on pointermove the center smoothly relocates and the noise continues unbroken. One continuous animation loop drives both ambient and interactive light — they're the same light, just with a moving target.
- 3D depth feel —
- CSS-first if it sells. Layered conic + radial gradients with
mix-blend-mode, perspective transforms, idle@keyframes, and JS-driven--mx/--my/--tiltcustom properties cover most cases without canvas/SVG-filter cost. Escalate to SVG filter normal-map pipeline (feImage+feDiffuseLighting+feSpecularLighting+fePointLight) only when CSS can't sell the depth — gold's specular weight, diamond's dispersion, ruby's saturated specular lobe are the likely escalation candidates. Future option: TSL/WebGPU canvas overlay for the most premium moments (dogfoods three-flatland), reserved for landing hero or brand-mark touchpoints if SVG filters aren't enough. - Reduced motion collapses ambient drift, kills tilt, pins the highlight to a single static pose. The surface still reads as gem-lit, just frozen.
- References: poke-holo.simey.me (CSS holo math), Apple's annual report HTML pages and Linear's hero (perspective + ambient breathing), Vercel/Rauno surfaces (subtle parallax + cursor light).
- All motion respects
prefers-reduced-motion: reduce— reveals collapse to instant, pointer-light disables, holo-sheen flattens to a single static gem-tinted gradient, scroll-driven animations short-circuit. This is non-negotiable.
- View transitions:
astro-vtbotfor page-order morphs, sidebar-state preservation, MFE border control; honorsprefers-reduced-motion. - Audio:
SoundToggleand audio-enabled examples stay; never autoplay; respect user mute.
Reference (in spirit): bearded-theme black variants (palette intent), Ableton Live Suite UI (information density + accent color usage), Figma's Variables UI (color-as-taxonomy), Material Theme Builder dark-on-jewel screenshots.
Anti-references: corporate-SaaS pastel palettes; the previous Materia substrate which was too desaturated and read "afterthought" rather than "designed"; designs that route every accent through a single primary hue.
Logo / Icon
The original retro pixel-art FL mark is the established visual identity. It was briefly replaced with a geometric refresh (e71f17d) during Phase 3 and then reverted on stakeholder direction — the pixel mark is the brand. It pairs naturally with Silkscreen as the wordmark typography. Do not redesign the icon.
Brand assets (BrandAsset.astro — banner, OG, wide, social-x compositions) are a separate layer: those do get redesigned, with layouts and surrounding graphics inspired by the new theme (gem palette, near-black, sub-perceptual texture). The retro pixel-art icon and Silkscreen "flatland" wordmark sit inside those new compositions — the assets compose around the existing brand mark, they don't replace it.
Design Principles
- Density without noise. Show the API, the example, the verification. Don't pad. Density is earned through clarity.
- Color is taxonomy, not decoration. Every gem in the palette is doing meaning-work somewhere — section identity, card accent, link affordance, aside type. If a color appears, it tells you something.
- Quiet layout, expressive accents. Layout and rhythm stay restrained so the chroma can carry the personality. The components are the actors; saturation is the lighting.
- Light and dark equal citizens. Token-driven through the
starlight-themeworkspace plugin. Neither mode is the afterthought; both are designed; both stay saturated. - Performance is the proof. Page transitions snappy, animations cheap, fonts subset, bundle lean.
prefers-reduced-motionis honored everywhere. - Audio belongs. The library lives at the seam of dev-tool and creative-tool; sound toggles and audio examples are part of that. They never autoplay; they always respect mute.
General Guidelines for working with Nx
- For navigating/exploring the workspace, invoke the
nx-workspaceskill first - it has patterns for querying projects, targets, and dependencies - When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through
nx(i.e.nx run,nx run-many,nx affected) instead of using the underlying tooling directly - Prefix nx commands with the workspace's package manager (e.g.,
pnpm nx build,npm exec nx test) - avoids using globally installed CLI - You have access to the Nx MCP server and its tools, use them to help the user
- For Nx plugin best practices, check
node_modules/@nx/<plugin>/PLUGIN.md. Not all plugins have this file - proceed without it if unavailable. - NEVER guess CLI flags - always check nx_docs or
--helpfirst when unsure
Scaffolding & Generators
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the
nx-generateskill FIRST before exploring or calling MCP tools
When to use nx_docs
- USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases
- DON'T USE for: basic generator syntax (
nx g @nx/react:app), standard commands, things you already know - The
nx-generateskill handles generator discovery internally - don't call nx_docs just to look up generator syntax
