How this portfolio draws itself
, 2 min read, Three.js, WebGL, Creative coding, Performance
Twelve thousand particles, one shader and a webcam. A look under the hood of the 3D story on my home page, and the performance tricks that keep it smooth on a phone.
I used to draw things before I built them, so I wanted my portfolio to be a drawing that becomes a machine. On the home page, about 12,000 chalk-coloured particles form a robot arm, then a pencil, an arc reactor, a JARVIS globe, a website, a stock chart, a Minecraft-style cube and finally my name, as you scroll. Here's how.
Every drawing is the same number of points
The trick that makes morphing easy: every shape is exactly N points. If the arm and the globe both have 12,000 points, morphing is just moving point i from one position to another.
Solid shapes are built from ordinary three.js geometry (cylinders, spheres, boxes stretched between two points), merged, and sampled with MeshSurfaceSampler, which picks random points on the surface weighted by area, so big parts get more particles. Text like "DEV" or "HELLO" is drawn on a hidden 2D canvas and the filled pixels are sampled. Everything uses a seeded random generator, so every visitor sees the same drawing.
The morph happens on the GPU
Each particle has two positions in the vertex buffer, aFrom and aTo, plus a random direction. A single uMix value from 0 to 1 blends between them in the vertex shader:
float m = uMix * uMix * (3.0 - 2.0 * uMix); // smoothstep easing
vec3 p = mix(aFrom, aTo, m);
p += aRand.xyz * sin(uMix * 3.14159) * 0.9; // lift off mid-morph, like blown chalk dust
The sin term is zero at both ends and peaks halfway, so particles scatter while they travel and settle exactly into the new shape. The whole scene is one draw call: the CPU only updates one number per frame.
If you scroll fast and a new shape arrives mid-morph, I snapshot where the particles currently are into aFrom before starting the next morph. Without that, they'd snap back to the old drawing first.
Your hand is a controller
Press "Control it with your hand" and the page asks for your webcam. MediaPipe's hand landmarker runs in the browser and gives 21 points per hand, about 30 times a second. I use three of them:
- Index fingertip becomes the pointer. Particles near it are pushed away in the shader.
- Thumb-to-index distance, divided by the size of your hand, detects a pinch. Pinching grabs the drawing; moving rotates it.
- Wrist speed: an open hand moving fast sideways switches to the next drawing.
The model and runtime are hosted on this site and no video leaves your device.
Keeping it smooth
- Phones get about half the particles and a lower pixel ratio.
- If frames take longer than about 34 ms for a couple of seconds, the page halves the particle count and drops to 1× resolution automatically.
- The canvas stops rendering once you scroll past the story, or switch tabs.
- With "reduce motion" turned on, morphs are instant and nothing drifts.
- The loader's progress is real: it waits for the fonts and the first rendered frame, not a fake timer.
The source for all of this lives in ParticleStage.tsx and shapes.ts. If you want to build something similar, the Three.js course is on my list, and you can vote for it.