- Add EvolutionController to manage spawning and physics of icons. - Introduce GradientController for animated gradient backgrounds. - Create iconLoader for fetching and caching SVG icons. - Implement modal functionality for settings adjustments. - Connect UI controls to controllers via SettingsController. - Add CSS styles for gradient animations, icons, and modal. - Create utility functions for color manipulation in gradients.
9.7 KiB
devpage — Agent Context File
This file is a compact reference for AI agents working on this project. Read it before making changes. It covers purpose, architecture, patterns, and current state.
What this project is
A personal developer page — a single-page app with an animated gradient background and a floating icon "evolution" system. Icons spawn at random intervals, drift around the screen with chaotic physics, and bounce off viewport edges. A settings modal lets the user tune the gradient colour, animation speed, icon movement speed, and gradient rotation.
It is intentionally a growing project. The base page and evolution system are done; future work will extend icon variety, add icon interactions, and build out the main content area.
Tech constraints
- Vanilla JS — no framework, no bundler, no build step.
- Pure ES modules (
type="module"in HTML). All imports use relative paths. - Requires a local HTTP server —
fetch()calls fail onfile://. Run withnpx serve .orpython3 -m http.serverthen openlocalhost. - Browser target: modern evergreen (CSS
@property,backdrop-filter,scaleas standalone property, private class fields all required).
File map
index.html ← HTML shell + all CSS links + module entry point
src/
js/
main.js ← Entry point. Instantiates all controllers, boots them.
constants.js ← Single source of truth for all numeric/flag defaults.
gradient.js ← GradientController. Drives CSS vars on <html>.
modal.js ← ModalController. Open/close, focus trap, Escape dismiss.
settings.js ← SettingsController. UI → controller bridge (no own state).
evolution.js ← EvolutionController. Spawn scheduler + rAF physics loop.
entity.js ← Entity class. One floating icon: physics + DOM lifecycle.
iconLoader.js ← fetch() + Map cache for SVGs. normalizeSvg() strips attrs.
utils/
colorUtils.js ← hexToHSL, hslToHex, deriveGradientPair.
styles/
main.css ← Base reset, typography (DM Mono), settings button.
gradient.css ← @property --gradient-angle, keyframes, body animation.
modal.css ← Glass morphism overlay, controls, toggle switch.
icons.css ← .evolution-container, .icon-entity, appear transition.
data/
icons.json ← Icon registry: groups, per-icon metadata, type colours, spawn config.
icons/
*.svg ← 34 SVG icons (Solar icon set, 24×24 viewBox, currentColor).
Architecture
Controller pattern
Each domain has one class. No shared mutable globals.
main.js
├── GradientController gradient.js state: color, speed, rotating
├── ModalController modal.js state: open/closed, trigger ref
├── SettingsController settings.js no state — reads DOM, forwards to others
└── EvolutionController evolution.js state: entities[], moveSpeed, timers
└── Entity[] entity.js state: x, y, vx, vy, alive, DOM refs
Data flow
User interaction (slider/picker/toggle)
→ settings.js listener
→ gradient.setColor() / gradient.setSpeed() / evolution.setMoveSpeed()
→ CSS custom property on <html> OR evolution speed multiplier updated live
CSS custom properties (set on <html> by GradientController)
--color-1 first gradient colour (derived from user's base colour)
--color-2 second gradient colour (hue-shifted companion)
--anim-duration animation duration in seconds (maps speed 1-10 → ~32s-3.5s)
--gradient-angle registered @property <angle>, animated by gradientRotate keyframe
Key patterns and why
Two-div entity structure
Each entity uses an outer div for JS position (transform: translate()) and an
inner div for the CSS appear animation (scale standalone property). Separating
them prevents JS transform writes from interrupting the CSS transition.
<div class="icon-entity"> ← JS sets transform: translate(x, y) every frame
<div class="icon-entity__body"> ← CSS transition: scale 0→1 on [data-state="alive"]
<svg>…</svg>
</div>
</div>
Appear animation trigger
bodyEl.dataset.state = 'spawning' is set synchronously before mount.
dataset.state = 'alive' is set two rAF ticks later. This guarantees the
browser has painted the initial scale: 0 state before the transition fires.
Rotation animation without restart
Both gradientFlow and gradientRotate animations are always declared on <body>.
Rotation starts with animation-play-state: paused. Toggling .gradient-rotating
class switches it to running. This avoids restarting gradientFlow on toggle.
Edge bounce
Math.abs() trick ensures correct direction regardless of penetration depth:
if (x - h <= 0) { x = h; vx = Math.abs(vx); } // left wall → go right
if (x + h >= vw) { x = vw - h; vx = -Math.abs(vx); } // right wall → go left
Colour derivation
deriveGradientPair(hex) in colorUtils.js produces a two-colour gradient from
one base colour using HSL math. Colour 1: same hue, deeper (sat ×1.15, light ×0.62).
Colour 2: hue +35°, lighter (sat ×0.88, light ×1.48, max 78).
SVG loading
iconLoader.js fetches each SVG once via fetch() and caches in a Map.
normalizeSvg() strips: XML declaration, HTML comments, width= attr, height= attr.
This lets CSS control icon size. All icons use fill="currentColor" (except
display-line-duotone.svg which uses stroke="currentColor").
icons.json loaded at runtime
import … assert { type: 'json' } has inconsistent browser support without a bundler.
EvolutionController loads icons.json via fetch() inside #loadIconsData().
Defaults (source of truth: src/js/constants.js)
GRADIENT_COLOR: '#4d22b3' // deep purple
GRADIENT_SPEED: 2 // slider value 1-10
GRADIENT_ROTATION: false
SPAWN_DELAY_MIN: 3_000 // ms before first/next icon spawns
SPAWN_DELAY_MAX: 20_000 // ms
ICON_SIZE: 24 // px
ICON_HALF: 12 // px, used in edge bounce math
MOVE_SPEED: 5 // slider default; multiplier = sliderValue / MOVE_SPEED
BASE_SPEED: 0.8 // px/frame at multiplier 1.0
MAX_SPEED_FACTOR: 2.5 // max speed = BASE_SPEED × MAX_SPEED_FACTOR
APPEAR_DURATION: 600 // ms, scale 0→1 CSS transition
DRIFT_CHANCE: 0.02 // probability per frame of a velocity kick
DRIFT_MAGNITUDE: 0.25 // max |Δv| per kick
Icon inventory
34 SVG files in src/icons/. 9 are registered in src/data/icons.json.
The other 25 are present on disk but not yet wired into the evolution system.
Registered in icons.json
| filename | label | type | group |
|---|---|---|---|
| dna-bold-duotone.svg | DNA | good | biology |
| bug-bold-duotone.svg | Bug | bad | tech |
| buildings-3-bold-duotone.svg | Buildings | good | economy |
| chat-round-dots-bold-duotone.svg | Chat | neutral | social |
| chat-round-money-bold-duotone.svg | Chat Money | neutral | economy |
| chat-round-unread-bold-duotone.svg | Unread Chat | neutral | social |
| database-bold-duotone.svg | Database | good | tech |
| delivery-bold-duotone.svg | Delivery | good | economy |
| display-line-duotone.svg | Display | neutral | tech |
Note: display-line-duotone.svg uses stroke not fill. Tinting via color
CSS property works, but the visual style differs from the fill-based icons.
On disk, not yet in icons.json (25 files)
android-old, angular, app-store, apple-brand, claude, code-1, docker, donut-bold-duotone, face-scan-square-bold-duotone, filters-bold-duotone, gamepad-bold-duotone, git, github, go, javascript, kubernetes, laravel, mysql, nodejs, open-ai, php, postgresql, python, signal-app, vs-code
To register any of these: add an entry to icons.icons in icons.json following the
existing schema { "label": "…", "type": "good|bad|neutral", "group": "…" }.
Create a new group in icons.groups if needed.
Current spawn behaviour
Only dna-bold-duotone spawns. The icon is set in icons.json → spawn.initial.
EvolutionController.#spawnEntity() always uses spawn.initial — it does not yet
pick from the full icon registry. This is intentional: the spawn logic is the next
thing to extend.
Settings modal controls
| Element ID | Type | Default | Wired to |
|---|---|---|---|
colorPicker |
color | #4d22b3 | gradient.setColor(hex) |
speedSlider |
range 1-10 | 2 | gradient.setSpeed(n) |
moveSpeedSlider |
range 1-10 | 5 | evolution.setMoveSpeed(n) |
rotationToggle |
checkbox | false | gradient.toggleRotation(bool) |
Known gaps / next steps
- Spawn variety —
#spawnEntityalways spawnsspawn.initial. Extend to pick randomly fromicons.icons(or weighted by type) to get diverse entities. - icons.json completeness — 25 icons on disk are unregistered. Add them to unlock their use in spawning.
- Icon interactions — no collision detection between entities yet.
- Main content area —
<main class="page-main">is empty. Reserved for content. - Entity cap — no maximum entity count; they accumulate indefinitely.
- Persistence — settings reset on page reload. No localStorage yet.