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;
#17130Epage ground shows through - Perlin noise engine ported directly from
reference/particle-field-demo/particleNetwork.js - All tunable values in a single
PARTICLE_CONFIGobject at the top of the file — nothing buried in render logic prefers-reduced-motion: animation loop pauses, canvas renders one static frame- Lifecycle:
useEffectmount/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: fixed → position: 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
- Run
npm run devand inspect all pages — no visual regressions in nav, content, or islands - Confirm particle canvas renders below all content and does not intercept pointer events (click through the canvas onto links/buttons)
- Confirm marks appear at section edges on each page, not scattered across the viewport
- Toggle
prefers-reduced-motion: reducein devtools — canvas should freeze, not disappear - Resize viewport — canvas should fill; marks should reposition relative to their sections
- Check dark/light theme toggle — mark filters and opacity should still apply correctly