p5.js Expert Article 4

Generative Art Systems

Design parameter spaces, manage output editions, build reproducible series, and think systematically about aesthetic rule-sets.

⏱ 25 min read generative parameter space seeds edition algorithmic composition

Thinking in parameter spaces

A generative system is a function: f(seed, params) → artwork. The interesting work is designing the space of possible outputs — the parameter space — so that every point in it produces something worth looking at.

Key design questions:

  • Which parameters control the character of the piece (not just scale or colour)?
  • Are all parameter combinations aesthetically valid, or are there constraints?
  • How much variation should come from the seed vs the parameters?

Separating seed from parameters

const PARAMS = {
  palette:     'cool',     // 'warm' | 'cool' | 'mono'
  density:     0.6,        // 0–1
  flowScale:   0.003,
  curveCount:  200,
  strokeAlpha: 40,
  strokeWidth: 0.8
};

let seed = 42;

function setup() {
  createCanvas(1200, 1200);
  noLoop();
  generate(seed, PARAMS);
}

function generate(s, p) {
  randomSeed(s);
  noiseSeed(s);

  background(p.palette === 'cool' ? '#0d0d1a' : '#1a0a00');
  drawFlowField(p);
}

function draw() {}   // noLoop — draw happens in generate()

function keyPressed() {
  if (key === 'n') {
    seed++;
    generate(seed, PARAMS);
    print('Seed:', seed);
  }
  if (key === 's') {
    saveCanvas(`piece-${seed}`, 'png');
  }
}

Incrementing the seed lets you explore the series. Saving with the seed in the filename means you can regenerate any piece later.

Parameter ranges and validation

Define a schema that describes valid parameter space:

const SCHEMA = {
  density:    { min: 0.1, max: 1.0, default: 0.5, type: 'float' },
  curveCount: { min: 50,  max: 500, default: 200, type: 'int'   },
  palette:    { options: ['cool', 'warm', 'mono', 'earth'], default: 'cool' }
};

function randomParams() {
  let p = {};
  for (let [key, def] of Object.entries(SCHEMA)) {
    if (def.options) {
      p[key] = random(def.options);
    } else if (def.type === 'int') {
      p[key] = floor(random(def.min, def.max));
    } else {
      p[key] = random(def.min, def.max);
    }
  }
  return p;
}

Palette systems

Pre-defined palettes keep a series visually coherent:

const PALETTES = {
  cool: {
    bg:      '#0a0a14',
    strokes: ['#3b82f6', '#6366f1', '#8b5cf6', '#06b6d4', '#a78bfa']
  },
  warm: {
    bg:      '#140a00',
    strokes: ['#f97316', '#ef4444', '#eab308', '#fb923c', '#fcd34d']
  },
  mono: {
    bg:      '#0a0a0a',
    strokes: ['#ffffff', '#cccccc', '#888888', '#444444', '#1a1a1a']
  },
  earth: {
    bg:      '#1a1008',
    strokes: ['#92400e', '#78350f', '#d97706', '#fbbf24', '#a16207']
  }
};

function getStroke(paletteName, index) {
  let palette = PALETTES[paletteName];
  return palette.strokes[index % palette.strokes.length];
}

Edition management

For a series of N works from N seeds:

class Edition {
  constructor(n, generatorFn) {
    this.n = n;
    this.generate = generatorFn;
    this.current  = 0;
    this.hashes   = {};
  }

  async exportAll(delay = 200) {
    for (let i = 0; i < this.n; i++) {
      this.current = i;
      this.generate(i);
      await new Promise(r => setTimeout(r, delay));
      saveCanvas(`edition-${String(i).padStart(3, '0')}`, 'png');
    }
  }

  preview(seed) {
    this.generate(seed);
  }
}

let edition = new Edition(100, s => generate(s, PARAMS));

function keyPressed() {
  if (key === 'e') edition.exportAll();  // export all 100
}

Deterministic randomness — hash functions

Hash the seed + a counter for reproducible per-object randomness without calling random() in sequence:

function hash(seed, index) {
  let h = (seed * 2654435761 + index * 40503) >>> 0;
  h ^= h >>> 16;
  h *= 0x45d9f3b;
  h ^= h >>> 16;
  return (h >>> 0) / 4294967296;
}

// Usage: deterministic random value for particle i
let x = hash(seed, i * 3)     * width;
let y = hash(seed, i * 3 + 1) * height;
let r = hash(seed, i * 3 + 2) * 20 + 5;

This way you can change other aspects of the sketch without shifting every random value downstream.

Aesthetic constraint systems

The most interesting generative systems impose hard aesthetic constraints:

  • Golden ratio compositions: all major proportions are φ ratios
  • Colour harmony rules: only complementary or analogous palettes
  • Spatial tension: no two major elements within N pixels of each other
  • Gestalt grouping: elements cluster around attractors with Gaussian falloff

Example — ensuring no isolated points by grouping into clusters:

function generateClusters(numClusters, pointsPerCluster) {
  let points = [];
  for (let c = 0; c < numClusters; c++) {
    let cx = random(100, width - 100);
    let cy = random(100, height - 100);
    let spread = random(30, 120);

    for (let p = 0; p < pointsPerCluster; p++) {
      points.push({
        x: cx + randomGaussian(0, spread),
        y: cy + randomGaussian(0, spread)
      });
    }
  }
  return points;
}

Key takeaways

  • Separate seed (which output) from params (what kind of output) — both are first-class
  • randomSeed() + noiseSeed() make every aspect of the output reproducible from a single integer
  • Define parameter schemas with valid ranges; generate random valid parameter sets for exploration
  • Pre-defined palettes enforce visual coherence across a series
  • Hash functions provide deterministic per-object randomness that doesn’t depend on call order
  • Aesthetic constraint systems are the difference between random noise and intentional generative work