Building this site: glitch effects, a highlight reel and a hidden game
- 4 min read
- #astro
- #webgl
- #performance
I wanted this site to feel less like a résumé and more like a club flyer: loud colour, glitchy type, something that reacts when you move. The catch is that flashy sites are usually slow ones. So the rule was simple: every effect is an extra layer on top of a plain, fast page, and nothing on the home page is allowed to cost more than it’s worth.
Here’s how the pieces work.
The background: a grid of tiny offsets
The background is a single still image. Over it sits a WebGL canvas that redraws the same image through an invisible 240 × 240 grid. Each cell stores a small 2D offset, and the shader samples the image a little to the side of where it should:
vec2 uv = (vUv - 0.5) * uCover + 0.5; // object-fit: cover
vec2 offset = texture(uData, vUv).rg; // this cell's offset
outColor = vec4(texture(uTexture, uv - 0.02 * offset).rgb, 1.0);
Three rules give it its character:
- It loads scrambled. Every cell starts with a random offset, so the picture arrives as noise.
- Offsets decay. Each frame, every offset is multiplied by 0.7, so the image snaps back into focus within about half a second.
- Your cursor pushes cells. The pointer’s velocity is added to nearby cells, falling off with distance. The radius covers the whole screen, so areas far from the cursor drift slightly while the area under it smears the most.
Two details keep it cheap. Offsets live in a tiny float texture, so the GPU does all the per-pixel work. And the animation loop goes to sleep once everything has settled; moving the mouse wakes it up again. A page that’s sitting still costs nothing.
The <img> underneath is the real content. If WebGL isn’t available, or you’ve asked your OS for reduced motion, the canvas never starts and you just see the picture.
The name: always the opposite colour
My name has no background panel. It’s white text with mix-blend-mode: difference, which subtracts it from whatever’s behind it. Over pink it turns green, over dark purple it turns pale, and as the background smears under your cursor the letters change colour with it.
One gotcha: blending only works against things in the same stacking context. Any parent with a transform, opacity or z-index quietly breaks it, and the text goes flat white. Most of the work was keeping that path clear.
Decoding text
Labels and nav items “decode” in: each letter cycles through random characters before landing on the real one. The rotating line under my name (“Shipping apps to billions”, “DJing NYC’s biggest clubs”, …) uses the same effect every three seconds. It’s one small script, and the real words are in the HTML from the start, so search engines never see the noise. With reduced motion turned on, it doesn’t run at all.
The highlight reel
A couple of seconds after the home page loads, a Play button fades in. Click it and the background fades to black, then my highlight reel plays as the background, through the same distortion effect, with the name and links still on top.
That works because the shader doesn’t care where its texture comes from. When the reel starts, the video element becomes the texture source, and requestVideoFrameCallback uploads a new frame only when the decoder actually has one. When the video ends, the texture switches back to the image, everything re-scrambles, and the button returns.
A few things make it behave:
- The video downloads only when you press Play, so the home page pays nothing for it.
- Playback starts inside the click handler. Browsers only allow sound after a user gesture, so it has to.
- It waits for the fade. If the video is ready before the screen is fully black, it holds on its first frame so the opening isn’t lost behind the fade.
A hidden game
The crosshair in the top-right corner opens Debug Mode, a small first-person shooter: glitching bugs fly at you in waves, and you squash them before they crash the build.
It’s built with three.js, which is the one heavy thing on the site (about 145 KB compressed). It loads only on that page, so nobody downloads it unless they click the crosshair. Shots are instant rays from the centre of the screen, the bugs are simple shapes with glowing edges, and every sound effect is synthesised on the fly with the Web Audio API, so there are no audio files to download. It works with a mouse and keyboard, and there are touch controls for phones.
What it all costs
The site is built with Astro, which renders every page to static HTML at build time. Pages ship JavaScript only for the parts that are actually interactive. On a production build:
- Home page JavaScript: under 4 KB compressed, covering the WebGL effect, the text decoding and the play button.
- Lighthouse: 100 in every category on desktop; 99 for performance and 100 for the rest on mobile.
- Largest contentful paint: about 0.8 s on desktop and 1.8 s on Lighthouse’s throttled mobile profile.
The effects are layered on top of a fast page rather than holding it up. That’s the main idea, and it’s why the site can afford to be loud.
