How this site works

An 80,000-particle field, text the particles flow around, page transitions printed in halftone, and the fallbacks that keep all of it honest.

Alex JardineOct 15, 20268 min read

This site is usually the first piece of my work people see, so I treated it like a client project: pick one idea worth committing to, then do the unglamorous engineering that makes it hold up on real devices, and for people who would rather the page sat still. Here’s how it works, the calls I made along the way, and where my evidence runs out.

One point cloud, many scenes

Everything moving behind the page is a single WebGL2 point cloud: 80,000 particles on desktop and 36,000 on screens narrower than 860 pixels, drawn in one call per frame. Each particle knows where it came from, where it’s going and a few random values of its own, and the vertex shader works out where it should be right now.

Every section of the site names a scene, and each scene is a small generator that writes positions and colours into buffers. My name on the homepage is sampled from the site’s display typeface, Syne, so the particles land on real letterforms. The knot is a parametric curve. The gears on Services are proper involute gears, 24 and 18 teeth with a shared module, set half a tooth out of phase so they stay in mesh as they turn. On Lambert Lighting, three pendants throw beams onto the floor. On the Writing page, a particle pencil writes a looping cursive line, and the same curve drives both the ink and the pencil tip.

None of the scenes sit at fixed coordinates. Each one is sized to the open space in its section, measured from the real layout, and follows that section on a light spring as you scroll.

Moving between shapes

When one scene hands over to the next, every particle flies its own curve from where it was to where it belongs. Two details stop that reading as 80,000 separate movements. Every hand-over shares one bend, with only a small spread per particle, so the cloud travels as a stream. And the side of the old shape facing the new one leaves first, so the shape seems to pour toward where it’s going.

People often scroll on before a shape has landed. When that happens, the shader that drew the last frame writes each particle’s on-screen position, colour and size into a buffer with transform feedback, and the next flight starts from exactly there. Nothing comes back to the CPU, so an interrupted hand-off costs no main-thread time and never jumps.

Particles that get out of the way of words

Particles behind text are a readability problem before they’re a visual one, so the big shapes flow around lines of text instead of sitting on them. While the page is still, I measure every line of text once, as a rectangle in page coordinates. Each frame just shifts that list by the scroll offset and sends the 32 rectangles nearest the middle of the screen to the shader. There, each particle checks its distance to each rounded rectangle, with a slightly different edge per particle so the boundary looks organic rather than boxed in. Particles near a word part around it, and the few that can’t clear it fade out.

My first version simply shoved particles out to a fixed margin, which piled them into bright ridges along the edge of every paragraph. Measuring the page on every frame was a mistake too. It forced a layout pass for as long as the page was moving, which is exactly when a phone needs that time for scrolling.

Deciding which scene owns the screen

As you scroll, the site works out how much of each section’s space is on screen. A new scene takes over once its share passes 30% and is at least 12 points ahead of the current one, so the field never flickers at a boundary.

That rule had a blind spot. The scene before Say Hello is usually a full-screen ambient field that always counts as fully visible, so Say Hello only won once that section had nearly left the screen, and the contact shape formed late. I added a weight, data-pull, set to 1.8 on Say Hello. A weighted scene takes over once its weighted share is full, at about 55% of its space on screen, and the same weight applies on the way out, so it can’t flip-flop. On a 1440 by 900 desktop, the homepage used to hand over when the top of the contact section was 48% of the way down the screen. Now it happens at 83%, as the section comes into view. On a phone, case studies went from 35% to 80%.

Fast scrolling gets its own rule. Above 4,500 pixels per second, the field stops trying to build a shape for every section it passes. The shape it’s holding disperses into a side field in its own colours, and nothing new forms until the page has slowed below 1,800 pixels per second for 140 milliseconds. At ordinary speeds, flights finish sooner the faster you scroll, up to three times quicker, so a shape is formed while its section is still in view.

Clicking Contact or Back to top is a different case. A smooth in-page scroll starts slowly, and the slow-scroll rule used to read those first 150 milliseconds as the reader stopping, so the field started forming every section the page then raced past. A jump now holds the field until the scroll has actually ended: one dispersal on the way down, then only the destination forms. Any wheel, touch or key input hands control straight back to you.

None of this geometry is built at the moment it’s needed. Every scene, and the side field each one disperses into, is generated in idle time ahead of use.

Page transitions printed in halftone

Every internal link on this site is a full page load. The transition makes it feel like one continuous piece anyway.

