rule 30

[github]

one cell grows a row at a time under rules 30, 90 and 110

generative 64×20 8 fps 2.4 kB

use it

html

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

<ascii-art piece="rule-30"></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 { rule30 } from "ascii.rest/pieces";

<Ascii piece={rule30} />

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

astro

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

<Ascii piece="rule-30" />

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 { rule30 } from "ascii.rest/pieces";

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

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.

rule-30.ts

[js] [github]
/*
 * rule 30: an elementary cellular automaton grown from one live cell, each
 * generation a row, scrolling up. It runs rule 30, then 90, then 110, each
 * from a fresh seed. Two generations to a character, so cells are square.
 */
import type { Frame, Meta } from "../types.ts";

export const meta = {
  name: "rule 30",
  category: "generative",
  note: "one cell grows a row at a time under rules 30, 90 and 110",
  cols: 64,
  rows: 20,
  fps: 8,
} satisfies Meta;

// Each rule, the column of its seed, and how many generations it runs: short
// enough that its triangle never reaches the sides. Rule 90 stops on the row
// that closes its fifth level of triangles; rule 110 grows only to the left,
// so its seed sits right of centre.
const RULES: [number, number, number][] = [
  [30, 31, 32],
  [90, 31, 32],
  [110, 51, 40],
];
const GAP = 12; // blank generations between one rule and the next
const SPEED = 8; // character rows a second, one a frame

export default function rule30(): Frame {
  const { cols, rows } = meta;
  const EXTRA = 48; // cells past each side, so the edges never show
  const W = cols + 2 * EXTRA;
  const tape: Uint8Array[] = []; // every generation of the loop, cropped to the frame

  for (const [rule, at, gens] of RULES) {
    let cur = new Uint8Array(W);
    let nxt = new Uint8Array(W);
    cur[EXTRA + at] = 1;
    for (let g = 0; g < gens; g++) {
      tape.push(cur.slice(EXTRA, EXTRA + cols));
      for (let i = 1; i < W - 1; i++) nxt[i] = (rule >> ((cur[i - 1] << 2) | (cur[i] << 1) | cur[i + 1])) & 1;
      [cur, nxt] = [nxt, cur];
    }
    for (let g = 0; g < GAP; g++) tape.push(new Uint8Array(cols));
  }
  // Every run and gap is an even number of generations, so the tape scrolls a
  // whole character row at a time and each glyph keeps the same two
  // generations from frame to frame.
  const LOOP = tape.length / 2;

  // The newest generation is always the bottom row. At t = 0 the first
  // triangle sits two rows down from the top.
  const GLYPH = [" ", "▀", "▄", "█"];
  return (t) => {
    const top = Math.floor(t * SPEED) - 2;
    const lines: string[] = [];
    for (let r = 0; r < rows; r++) {
      const k = (((top + r) % LOOP) + LOOP) % LOOP;
      const a = tape[2 * k], b = tape[2 * k + 1];
      let line = "";
      for (let c = 0; c < cols; c++) line += GLYPH[a[c] + 2 * b[c]];
      lines.push(line);
    }
    return lines.join("\n");
  };
}