diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-29 01:50:33 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-29 01:50:33 -0700 |
| commit | 2ead51ef77c4872a188217cca8b82f5b02053ecd (patch) | |
| tree | 1755d5b644c32a913cb9d4a88a3038603b00a886 | |
| parent | 41c352e939a3389b9c7a18065c1c25c005e98bfe (diff) | |
chrome: add moving shard background + CLAUDE.md
Implements the chrome theme's metallic shard ambient field from the
redesign handoff. Adds spawnShards() to aero.js, shard CSS (clip-path
polygon, chrome gradient, specular streak, shard-rise keyframes) to
aero.css, and wires up calls in index.js and enter.html. Shard field
is hidden by default; shown only when body[data-theme="chrome"].
Also adds prefers-reduced-motion guard for both fields.
Adds CLAUDE.md with architecture overview for future sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | CLAUDE.md | 70 | ||||
| -rw-r--r-- | aero.css | 46 | ||||
| -rw-r--r-- | aero.js | 26 | ||||
| -rwxr-xr-x | enter.html | 1 | ||||
| -rw-r--r-- | index.js | 1 |
5 files changed, 143 insertions, 1 deletions
diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c8c8c62 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,70 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A static personal website at `fun.tylerhoang.xyz` styled as a retro desktop OS. There is no build system, no framework, no package manager. Everything is plain HTML/CSS/JS served directly by a web server. + +## Development + +Preview locally with any static server: + +```bash +python3 -m http.server 8080 +# or +npx serve . +``` + +Syntax-check JS without running it: + +```bash +node --check aero.js +node --check index.js +node --check articles.js +``` + +There are no tests and no lint step. + +## Architecture + +### Entry flow + +`enter.html` → welcome splash/gate → user clicks "enter" → `index.html` main desktop + +Both pages load `aero.css` and `aero.js`. `index.html` additionally loads `articles.js` then `index.js`. + +### Shared library: `aero.js` + +Exports everything under `window.Aero`: + +- **Background fields**: `spawnBubbles(host, count, speed)`, `spawnShards(host, count, kind, speed)` — generate DOM elements for the ambient animated background. Called once on mount; animation is pure CSS. Both fields are always in the DOM; CSS shows only the active-theme one. +- **Theme API**: `getTheme()`, `setTheme(t)`, `mountThemeSwitcher()`, `initTheme()` — source of truth is `document.body[data-theme]` = `"aero"` | `"chrome"`, persisted in `localStorage["tyler.theme"]`. A tiny inline script at the top of `<body>` applies the saved theme before paint. +- **Window management**: `makeDraggable(el, handle)`, `makeResizable(el, options)` — used for all `.win` elements in `index.html`. +- **UI helpers**: `counterHTML()`, `nowPlayingHTML()`, `animateEq()`, `musicToggleHTML()`, `makeClouds()`, `sparkleCursor()` +- **API calls**: `fetchLastFm()`, `fetchFilms()`, `fetchVisitorCount()`, `fetchReelMouthFeed()` + +### Stylesheets + +- `aero.css` — the entire design system. Aero (Frutiger) styles are the base; `body[data-theme="chrome"]` overrides apply the Y2K liquid-metal treatment. All `!important` on chrome overrides is intentional — the base styles set defaults that chrome must override. +- `index.css` — desktop-specific layout (`.desk`, `.win`, `.taskbar`, `.icons`, `.icon`, etc.) + +### Content + +- `articles.js` — defines the `ARTICLES` array. Each article is `{ slug, title, date, tag, excerpt, body }` where `body` is an HTML string. The faux browser in `index.html` renders these client-side — there is no server routing for articles. + +### Server-side (PHP) + +- `counter.php` — visitor counter backed by a flat file (`counter.dat`), deduplicated by 24h cookie. +- `podcast.php` — RSS proxy for the Reel Mouth podcast feed (Anchor/Spotify), returns `{ art, episodes }`. + +### Asset conventions + +- Colors: OKLCH throughout. Do not convert to hex/rgb. +- Fonts: loaded from Google Fonts in each HTML `<head>`. Aero uses Plus Jakarta Sans + IBM Plex Mono + Caveat. Chrome uses Audiowide + Michroma + Space Grotesk + IBM Plex Mono. +- Images: `img/wallpaper.png` (aero), `img/wallpaper-chrome.png` (chrome), static assets in `img/static/`. +- Music: `/mus/` — theme-specific tracks swapped live on theme change. + +### Adding an article + +Add an entry to the `ARTICLES` array in `articles.js`. The browser window in the desktop renders it automatically — no routing changes needed. @@ -66,6 +66,46 @@ html, body { 100% { transform: translate(var(--drift, 30px), -110vh) scale(1.05); opacity: 0; } } +/* SHARDS (chrome theme counterpart to bubbles) */ +.shard-field { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 0; display: none; } +.chrome-shard { + position: absolute; bottom: -170px; + background: linear-gradient(135deg, + oklch(96% 0.02 240) 0%, + oklch(78% 0.04 280) 30%, + oklch(45% 0.06 290) 52%, + oklch(72% 0.04 240) 75%, + oklch(92% 0.02 240) 100%); + filter: + drop-shadow(0 0 8px oklch(60% 0.20 290 / 0.55)) + drop-shadow(0 4px 12px oklch(5% 0.03 270 / 0.7)); + animation: shard-rise linear infinite; +} +.chrome-shard.shard { clip-path: polygon(50% 0%, 70% 38%, 56% 100%, 30% 64%); } +.chrome-shard.blade { clip-path: polygon(50% 0%, 57% 50%, 50% 100%, 43% 50%); } +.chrome-shard::before { + content: ""; + position: absolute; inset: 0; + background: linear-gradient(115deg, + transparent 20%, + oklch(99% 0.02 200 / 0.65) 42%, + oklch(92% 0.08 320 / 0.35) 50%, + transparent 68%); + background-size: 200% 200%; + animation: shard-streak 3s ease-in-out infinite alternate; +} +@keyframes shard-rise { + 0% { transform: translate(0, 0) rotate(var(--rot0, 0deg)); opacity: 0; } + 8% { opacity: 0.95; } + 90% { opacity: 0.75; } + 100% { transform: translate(var(--drift, 0px), -130vh) rotate(var(--rot1, 120deg)); opacity: 0; } +} +@keyframes shard-streak { + 0% { background-position: 200% 0%; } + 100% { background-position: -20% 0%; } +} +body[data-theme="chrome"] .shard-field { display: block; } + /* GLOSSY AQUA BUTTON */ .aqua { position: relative; display: inline-flex; align-items: center; gap: 6px; @@ -1834,3 +1874,9 @@ body[data-theme="aero"] .mark::after { pointer-events: none; opacity: 0.8; } + +/* Disable ambient motion for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + .bubble-field, .shard-field { display: none !important; } + .sparkle { display: none !important; } +} @@ -17,6 +17,30 @@ function spawnBubbles(host, count = 22) { host.appendChild(f); } +function spawnShards(host, count = 30, kind = 'shard', speed = 1) { + const f = document.createElement('div'); + f.className = 'shard-field'; + for (let i = 0; i < count; i++) { + const s = document.createElement('div'); + s.className = 'chrome-shard ' + kind; + const w = kind === 'blade' ? 8 + Math.random() * 14 : 16 + Math.random() * 34; + const h = kind === 'blade' ? 64 + Math.random() * 130 : 36 + Math.random() * 92; + s.style.width = w + 'px'; + s.style.height = h + 'px'; + s.style.left = Math.random() * 100 + '%'; + s.style.setProperty('--drift', ((Math.random() - 0.5) * 170) + 'px'); + const rot0 = Math.round(Math.random() * 360); + const spin = (Math.random() < 0.5 ? -1 : 1) * (110 + Math.random() * 250); + s.style.setProperty('--rot0', rot0 + 'deg'); + s.style.setProperty('--rot1', (rot0 + spin) + 'deg'); + s.style.animationDuration = ((18 + Math.random() * 16) / speed) + 's'; + s.style.animationDelay = (-Math.random() * 30) + 's'; + f.appendChild(s); + } + host.appendChild(f); + return f; +} + function makeClouds(host) { const svg = `<svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" style="width:100%;height:100%;"> <defs> @@ -270,7 +294,7 @@ async function fetchReelMouthFeed(limit = 6) { } window.Aero = { - spawnBubbles, makeClouds, sparkleCursor, makeDraggable, makeResizable, + spawnBubbles, spawnShards, makeClouds, sparkleCursor, makeDraggable, makeResizable, counterHTML, nowPlayingHTML, animateEq, musicToggleHTML, // theme api getTheme, setTheme, mountThemeSwitcher, initTheme, THEMES, @@ -216,6 +216,7 @@ <script> Aero.initTheme(); Aero.spawnBubbles(document.getElementById('bub-stage'), 26); + Aero.spawnShards(document.getElementById('bub-stage'), 30, 'shard', 1.5); Aero.sparkleCursor(); Aero.mountThemeSwitcher(); </script> @@ -2,6 +2,7 @@ const desk = document.getElementById('desk'); Aero.makeClouds(document.getElementById('clouds')); Aero.spawnBubbles(desk, 24); + Aero.spawnShards(desk, 30, 'shard', 1.5); Aero.sparkleCursor(); Aero.mountThemeSwitcher(); |
