Processing Expert Article 4

Generative Art Systems

Design reproducible generative systems with exposed parameter spaces, aesthetic constraints, SVG export, and automated batch rendering.

⏱ 50 min generative randomSeed SVG batch rendering design systems

A generative art system is more than a random drawing function. It is a designed parameter space that reliably produces coherent output within an aesthetic envelope. This article covers how to architect such a system for reproducibility, series generation, and archival output.

Designing the Parameter Space

Start by identifying which variables should be randomised and which should be fixed constraints that define your aesthetic. The ratio of constrained to free parameters determines how consistent your series looks.

// Fixed aesthetic constraints — define the system's identity
final int   PALETTE_INDEX  = 2;
final float MIN_SPLIT_RATIO = 0.3;
final float MAX_SPLIT_RATIO = 0.7;
final int   MAX_DEPTH       = 7;

// Free parameters — randomised per seed
long   seed;
int    rootHue;
float  splitBias;     // tendency to split horizontally vs. vertically
float  colorShift;    // hue rotation per depth level

void randomiseParams(long s) {
  seed       = s;
  randomSeed(seed);
  noiseSeed((int)seed);

  rootHue    = (int)random(360);
  splitBias  = random(0.3, 0.7);
  colorShift = random(12, 45);
}

Reproducibility with randomSeed

randomSeed(long) resets Processing’s pseudorandom number generator to a deterministic state. Every call to random() after the seed produces the same sequence. noiseSeed(int) does the same for Perlin noise.

The seed is the entire description of a generated image. Store it alongside every output file.

void setup() {
  size(2400, 2400, P2D);
  pixelDensity(2);  // high-DPI output
  noLoop();
}

void draw() {
  randomiseParams(seed);
  renderSystem();
  saveFrame("output/seed-" + seed + ".png");
}

void keyPressed() {
  if (key == ' ') {
    seed = System.currentTimeMillis();  // new random seed
    redraw();
  }
  if (key == 's') {
    // Save current seed explicitly
    println("Seed: " + seed);
  }
}

Recursive Subdivision with Aesthetic Constraints

The core algorithm splits rectangles recursively. The constraints — minimum area, depth limit, and split ratio bounds — keep results readable regardless of the seed.

color[] palette;

void renderSystem() {
  background(20);
  buildPalette();
  splitRect(0, 0, width, height, 0);
}

void buildPalette() {
  colorMode(HSB, 360, 100, 100);
  palette = new color[6];
  for (int i = 0; i < 6; i++) {
    float h = (rootHue + i * colorShift) % 360;
    float s = random(55, 95);
    float b = random(60, 98);
    palette[i] = color(h, s, b);
  }
  colorMode(RGB, 255);
}

void splitRect(float x, float y, float w, float h, int depth) {
  // Termination: too deep, too small, or random stop
  if (depth >= MAX_DEPTH || w * h < 400 || random(1) < 0.08) {
    drawLeaf(x, y, w, h, depth);
    return;
  }

  boolean horizontal = random(1) < splitBias;

  if (horizontal) {
    float cut = lerp(MIN_SPLIT_RATIO, MAX_SPLIT_RATIO, random(1));
    splitRect(x, y,              w, h * cut,       depth + 1);
    splitRect(x, y + h * cut,   w, h * (1 - cut), depth + 1);
  } else {
    float cut = lerp(MIN_SPLIT_RATIO, MAX_SPLIT_RATIO, random(1));
    splitRect(x,           y, w * cut,       h, depth + 1);
    splitRect(x + w * cut, y, w * (1 - cut), h, depth + 1);
  }
}

void drawLeaf(float x, float y, float w, float h, int depth) {
  int ci = depth % palette.length;
  fill(palette[ci]);
  noStroke();
  rect(x + 2, y + 2, w - 4, h - 4);

  // Occasional accent line
  if (random(1) < 0.3) {
    stroke(lerpColor(palette[ci], color(255), 0.5));
    strokeWeight(1.5);
    line(x + 8, y + 8, x + w - 8, y + h - 8);
    noStroke();
  }
}

Series Generation: Rendering N Variations

To generate a series of 20 images automatically, drive a count variable from the draw loop. Because noLoop() is active, redraw() must be called explicitly for each iteration.

int count     = 0;
int totalRuns = 20;
long baseSeed = 100;

void setup() {
  size(2400, 2400, P2D);
  noLoop();
}

void draw() {
  if (count >= totalRuns) {
    println("Done generating " + totalRuns + " images.");
    exit();
    return;
  }

  seed = baseSeed + count;
  randomiseParams(seed);
  renderSystem();

  String filename = String.format("output/series_%03d_seed%d.png", count, seed);
  saveFrame(filename);
  println("Saved: " + filename);

  count++;
  redraw();  // schedule next frame
}

SVG Export

