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.

The day I open-sourced it: the burn, the hand tracking and the editor, all of it.

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.

One frame of a real burn, taken apart. Bottom to top: the damage grid, the sheet drawing it, the flames, what the burn throws off, and the light.

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.

A strip of seven frames from one burn, each with its time: contact at 0.35 seconds, scorch, catch, peak at 9.25 seconds, dying, smolder, and cold at 15.4 seconds

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.

Two sheets with the front of the burn traced every two seconds. Lit in the middle, the rings grow as a widening oval. Lit in the corner, they run along the bottom edge faster than they climb.
The front of the burn, traced every two seconds from the real simulation.
Lit in a corner and left alone. It runs along the bottom first, with the grain, and only then climbs.

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.

A labeled cross-section of a burned edge, measured from the cut: the hole, a pale ash lip of 3.5 mm, a glowing ember line of 1.25 mm, near-black char of 3.5 mm, a brown scorch of 9 mm, then untouched paper

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.

A diagram of one step of the flame solver as a loop of seven stages: release gas, move it along, burn, add forces, add back small swirls, balance the pressure, and draw, 120 times a second

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.

Four sections of one flame on shared axes: fuel, oxygen, soot and the heat of burning. The fuel core narrows to a point where the oxygen closes in, and that point is the tip.

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.
A real tongue of flame from the renderer with its four parts labeled: tip, body, core and root

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 paper tilts, and the flames stay upright.

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.

The same sheet burning at its height, lit warm by its own flamesThe same sheet after the fire, cold, back in the room's plain light
The same sheet at the fire's height, and cold. While it burns, the fire is the light on the paper.

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 that sin a 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 no sin in 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.