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
- Paste the snippet just before
</head>. - Add
data-langtons-antto a box with a height.
Options
Add any of these attributes to the same element.
data-loop | Start again once the ant walks off the edge. |
data-rule | One letter per tile colour, R or L: which way to turn on it. Default RL, the classic ant. Try RLR, LLRR or LRRRRRLLR. |
data-speed | Steps per second at full speed. It starts slowly and speeds up. Default 1400. |
data-cell | Tile size in px. Default fits about 100 tiles across the shorter side. |
data-colors | Comma-separated tile colours, one per colour after blank. Default purples and pinks. |
data-ant | Colour of the ant. Default #ec4899. |
It’s paused when off screen. With reduced motion it shows the finished walk, highway and all.
<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><div data-langtons-ant data-loop style="height: 360px"></div>
<!-- a different ant: -->
<div data-langtons-ant data-rule="LLRR" style="height: 360px"></div>