← Generative art

Game of Life

Conway’s Game of Life, drawn live: cells are born, live and die across a grid by three simple rules. Newborns swell in and the dead fade away, and when the board settles it seeds itself again. Drag across it to draw your own cells.

Drag to draw cells

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-game-of-life to a box with a height and a dark background.
  3. Anything inside the box sits on top of the cells.

Options

Add any of these attributes to the same element.

data-cellCell size in px. Default 10.
data-speedGenerations per second. Default 10.
data-densityShare of cells alive at the start, 0 to 1. Default 0.3.
data-colorCell colour. Default #a855f7.
data-freshColour of newborn cells. Default #f0abfc.
data-reseed="false"Let the board settle and stay settled, instead of seeding it again.
data-draw="false"Ignore the pointer.

A cell with exactly three live neighbours is born; one with two or three survives; every other cell dies. The edges wrap round. It’s paused when off screen, and with reduced motion it shows a still frame.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Game of Life: Conway's cellular automaton drawn live on a canvas. Each
    // generation, a cell with three live neighbours is born, one with two or
    // three survives, and every other cell dies. Newborns swell in, the dead
    // fade out, and when the board settles down it seeds itself again.
    // Press or drag on it to draw live cells. Paused when off screen.
    // Usage: <div data-game-of-life style="height:360px;background:#120a24"></div>
    // Optional: data-cell (cell size in px, default 10), data-speed
    // (generations per second, default 10), data-density (share of cells alive
    // at the start, default 0.3), data-color (default #a855f7), data-fresh
    // (newborn colour, default #f0abfc), data-reseed="false" (let it settle and
    // stay settled), data-draw="false" (ignore the pointer).
    var DEFAULTS = { cell: 10, speed: 10, density: 0.3, color: '#a855f7', fresh: '#f0abfc' };

    var css =
      '[data-game-of-life]{position:relative;isolation:isolate;overflow:hidden}' +
      '.gol-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'gol-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var size = +d.cell || DEFAULTS.cell;
      var interval = 1000 / (+d.speed || DEFAULTS.speed);
      var density = d.density !== undefined ? +d.density : DEFAULTS.density;
      var color = d.color || DEFAULTS.color;
      var fresh = d.fresh || DEFAULTS.fresh;
      var reseed = d.reseed !== 'false';
      var w = 0, h = 0, dpr = 1, cols = 0, rows = 0, ox = 0, oy = 0;
      var cur, nxt, age, ghost, history = [], acc = 0;

      function seed() {
        for (var i = 0; i < cur.length; i++) {
          var alive = Math.random() < density ? 1 : 0;
          if (cur[i] && !alive) ghost[i] = 1;
          cur[i] = alive;
          age[i] = alive;
        }
        history = [];
      }

      function resize() {
        var nw = el.clientWidth, nh = el.clientHeight;
        if (nw === w && nh === h) return;
        w = nw; h = nh;
        dpr = Math.min(2, window.devicePixelRatio || 1);
        canvas.width = Math.round(w * dpr);
        canvas.height = Math.round(h * dpr);
        cols = Math.max(4, Math.ceil(w / size));
        rows = Math.max(4, Math.ceil(h / size));
        ox = (w - cols * size) / 2;
        oy = (h - rows * size) / 2;
        cur = new Uint8Array(cols * rows);
        nxt = new Uint8Array(cols * rows);
        age = new Uint16Array(cols * rows);
        ghost = new Float32Array(cols * rows);
        seed();
      }

      // One generation, on a board whose edges wrap round.
      function generation() {
        var pop = 0;
        for (var y = 0; y < rows; y++) {
          var up = ((y + rows - 1) % rows) * cols, mid = y * cols, dn = ((y + 1) % rows) * cols;
          for (var x = 0; x < cols; x++) {
            var l = (x + cols - 1) % cols, r = (x + 1) % cols;
            var n = cur[up + l] + cur[up + x] + cur[up + r] + cur[mid + l] + cur[mid + r] + cur[dn + l] + cur[dn + x] + cur[dn + r];
            var i = mid + x, alive = cur[i];
            var next = n === 3 || (alive && n === 2) ? 1 : 0;
            nxt[i] = next;
            if (next) { age[i] = alive ? Math.min(age[i] + 1, 999) : 1; pop++; }
            else if (alive) { age[i] = 0; ghost[i] = 1; }
          }
        }
        var t = cur; cur = nxt; nxt = t;
        // Settled? A still life or a blinker repeats the same few counts.
        history.push(pop);
        if (history.length > 40) history.shift();
        if (reseed && history.length === 40) {
          var seen = {};
          var distinct = 0;
          history.forEach(function (p) { if (!seen[p]) { seen[p] = 1; distinct++; } });
          if (distinct <= 3 || pop === 0) seed();
        }
      }

      function draw(phase) {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        var gap = size > 5 ? 1.5 : 0.5, s = size - gap;
        var grow = 1 - Math.pow(1 - Math.min(1, phase * 1.6), 3);
        // Ghosts of cells that just died: shrinking, fading.
        ctx.fillStyle = color;
        for (var i = 0; i < cur.length; i++) {
          if (cur[i] || ghost[i] <= 0) continue;
          var g = ghost[i], gs = s * (0.35 + 0.65 * g);
          ctx.globalAlpha = g * 0.55;
          ctx.fillRect(ox + (i % cols) * size + (size - gs) / 2, oy + ((i / cols) | 0) * size + (size - gs) / 2, gs, gs);
        }
        ctx.globalAlpha = 1;
        // Older cells in the main colour.
        for (i = 0; i < cur.length; i++) {
          if (!cur[i] || age[i] < 2) continue;
          ctx.globalAlpha = age[i] > 12 ? 0.8 : 1;
          ctx.fillRect(ox + (i % cols) * size + gap / 2, oy + ((i / cols) | 0) * size + gap / 2, s, s);
        }
        // Newborns, swelling in.
        ctx.globalAlpha = 1;
        ctx.fillStyle = fresh;
        var ns = s * (0.3 + 0.7 * grow);
        for (i = 0; i < cur.length; i++) {
          if (!cur[i] || age[i] !== 1) continue;
          ctx.fillRect(ox + (i % cols) * size + (size - ns) / 2, oy + ((i / cols) | 0) * size + (size - ns) / 2, ns, ns);
        }
      }

      function advance(dt) {
        acc += dt;
        for (var i = 0; i < ghost.length; i++) if (ghost[i] > 0) ghost[i] -= dt / (interval * 2.5);
        if (acc >= interval) {
          acc %= interval;
          generation();
        }
      }

      // Press or drag to bring cells to life.
      function paint(e) {
        var r = el.getBoundingClientRect();
        var cx = Math.floor((e.clientX - r.left - ox) / size), cy = Math.floor((e.clientY - r.top - oy) / size);
        for (var y = -1; y <= 1; y++) {
          for (var x = -1; x <= 1; x++) {
            if (Math.abs(x) + Math.abs(y) === 2 && Math.random() < 0.5) continue;
            var gx = (cx + x + cols) % cols, gy = (cy + y + rows) % rows, i = gy * cols + gx;
            if (!cur[i]) { cur[i] = 1; age[i] = 1; }
          }
        }
        history = [];
      }

      resize();
      if (reduced()) {
        // A still frame, some way into the game.
        for (var k = 0; k < 60; k++) generation();
        ghost.fill(0);
        draw(1);
        if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(1); }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      if (d.draw !== 'false') {
        var down = false;
        el.addEventListener('pointerdown', function (e) { down = true; paint(e); });
        el.addEventListener('pointermove', function (e) { if (down) paint(e); });
        ['pointerup', 'pointerleave', 'pointercancel'].forEach(function (t) {
          el.addEventListener(t, function () { down = false; });
        });
      }

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          advance(dt);
          draw(acc / interval);
        }
      });
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-game-of-life]').forEach(function (el) {
        if (el._gol) return;
        el._gol = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-game-of-life style="height: 360px; background: #120a24"></div>