Rule 30
Rule 30 grows from one cell into a triangle of irregular cells, its random centre column picked out.
Made with Claude Opus 5.5
- Technique
- simulations, pixel art
- Shape
- Any screen
- Added
- 27 September 2026
Versions
Colours
- #1C1B1Abackground
- #DAD8CEforeground
- #CF6A4Caccent
Export
FormatThis browser can’t make WebP files.
Shapecropped from 16:9
Crop
SizeThis browser can’t draw a file that large.
Notes
Each row is the next generation of the row above: a cell becomes left XOR (centre OR right) of the three cells over it. Diagonal stripes run down the left flank, while the rest shows no repeating pattern. Wolfram proposed the centre column as a random number generator, and Mathematica once used it to make random integers.
Sources
- Stephen Wolfram, A New Kind of Science, 2002.
- Rule 30.
Source code
wallpapers/rule-30/design.py, 59 lines
"""An elementary cellular automaton, Rule 30 by default, grown one generation per row from a single live cell at the top edge in square cells, with one line of its cells picked out."""
from typing import Literal
import numpy as np
from numpy.typing import NDArray
from walldye import ACCENT, ACCENT_1, ACCENT_2, ACCENT_3, BG_ALT, UI, Canvas, Params, design, knob
from walldye.pixel import grid_runs
class Automaton(Params):
rule: int = knob(default=30, choices=(30, 90, 110), doc="Wolfram code of the automaton")
trace: Literal["column", "edges"] = knob(
default="column", doc="cells picked out: the seed's column, or both edges of the growth"
)
# Rule 90's centre column dies after the seed, so it picks out its two always-live edges;
# Rule 110 grows one way only, so its seed column is its straight edge.
VARIANTS = {"rule-90": Automaton(rule=90, trace="edges"), "rule-110": Automaton(rule=110)}
CELL, CORE = 8, 18 # cell size; half-width of the brighter core band around the seed column
# Picked-out cells step down the accent ramp, from ACCENT at the seed to ACCENT_3, at these rows.
HEAD_ROWS = (40, 72, 104)
PALETTE = (None, BG_ALT, UI, ACCENT, ACCENT_1, ACCENT_2, ACCENT_3) # empty, field, core, head
def evolve(rule: int, rows: int, cols: int, seed: int) -> NDArray[np.bool_]:
"""The first `rows` generations of the elementary automaton with Wolfram code `rule`, grown
from one live cell at column `seed`, as a `(rows, cols)` grid. Growth past either side of
the grid carries on in a margin, so the visible edge columns stay exact."""
table = np.array([(rule >> k) & 1 for k in range(8)], dtype=bool)
pad = rows + 1 # the pattern spreads at most one cell per row, so np.roll never wraps it
row = np.zeros(cols + 2 * pad, dtype=bool)
row[seed + pad] = True
out = np.empty((rows, cols), dtype=bool)
for j in range(rows):
out[j] = row[pad : pad + cols]
left, right = np.roll(row, 1), np.roll(row, -1)
row = table[4 * left + 2 * row + right]
return out
@design(aspects="any", variants=VARIANTS)
def draw(s: Canvas[Automaton]) -> None:
# Right of centre, leaving the upper left empty. The right flank runs off the right edge,
# except on the widest screens, where the whole triangle fits and keeps clear of it.
fx = 0.75 if s.w < 3 * s.h else 0.66
seed = s.pick(landscape=(fx, 0), portrait=(0.7, 0), snap=CELL)
rows, cols, c0 = s.h // CELL + 1, s.w // CELL + 1, int(seed.x) // CELL
live = evolve(s.params.rule, rows, cols, c0)
depth = np.arange(rows)[:, None]
dist = np.abs(np.arange(cols) - c0)
grid = np.where(live, np.where(dist <= np.minimum(depth, CORE), 2, 1), 0)
head = 3 + np.searchsorted(HEAD_ROWS, np.arange(rows), side="right")[:, None]
traced = (dist == 0) if s.params.trace == "column" else (dist == depth)
grid = np.where(traced, np.where(live, head, 0), grid)
grid_runs(s, grid, PALETTE, CELL)"""An elementary cellular automaton, Rule 30 by default, grown one generation per row from a single live cell at the top edge in square cells, with one line of its cells picked out."""
from typing import Literal
import numpy as np
from numpy.typing import NDArray
from walldye import ACCENT, ACCENT_1, ACCENT_2, ACCENT_3, BG_ALT, UI, Canvas, Params, design, knob
from walldye.pixel import grid_runs
class Automaton(Params):
rule: int = knob(default=30, choices=(30, 90, 110), doc="Wolfram code of the automaton")
trace: Literal["column", "edges"] = knob(
default="column", doc="cells picked out: the seed's column, or both edges of the growth"
)
# Rule 90's centre column dies after the seed, so it picks out its two always-live edges;
# Rule 110 grows one way only, so its seed column is its straight edge.
VARIANTS = {"rule-90": Automaton(rule=90, trace="edges"), "rule-110": Automaton(rule=110)}
CELL, CORE = 8, 18 # cell size; half-width of the brighter core band around the seed column
# Picked-out cells step down the accent ramp, from ACCENT at the seed to ACCENT_3, at these rows.
HEAD_ROWS = (40, 72, 104)
PALETTE = (None, BG_ALT, UI, ACCENT, ACCENT_1, ACCENT_2, ACCENT_3) # empty, field, core, head
def evolve(rule: int, rows: int, cols: int, seed: int) -> NDArray[np.bool_]:
"""The first `rows` generations of the elementary automaton with Wolfram code `rule`, grown
from one live cell at column `seed`, as a `(rows, cols)` grid. Growth past either side of
the grid carries on in a margin, so the visible edge columns stay exact."""
table = np.array([(rule >> k) & 1 for k in range(8)], dtype=bool)
pad = rows + 1 # the pattern spreads at most one cell per row, so np.roll never wraps it
row = np.zeros(cols + 2 * pad, dtype=bool)
row[seed + pad] = True
out = np.empty((rows, cols), dtype=bool)
for j in range(rows):
out[j] = row[pad : pad + cols]
left, right = np.roll(row, 1), np.roll(row, -1)
row = table[4 * left + 2 * row + right]
return out
@design(aspects="any", variants=VARIANTS)
def draw(s: Canvas[Automaton]) -> None:
# Right of centre, leaving the upper left empty. The right flank runs off the right edge,
# except on the widest screens, where the whole triangle fits and keeps clear of it.
fx = 0.75 if s.w < 3 * s.h else 0.66
seed = s.pick(landscape=(fx, 0), portrait=(0.7, 0), snap=CELL)
rows, cols, c0 = s.h // CELL + 1, s.w // CELL + 1, int(seed.x) // CELL
live = evolve(s.params.rule, rows, cols, c0)
depth = np.arange(rows)[:, None]
dist = np.abs(np.arange(cols) - c0)
grid = np.where(live, np.where(dist <= np.minimum(depth, CORE), 2, 1), 0)
head = 3 + np.searchsorted(HEAD_ROWS, np.arange(rows), side="right")[:, None]
traced = (dist == 0) if s.params.trace == "column" else (dist == depth)
grid = np.where(traced, np.where(live, head, 0), grid)
grid_runs(s, grid, PALETTE, CELL)
Run it yourself
$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render rule-30 --theme fireproof -o rule-30-fireproof-16x9.svg