Skip to content

ASCII Fire ​

On Fire 🔥: a classic fire effect on the terminal renderer. Each cell takes the heat of a cell below it minus a random decay, the bottom row is fed by noise, and the heat colors the cell's background with cm.mapAttrs. Made for Genuary 2024, "ASCII".

js
const cols = 80;
const rows = 30;
const fire = Array.from({length: rows}, () => new Array(cols).fill(0));
const renderer = cm.terminal({cols, rows});
const root = cm.flow().renderer(renderer);
const randomChar = () => String.fromCharCode(33 + Math.floor(Math.random() * 94));

function update() {
  const noise = cm.noise(0, rows);
  for (let y = 0; y < rows - 1; y++) {
    for (let x = 0; x < cols; x++) {
      const decay = Math.floor(Math.random() * 3);
      const spread = Math.floor(Math.random() * 2) - 1;
      const index = Math.min(Math.max(0, x - spread), cols - 1);
      fire[y][x] = Math.max(0, fire[y + 1][index] - decay);
    }
  }
  for (let x = 0; x < cols; x++) fire[rows - 1][x] = noise(x / 10) | 0;
}

function draw() {
  root.append(cm.clear, {fill: "black"});
  root
    .data(fire)
    .append(cm.group, {x: 0, y: (_, i) => i})
    .data((row) => row)
    .append(cm.point, {
      x: (_, i) => i,
      y: 0,
      stroke0: (heat) => (heat === 0 ? " " : randomChar()),
      stroke2: (heat) => (heat === 0 ? null : heat),
    })
    .transform(cm.mapAttrs, {stroke2: {domain: [0, rows], interpolate: d3.interpolateCool}});

  root.render();
}

setInterval(() => {
  update();
  draw();
}, 1000 / 15);

document.body.append(renderer.node());