A 1990s trick, the one line it was missing, and a language model reading the fire's name.
loading a 23 MB sentence model into your browser…
That is the whole 1990s effect, the one that fit in under 256 bytes of assembler. Random embers on the bottom row, and each frame every cell looks one row down and a random step sideways, averages with its neighbours, and loses a little heat. Colour is a lookup from black through red and yellow to white.
Written as a limit, "look one row down" is an advection term v ∂T/∂y, "a random step, then average" is Fourier's diffusion D ∂²T/∂x², and "lose a little" is decay −kT. The demo coders were solving the heat equation from 1822 by finite differences at sixty frames a second on a 486, and calling it a trick.
What it does not have is a reason to go up. The updraft v is a constant the coder typed in. In a real fire the air rises because it is hot, and that one fact, hot air is light, is where all the flicker comes from. So the fire at the top of this page adds it: a velocity field pushed upward by temperature, kept incompressible, carrying the heat with it. Navier–Stokes with the Boussinesq approximation, solved the way Jos Stam did it in 1999.
Left, a campfire filmed on 7 September 2026. Right, the equations, with four constants. Drop your own clip on the left pane. The brightness profile is what you fit by hand. The flicker spectrum is what buoyancy gives you for free.
Each row's mean brightness, normalised to the brightest row. Both models can fit this: cooling sets the height, embers the base.
Power of whole-frame brightness over the last ~10 s. A real fire puffs at about 1.5/√d Hz for a fire d metres across. The equations puff too, because hot air is light. The trick is flat noise whatever you set.
The sentence at the top goes through a small sentence encoder, all-MiniLM-L6-v2, 22 million parameters, running in this tab. It turns the sentence into a point in a 384-dimensional space. Nothing is generated. The model never answers.
At load, thirty-two sentences are embedded: sixteen about a roaring fire, sixteen about dying embers. The difference between the two mean points is a direction. That is the same object people add to a model's activations to steer it, a steering vector by difference of means. Here it is not added. It is read: your sentence is projected onto it, and the projection is the position of the dot between "embers" and "inferno". That number sets buoyancy, cooling and ember density.
Then each word is removed in turn and the sentence embedded again. How far the projection falls when a word is missing is that word's share of the fire, and that share is its heat on the bottom row. The flames rise from the words that carry them. This is occlusion, the oldest attribution method there is, and it is shown here instead of an attention map because the exported model does not expose its attention, and because a heat that you can remove and watch go out is easier to believe than a heat you are told about.
Each word's share of the fire
The originals were under 256 bytes of x86 assembler, and the trick was often code that wrote the inner loop for itself. This is the same loop in JavaScript. The version with physics is fluid.js, about ninety lines, and it is not a trick.