When you click a link, an ink layer erases the page in an ordered-dither wave that starts where you clicked, while the particles fly into a full-screen halftone in the next page’s colours. The halftone is an 8 by 8 Bayer threshold over a soft radial tone, the way a print screen renders an image. Then the browser navigates.

The next page has to show that same halftone on its very first frame, before any of its scripts have loaded. A small inline script in the document head reads a flag the previous page left behind and paints the same cells in the same colours. Once the title has been parsed, the halftone dissolves outward from it, and the particle field joins in behind as soon as it boots. Opening a work card speaks the same language: the card’s picture is rebuilt from the field’s own particles right where it sits, then streams into the halftone.

The weak point is that the new page boots on the same thread that draws the dissolve, and one long task would freeze it halfway. Where the browser can render WebGL off the main thread, a worker takes the animation over on a second, identical canvas and carries on from the same point. Without a worker, the main thread keeps drawing. Without WebGL, the cover is a plain 2D fade. Safety timers clear the cover within a few seconds whatever happens, and a navigation that never completes uncovers the page again.

I also took something out. The browser’s own cross-document fade held the last frame frozen for about a third of a second before the next page could paint. The halftone already covers that moment, so dither navigations skip it.

Motion is optional

The header has a Motion switch, with the same control in the menu on phones. Turning it off parks the field: the homepage keeps my name, and other pages let the particles rest at the edges of the screen. The site remembers your choice for next time. WCAG 2.2.2 asks for a way to pause moving content, and some people just prefer a still page.

If your system asks for reduced motion, the site listens without being asked twice. There’s no homepage greeting, shapes are placed instead of flown in, the shimmer and cursor lens are off, the pencil holds still beside a finished line, and page transitions become ordinary navigations. The content is identical either way, and nothing ever waits on an animation.

The interface follows the same rule: the art direction lives in the background, and the controls stay put. I removed magnetic buttons, card lifts and a label-scrambling hover, because each one moved a control under the pointer. The main call to action used to shift 17 pixels on hover. Now it doesn’t move at all.

When the GPU isn’t up to it

The most useful lesson was about devices without a usable GPU. Lab tools like PageSpeed Insights often run WebGL in software, and the live site scored badly there: Total Blocking Time between 1.4 and 26 seconds, with single tasks of half a second or more. Real devices with GPUs do far better, but anyone whose browser falls back to software rendering would have seen the same freezes.

WebGL has an option for exactly this. Creating a context with failIfMajorPerformanceCaveat asks the browser to say no if it would run much slower than native, which usually means rendering in software. The particle field, the transition ink, the arrival cover and its worker, and the About portrait all ask for it now. A refused context falls back to the path each one already had: a designed static page with a dotted texture where the field would be, plain navigation, a 2D cover fade, and the still portrait.

I measured the same build locally with the option on and off, watching long tasks for four seconds after load plus ten wheel scrolls:

  • Homepage, software WebGL: Total Blocking Time fell from 514 ms to 31 ms, and the worst single task from 390 ms to 81 ms.
  • Services: 522 ms to 17 ms. About: 565 ms to 14 ms. Work: 211 ms to 0 ms.
  • With a hardware GPU the field still runs, and the numbers didn’t change beyond noise.

To be clear about the limits: these are local lab measurements in Chrome. I haven’t re-run PageSpeed Insights on the deployed build yet, Safari and Firefox make their own calls about software rendering and I didn’t measure them, and there’s no field data from real visitors yet.

How it’s checked

The production build won’t finish unless the writing, image, particle, navigation and case-study tests and a typecheck all pass. The particle and navigation suites alone run more than 60 tests. Release QA drives the site in Chromium, WebKit and Firefox at widths from 320 to 2,560 pixels, through fast scrolling, transitions, no WebGL, reduced motion, slow networks and a long session. The latest long session in Chromium held memory flat with no frames over 33 milliseconds.

What it hasn’t had yet is proper time on a physical iPhone in Safari, which no automated browser fully stands in for. That’s next on my list.

Why this matters for your project

Very few sites need 80,000 particles. Most need what sits underneath them: a reason for every effect, a cost I’ve actually measured, a fallback that was designed rather than left to chance, and honest notes on what’s been tested and what hasn’t. That’s the standard I hold every project to, whether it has particles or not.

If you’re hiring for work like this, or want to talk about a site of your own, email me at alex@jardinestudio.com, or have a look through the case studies.

Available now for projects, agency work and contract roles

Have a project or role in mind?

alex@jardinestudio.com

Tell me about the project or the role.

What’s this about?

Toronto, CanadaRemote across Canada and the USIn person across the GTA