If you clicked the Paperlab card on my home page from a computer, you set it on fire before this page opened. That wasn't a video. The card is a sheet of Paperlab paper laid over the HTML card, and the fire is the library's own, lit where your click landed and held there like a match. It runs four times faster than I tuned it, with every rate scaled together, so it looks the same, only sooner. This page opens 3.2 seconds after the click.
This is how that fire is made.
I kept one gesture, and it was fire
Paperlab is an open-source React library that renders paper as real 3D geometry, so what you put on a sheet folds, rolls and peels with it. When I added hand tracking, I built twelve gestures: grab, fold, crush, tear, throw, and seven more. It worked, and it was a demo reel.
So I deleted them, and kept one thing to do to the paper: burn it. It's the most physical thing that can happen to a sheet of paper, and there's no going back from it.
Every frame of fire is five layers
Here is one frame of a burn with its layers pulled apart. Each one is a different system, and they only meet at the end.
At the bottom is a grid that knows what happened to the paper. The sheet draws that grid as a burned edge. The flames are a fluid simulation standing on that edge. Embers and smoke leave the burning edge at a rate set by how long it is, so a fire gets busier as it grows, and ash only leaves from the cells that burned through that frame. The light sits on top of everything.

The paper burns on a grid
Under the sheet is a 64×64 grid with four numbers per cell: char, water, heat, and how much paper is still there. Heat spreads from cell to cell. Paper that gets hot enough turns to char, and char eats the paper away.
Paper has a grain, so the grid has one too. Heat spreads three times faster along the fiber than across it, and that gives the fire its shape. Lit in the middle, the hole opens as a wide oval instead of a circle. Lit in a corner, it runs along the bottom edge faster than it climbs. I never drew either shape.

The grid runs on the CPU, because most of what reads it runs there. The cloth physics needs to know which paper is gone, so a piece the fire cuts loose actually falls. The ash leaves from the cells that burned through, and the sound follows the length of the burning edge. A grid on the GPU would have meant reading pixels back every frame, and phones pay the most for that. The sheet's shader gets an 8-bit texture of the grid instead.
A burned edge has measurements
Up close, a real burned edge has four bands, and you can measure each one. So the sheet draws them in millimeters of A4, from the cut outward: a pale ash lip of 3.5 mm, a beaded ember line of 1.25 mm, a black char band of 3.5 mm, and a scorch that reaches 9 mm into clean paper. The ember line is the only part of the paper that gives off light.

The char was the part I got wrong. I tuned it against a reference image, and it came out a warm brown that stayed orange even on frames with no fire in them. The char in that image was lit by its own flames, and I had painted that light into the paper. Real char is close to black, about 11% as bright as the paper around it. Now the warmth on it comes from the fire's light, and it goes when the fire does.
My first flames were hot smoke
The flames are a real fluid simulation: a small 2D grid on the GPU, the kind real-time fire has been built on since Jos Stam's Stable Fluids. Every step, it releases gas along the burning edge, carries everything along the flow, burns what it can, and adds the forces of rising heat, wind and turbulence.

My first version of it made a fire that looked, in my own words at the time, bad. It took me a while to see why. Every cell of the grid was topped back up with oxygen, inside the flame as much as outside it. So fuel burned the moment it left the paper, wherever it was. What I had built was hot smoke, colored by its temperature.
In a real flame, fuel can only burn where it touches air. The gas leaving the paper is all fuel in the middle, and air works its way in from the sides, so the middle gets eaten from the outside until it closes to a point. That point is the tip, and the flame is the surface around it.

So the rebuilt solver burns fuel only where air has reached it. It keeps track of soot on its own, takes the flame's shape from the soot, and takes its color from the heat of burning.
A flame has four parts
That gave the flame four zones, from its base to its tip, and each one is a setting you can tune:
- Root. The blue base. Almost off by default, because blue over cream paper reads as lavender.
- Core. Pale, and the brightest part of the flame.
- Body. Yellow to orange, and not as bright as white paper.
- Tip. Dimmer and redder, where the tongue tears apart.

The most useful thing I learned is that cooling sets the height. Soot only glows while the gas is hot, so how fast the gas cools decides how tall a flame gets. I swept it in live play: at a cooling of 1.15 the tongues reached the text, about 150 mm up the sheet. At 2.1 they were short licks of about 60 mm. The default is 1.8.
And one rule I hold the flames to: a fire around a hole is a living, uneven ring, never a crown. The code that places flames along the rim checks how even their heights came out, and if a frame is too regular, it scatters them again, wider.
The flames also stand in their own plane, upright on the burning edge and always turned to face the camera. However the sheet is tilted, they rise with the world, not with the paper.
The fire is the light in the room
The burn lights the sheet it's on, and the room dims around it. On my home page, that's why the other cards went darker while the Paperlab card burned: at the fire's height, the room keeps 45% of its light.


Getting the glow right took a unit. Brightness used to be written four different ways in four files, with nothing converting between them. The flames, the biggest light in the frame, peaked under half of the bloom threshold, so they never glowed, and turning bloom on at the peak of a burn changed 648 pixels out of 960,000. Now everything is set in multiples of paper white, the brightest any lighting preset makes the paper, and the bloom threshold sits at 2.25 of them. The flames bloom, and the paper never does.
Two bugs that looked fine on my laptop
- The fire burned at the speed of your computer. The grid took a fixed number of steps per frame, so a slow frame burned less. Measured after the same simulated time, the hole's radius was 0.20 of the sheet at 30 fps, 0.29 at 60 and 0.44 at 144. Now the grid keeps its own clock, 120 steps a second, and holds it at any frame rate down to 15 fps.
- Every GPU drew a different fire. The flame noise used the classic
fract(sin(x) * 43758.5)hash, and every GPU works out thatsina little differently. The same fire differed in 13% of its pixels between my Mac and a software renderer, and bloomed 1.47% of the frame on one and 0.02% on the other. The hash has nosinin it now.
What's still open
The fire doesn't read prefers-reduced-motion. The sheet does, but anyone who wires a fire in decides what a reduced-motion burn looks like. On my home page, it means no burn: the card opens this page straight away.
Lights Left On, the letter product Paperlab came out of, still doesn't have a letter you can burn.
You can burn a sheet yourself with your hands, through a webcam, or open every setting behind the fire in the FX Lab. The code is on GitHub.
