jb-website/docs/superpowers/specs/2026-06-03-particle-field-greebling-design.md
Josh Bairstow f4b8e1a7d8
All checks were successful
deploy / build-and-deploy (push) Successful in 31s
particle field planning
2026-07-23 23:14:04 +10:00

5.6 KiB

Particle Field + Greebling Restructure

Date: 2026-06-03
Status: Approved

Context

The site currently uses fixed-position SVG accent marks (Mark.astro) placed at hardcoded viewport percentages. They feel scattered rather than connected to anything structural. Alongside this, a new ambient background visual — a floating particle field — is to be introduced to add depth and visual interest.

The two changes are complementary: both join the same atmospheric layer of the site. The marks become structurally grounded; the particle field adds a new living layer beneath everything.


Part 1: ParticleField Component

What it does

A full-viewport <canvas> rendered as a fixed background layer below the content stage. Particles drift slowly across the canvas, connected by faint lines when within reach. An underlying Perlin/fBm noise field modulates opacity and glow per-particle, producing an organic pulsing quality. The cursor brightens nearby lines.

Layer position

.stage (relative, z-index: 1)        ← content, unchanged
ParticleField canvas (fixed, z-index: 0, pointer-events: none)   ← new
body::before grain (fixed, z-index: 0)   ← unchanged

The canvas sits below the stage (z:0 vs z:1) and carries pointer-events: none. No existing display or interactive component is affected.

New file

src/components/islands/ParticleField.tsx

  • Canvas: position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 0
  • No background fill — canvas is transparent; #17130E page ground shows through
  • Perlin noise engine ported directly from reference/particle-field-demo/particleNetwork.js
  • All tunable values in a single PARTICLE_CONFIG object at the top of the file — nothing buried in render logic
  • prefers-reduced-motion: animation loop pauses, canvas renders one static frame
  • Lifecycle: useEffect mount/unmount, resize observer to keep canvas dimensions in sync

Integration

Added to src/layouts/BaseLayout.astro as:

<ParticleField client:idle />

Placed in the DOM before .stage so stacking order is correct without needing a higher z-index on the stage.

Colour palette

Warm neutral register, matching --fg-on-dark-2 / --fg-on-dark-3 range — embers, not highlights.

Token Value Replaces (reference cool blue)
Particle fill rgb(200, 178, 148) rgb(180, 200, 255)
Connecting lines 174, 140, 100 80, 160, 240
Mouse lines 210, 185, 150 160, 220, 255
Glow inner 200, 170, 130 180, 220, 255
Glow mid 155, 120, 82 100, 170, 255

Starting config values will be tuned down from the reference defaults (fewer particles, lower line alpha) for a content site context. All values live in PARTICLE_CONFIG for immediate visual iteration.


Part 2: Mark Restructure

What changes

Mark.astro switches from position: fixed to position: absolute. All existing props (kind, width, top, left, right, bottom, rotate) are unchanged — only the positioning context shifts from viewport to nearest position: relative ancestor.

The <slot name="marks"> in BaseLayout.astro is cleared of its defaults. Marks migrate to live inside the specific section containers they visually belong near.

Per-section anchoring

Each host section gets position: relative added. Marks use small offsets (including negative values) to sit at or just outside the section's visible edge. Absolutely-positioned elements are out of flow, so section content and layout are unaffected.

Example (home page):

Mark Section Edge
ringdots 84px .intro top-left, slight outward offset
dots 52px .hero top-right edge
pipes 28px .hero bottom-left edge
concentric 42px .index bottom-right edge

The same anchoring logic applies to 404.astro, blog/index.astro, code/index.astro, art/index.astro, and PostLayout.astro.

Future enhancement

The current placement is a hand-curated first pass. A future generateMarks(sections) utility could sample from available section anchors and mark types with seeded randomness, removing the fingerprints of manual placement and producing layouts that feel discovered rather than designed. This is intentionally deferred — visual inspection of the curated pass will inform what the generative version should optimise for.


Files to modify

File Change
src/components/atoms/Mark.astro position: fixedposition: absolute
src/layouts/BaseLayout.astro Clear slot defaults; add <ParticleField client:idle />
src/pages/index.astro Move marks into sections
src/pages/404.astro Move marks into sections
src/pages/blog/index.astro Move marks into sections
src/pages/code/index.astro Move marks into sections
src/pages/art/index.astro Move marks into sections
src/layouts/PostLayout.astro Move marks into sections
src/components/islands/ParticleField.tsx New file

Verification

  1. Run npm run dev and inspect all pages — no visual regressions in nav, content, or islands
  2. Confirm particle canvas renders below all content and does not intercept pointer events (click through the canvas onto links/buttons)
  3. Confirm marks appear at section edges on each page, not scattered across the viewport
  4. Toggle prefers-reduced-motion: reduce in devtools — canvas should freeze, not disappear
  5. Resize viewport — canvas should fill; marks should reposition relative to their sections
  6. Check dark/light theme toggle — mark filters and opacity should still apply correctly