The processing.svg library exports resolution-independent vector files. Load it with import processing.svg.*; and swap your renderer:

import processing.svg.*;

void exportSVG() {
  PGraphics svg = createGraphics(2400, 2400, SVG,
    "output/seed-" + seed + ".svg");
  svg.beginDraw();
  svg.background(20);
  // Call your drawing code passing the PGraphics:
  drawOnto(svg, seed);
  svg.endDraw();
  svg.dispose();
  println("SVG saved.");
}

Restructure drawing code to accept a PGraphics g parameter so the same logic works for both screen and SVG output. Use g.fill(), g.rect(), g.beginShape() etc. rather than the top-level functions.

void drawOnto(PGraphics g, long s) {
  randomiseParams(s);
  buildPalette();
  splitRectG(g, 0, 0, g.width, g.height, 0);
}

void splitRectG(PGraphics g, float x, float y, float w, float h, int depth) {
  if (depth >= MAX_DEPTH || w * h < 400 || random(1) < 0.08) {
    g.fill(palette[depth % palette.length]);
    g.noStroke();
    g.rect(x + 2, y + 2, w - 4, h - 4);
    return;
  }
  boolean horiz = random(1) < splitBias;
  if (horiz) {
    float cut = lerp(MIN_SPLIT_RATIO, MAX_SPLIT_RATIO, random(1));
    splitRectG(g, x, y, w, h * cut, depth + 1);
    splitRectG(g, x, y + h * cut, w, h * (1 - cut), depth + 1);
  } else {
    float cut = lerp(MIN_SPLIT_RATIO, MAX_SPLIT_RATIO, random(1));
    splitRectG(g, x, y, w * cut, h, depth + 1);
    splitRectG(g, x + w * cut, y, w * (1 - cut), h, depth + 1);
  }
}

Output Management and Metadata

Embed metadata in the filename and optionally in an accompanying .txt sidecar file:

void saveWithMeta(long s) {
  String base = String.format("output/%04d_s%d_h%d_b%.2f",
    count, s, rootHue, splitBias);
  saveFrame(base + ".png");

  // Sidecar metadata
  String[] meta = {
    "seed: " + s,
    "rootHue: " + rootHue,
    "splitBias: " + splitBias,
    "colorShift: " + colorShift,
    "depth: " + MAX_DEPTH,
    "date: " + year() + "-" + nf(month(),2) + "-" + nf(day(),2)
  };
  saveStrings(base + ".txt", meta);
}

Complete Runnable Sketch

import processing.svg.*;

long   seed        = 42;
int    count       = 0;
int    totalRuns   = 20;
color[] palette;
int    rootHue;
float  splitBias, colorShift;

final float MIN_RATIO = 0.3, MAX_RATIO = 0.7;
final int   MAX_DEPTH = 7;

void setup() {
  size(2400, 2400, P2D);
  noLoop();
  new java.io.File(sketchPath("output")).mkdirs();
}

void draw() {
  if (count >= totalRuns) { println("Complete."); exit(); return; }
  seed = 100 + count;
  randomSeed(seed);
  buildPalette();
  background(20);
  splitRect(0, 0, width, height, 0);
  saveWithMeta(seed);
  count++;
  redraw();
}

void buildPalette() {
  colorMode(HSB, 360, 100, 100);
  rootHue    = (int)random(360);
  splitBias  = random(0.25, 0.75);
  colorShift = random(18, 55);
  palette    = new color[6];
  for (int i = 0; i < 6; i++)
    palette[i] = color((rootHue + i * colorShift) % 360, random(60,95), random(65,98));
  colorMode(RGB, 255);
}

void splitRect(float x, float y, float w, float h, int d) {
  if (d >= MAX_DEPTH || w*h < 600 || random(1) < 0.07) {
    fill(palette[d % palette.length]);
    noStroke();
    rect(x+2, y+2, w-4, h-4);
    return;
  }
  boolean horiz = random(1) < splitBias;
  float cut = lerp(MIN_RATIO, MAX_RATIO, random(1));
  if (horiz) {
    splitRect(x, y,            w, h*cut,       d+1);
    splitRect(x, y+h*cut,      w, h*(1-cut),   d+1);
  } else {
    splitRect(x,       y, w*cut,     h, d+1);
    splitRect(x+w*cut, y, w*(1-cut), h, d+1);
  }
}

void saveWithMeta(long s) {
  String base = String.format(sketchPath("output/s%04d_%d"), count, s);
  saveFrame(base + ".png");
  saveStrings(base + ".txt", new String[]{
    "seed="+s, "rootHue="+rootHue,
    "splitBias="+splitBias, "colorShift="+colorShift });
}

Run the sketch with Sketch > Run and the output/ folder fills with 20 numbered PNG files and their companion metadata text files, each fully reproducible from its seed.