langton's ant

[github]

order, then chaos, then the ant builds its highway

generative 72×25 30 fps 3.1 kB

use it

html

<script type="module" src="https://ascii.rest/ascii.js"></script>

<ascii-art piece="langtons-ant"></ascii-art>

No install and no build step. ascii.js defines the <ascii-art> tag, loads the piece from ascii.rest, plays it while it is on screen, and holds the first frame for anyone who prefers reduced motion.

react

// npm install github:bas3line/ascii
import { Ascii } from "ascii.rest/react";
import { langtonsAnt } from "ascii.rest/pieces";

<Ascii piece={langtonsAnt} />

A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="langtons-ant", to fetch the piece only when it mounts.

astro

---
// npm install github:bas3line/ascii
import Ascii from "ascii.rest/astro";
---

<Ascii piece="langtons-ant" />

Renders the first frame on the server, so the page is whole before any script runs, then plays the piece once it loads.

typescript

// npm install github:bas3line/ascii
import { mount } from "ascii.rest";
import { langtonsAnt } from "ascii.rest/pieces";

const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, langtonsAnt);

mount plays a piece in a <pre>, or on a <canvas> for the coloured ones, and returns a function that stops it. Every piece, option and frame is typed.

langtons-ant.ts

[js] [github]
/*
 * langton's ant: an ant that turns right on a blank square and left on an
 * inked one, flipping each square it leaves. Two squares to a character.
 */
import type { Frame, Meta } from "../types.ts";

export const meta = {
  name: "langton's ant",
  category: "generative",
  note: "order, then chaos, then the ant builds its highway",
  cols: 72,
  rows: 25,
  fps: 30,
} satisfies Meta;

const N = 200; // the board, in squares; the frame shows part of it
const X0 = 31, Y0 = -24; // the board square at the frame's top left, from the start
// Play time against steps taken. The cycle opens as the highway sets off, so
// the first frame already shows all three phases; then the ant walks out of
// the frame, the board fades and it starts again from a blank board. The
// highway starts at step 9977.
const KEYS: [number, number][] = [
  [0, 10350],
  [2.0, 11330],
  [3.2, 11330],
  [3.2, 0],
  [4.3, 100],
  [5.6, 520],
  [12.4, 9977],
  [13.2, 10350],
];
const PERIOD = 13.2;
const FADE = [2.4, 3.2];

function mulberry32(a: number) {
  return () => {
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

export default function langtonsAnt(): Frame {
  const { cols, rows } = meta;
  const board = new Uint8Array(N * N);
  const DX = [0, 1, 0, -1], DY = [-1, 0, 1, 0];
  let x: number, y: number, dir: number, steps: number;
  const reset = () => {
    board.fill(0);
    x = y = N / 2;
    dir = 0;
    steps = 0;
  };
  const step = () => {
    const i = y * N + x;
    dir = (dir + (board[i] ? 3 : 1)) & 3;
    board[i] ^= 1;
    x += DX[dir];
    y += DY[dir];
    steps++;
  };

  // Each square fades at its own moment.
  const rand = mulberry32(7);
  const fadeAt = new Float32Array(cols * rows * 2).map(() => rand());

  const target = (p: number) => {
    let k = 1;
    while (KEYS[k][0] < p) k++;
    const [p0, s0] = KEYS[k - 1], [p1, s1] = KEYS[k];
    return Math.round(s0 + ((s1 - s0) * (p - p0)) / (p1 - p0));
  };

  // The frame is mirrored so the highway runs off to the lower right.
  const GLYPH = [" ", "▀", "▄", "█"];
  const at = (c: number, r: number) => {
    const bx = N / 2 + X0 - c, by = N / 2 + Y0 + r;
    return board[by * N + bx];
  };

  reset();
  return (t) => {
    const p = t % PERIOD;
    const want = target(p);
    if (want < steps) reset();
    while (steps < want) step();

    const fade = p > FADE[0] && p <= FADE[1] ? (p - FADE[0]) / (FADE[1] - FADE[0]) : 0;
    const ac = N / 2 + X0 - x, ar = Math.floor((y - N / 2 - Y0) / 2);
    const lines: string[] = [];
    for (let r = 0; r < rows; r++) {
      let line = "";
      for (let c = 0; c < cols; c++) {
        if (c === ac && r === ar && fade === 0) {
          line += "@";
          continue;
        }
        const k = (r * cols + c) * 2;
        const top = at(c, 2 * r) && fadeAt[k] >= fade ? 1 : 0;
        const bottom = at(c, 2 * r + 1) && fadeAt[k + 1] >= fade ? 2 : 0;
        line += GLYPH[top + bottom];
      }
      lines.push(line);
    }
    return lines.join("\n");
  };
}