Home

19. Demoscene · 1993

Palette fire

Colors and knobs

These rewrite the live file. Copy HTML picks up whatever you set.

About

The 90s fire: a low-res buffer, each pixel is the average of the three below it minus a cooling term, then a palette from black through the tweak color into yellow and white. The bottom row is random sparks. Move the pointer to poke the fire.

Move the pointer over the preview to feed the fire.

Browser APIs

  • Canvas 2D
  • ImageData
  • Pointer Events
  • requestAnimationFrame
  • matchMedia

If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. prefers-reduced-motion freezes the first still.

Source

(function () {
  const PARAMS = {
    color: "#ff6622",
    heat: 1,
    wind: 0
  };
  function hexToRgb(hex) {
    const n = parseInt(String(hex).replace("#", ""), 16);
    if (isNaN(n)) return [255, 102, 34];
    return [n >> 16 & 255, n >> 8 & 255, n & 255];
  }

  const canvas = document.getElementById("c");
  const fallback = document.getElementById("fallback");
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    fallback.hidden = false;
    fallback.textContent = "Canvas 2D is not available in this browser.";
    return;
  }

  const W = 160, H = 100;
  const buf = document.createElement("canvas");
  buf.width = W; buf.height = H;
  const bctx = buf.getContext("2d");
  const img = bctx.createImageData(W, H);
  const px = img.data;
  const heat = new Uint8Array(W * (H + 2));
  const pal = new Uint8Array(256 * 3);
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  function buildPal() {
    const rgb = hexToRgb(PARAMS.color);
    for (let i = 0; i < 256; i++) {
      const t = i / 255;
      let r, g, b;
      if (t < 0.18) {
        const k = t / 0.18;
        r = 8 * k; g = 4 * k; b = 2 * k;
      } else if (t < 0.45) {
        const k = (t - 0.18) / 0.27;
        r = 8 + (rgb[0] - 8) * k;
        g = 4 + (rgb[1] * 0.25 - 4) * k;
        b = 2 + (rgb[2] * 0.12) * k;
      } else if (t < 0.75) {
        const k = (t - 0.45) / 0.3;
        r = rgb[0] + (255 - rgb[0]) * k;
        g = rgb[1] * 0.25 + (220 - rgb[1] * 0.25) * k;
        b = rgb[2] * 0.12 + 40 * k;
      } else {
        const k = (t - 0.75) / 0.25;
        r = 255; g = 220 + 35 * k; b = 40 + 200 * k;
      }
      pal[i * 3] = r; pal[i * 3 + 1] = g; pal[i * 3 + 2] = b;
    }
  }
  buildPal();

  let pokeX = -1, pokeY = -1;
  canvas.addEventListener("pointermove", function (ev) {
    const r = canvas.getBoundingClientRect();
    pokeX = ((ev.clientX - r.left) / r.width) * W;
    pokeY = ((ev.clientY - r.top) / r.height) * H;
  });
  canvas.addEventListener("pointerleave", function () { pokeX = -1; });

  function step() {
    buildPal();
    const cool = Math.max(1, 3.2 / PARAMS.heat);
    const wind = PARAMS.wind | 0;
    for (let y = 0; y < H; y++) {
      for (let x = 0; x < W; x++) {
        const x0 = (x - 1 + wind + W) % W;
        const x1 = x;
        const x2 = (x + 1 + wind + W) % W;
        const a = heat[(y + 1) * W + x0];
        const b = heat[(y + 1) * W + x1];
        const c = heat[(y + 1) * W + x2];
        const d = heat[(y + 2) * W + x1];
        let v = (a + b + c + d) * 0.25 - cool;
        if (v < 0) v = 0;
        heat[y * W + x] = v;
      }
    }
    for (let x = 0; x < W; x++) {
      const spark = Math.random() < 0.46 * PARAMS.heat ? 180 + Math.random() * 75 : Math.random() * 50;
      heat[H * W + x] = spark;
      heat[(H + 1) * W + x] = spark;
    }
    if (pokeX >= 0) {
      const cx = pokeX | 0, cy = Math.max(0, pokeY | 0);
      for (let dy = -3; dy <= 3; dy++) {
        for (let dx = -3; dx <= 3; dx++) {
          const xx = (cx + dx + W) % W;
          const yy = cy + dy;
          if (yy >= 0 && yy < H + 2) heat[yy * W + xx] = 255;
        }
      }
    }
    for (let i = 0; i < W * H; i++) {
      const h = heat[i];
      px[i * 4] = pal[h * 3];
      px[i * 4 + 1] = pal[h * 3 + 1];
      px[i * 4 + 2] = pal[h * 3 + 2];
      px[i * 4 + 3] = 255;
    }
    bctx.putImageData(img, 0, 0);
    ctx.imageSmoothingEnabled = false;
    ctx.drawImage(buf, 0, 0, canvas.width, canvas.height);
  }

  function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
    if (canvas.width !== w || canvas.height !== h) {
      canvas.width = w;
      canvas.height = h;
    }
  }

  function frame() {
    resize();
    step();
    if (!reduced) requestAnimationFrame(frame);
  }

  if (reduced) {
    resize();
    for (let i = 0; i < 50; i++) step();
  } else requestAnimationFrame(frame);
})();