MAKING-OF
How this site was built
The idea
Most portfolios are a grid of thumbnails. I wanted the front page to argue something — that the work wasn’t a list of logos, but a current that ran through them.
So the hero is an Influence Network: three streams of colored light, each carrying a population of particles, flowing out from three anchors in my career — AKQA, Meta, Apple. The particles are people; the streams are influence; where they pass close, their colors bleed. The work didn’t just ship products — it moved through networks of people, and picked up color from everything it passed.
That’s the thesis. The rest is how it got built, including the parts that fought back.
Previous lives
This isn’t the first version of this site — it’s roughly the fifth, and the through-line is that each rebuild quietly tracks whatever the frontend world was doing that year.
- WordPressPHP, a theme, a database a portfolio didn’t need
- Middleman + jQueryRuby-generated static pages, DOM by hand
- Grunt + vanilla JSno framework, better build tooling
- Reactcomponents finally worth the weight
- + Sanitycontent moves out of the code into a headless CMS
- Next.js + R3Fthis one — the influence network
Each version threw away almost all of the last one’s code and kept none of its tools. What survived every time was the content and the URLs — which is exactly why this rebuild treats content and routes as the durable layer and everything else as replaceable. I’ve replaced everything else four times already.
Two layers, not two sites
The decision that shaped everything after it: the fancy version is not the site.
Two presentation layers sit over one set of content. The base layer is semantic, server-rendered HTML — readable with JavaScript disabled or WebGL missing, and it’s what phones, reduced-motion users, and crawlers get. The canvas layer — the Three.js network — takes over only when the device and preferences can carry it. The base layer isn’t a fallback apology; it’s a complete site that happens to have a spectacular version on top. If the canvas never loads, you’d never know anything was missing.
That’s also why this page can exist: everyone can experience the thing, and the curious can read how it works.
Painting influence
The concept started as a literal graph — nodes and edges — which is to say it looked like homework. The fix was to drop topology for continuous flow: each chapter is a stream of additive lines plus particles riding a shared wave. No wires, just current.
Every point’s color is a soft blend of the three anchor colors, weighted by how close it sits to each. On its own that’s just gradients. What makes it read as influence spreading is a second rule I call resistance: a stream keeps its parent color until a particle drifts near a foreign anchor, then it takes on that hue. Near home you keep your color; wander into someone else’s territory and you start to become theirs. The thesis, as a distance function.
The performance war
The honest section. The network is beautiful on a strong machine and a slideshow on a weak one, so it’s gated behind a capability check — and the check took three tries to get right.
- Viewport + touch heuristicgated a new iPhone off, would’ve let a thermal-throttling iPad on
- Ask the browser how many cores it hasSafari reports the same number for every iOS device — the signal is gone
- Measure the frame rate directlystop guessing, time a few frames, then decide
The first check was wrong both ways. Asking the browser for a core count hit a wall too: Safari reports the same number on every iOS device to prevent fingerprinting, so a three-year-old iPad and the newest iPhone look identical. So for the ambiguous tier where every iOS device lands, I stopped guessing and measured: a brief hidden render, a handful of frames, and a verdict.
Fast enough and the device earned the canvas. The verdict is remembered for a while, nothing identifying is stored, and a manual toggle always wins. It split the devices the heuristic couldn’t, with none special-cased by name. The lesson: when the platform hides the signal you want, stop inferring it and measure the thing you actually care about.
The sound
There’s an ambient generative score — a pad-and-bass bed, live plucks, and a synthesized “voice” that speaks each brand’s name on hover.
The bed started as live synthesis, and it crackled — but only sometimes, the worst kind of bug. With the 3D scene running flat-out, the audio thread would occasionally starve, and a starved audio thread is a click. Pre-rendered sound survived the contention, but rendering it on the user’s machine was too slow under that same load. The fix moves the work off their machine entirely: the bed is rendered once, at build time, and shipped as a loop — while the plucks and the hover voice stay live on top.
The voice is synthesized speech in the oldest sense: shaped vowels, one stressed syllable at a time. “AKQA” spells four letters; “Meta” is “MAY-tah”. No recordings, and it only speaks on a deliberate hover.
Navigation without page loads
Click a project and its detail opens as a modal — but the canvas never unmounts, keeps flowing, and pushes in to isolate that project’s stream as the backdrop. A full navigation would tear down WebGL and audio, and rebuilding both would feel like a hiccup.
So an in-app click is intercepted and opens as a modal over the living hero, while a direct visit, refresh, or crawler gets the real server-rendered page at the same URL. One route, two presentations — the two-layer philosophy applied to navigation.
Accessibility wasn’t the last step
The motion is deliberately calm — slow eases, text beside movement, not on top of it — and prefers-reduced-motion is a hard gate: set it and you get the base layer, full stop. Contrast holds to WCAG AA against the brightest ribbon pixel, and everything navigable in the canvas is a real focusable link with a visible ring.
“Make it accessible” is usually end-of-project cleanup. On a site whose whole premise is a fancy layer some people won’t get, the un-fancy layer has to be first-class — or the premise collapses.
Colophon
Next.js · React · TypeScript · Three.js · Tone.js · Sanity · Vercel.
