← Home

[ 00 / 01 ] · Teardown

This site, taken apart piece by piece. Each part runs live next to what it does, why it's there and how it works, with what it costs to run.

06
01 · Components
03
02 · Live data sources
[ 01 / 01 ]ComponentsLive demos · code in the repo

Components

C—01app/scenes/GlobeScene.ts

Dot globe

The hero Earth: about 28,000 dots, real coastlines, a marker on Long Beach. Drag it.

Why
A hero built from real data instead of a stock photo, and it says where I work from at a glance.
How
Natural Earth 1:50m land, rasterised offline into a 720 × 360 one-bit mask (43 KB). Dots sit on latitude rows; a point shader lights land and fades the limb. An invisible depth-only core hides the far side of each orbit. Dragging rotates it with inertia.
Cost
Three.js · ~131 KB gzipped, after first paint
Trade-off —
The home page now always loads Three.js. It arrives after the headline renders and stops drawing when the hero is off screen or the tab is hidden.

Live

Drag to rotate

C—02server/api/satellites.get.ts · app/scenes/orbits.ts

Live satellites

The ISS, Hubble, Landsat 9 and NOAA-21 where they are right now, recomputed every second.

Why
Live data makes the globe true rather than decorative.
How
A server route pulls orbital elements (TLEs) from CelesTrak and caches them for two hours, matching their update cycle. The browser propagates each one with SGP4 via satellite.js. I checked the ISS against an independent tracker and the positions matched.
Cost
1 upstream call per 2 hours
Trade-off —
CelesTrak refuses repeat downloads of large groups inside one update window, so the Starlink sample is cached separately and can be empty for a while after a deploy.

Live

Fetching orbital elements…

SGP4 · elements from CelesTrak · 0 Starlink in the swarm

C—03app/scenes/RocketScene.ts · app/scenes/AsciiStage.ts

ASCII launch

A Falcon 9-style mission drawn in characters: pad, ignition, staging, fairing separation, deploy.

Why
Plan, build, ship, told as a launch instead of a list.
How
The scene renders into a tiny target, one texel per character cell. A fragment shader picks a glyph by brightness and keeps warm colours, so the exhaust stays orange. Every part is a pure function of scroll progress, so scrolling back plays it in reverse. Scrub it here.
Cost
Renders only while on screen
Trade-off —
The home section pins for 4.2 screen heights. That much scroll is the point, but it is a long way to the next section.

Live

30%

C—04app/components/SiteCursor.vue

Cursor

An arrow with a trailing signal square. It changes over links and drag zones, and steps aside in Work.

Why
A small, consistent detail that matches the readout style without hiding the actual pointer position.
How
The arrow follows the pointer through GSAP quickSetter (no lag); the square uses quickTo with easing. State comes from one delegated pointerover listener checking what is under the pointer.
Cost
One listener, transforms only
Trade-off —
Off on touch screens and with reduced motion, where the native cursor is the right answer anyway.

Live

01 · Idle

Move across this panel. The arrow tracks the pointer exactly; the orange square trails it.

02 · Links

Hover me

03 · Drag zones

Anything draggable, like the hero globe, gets a tag.

C—05app/components/UsNetworkMap.vue · server/api/served-from.get.ts

Network map

The lower 48 seen from the Gulf, with the US regions this site can run in, and the one that served you.

Why
Infrastructure is part of the job; this shows it instead of saying it.
How
The same land mask as the globe, clipped to a rough US outline and projected onto a ground plane with simple perspective. "Served from" reads VERCEL_REGION from a no-store API route, so it is the real region that ran the request.
Cost
Dots drawn once, arcs per frame
Trade-off —
Locally there is no Vercel region, so it reports the dev server.

Live

US regions · IAD1 · CLE1 · PDX1 · SFO1

C—06server/api/assistant.post.ts · server/utils/assistantGuard.ts

AI assistant

A Claude-backed chat that answers only about me and this site, built so strangers can’t spend my API credits.

Why
Recruiters can ask a direct question and get an answer from what is published here.
How
Seven checks run before the model is called, cheapest first. Counters are atomic Upstash INCRs so concurrent bursts can’t slip through, and replies are HMAC-signed so a visitor can’t invent earlier answers. An independent security review found four issues; all four are fixed.
Cost
Capped daily, plus a Console spend limit
Trade-off —
Production refuses to run it without shared storage. Safer, but one more service to keep up.

Live

$ POST /api/assistant

  1. 01No key or shared storage → offline503
  2. 02Same-origin requests only403
  3. 03Body ≤ 16 KB, question ≤ 500 chars413 / 400
  4. 04Assistant turns must carry a server signature400
  5. 058 per 10 min · 25 per day per visitor (IPv6 by /64)429
  6. 06Global daily cap, atomic Upstash INCR429
  7. 07Scope-locked prompt · 1,024 token cap · low effort200

Assistant offline on this server

● app/scenes/AsciiStage.ts · glyph lookup
vec4 src = texture2D(uScene, (cell + 0.5) / uCells);
float lum = dot(src.rgb, vec3(0.299, 0.587, 0.114));
float idx = floor(clamp(pow(lum, 0.8), 0.0, 0.999) * uGlyphCount);
vec2 guv = vec2((idx + local.x) / uGlyphCount, local.y);
float mask = texture2D(uGlyphs, guv).r;

Stack

Nuxt 4 and Vue 3, Tailwind 4, GSAP and Lenis, Three.js, a Sanity-backed blog, Vitest, on Vercel. Type is Big Shoulders Display, Archivo and Geist Mono.