Generative Art in 50 Lines of Canvas 2D
You don't need WebGL, a shader, or a framework to make generative art. This tutorial builds a complete, self-contained sketch — a seeded, randomised grid of rotated arcs that tile into flowing lines — in under 50 lines of plain Canvas 2D, runnable by opening a single HTML file.
1. The Canvas Skeleton
Every Canvas 2D sketch starts with the same four lines: grab the
<canvas> element, get its 2D rendering context,
and size it. We'll fix the canvas at a square size so the grid math
stays simple:
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = canvas.height = 600;
ctx.fillStyle = '#0a0e1a';
ctx.fillRect(0, 0, 600, 600); // dark background fill
<script> tag in
an .html file next to a single <canvas id="c">
element. No bundler, no npm install.
2. A Seeded Random Number Generator
Math.random() cannot be seeded, which means every
reload produces a different, non-reproducible piece — a problem if
you ever want to save a specific composition or make an edition of
numbered variants. A tiny mulberry32 PRNG (a
public-domain algorithm by Tommy Ettinger) fixes that in four
lines:
function mulberry32(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const SEED = 1337; // change this → new but reproducible piece
const rand = mulberry32(SEED);
From now on, use rand() everywhere instead of
Math.random(). Every value it returns is fully
determined by SEED, so the same seed always produces
the exact same artwork — essential for generative art collections
where a specific output needs to be reproducible on demand.
3. The Grid
Lay out a grid of N × N cells across the canvas.
Instead of computing absolute coordinates for every shape, we
translate the drawing context to each cell's
centre before drawing — this makes the per-cell drawing code
origin-independent and much easier to reason about:
const N = 12; // 12×12 grid
const cell = 600 / N;
for (let row = 0; row < N; row++) {
for (let col = 0; col < N; col++) {
ctx.save();
ctx.translate((col + 0.5) * cell, (row + 0.5) * cell);
drawMotif(cell); // defined in the next section
ctx.restore();
}
}
4. One Motif: the Rotated Arc
The whole sketch hinges on a single repeated motif: a quarter-circle arc from one edge of the cell to another, rotated by a random multiple of 90°. Because the arc always touches the midpoints of two adjacent edges, neighbouring cells connect into continuous flowing curves — the classic Truchet tile effect, discovered by Sébastien Truchet in 1704 and rediscovered by every generation of generative artists since:
function drawMotif(cell) {
const r = cell / 2;
const turn = Math.floor(rand() * 4) * (Math.PI / 2); // 0, 90, 180 or 270°
ctx.rotate(turn);
ctx.strokeStyle = `hsla(${200 + rand() * 60}, 70%, 65%, 0.9)`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(-r, -r, r, 0, Math.PI / 2); // quarter circle in the top-left corner
ctx.stroke();
}
5. The Four Rules
These four constraints separate deliberate generative art from "random noise with extra steps":
- Constrain the palette. Pick hues from a narrow band (here, 200–260, blues into violets) instead of the full hue wheel — a limited palette reads as intentional.
- Constrain the motif. One shape, a handful of transformations (rotate, scale, flip) — not a different random shape every time.
- Always seed your randomness. A seeded PRNG turns "random" into "chosen" — you can regenerate, compare, and curate outputs instead of losing every good result the moment you refresh.
- Leave breathing room. Not every cell needs to be filled or maximally busy — vary line weight, add empty cells with some probability, or fade opacity toward the edges.
6. Full Source (50 Lines)
Paste this into an HTML file with a
<canvas id="c" width="600" height="600"></canvas>
— it runs standalone, no dependencies:
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = canvas.height = 600;
// 1. Seeded PRNG — same seed always gives the same artwork
function mulberry32(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const SEED = 1337;
const rand = mulberry32(SEED);
// 2. Background
ctx.fillStyle = '#0a0e1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 3. Single motif: quarter arc, randomly rotated
function drawMotif(cell) {
const r = cell / 2;
const turn = Math.floor(rand() * 4) * (Math.PI / 2);
ctx.rotate(turn);
ctx.strokeStyle = `hsla(${200 + rand() * 60}, 70%, 65%, 0.9)`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(-r, -r, r, 0, Math.PI / 2);
ctx.stroke();
}
// 4. Grid of cells, each drawn in its own local origin
const N = 12;
const cell = canvas.width / N;
for (let row = 0; row < N; row++) {
for (let col = 0; col < N; col++) {
ctx.save();
ctx.translate((col + 0.5) * cell, (row + 0.5) * cell);
drawMotif(cell);
ctx.restore();
}
}
↑ This is the actual 50-line sketch above, rendered live on this page (seed 1337).
Frequently Asked Questions
What will I learn in this tutorial?
Build a complete generative-art sketch in under 50 lines of plain Canvas 2D: a randomised grid of rotated arcs, no libraries, no build step, just a canvas tag and a script.
What topics are covered in this tutorial?
This tutorial covers: The Canvas Skeleton, A Seeded Random Number Generator, The Grid, One Motif: the Rotated Arc, The Four Rules, Full Source (50 Lines).
How long does this tutorial take?
This tutorial takes approximately 20 minutes to complete.
What prerequisites do I need before starting?
This is a Beginner-level tutorial — no special preparation beyond basic JavaScript is assumed.