Generative Patterns
Explore L-systems, reaction-diffusion, Conway's Game of Life, and Voronoi diagrams to create endlessly evolving algorithmic artwork.
Generative patterns emerge from simple rules applied repeatedly. The results are often more complex and beautiful than anything you could draw by hand. This tutorial covers four of the most powerful generative systems, culminating in an animated L-system plant.
L-Systems: Grammar-Based Growth
An L-system (Lindenmayer system) is a string-rewriting grammar. You start with an axiom string and repeatedly apply production rules that replace each symbol with a new string. After several iterations you interpret the result as turtle graphics instructions.
Classic symbols:
| Symbol | Action |
|---|---|
F |
Move forward, draw a line |
+ |
Turn left by angle δ |
- |
Turn right by angle δ |
[ |
Push turtle state (position, angle) onto stack |
] |
Pop turtle state from stack |
Writing the Parser
String axiom = "X";
float angle = 25;
int iterations = 5;
String sentence;
HashMap<Character, String> rules = new HashMap<Character, String>();
void buildRules() {
rules.put('X', "F+[[X]-X]-F[-FX]+X");
rules.put('F', "FF");
}
String applyRules(String s) {
StringBuilder next = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (rules.containsKey(c)) {
next.append(rules.get(c));
} else {
next.append(c);
}
}
return next.toString();
}
void generateSentence() {
sentence = axiom;
for (int i = 0; i < iterations; i++) {
sentence = applyRules(sentence);
}
}
Drawing with Turtle Graphics
float stepLen = 6;
float turtleAngle;
PVector turtlePos;
ArrayDeque<float[]> stack = new ArrayDeque<float[]>();
void drawLSystem() {
turtlePos = new PVector(width / 2, height);
turtleAngle = -HALF_PI; // start pointing upward
stack.clear();
strokeWeight(1);
for (int i = 0; i < sentence.length(); i++) {
char c = sentence.charAt(i);
switch (c) {
case 'F':
PVector next = new PVector(
turtlePos.x + cos(turtleAngle) * stepLen,
turtlePos.y + sin(turtleAngle) * stepLen
);
// colour by depth approximated by stack size
float green = map(stack.size(), 0, 6, 80, 200);
stroke(40, green, 20);
line(turtlePos.x, turtlePos.y, next.x, next.y);
turtlePos = next;
break;
case '+': turtleAngle += radians(angle); break;
case '-': turtleAngle -= radians(angle); break;
case '[':
stack.push(new float[]{turtlePos.x, turtlePos.y, turtleAngle});
break;
case ']':
float[] state = stack.pop();
turtlePos.set(state[0], state[1]);
turtleAngle = state[2];
break;
// X is a non-terminal, no drawing action
}
}
}
Reaction-Diffusion: Gray-Scott Model
The Gray-Scott model simulates two chemicals, U and V, that react and diffuse across a grid. With carefully chosen parameters it produces spots, stripes, labyrinthine patterns, and more.
float[][] U, V, nextU, nextV;
int W, H;
float feed = 0.055, kill = 0.062; // try different values
float dU = 1.0, dV = 0.5;
void initRD() {
W = width; H = height;
U = new float[W][H]; V = new float[W][H];
nextU = new float[W][H]; nextV = new float[W][H];
for (int x = 0; x < W; x++)
for (int y = 0; y < H; y++) { U[x][y] = 1; V[x][y] = 0; }
// seed a small patch of V
for (int x = W/2-10; x < W/2+10; x++)
for (int y = H/2-10; y < H/2+10; y++) { V[x][y] = 1; }
}
void stepRD() {
for (int x = 1; x < W-1; x++) {
for (int y = 1; y < H-1; y++) {
float u = U[x][y], v = V[x][y];
float lapU = U[x-1][y] + U[x+1][y] + U[x][y-1] + U[x][y+1] - 4*u;
float lapV = V[x-1][y] + V[x+1][y] + V[x][y-1] + V[x][y+1] - 4*v;
float uvv = u * v * v;
nextU[x][y] = u + (dU * lapU - uvv + feed * (1 - u));
nextV[x][y] = v + (dV * lapV + uvv - (kill + feed) * v);
nextU[x][y] = constrain(nextU[x][y], 0, 1);
nextV[x][y] = constrain(nextV[x][y], 0, 1);
}
}
float[][] tmp = U; U = nextU; nextU = tmp;
tmp = V; V = nextV; nextV = tmp;
}
Render by mapping V[x][y] to a colour. Iterating the simulation several times per frame speeds up pattern formation.
Conway’s Game of Life
Life is a cellular automaton on a binary grid. Each cell is alive or dead; it survives, dies, or is born based on how many of its eight neighbours are alive.
boolean[][] grid, next;
int CELL = 6;
int COLS, ROWS;
void initLife() {
COLS = width / CELL; ROWS = height / CELL;
grid = new boolean[COLS][ROWS];
next = new boolean[COLS][ROWS];
for (int c = 0; c < COLS; c++)
for (int r = 0; r < ROWS; r++)
grid[c][r] = random(1) < 0.3;
}
void stepLife() {
for (int c = 0; c < COLS; c++) {
for (int r = 0; r < ROWS; r++) {
int neighbours = 0;
for (int dc = -1; dc <= 1; dc++)
for (int dr = -1; dr <= 1; dr++) {
if (dc == 0 && dr == 0) continue;
int nc = (c + dc + COLS) % COLS;
int nr = (r + dr + ROWS) % ROWS;
if (grid[nc][nr]) neighbours++;
}
boolean alive = grid[c][r];
next[c][r] = alive ? (neighbours == 2 || neighbours == 3)
: (neighbours == 3);
}
}
boolean[][] tmp = grid; grid = next; next = tmp;
}
The modular arithmetic wraps the edges so the grid tiles toroidally.
Voronoi Diagrams
A Voronoi diagram colours each pixel by whichever seed point is nearest. The brute-force approach is O(width × height × seeds), which works fine for small grids or when done offline:
PVector[] seeds;
color[] seedColors;
void drawVoronoi() {
loadPixels();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
float minD = Float.MAX_VALUE;
int closest = 0;
for (int i = 0; i < seeds.length; i++) {
float d = dist(x, y, seeds[i].x, seeds[i].y);
if (d < minD) { minD = d; closest = i; }
}
pixels[y * width + x] = seedColors[closest];
}
}
updatePixels();
}
For interactive speeds with many seeds, compute Voronoi on a downsampled grid and scale up.
Full Example: Animated L-System Plant
String sentence;
float angle = 25;
float stepLen;
boolean needsRebuild = true;
int currentIter = 1;
void setup() {
size(800, 700);
buildRules();
generateSentence();
}
void draw() {
background(20, 30, 20);
// redraw the plant every frame to animate angle
angle = 20 + 10 * sin(frameCount * 0.01);
stepLen = map(currentIter, 1, 7, 18, 4);
drawLSystem();
fill(200, 220, 180);
noStroke();
text("Iteration: " + currentIter + " | Press +/- to change", 12, 20);
}
void keyPressed() {
if (key == '+' && currentIter < 7) {
currentIter++;
generateSentence();
}
if (key == '-' && currentIter > 1) {
currentIter--;
generateSentence();
}
}
// (generateSentence, applyRules, drawLSystem as defined above)
// The drawLSystem function additionally draws small ellipses
// at ] positions to simulate leaves:
void drawLeaves() {
// After the main draw, add a second pass:
turtlePos = new PVector(width / 2, height);
turtleAngle = -HALF_PI;
stack.clear();
for (int i = 0; i < sentence.length(); i++) {
char c = sentence.charAt(i);
switch (c) {
case 'F':
turtlePos.add(cos(turtleAngle) * stepLen, sin(turtleAngle) * stepLen);
break;
case '+': turtleAngle += radians(angle); break;
case '-': turtleAngle -= radians(angle); break;
case '[': stack.push(new float[]{turtlePos.x, turtlePos.y, turtleAngle}); break;
case ']':
float[] st = stack.pop();
// draw leaf at the branch tip
fill(60, 160, 40, 160);
noStroke();
pushMatrix();
translate(st[0], st[1]);
rotate(st[2]);
ellipse(0, 0, 6, 10);
popMatrix();
turtlePos.set(st[0], st[1]);
turtleAngle = st[2];
break;
}
}
}
Press + to increase iterations and watch the plant grow in complexity. The oscillating angle variable makes the plant sway gently as if in a breeze.
Key Takeaways
- L-system string rewriting + turtle graphics generates fractal-like organic forms from a handful of rules.
- The stack (
[/]) is what makes branching structures possible. - Gray-Scott reaction-diffusion only needs eight lines of per-pixel math to produce stunning biological patterns.
- Conway’s Life wraps edges with modular arithmetic to eliminate boundary conditions.
- Brute-force Voronoi is simple and sufficient for small seed counts or offline rendering.