Tutorial · Generative Art · Canvas 2D · JavaScript
📅 July 2026 ⏱ ≈ 20 min 🎯 Beginner

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
No build step: Everything in this tutorial runs by pasting the final script into a <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();
}
Why this reads as "designed": Every cell uses the exact same motif — only its rotation and hue vary. This constraint is what makes the final image cohere as a single composition instead of looking like unrelated random shapes scattered on a grid.

5. The Four Rules

These four constraints separate deliberate generative art from "random noise with extra steps":

  1. 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.
  2. Constrain the motif. One shape, a handful of transformations (rotate, scale, flip) — not a different random shape every time.
  3. 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.
  4. 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).

Next step: Swap the quarter-arc for a Perlin-noise flow field and you get the next tutorial in this series — Animating a Flow Field with PNG Export.

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.