← Generative art

Langton's Ant

An ant walks a grid, turning right on blank tiles and left on coloured ones, and flipping each tile as it leaves. For ten thousand steps it makes a mess. Then, out of nowhere, it builds a straight diagonal highway and marches off the edge.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-langtons-ant to a box with a height.

Options

Add any of these attributes to the same element.

data-loopStart again once the ant walks off the edge.
data-ruleOne letter per tile colour, R or L: which way to turn on it. Default RL, the classic ant. Try RLR, LLRR or LRRRRRLLR.
data-speedSteps per second at full speed. It starts slowly and speeds up. Default 1400.
data-cellTile size in px. Default fits about 100 tiles across the shorter side.
data-colorsComma-separated tile colours, one per colour after blank. Default purples and pinks.
data-antColour of the ant. Default #ec4899.

It’s paused when off screen. With reduced motion it shows the finished walk, highway and all.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Langton's Ant: an ant walks a grid of tiles. On a blank tile it turns
    // right, on a coloured one it turns left, and it flips every tile it
    // leaves. For about ten thousand steps it makes a mess, then it suddenly
    // builds a straight diagonal "highway" and marches off. It starts slowly,
    // so you can follow it, then speeds up. Paused when off screen.
    // Usage: <div data-langtons-ant style="height:360px"></div>
    // Optional: data-rule (one letter per tile colour, R or L; default "RL",
    // the classic ant; try "RLR", "LLRR" or "LRRRRRLLR"), data-speed (steps
    // per second at full speed, default 1400), data-cell (tile size in px,
    // default fits about 100 tiles across the shorter side), data-colors
    // (comma-separated tile colours, default purples and pinks), data-ant
    // (ant colour, default #ec4899), data-loop (start again once the ant
    // walks off the edge).
    var DEFAULTS = {
      rule: 'RL', speed: 1400, ant: '#ec4899',
      colors: ['#6a0dad', '#c084fc', '#f472b6', '#fbbf24', '#38bdf8', '#34d399', '#818cf8', '#fb7185']
    };
    var DX = [0, 1, 0, -1], DY = [-1, 0, 1, 0]; // up, right, down, left

    var css =
      '[data-langtons-ant]{position:relative;isolation:isolate;overflow:hidden}' +
      '.lga-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; }

    // '#rrggbb' as a little-endian RGBA pixel.
    function pixel(hex) {
      var n = parseInt(hex.replace('#', ''), 16);
      return (255 << 24 | (n & 255) << 16 | (n >> 8 & 255) << 8 | n >> 16) >>> 0;
    }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'lga-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var rule = (d.rule || DEFAULTS.rule).toUpperCase().replace(/[^RL]/g, '');
      if (rule.length < 2) rule = DEFAULTS.rule;
      var colors = d.colors ? d.colors.split(',') : DEFAULTS.colors;
      var pixels = [0];
      for (var c = 1; c < rule.length; c++) pixels.push(pixel(colors[(c - 1) % colors.length].trim()));
      var speed = +d.speed || DEFAULTS.speed;
      var antColor = d.ant || DEFAULTS.ant;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var w = 0, h = 0, dpr = 1, cols = 0, rows = 0, size = 4;
      var grid, img, buf, off = document.createElement('canvas'), octx = off.getContext('2d');
      var ant, steps = 0, clock = 0, owed = 0, done = false, waiting = false;

      function reset() {
        grid = new Uint8Array(cols * rows);
        buf.fill(0);
        ant = { x: cols >> 1, y: rows >> 1, dir: 0 };
        steps = 0; clock = 0; owed = 0; done = false;
      }

      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);
        size = +d.cell || Math.max(1.5, Math.min(w, h) / 104);
        cols = Math.max(8, Math.ceil(w / size));
        rows = Math.max(8, Math.ceil(h / size));
        off.width = cols; off.height = rows;
        img = octx.createImageData(cols, rows);
        buf = new Uint32Array(img.data.buffer);
        reset();
      }

      // Turn, flip the tile, step forward. False once the ant walks off.
      function step() {
        var i = ant.y * cols + ant.x, s = grid[i];
        ant.dir = (ant.dir + (rule.charAt(s) === 'R' ? 1 : 3)) % 4;
        s = (s + 1) % rule.length;
        grid[i] = s;
        buf[i] = pixels[s];
        ant.x += DX[ant.dir];
        ant.y += DY[ant.dir];
        steps++;
        return ant.x >= 0 && ant.y >= 0 && ant.x < cols && ant.y < rows && steps < 200000;
      }

      function draw() {
        octx.putImageData(img, 0, 0);
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        ctx.imageSmoothingEnabled = false;
        var ox = (w - cols * size) / 2, oy = (h - rows * size) / 2;
        ctx.drawImage(off, ox, oy, cols * size, rows * size);
        if (done) return;
        // The ant: a bright dot with a soft halo, so it can be followed.
        var ax = ox + (ant.x + 0.5) * size, ay = oy + (ant.y + 0.5) * size, r = Math.max(2.5, size * 0.9);
        ctx.fillStyle = antColor;
        ctx.globalAlpha = 0.25;
        ctx.beginPath(); ctx.arc(ax, ay, r * 2.2, 0, Math.PI * 2); ctx.fill();
        ctx.globalAlpha = 1;
        ctx.beginPath(); ctx.arc(ax, ay, r, 0, Math.PI * 2); ctx.fill();
      }

      function advance(dt) {
        if (done) return;
        clock += dt;
        // Ease up from a walk to full speed over the first few seconds.
        var ramp = Math.min(1, clock / 4000);
        owed += (30 + (speed - 30) * ramp * ramp) * dt / 1000;
        var n = Math.floor(owed);
        owed -= n;
        while (n-- > 0) {
          if (!step()) { done = true; finish(); break; }
        }
      }

      function finish() {
        if (!loop || waiting) return;
        waiting = true;
        setTimeout(function () {
          anime.animate(canvas, {
            opacity: 0, duration: 600, ease: 'inOut(2)',
            onComplete: function () {
              reset();
              waiting = false;
              anime.animate(canvas, { opacity: 1, duration: 400 });
            }
          });
        }, 1800);
      }

      resize();
      if (reduced()) {
        // A still frame of the finished walk: the mess and its highway.
        while (step()) {}
        done = true;
        draw();
        if (window.ResizeObserver) new ResizeObserver(function () {
          resize();
          if (!steps) while (step()) {}
          done = true;
          draw();
        }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      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();
        }
      });
      // 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-langtons-ant]').forEach(function (el) {
        if (el._lga) return;
        el._lga = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-langtons-ant data-loop style="height: 360px"></div>

<!-- a different ant: -->
<div data-langtons-ant data-rule="LLRR" style="height: 360px"></div>