Generative Art Systems
Design reproducible generative systems with exposed parameter spaces, aesthetic constraints, SVG export, and automated batch rendering.
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.