Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Doom Fire 🔥

Doom Fire preview

Note

A JavaScript implementation of the fire effect from the DOOM (1993) title screen, originally reverse-engineered and documented by Fabien Sanglard.

How it works ⚙️

The fire is simulated on a flat pixel grid. Every cell holds an intensity value from 0 (cold, near-black) to 36 (white-hot). On each tick, every pixel reads the intensity of the pixel directly below it, reduces it by a small random amount, and writes the result to a slightly shifted horizontal position. Over many iterations, this single rule produces the upward-spreading, flickering flame shape.

The bottom row is permanently set to maximum intensity (36) — it acts as the heat source. Everything above it is shaped by propagation alone.

Rendering 🖼️

Why Canvas instead of HTML table

The original approach rendered the fire as an HTML <table>, rebuilding the entire DOM — up to 2,400 <td> elements — on every frame. Each rebuild triggers HTML parsing, layout recalculation, and paint. At 20 frames per second that is 48,000 DOM node operations per second.

A <canvas> element sidesteps the DOM entirely. Pixels are drawn directly into a GPU-backed bitmap with no layout cost, no element creation, and no garbage collection pressure from discarded nodes.

Why createImageData instead of fillRect

Even on canvas, calling ctx.fillStyle = "rgb(...)" + ctx.fillRect(...) per pixel has meaningful overhead:

  • String parsing — every fillStyle assignment parses a CSS color string.
  • Per-pixel draw calls — each fillRect is a separate graphics API call with state validation and rasterization setup.

ImageData eliminates both. The color palette is pre-computed once as a flat Uint8ClampedArray of raw RGBA bytes. Each frame, the render loop writes bytes directly into a reused memory buffer and uploads the entire result to the canvas in one putImageData() call — no string parsing, no individual draw calls.

fillRect approach  →  2,400 fillStyle parses  +  2,400 draw calls  per frame
ImageData approach →  2,400 byte writes       +  1 putImageData    per frame

Animation loop 🖌️

Why requestAnimationFrame instead of setInterval

setInterval(fn, 50) fires on a fixed wall-clock timer regardless of what the browser is doing. This causes several problems:

  • Drift — timer callbacks can stack up if a frame takes longer than 50 ms, causing stuttering bursts.
  • No sync with display — updates can land mid-frame, producing tearing.
  • Wasted work — the interval keeps firing even when the tab is hidden.

requestAnimationFrame is driven by the browser's rendering pipeline. It passes a high-resolution timestamp to the callback, pauses automatically when the tab is backgrounded, and aligns updates with the display's refresh cycle.

Since the fire runs at ~20 fps (one update every 50 ms) but displays typically refresh at 60 Hz, the loop uses a timestamp threshold to skip frames without accumulating lag:

#loop(timestamp) {
  if (timestamp - this.#lastTime >= 50) {
    this.#lastTime = timestamp;
    this.#calculatePropagation();
  }
  requestAnimationFrame(this.#boundLoop);
}

Canvas dimensions 📏

The canvas size is determined by three values:

Variable Default Description
data-width 40 Fire grid width in cells
data-height 40 Fire grid height in cells
Fire.pixelSize 4 Canvas pixels per fire cell

The rendered canvas will be:

canvas width  = data-width  × pixelSize
canvas height = data-height × pixelSize

For example, a grid of 253 × 151 cells at pixelSize = 4 produces a 1012 × 604 px canvas. Increasing pixelSize gives a chunkier, more pixelated look; decreasing it gives finer detail at the cost of a smaller visible result.

Configure the grid size directly in the HTML:

<canvas id="fireCanvas" data-width="253" data-height="151"></canvas>

To change the size of each cell, update the static property in script.js:

static pixelSize = 4;

Running 🚀

Open index.html directly in a browser. No build step or server required.

About

A JavaScript implementation of the DOOM (1993) fire effect — rendered on Canvas using ImageData for pixel-level performance, animated with requestAnimationFrame.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages