Generative Art
Purpose
Create visual work through code, where the interesting output comes from constrained randomness rather than unconstrained noise. Pure randomness produces mush; the craft is in the constraints.
When to Use
- Creating algorithmic or procedural visual work.
- Building a generative system that produces a family of related outputs.
- Data-driven visual work.
- Exploring composition computationally.
Capabilities
- Controlled randomness: distributions, noise fields, seeded reproducibility.
- Composition: grids, subdivision, packing, flow fields.
- Colour systems for generative palettes.
- Output at print resolution.
Inputs
- The visual idea or the underlying system.
- Constraints: palette, format, medium.
- Whether outputs must be reproducible (they should be).
Outputs
- A system producing a family of related, non-identical works.
- Seeded, reproducible output.
- Assets at the resolution the medium requires.
Workflow
- Seed the randomness — Always. An unseeded generative system produces something beautiful once and can never produce it again. This is the first thing to get right and the most commonly skipped.
- Constrain the space — Randomness within a structure. A grid whose cells vary, a palette whose members are sampled, an angle that jitters within a range. Unconstrained randomness is visual noise.
- Use noise, not uniform random, for anything spatial — Perlin or simplex noise produces coherent variation.
Math.random() per pixel produces static.
- Build the palette as a system — Sample from a small set, or from a ramp. Per-element random colour destroys any coherence the composition had.
- Generate many, select few — The output of a generative system is a distribution. Produce a hundred, choose the ones that work, and adjust the constraints toward them.
- Render at the output resolution — Not scaled up afterwards. A generative composition designed at 800px and upscaled to print looks exactly like that.
Best Practices
- An unseeded system is a system that cannot be reproduced, iterated on, or printed at a different size. Seed it from the start.
- Perlin and simplex noise are what make generative work look organic rather than random. The difference between
random() and noise(x, y) is the difference between static and a landscape.
- Constrain the colour: sample from a palette of four, or from a single ramp. Fully random colour is the most reliable way to make generative work look amateurish.
- Symmetry and repetition give the eye something to hold. Pure variation with no structure is exhausting to look at.
- The best generative systems have one or two parameters that meaningfully change the output. A system with forty knobs is unexplorable.
- Produce original systems. Do not reimplement a recognizable artist's signature work.
Examples
Seeded, constrained, and using noise for coherence:
import { createNoise2D } from "simplex-noise";
import Alea from "alea";
// Seeded: this exact image can be regenerated, at any resolution, forever.
const SEED = "2026-07-14-a";
const prng = new Alea(SEED);
const noise2D = createNoise2D(prng);
// A palette, sampled — not random colour per element.
const PALETTE = ["#0f172a", "#1e40af", "#0891b2", "#f59e0b"];
const PAPER = "#faf8f3";
function draw(ctx, width, height) {
ctx.fillStyle = PAPER;
ctx.fillRect(0, 0, width, height);
const GRID = 48;
const cell = width / GRID;
for (let x = 0; x < GRID; x++) {
for (let y = 0; y < GRID; y++) {
// Noise, not random: adjacent cells are RELATED, which is what makes
// the field read as a structure rather than as static.
const n = noise2D(x * 0.06, y * 0.06); // -1..1, spatially coherent
const angle = n * Math.PI; // a flow field
const length = cell * (0.4 + Math.abs(n) * 0.9); // varies with the field
// Colour is sampled from the palette, indexed by the field — so colour
// varies coherently too, rather than speckling.
const colour = PALETTE[Math.floor(((n + 1) / 2) * PALETTE.length * 0.999)];
// Sparse: skip cells where the field is weak. Negative space is composition.
if (Math.abs(n) < 0.18) continue;
const cx = x * cell + cell / 2;
const cy = y * cell + cell / 2;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.strokeStyle = colour;
ctx.lineWidth = cell * 0.14;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(-length / 2, 0);
ctx.lineTo(length / 2, 0);
ctx.stroke();
ctx.restore();
}
}
}
// Print: render at the final resolution, not upscaled.
// A2 at 300 DPI = 4961 x 7016 px. The grid and line widths scale with `width`,
// so the same seed produces the same composition at any size.
Notes
- The line
if (Math.abs(n) < 0.18) continue; is doing a disproportionate amount of the compositional work: it creates negative space where the field is weak, which gives the piece structure. A generative system that fills every cell is almost always worse than one that does not.
- Seeded PRNGs (Alea, and similar) are essential.
Math.random() cannot be seeded in JavaScript, which makes any system built on it irreproducible.
- Generate in batches and curate. The system's output is a distribution; your job is to shape the distribution and then choose from it.
1---2name: generative-art3description: Use when creating algorithmic or generative visual art. Covers composition through code, controlled randomness, colour systems, and building work that is varied without being arbitrary.4---56# Generative Art78## Purpose910Create visual work through code, where the interesting output comes from constrained randomness rather than unconstrained noise. Pure randomness produces mush; the craft is in the constraints.1112## When to Use1314- Creating algorithmic or procedural visual work.15- Building a generative system that produces a family of related outputs.16- Data-driven visual work.17- Exploring composition computationally.1819## Capabilities2021- Controlled randomness: distributions, noise fields, seeded reproducibility.22- Composition: grids, subdivision, packing, flow fields.23- Colour systems for generative palettes.24- Output at print resolution.2526## Inputs2728- The visual idea or the underlying system.29- Constraints: palette, format, medium.30- Whether outputs must be reproducible (they should be).3132## Outputs3334- A system producing a family of related, non-identical works.35- Seeded, reproducible output.36- Assets at the resolution the medium requires.3738## Workflow39401. **Seed the randomness** — Always. An unseeded generative system produces something beautiful once and can never produce it again. This is the first thing to get right and the most commonly skipped.412. **Constrain the space** — Randomness within a structure. A grid whose cells vary, a palette whose members are sampled, an angle that jitters within a range. Unconstrained randomness is visual noise.423. **Use noise, not uniform random, for anything spatial** — Perlin or simplex noise produces coherent variation. `Math.random()` per pixel produces static.434. **Build the palette as a system** — Sample from a small set, or from a ramp. Per-element random colour destroys any coherence the composition had.445. **Generate many, select few** — The output of a generative system is a distribution. Produce a hundred, choose the ones that work, and adjust the constraints toward them.456. **Render at the output resolution** — Not scaled up afterwards. A generative composition designed at 800px and upscaled to print looks exactly like that.4647## Best Practices4849- An unseeded system is a system that cannot be reproduced, iterated on, or printed at a different size. Seed it from the start.50- Perlin and simplex noise are what make generative work look organic rather than random. The difference between `random()` and `noise(x, y)` is the difference between static and a landscape.51- Constrain the colour: sample from a palette of four, or from a single ramp. Fully random colour is the most reliable way to make generative work look amateurish.52- Symmetry and repetition give the eye something to hold. Pure variation with no structure is exhausting to look at.53- The best generative systems have one or two parameters that meaningfully change the output. A system with forty knobs is unexplorable.54- Produce original systems. Do not reimplement a recognizable artist's signature work.5556## Examples5758**Seeded, constrained, and using noise for coherence:**5960```javascript61import { createNoise2D } from "simplex-noise";62import Alea from "alea";6364// Seeded: this exact image can be regenerated, at any resolution, forever.65const SEED = "2026-07-14-a";66const prng = new Alea(SEED);67const noise2D = createNoise2D(prng);6869// A palette, sampled — not random colour per element.70const PALETTE = ["#0f172a", "#1e40af", "#0891b2", "#f59e0b"];71const PAPER = "#faf8f3";7273function draw(ctx, width, height) {74 ctx.fillStyle = PAPER;75 ctx.fillRect(0, 0, width, height);7677 const GRID = 48;78 const cell = width / GRID;7980 for (let x = 0; x < GRID; x++) {81 for (let y = 0; y < GRID; y++) {82 // Noise, not random: adjacent cells are RELATED, which is what makes83 // the field read as a structure rather than as static.84 const n = noise2D(x * 0.06, y * 0.06); // -1..1, spatially coherent85 const angle = n * Math.PI; // a flow field86 const length = cell * (0.4 + Math.abs(n) * 0.9); // varies with the field8788 // Colour is sampled from the palette, indexed by the field — so colour89 // varies coherently too, rather than speckling.90 const colour = PALETTE[Math.floor(((n + 1) / 2) * PALETTE.length * 0.999)];9192 // Sparse: skip cells where the field is weak. Negative space is composition.93 if (Math.abs(n) < 0.18) continue;9495 const cx = x * cell + cell / 2;96 const cy = y * cell + cell / 2;9798 ctx.save();99 ctx.translate(cx, cy);100 ctx.rotate(angle);101 ctx.strokeStyle = colour;102 ctx.lineWidth = cell * 0.14;103 ctx.lineCap = "round";104 ctx.beginPath();105 ctx.moveTo(-length / 2, 0);106 ctx.lineTo(length / 2, 0);107 ctx.stroke();108 ctx.restore();109 }110 }111}112113// Print: render at the final resolution, not upscaled.114// A2 at 300 DPI = 4961 x 7016 px. The grid and line widths scale with `width`,115// so the same seed produces the same composition at any size.116```117118## Notes119120- The line `if (Math.abs(n) < 0.18) continue;` is doing a disproportionate amount of the compositional work: it creates negative space where the field is weak, which gives the piece structure. A generative system that fills every cell is almost always worse than one that does not.121- Seeded PRNGs (Alea, and similar) are essential. `Math.random()` cannot be seeded in JavaScript, which makes any system built on it irreproducible.122- Generate in batches and curate. The system's output is a distribution; your job is to shape the distribution and then choose from it.