Getting Started
Your first evolving artwork in 50 lines of Python/NumPy.
What “evolving art” actually means
An evolving artwork is a generative system whose state unfolds over time. You start it, you let it run, and at every step a small piece of structure is added to a canvas — a particle drops a grain of pigment, an iterated map pushes a point one tick further along its attractor, a cellular automaton transitions one generation. The work is not the static PNG you take at the end; the work is the trajectory through state-space that produces the PNG.
Three properties follow from this definition, and they are what separate evolving art from every other category of generative image:
- It changes. Frame 1, frame 200, and frame 10,000 look meaningfully different — not just in colour but in the topology of what’s been drawn.
- It is deterministic. Given the same seed and the same parameter values, the system produces the exact same trajectory on any machine, forever.
- It is reproducible. That determinism is not a happy accident — it is the contract. Anyone with the seed, the parameters, and the code can regenerate the exact same image, byte-for-byte.
The third point is what makes evolving art sellable. A static PNG is a one-off you must defend. An evolving artwork is a function you can publish — seed in, image out — and that function is the artifact.
How this differs from “random art”
Random art — np.random.rand(512, 512, 3) — looks busy and has no reproducibility story. There is no seed that means anything, no parameter that produces a family of related outputs, no progression from one frame to the next.
Evolving art fixes each of those:
| Property | Random art | Evolving art |
|---|---|---|
| Same input → same output | ❌ | ✅ |
| Has a “parameter space” | ❌ | ✅ |
| Changes meaningfully over time | ❌ | ✅ |
| Can be proven to be authentic | ❌ | ✅ |
| Has a “family” of related outputs | ❌ | ✅ |
If your generator doesn’t satisfy the four-point contract, it’s not yet an evolving artwork in the sense this tutorial uses.
Why this sells
Three structural reasons, none of them hype:
- Every run is unique. A 64-bit seed gives you ~18 quintillion distinct artworks in the same algorithm. No two collectors land on the same one.
- Every run is reproducible. The collector can verify authenticity by re-running with their token ID. The provenance is the function, not the artist.
- The work keeps moving. An evolving artwork minted at frame 500 can be rendered again at frame 50,000 by the collector — the image they own is a snapshot of a longer trajectory, and the trajectory itself is the actual artifact.
Art Blocks, fxhash, and the broader generative-art market have all converged on this contract because it solves the deepest problem in digital art: how do you prove an image wasn’t doctored? You publish the seed, publish the algorithm, and the hash of the resulting image is the proof.
The contract every evolving artwork satisfies
Across the 153 works in this corpus — ported from Jared Tarbell’s Gallery of Computation, sourced from Casey Reas, Inigo Quilez, Memo Akten, Quayola, Refik Anadol, Daniel Shiffman, Karsten Schmidt, the canonical Shadertoy crew, the Art Blocks generation, and others — every single one exposes the same five-method API:
class Sketch:
family: ClassVar[str] # "strange_attractors" | "particles_paths" | ...
sketch: ClassVar[str] # "peterdejong"
def __init__(self, **params) -> None: ...
def reset(self, seed: int | None = None) -> None: ...
def step(self, n: int = 1) -> None: ...
def render(self) -> np.ndarray: ... # HxWx3 uint8
def params(self) -> dict: ...
Anything with .step() + .render() is a universe citizen. That uniformity is not decorative — it’s how you can build tooling that runs against any of them, how you can wrap one inside another (see Chapter 2’s IrritableArtwork), and how a buyer can re-render their NFT with python -m sketch family=… sketch=… seed=… step=….
Your first sketch in 50 lines
Let’s port a Lindenmayer L-system from scratch. This is the canonical “rewriting rules + turtle drawing” pipeline used in everything from plant-biology simulation (Aristid Lindenmayer’s original 1968 use case) to generative typography.
The two ingredients:
- An axiom and production rules that rewrite a string symbol-by-symbol each iteration.
- A turtle that walks the resulting string, drawing forward on
F, turning on+/-, pushing/popping state on[/].
import numpy as np
import imageio.v2 as imageio
DIM = 600
SEED = 7
# 1. The L-system. Axiom + rules + angle, rewritten 5 times.
AXIOM = "X"
RULES = {"X": "F+[[X]-X]-F[-FX]+X", "F": "FF"}
ANGLE_DEG = 22.5
ITERATIONS = 5
s = AXIOM
for _ in range(ITERATIONS):
s = "".join(RULES.get(ch, ch) for ch in s)
# 2. The turtle. Walks the string, emitting line segments.
cx, cy, heading = DIM / 2, DIM - 10, np.deg2rad(-90)
angle = np.deg2rad(ANGLE_DEG)
step_len = 6.0 * (0.72 ** ITERATIONS)
stack = []
segments = [] # list of (x0, y0, x1, y1)
for ch in s:
if ch == "F":
nx = cx + step_len * np.cos(heading)
ny = cy + step_len * np.sin(heading)
segments.append((cx, cy, nx, ny))
cx, cy = nx, ny
elif ch == "+": heading += angle
elif ch == "-": heading -= angle
elif ch == "[": stack.append((cx, cy, heading))
elif ch == "]":
if stack: cx, cy, heading = stack.pop()
# 3. Rasterise onto a canvas.
rng = np.random.default_rng(SEED)
canvas = np.full((DIM, DIM, 3), 252, dtype=np.uint8) # warm paper
ink = np.array([30, 55, 40], dtype=np.float32) # deep moss
for x0, y0, x1, y1 in segments:
n = max(int(max(abs(x1 - x0), abs(y1 - y0))), 1) * 3 + 1
xs = np.linspace(x0, x1, n).astype(np.int32)
ys = np.linspace(y0, y1, n).astype(np.int32)
mask = (xs >= 0) & (xs < DIM) & (ys >= 0) & (ys < DIM)
xs, ys = xs[mask], ys[mask]
canvas[ys, xs] = (0.85 * canvas[ys, xs] + 0.15 * ink).astype(np.uint8)
imageio.imwrite("first_sketch.png", canvas)
print(f"drawn {len(segments)} segments, final length of string: {len(s)}")
The output is a fractal_weed — a curling fractal plant with self-similar branches. With SEED = 7 you get one specific branching pattern; with SEED = 8 you get a different one of the same family.
What the code is actually doing
Step 1 rewrites the axiom. Start with "X". After one iteration, X → F+[[X]-X]-F[-FX]+X. After two iterations, that string becomes the input and gets rewritten again, except F doesn’t have a rule so it stays as FF. The string grows exponentially: len(s) = 4,361 after five iterations. That’s why we cap iterations — past 7 or 8 the string is too long to render in a sane amount of memory.
Step 2 walks the resulting string as a 2D turtle. F moves and draws, +/- rotate by 22.5°, [ pushes the current state (position and heading) onto a stack, ] pops it back. The push/pop is what creates branching: the turtle enters a [, draws a sub-branch, comes back to the saved state, and continues from there.
Step 3 rasterises every F segment as a series of pixel writes, blending slightly with whatever was there before so the lines have anti-aliased-looking edges. The math is the same one used by the full Lindenmayer class in this corpus — the difference is just packaging.
Now match it against the contract
class FirstSketch:
family = "reas_processing"
sketch = "lindenmayer_minimal"
def __init__(self, **params):
self.dim = params.get("dim", 600)
self.seed = params.get("seed", 7)
self._build()
def reset(self, seed=None):
if seed is not None: self.seed = seed
self._build()
def step(self, n=1):
# L-systems are grown once per iteration count, so "step"
# here means "grow one more iteration if we can".
self._iterations = min(self._iterations + n, 8)
self._build()
def _build(self):
# ... same string-rewrite + turtle walk as above ...
pass
def render(self):
return self._canvas.copy()
def params(self):
return {"dim": self.dim, "iterations": self._iterations,
"seed": self.seed}
Five methods. Same shape as PeterDeJong, Lindenmayer, Guts, BoidsFlock, every other work in the library. That is the contract — and once you’ve written one class this way, you’ve written them all.
Where to go next
You’ve built the simplest possible evolving artwork. In Chapter 2 we’ll look at what makes a good one: how to choose which knobs to expose, why constrained parameter spaces produce more beautiful output than open ones, and how to compose multiple formulas using the IrritableArtwork wrapper. In Chapter 3 we’ll automate the search through that parameter space. In Chapter 4 we’ll lock down the determinism so you can mint the result.
For a richer L-system with four presets (bush, stochastic_tree, fractal_weed, koch_snowflake) and full stochastic-rule support, see lindenmayer in the library. For an iterated-map attractor — the other canonical starter — see peterdejong. For noise-driven flow lines, see perlinStreamlines.
Run any of them with:
python3 -m evolving_art.library_external.reas_processing.lindenmayer.reference