All posts

p5js

13 posts

p5.js Session 13: Colour, Hex and Palettes

Session 12 covered RGB and HSB, two ways of describing a colour to p5.js. There is a third, and it is the one you will meet most often outside the editor.

Hex: one more way to write a colour

A hex code looks like #0077B6. The hash is followed by six characters, read as three pairs: 00 for red, 77 for green, B6 for blue. Each pair is one channel written in base 16, which counts 0 to 9 then A to F, so FF is 255 and 00 is 0. It is the same RGB colour, written shorter.

p5.js takes a hex code as a string, quotes included, anywhere it takes a colour. Here is one blue three times over, written three different ways.

function setup() {
  createCanvas(600, 300);
  noLoop();
  noStroke();
}
function draw() {
  background(0);

  // RGB: red, green, blue, each 0 to 255
  fill(0, 119, 182);
  circle(150, 150, 140);

  // Hex: the same colour as a string, three pairs after the hash
  fill('#0077B6');
  circle(300, 150, 140);

  // HSB: hue, saturation, brightness
  colorMode(HSB, 360, 100, 100);
  fill(200.8, 100, 71.4);
  circle(450, 150, 140);
}

Three identical blue circles on black, the same colour written as RGB, as hex, and as HSB

  • fill(0, 119, 182) and fill('#0077B6') are the same colour. 77 in hex is 119, B6 is 182.
  • The quotes matter. '#0077B6' is a string, and p5.js reads a string as a colour. Without them it is not valid code.
  • The third circle switches over with colorMode(HSB, 360, 100, 100), then names that same blue as hue 200.8, saturation 100, brightness 71.4.
  • The decimals are not fussiness. Converting a colour into HSB rarely lands on whole numbers, and rounding them would leave the third circle a shade off the other two.

Hex is worth knowing because it is what the rest of the world uses. Design tools export it, CSS uses it, and every palette site hands you a row of hex codes ready to paste.

Gathering colours into a palette

Generative art rarely picks colours one at a time. It works from a chosen set, and holding that set in an array means you write the codes once and refer to them by position afterwards.

The nine below are Ocean Blue Serenity, taken from the trending palettes on Coolors, which is where I go when I want a set of colours that already agree with each other.

One script does both halves of the job. setup() creates the palette, draw() uses it, one swatch per colour.

let palette;
function setup() {
  createCanvas(650, 350);
  noLoop();
  noStroke();
  palette = [
    '#03045E', '#023E8A', '#0077B6', '#0096C7', '#00B4D8',
    '#48CAE4', '#90E0EF', '#ADE8F4', '#CAF0F8'
  ];
}
function draw() {
  background(0);
  for (let i = 0; i < palette.length; i++) {
    fill(palette[i]);
    rect((i * 70) +10, 50, 70, 250);
  }
}

Nine swatches in a row on black, the Ocean Blue Serenity palette from darkest navy to palest blue

  • let palette; sits outside setup(), so every function in the sketch can see it.
  • Filling it inside setup() means it is built once, when the sketch starts, not on every frame.
  • Square brackets make an array. The nine hex codes go in as strings, separated by commas.
  • palette[0] is the first colour and palette[8] the last, because arrays count from 0.
  • palette[i] in the loop is the using half. Swatch one takes colour one, swatch two takes colour two, straight through to the ninth.
  • palette.length is 9, so the loop covers the whole palette without you counting anything.

A circle divided among the palette

A palette of nine and a circle of 360 degrees fit together neatly: nine wedges of 40 degrees each, one colour per wedge.

let palette;
function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  noLoop();
  noStroke();
  palette = [
    '#03045E', '#023E8A', '#0077B6', '#0096C7', '#00B4D8',
    '#48CAE4', '#90E0EF', '#ADE8F4', '#CAF0F8'
  ];
}
function draw() {
  background(0);
  let wedgeAngle = 360 / palette.length;
  for (let i = 0; i < palette.length; i++) {
    fill(palette[i]);
    arc(300, 300, 400, 400, i * wedgeAngle, (i + 1) * wedgeAngle, PIE);
  }
}

A circle divided into nine equal wedges on black, each wedge a colour from the Ocean Blue Serenity palette

  • arc(x, y, w, h, start, stop) draws a slice of an ellipse, set by the two angles.
  • PIE closes that slice back to the centre, which is what makes a wedge rather than a curved line.
  • angleMode(DEGREES), from Session 7, lets the angles be written as 0 to 360 instead of radians.
  • wedgeAngle is 360 / palette.length, so the wedges divide the circle evenly whatever size the palette is. Nine colours give 40 degrees each.
  • Wedge i runs from i * wedgeAngle to (i + 1) * wedgeAngle, so each one starts exactly where the last finished.

Nine colours, nine wedges, the whole palette visible at once. Change one hex code and one slice of the circle changes with it.

The scripts in this session were written for p5.js 2.3.2, the version the web editor runs by default.


Next session, we get a sketch out of the browser: saving your work as an image file.

p5.js Session 12: Colour, HSB and RGB

In Session 11 we built our own shapes with vertex() and splineVertex(). This session leaves shape aside and looks at colour itself.

RGB, a recap

Every colour we’ve used so far has been RGB: red, green, and blue, each from 0 to 255. Mixing the three gives you any colour on screen, but picking a specific hue this way takes some guessing, there’s no single number that means “orange.”

HSB: hue, saturation, brightness

HSB describes colour differently: hue is the colour itself, one number around a wheel from 0 to 360. Saturation is how vivid it is, 0 is grey, 100 is fully saturated. Brightness is how light or dark it is. colorMode(HSB) switches p5.js over to reading colours this way.

Let’s put twelve circles in a row and give each one a hue a little further around the wheel than the last.

function setup() {
  createCanvas(650, 200);
  colorMode(HSB, 360, 100, 100);
  noLoop();
  noStroke();
}
function draw() {
  background(0, 0, 0);
  for (let i = 0; i < 12; i++) {
    let hueValue = i * 30;
    fill(hueValue, 80, 90);
    circle(50 + i * 50, 100, 40);
  }
}

A row of 12 circles shifting through the full colour spectrum using HSB

  • colorMode(HSB, 360, 100, 100) sets the scale for each value: hue up to 360, saturation and brightness up to 100.
  • hueValue climbs by 30 on every pass of the loop, so twelve circles cover the whole wheel.
  • Saturation and brightness stay fixed at 80 and 90, so nothing changes from circle to circle except the colour itself.

This is why HSB suits generative art so well: cycling through colour is just counting, rotating a single number around the wheel, instead of juggling three RGB values at once.

Opacity: the alpha channel

Both RGB and HSB accept a fourth number in fill(): alpha, or opacity. 0 is fully invisible, the maximum (255 in RGB, 100 in HSB) is fully opaque.

Two overlapping circles, neither of them solid, show what that actually does.

function setup() {
  createCanvas(600, 600);
  noLoop();
  noStroke();
}
function draw() {
  background(0);
  fill(200, 50, 50, 150);
  circle(240, 300, 300);
  fill(50, 100, 200, 150);
  circle(360, 300, 300);
}

Two overlapping circles, red and blue, both partially transparent, blending into a third colour where they meet

  • The fourth number in fill() is the alpha. Both circles use 150 out of 255, so both are translucent rather than solid.
  • Where the two circles overlap, the red and the blue blend into a third colour.
  • The black background shows faintly through both, because neither one covers it completely.

Alpha works exactly the same way in colorMode(HSB), just add it as the fourth value there too.

The scripts in this session were written for p5.js 2.3.2, the version the web editor runs by default.


Next session, we take colour further: building a reusable palette, stored in an array, instead of picking values one at a time.

p5.js Session 11: Custom Shapes

In Session 10 we let random() decide where, what colour, and what shape to draw. This session steps back to something more basic: shapes p5.js doesn’t already have a name for.

Building a shape from points

rect(), ellipse(), and triangle() cover a lot of ground, but not everything. Let’s build a shape p5.js has no built-in name for, one point at a time.

function setup() {
  createCanvas(600, 600);
  noLoop();
  noStroke();
  fill(250, 250, 50);
}
function draw() {
  background(0);
  beginShape();
  vertex(300, 90);
  vertex(360, 240);
  vertex(510, 240);
  vertex(390, 330);
  vertex(435, 480);
  vertex(300, 390);
  vertex(165, 480);
  vertex(210, 330);
  vertex(90, 240);
  vertex(240, 240);
  endShape(CLOSE);
}

A yellow five-point star built entirely from vertex() calls

  • beginShape() opens the shape and starts collecting points. Nothing appears on the canvas yet.
  • vertex(x, y) adds one corner. They connect in the order you call them, so the order is the drawing.
  • endShape(CLOSE) finishes the shape and joins the last point back to the first.

Ten points, and a star comes out. Move any one of them and the star becomes something else entirely, which is the appeal of building shapes this way.

splineVertex(): the same points, a curved line

Same ten points, same star, but this time the edges bend instead of running straight.

function setup() {
  createCanvas(600, 600);
  noLoop();
  noStroke();
  fill(250, 250, 50);
}
function draw() {
  background(0);
  beginShape();
  splineVertex(300, 90);
  splineVertex(360, 240);
  splineVertex(510, 240);
  splineVertex(390, 330);
  splineVertex(435, 480);
  splineVertex(300, 390);
  splineVertex(165, 480);
  splineVertex(210, 330);
  splineVertex(90, 240);
  splineVertex(240, 240);
  endShape(CLOSE);
}

The same star outline, now with curved edges instead of straight ones, using splineVertex()

  • splineVertex(x, y) replaces vertex(x, y), one call per point as before, but the line between the points curves through them instead of running straight.
  • The curve passes through every point you give it, so the ten points stay exactly where they were.
  • endShape(CLOSE) still closes the shape, this time joining the last point back to the first with a curve.

The star softens into something closer to a flower. Nothing changed but the word vertex.

One warning if you go looking for more on this. Before p5.js 2.0 the function was called curveVertex(), and it worked differently: the first and last points had to be repeated, because those calls only steered the curve and were never drawn. The same star looked like this:

function setup() {
  createCanvas(600, 600);
  noLoop();
  noStroke();
  fill(250, 250, 50);
}
function draw() {
  background(0);
  beginShape();
  curveVertex(300, 90);
  curveVertex(300, 90);
  curveVertex(360, 240);
  curveVertex(510, 240);
  curveVertex(390, 330);
  curveVertex(435, 480);
  curveVertex(300, 390);
  curveVertex(165, 480);
  curveVertex(210, 330);
  curveVertex(90, 240);
  curveVertex(240, 240);
  curveVertex(300, 90);
  curveVertex(300, 90);
  endShape(CLOSE);
}

Run that in the current editor and you get ReferenceError: curveVertex is not defined. Worth knowing, because a lot of the p5.js writing online still uses the old name.

The scripts in this session were written for p5.js 2.3.2, the version the web editor runs by default. If you are following an older tutorial, some function names will differ. The one that matters here is flagged above.


Next session, we leave shapes behind for colour itself: RGB, HSB, and how to make a sketch shift smoothly through the spectrum.

p5.js Session 10: Randomness and Variation

In Session 9 we used a function to place flowers exactly where we wanted, corner by corner. Now let’s hand that decision over to chance.

random()

random(min, max) returns a decimal number somewhere between the two.

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  noLoop();
  noStroke();
}

function draw() {
  background(0);
  let x = random(100, 500);
  let y = random(100, 500);
  drawFlower(x, y);
}

function drawFlower(x, y) {
  push();
  translate(x, y);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
  pop();
}

A single flower placed at a random position within the canvas

Every time this sketch runs, the flower lands somewhere new. drawFlower() doesn’t care where its numbers come from, fixed or random, it just uses whatever it’s given.

A random number of flowers, each with its own colour

Random position was just the start. Let’s bring random() into colour and count too.

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  noLoop();
  noStroke();
}

function draw() {
  background(0);
  let count = floor(random(4, 8));
  for (let i = 0; i < count; i++) {
    let x = random(80, 520);
    let y = random(80, 520);
    let r = int(random(255));
    let g = int(random(255));
    let b = int(random(255));
    drawFlower(x, y, r, g, b);
  }
}

function drawFlower(x, y, r, g, b) {
  push();
  translate(x, y);
  for (let i = 0; i < 24; i++) {
    rotate(20);
    fill(r, g, b);
    ellipse(60, 0, 75, 15);
  }
  fill(255, 0, 0);
  circle(0, 0, 35);
  pop();
}

Several flowers, somewhere between four and seven, each at a random position and a random colour

  • count decides how many flowers to draw this run, 4 to 7.
  • x and y place each one at random, same as before.
  • r, g, and b are three random numbers between 0 and 255, one flower’s own colour.
  • int() rounds each down to a whole number, since colour values only work as whole numbers.

switch: mapping a number to a choice

Randomness isn’t just useful for position and colour, it can decide what to draw too. This time each cell gets two independent rolls: one switch for colour, one for shape.

function setup() {
  createCanvas(600, 600);
  noLoop();
  noStroke();
}

function draw() {
  background(0);
  let cols = 6;
  let cellSize = width / cols;

  for (let row = 0; row < cols; row++) {
    for (let col = 0; col < cols; col++) {
      let x = col * cellSize;
      let y = row * cellSize;
      let half = cellSize / 2;

      let colourChoice = floor(random(3));
      switch (colourChoice) {
        case 0:
          fill(187, 20, 20);
          break;
        case 1:
          fill(255, 163, 34);
          break;
        case 2:
          fill(0, 0, 178);
          break;
      }

      let shapeChoice = floor(random(8));
      switch (shapeChoice) {
        case 0:
          rect(x, y, cellSize, half);           // top half
          break;
        case 1:
          rect(x, y + half, cellSize, half);    // bottom half
          break;
        case 2:
          rect(x, y, half, cellSize);           // left half
          break;
        case 3:
          rect(x + half, y, half, cellSize);    // right half
          break;
        case 4:
          triangle(x, y, x + cellSize, y, x, y + cellSize);               // top-left corner
          break;
        case 5:
          triangle(x, y, x + cellSize, y, x + cellSize, y + cellSize);    // top-right corner
          break;
        case 6:
          triangle(x, y, x, y + cellSize, x + cellSize, y + cellSize);    // bottom-left corner
          break;
        case 7:
          triangle(x + cellSize, y, x + cellSize, y + cellSize, x, y + cellSize); // bottom-right corner
          break;
      }
    }
  }
}

A 6x6 grid of half-rectangles and corner triangles, each cell a random one of three colours

  • colourChoice picks one of three fixed colours, red, orange, or blue, before anything is drawn.
  • shapeChoice picks one of eight shapes: a half-rectangle (top, bottom, left, or right) or a triangle filling one corner (top-left, top-right, bottom-left, or bottom-right).
  • half is just cellSize / 2, used to size and position the rectangle halves.

Colour and shape are chosen independently, so any of the three colours can land on any of the eight shapes. Run it again and the grid looks completely different: same three colours, same eight shapes, but a different roll for every cell.


In Session 11 we’ll look at building shapes from scratch with our own points, instead of relying on the built-in ones.

p5.js Session 9: Functions

Last session ended with four nearly identical blocks of code, one per flower, differing only in two numbers. Functions are how we stop writing that twelve times over.

Naming a block of code

A function is a block of code with a name. You write it once, then call that name whenever you want it to run.

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  noLoop();
  noStroke();
}

function draw() {
  background(0);
  drawFlower();
}

function drawFlower() {
  push();
  translate(300, 300);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
  pop();
}

A single flower drawn by calling the drawFlower() function once

drawFlower() is the whole push()/pop() block from last session, just given a name. draw() doesn’t need to know what’s inside it, it just calls drawFlower() and trusts it to handle the rest.

Parameters

Right now drawFlower() always draws in the same spot, (300, 300), because that’s hardcoded inside it. Parameters let us pass a value in each time we call the function, so it can behave differently on demand.

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  noLoop();
  noStroke();
}

function draw() {
  background(0);
  drawFlower(150, 150);
  drawFlower(450, 150);
  drawFlower(150, 450);
  drawFlower(450, 450);
}

function drawFlower(x, y) {
  push();
  translate(x, y);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
  pop();
}

Four flowers, one in each corner, produced by calling drawFlower() four times with different x and y values

Same four-corner result as last session, but now it’s one function and four short calls instead of four copies of the same dozen lines. If we ever want to change how the flower looks, petal colour, count, size, we change it in one place and every call updates.


In Session 10 we let random() decide where, and how many times, drawFlower() gets called.

p5.js Session 8: Push and Pop

Last session’s flower lived on its own in the centre of the canvas. This time we want four of them, one in each corner. That’s where translate() and rotate() start to show their real behaviour: every change to the canvas carries over into everything drawn after it.

Translating from corner to corner

We want four flowers, one in each corner of the square: 150, 150, then 450, 150, then 450, 450, then 150, 450. The simplest option is to repeat last session’s script four times, changing only the translate() coordinates. Let’s try that and see what actually happens.

function setup() {
  createCanvas(1200, 1200);
  angleMode(DEGREES);
  noLoop();
  noStroke();
}

function draw() {
  background(220);
  fill(0);
  rect(0, 0, 600, 600);

  translate(150, 150);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);

  translate(450, 150);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);

  translate(450, 450);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);

  translate(150, 450);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
}

Only the first flower lands where intended, at 150, 150. Each translate() after it moves from wherever the origin already sits, not from the canvas corner. The second call adds 450, 150 onto the first flower’s position and lands at 600, 300; the third and fourth compound further still, and the last flower ends up almost off the canvas. Every translate and rotate call carries the ones before it.

Four flowers meant to sit inside the black square, but each translate() pushes further off course than the last

push() and pop()

push() saves the current drawing state, position, rotation, fill, stroke, like a bookmark. pop() returns to that bookmark. Anything between the two, any translate() or rotate(), is undone the moment pop() runs.

function setup() {
  createCanvas(1200, 1200);
  angleMode(DEGREES);
  noLoop();
  noStroke();
}

function draw() {
  background(220);
  fill(0);
  rect(0, 0, 600, 600);

  push();
  translate(150, 150);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
  pop();

  push();
  translate(450, 150);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
  pop();

  push();
  translate(450, 450);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
  pop();

  push();
  translate(150, 450);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(60, 0, 90, 35);
  }
  fill(255, 0, 0);
  circle(0, 0, 25);
  pop();
}

Each push() / pop() pair works in its own bubble. Whatever happens to the canvas inside it gets discarded the moment pop() runs, so the next block starts fresh from the canvas’s real origin. That’s why all four flowers land exactly on their corners this time.

Four flowers, each landing exactly on its corner position inside the black square, thanks to push() and pop()

It works, but look at what we just did: the exact same dozen lines, copied four times, with only the translate coordinates changed.


In Session 9 we fix that: functions let us name this block of code once and call it as often as we like.

p5.js Session 7: Translate and Rotate

Grids gave us control over position. This session is about control over orientation: moving where you draw from, then turning what you draw.

Moving the origin with translate()

let cx;
let cy;

function setup() {
  createCanvas(600, 600);
  noLoop();
  stroke(255, 255, 255);

  cx = width / 2;
  cy = height / 2;
}

function draw() {
  background(0);
  translate(cx, cy);
  ellipse(0, 0, 150, 150);
}
  • cx and cy store the centre point of the canvas, calculated once in setup() so we don’t repeat width / 2 and height / 2 every time we need them.
  • translate(cx, cy) moves the origin there, so ellipse(0, 0, 150, 150) lands dead centre instead of in the top-left corner.

Circle outline drawn at the canvas centre using translate()

Rotating around the origin

let cx;
let cy;

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  noLoop();
  stroke(255, 255, 255);
  cx = width / 2;
  cy = height / 2;
}

function draw() {
  background(0);
  translate(cx, cy);
  circle(0, 0, 15);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(255, 255, 255);
    rect(100, 0, 60, 60);
    fill(255, 0, 0);
    circle(100, 0, 15);
  }
}
  • angleMode(DEGREES) tells p5.js to read angles in degrees rather than radians.
  • rotate(45) turns the canvas a little further each time round, eight times in total, adding up to a full 360-degree turn.
  • Each square is drawn at the same offset point, (100, 0), but lands somewhere new around the centre because the canvas has already been rotated by a different amount.
  • The small red circle marks that anchor point on each square.
  • The white circle at (0, 0) is just a reference dot showing where the origin sits after translate().

Eight white squares arranged in a ring around the canvas centre, each marked with a red anchor point, rotated using rotate() inside a for loop

We’ve already got everything we need: a centre point, a loop, and a rotation. From here it’s just a matter of swapping the shapes for something that reads as petals instead of squares.

Building the flower

let cx;
let cy;

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  noLoop();
  noStroke();
  cx = width / 2;
  cy = height / 2;
}

function draw() {
  background(0);
  translate(cx, cy);
  for (let i = 0; i < 8; i++) {
    rotate(45);
    fill(0, 0, 255);
    ellipse(130, 0, 200, 75);
  }
  fill(255, 0, 0);
  circle(0, 0, 60);
}

Same loop and rotation as before, but the squares are gone. Each turn of the loop draws a wide, flattened blue ellipse offset from the centre, which reads as a petal once there are eight of them overlapping around a point. The red circle goes last, sitting on top in the middle as the flower’s core.

Finished flower sketch: eight blue ellipse petals rotated around a centre, with a red circle at the middle


In Session 8 we look at push() and pop(): saving and restoring the canvas position.

p5.js Session 6: Conditions

In Session 4 we built a grid where every cell looked exactly the same. That uniformity is useful, but it is also a little rigid. In this session we use conditions to make choices, so different cells can look different depending on a rule we define.

What an if statement does

Think about a traffic light. It does not show all three colours at once. It looks at the current state and picks one. An if statement works the same way: it checks whether something is true, and only runs its code if it is.

if (i % 2 === 0) {
  fill(200, 50, 50);
}

% is the remainder operator. i % 2 gives you the remainder when i is divided by 2. When i is 0, 2, or 4 the remainder is 0, so the condition is true and the fill turns red. When i is 1, 3, or 5 the remainder is 1, the condition is false, and the fill is not changed.

Here is a row of circles using that condition. Every circle starts at size 40. The if makes every other one larger. When the condition is false, nothing changes.

function setup() {
  createCanvas(600, 200);
  noLoop();
}
function draw() {
  background(0);
  fill(200, 50, 50);
  noStroke();
  for (let i = 0; i < 6; i++) {
    let d = 40;
    if (i % 2 === 0) {
      d = 80;
    }
    ellipse(75 + i * 90, 100, d, d);
  }
}
// what each line does inside the loop:
// let d = 40             <- start with a default diameter every time
// if (i % 2 === 0)       <- check: is this an even position?
//   d = 80               <- yes: override the diameter
//                        <- no: d stays at 40, nothing else happens
// ellipse(...)           <- draws using whichever d was set

Row of 6 circles alternating between large and small on a black canvas

The if / else statement

A plain if only acts when the condition is true, and does nothing when it is false. if / else covers both cases explicitly: one outcome when the condition is true, a different one when it is not.

function setup() {
  createCanvas(600, 200);
  rectMode(CENTER);
  noLoop();
}
function draw() {
  background(0);
  noStroke();
  for (let i = 0; i < 6; i++) {
    if (i % 2 === 0) {     // even position
      fill(200, 50, 50);   // red
      rect(75 + i * 90, 100, 60, 60);
    } else {               // odd position
      fill(50, 100, 200);  // blue
      ellipse(75 + i * 90, 100, 60, 60);
    }
  }
}
// what each line does inside the loop:
// if (i % 2 === 0)        <- check: is this an even position?
//   fill(200, 50, 50)     <- yes: red fill
//   rect(...)             <- and draw a square
// else                    <- no, it must be odd
//   fill(50, 100, 200)    <- blue fill
//   ellipse(...)          <- and draw a circle

Every shape is handled by one branch or the other. Not just the colour changes — the shape itself changes. Each branch is completely independent.

Row alternating red squares and blue circles on a black canvas

The if / else if / else statement

Two outcomes cover a lot, but sometimes you need more. else if lets you chain conditions. The sketch checks each one in order and runs the first branch that is true. If none match, else catches everything else.

function setup() {
  createCanvas(600, 200);
  noLoop();
}
function draw() {
  background(0);
  noStroke();
  for (let i = 0; i < 6; i++) {
    if (i % 3 === 0) {
      fill(200, 50, 50);
    } else if (i % 3 === 1) {
      fill(255);
    } else {
      fill(50, 100, 200);
    }
    ellipse(50 + i * 90, 100, 60, 60);
  }
}
// what each line does inside the loop:
// if (i % 3 === 0)        <- check: is the remainder 0? (i = 0, 3)
//   fill(200, 50, 50)     <- yes: red
// else if (i % 3 === 1)   <- no. check again: is the remainder 1? (i = 1, 4)
//   fill(255)             <- yes: white
// else                    <- remainder must be 2 (i = 2, 5)
//   fill(50, 100, 200)    <- blue
// ellipse(...)            <- draw using whichever fill was set

i % 3 cycles through 0, 1, and 2. The six circles get three colours in sequence, repeating twice across the row.

Row of 6 circles cycling through red, white, and blue on a black canvas

Into the grid

Now take all three forms of condition into a larger grid. This is a 21x21 grid on an 800x800 canvas with 50 pixels of margin on every side. Each cell starts with a black rectangle inset 5 pixels from the grid lines. A condition then decides what goes on top — six outcomes, black and white only.

function setup() {
  createCanvas(800, 800);
  noLoop();
}
function draw() {
  background(0);

  let margin = 50;
  let cols = 21;
  let cellSize = (width - margin * 2) / cols;
  let inset = 5;

  // white grid
  stroke(255);
  strokeWeight(1);
  noFill();
  for (let i = 0; i <= cols; i++) {
    line(margin + i * cellSize, margin, margin + i * cellSize, height - margin);
    line(margin, margin + i * cellSize, width - margin, margin + i * cellSize);
  }

  noStroke();
  for (let row = 0; row < cols; row++) {
    for (let col = 0; col < cols; col++) {
      let x = margin + col * cellSize;
      let y = margin + row * cellSize;
      let x1 = x + inset;
      let y1 = y + inset;
      let x2 = x + cellSize - inset;
      let y2 = y + cellSize - inset;

      // black rectangle in every cell
      fill(0);
      rect(x1, y1, cellSize - inset * 2, cellSize - inset * 2);

      // condition decides what goes on top
      let c = col % 6;
      if (c === 0) {
        fill(255);
        rect(x1, y1, cellSize - inset * 2, cellSize - inset * 2);  // full white
      } else if (c === 1) {
        fill(255);
        triangle(x1, y1, x2, y1, x1, y2);   // top-left triangle
      } else if (c === 2) {
        fill(255);
        triangle(x1, y1, x1, y2, x2, y2);   // bottom-left triangle
      } else if (c === 3) {
        fill(255);
        triangle(x2, y1, x1, y2, x2, y2);   // bottom-right triangle
      } else if (c === 4) {
        fill(255);
        triangle(x1, y1, x2, y1, x2, y2);   // top-right triangle
      }
      // c === 5: full black, nothing added
    }
  }
}
// inside the nested loop:
// x1, y1               <- top-left corner of the inset area
// x2, y2               <- bottom-right corner of the inset area
// fill(0) + rect        <- black rectangle, drawn in every cell
// let c = col % 6       <- cycles through 0, 1, 2, 3, 4, 5 across columns
// if c === 0            <- full white: white rect covers the black one
// if c === 1            <- white triangle in the top-left corner
// if c === 2            <- white triangle in the bottom-left corner
// if c === 3            <- white triangle in the bottom-right corner
// if c === 4            <- white triangle in the top-right corner
// c === 5               <- full black: nothing added, black rect stays

col % 6 cycles through six values. With 21 columns, the pattern repeats three full times across the grid, with the first three outcomes appearing once more in the final columns. The grid runs the same nested loop from Session 4. The only new ingredient is the condition that decides what each cell contains.

21x21 grid in black and white, cells showing full white, corner triangles, and full black based on column


In Session 7 we look at translate() and rotate(): translate() changes where you draw from, rotate() turns the canvas itself, and together they build a flower from rotated petals.

p5.js Session 5: While Loops

In Session 4 the for loop knew in advance how many times to run. Sometimes you don’t know. You just want to keep going until something stops you.

The while loop

Think of filling a shelf with books. You don’t count them in advance. You pick one up from the pile, check whether there’s space, place it on the shelf, and repeat. The moment the shelf is full, you stop.

A while loop works the same way. It checks a condition before each run. If the condition is true, it runs. If it is false, it stops.

function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(0);
  fill(200, 50, 50);
  noStroke();
  let x = 50;
  while (x < width) {
    ellipse(x, 200, 40, 40);
    x += 70;
  }
}

x starts at 50. After each circle, x increases by 70. The loop keeps going as long as x is less than the canvas width. When x reaches or passes 400, the condition is false and the loop stops. How many circles? p5.js works it out at runtime. You just described when to stop.

Red circles in a row on a black canvas

Shrinking rectangles

A while loop becomes more interesting when multiple things change with each step. Here four variables update together: x and y move the starting corner diagonally, w and h shrink the size. The condition is spatial: stop when x is 80 pixels from the right edge.

function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(0);
  noFill();
  stroke(200, 50, 50);
  strokeWeight(2);
  let x = 20;
  let y = 20;
  let w = 120;
  let h = 120;
  while (x < width - 80) {
    rect(x, y, w, h);
    x += 30;
    y += 30;
    w -= 4;
    h -= 4;
  }
}

Each rectangle steps 30 pixels down and to the right, and shrinks by 4 pixels on each side. No fill, just an outline: every rectangle layers over the last, and the overlapping edges create a staircase pattern across the canvas. Four things changing at once, one condition controlling when it stops.

Red outlined rectangles stepping diagonally across a black canvas, each smaller than the last

Infinite loops

Every while loop needs something inside it that eventually makes the condition false. In the example above, x += 30 is that thing. Without it, x never changes, the condition stays true, and the sketch freezes. p5.js stops responding and you have to reload the page. Before running any while loop, check that the update is there.


In Session 6 we look at conditions: how to make different things happen in different parts of the canvas.

p5.js Session 4: Loops and Grids

In Session 3 we placed each shape manually, one line of code at a time. A loop lets you describe a shape once and repeat it as many times as you want.

The for loop

Think of stamping a shape along a line. You pick it up, press it down, move it along, and repeat. A for loop does exactly that in code.

for (let i = 0; i < 5; i++) {
  // this runs 5 times
}

Three parts, separated by semicolons. let i = 0 sets the starting point. i < 5 is the condition: keep going as long as it is true. i++ increases i by 1 after each run. When i reaches 5 the condition is false and the loop stops.

function setup() {
  createCanvas(600, 400);
  noLoop();
}
function draw() {
  background(0);
  fill(200, 50, 50);
  noStroke();
  for (let i = 0; i < 6; i++) {
    ellipse(100 + i * 75, 200, 40, 40);
  }
}

Six equal circles, evenly spaced across a black canvas. Each one is placed at 100 + i * 75: when i is 0 that is 100, when i is 1 that is 175, and so on.

Six equal red circles evenly spaced in a row on a black canvas

i is not just a counter. It is a variable, and you can use it to change any property of the shape. Here it drives the size: each circle is 15 pixels wider than the last.

function setup() {
  createCanvas(600, 400);
  noLoop();
}
function draw() {
  background(0);
  fill(200, 50, 50);
  noStroke();
  for (let i = 0; i < 6; i++) {
    ellipse(50 + i * 90, 200, 15 + i * 15, 15 + i * 15);
  }
}

When i is 0 the diameter is 15. When i is 5 the diameter is 90. The same variable controls where each circle sits and how large it is.

Six red circles in a row growing from small on the left to large on the right

Once you are comfortable with a single loop, you can combine shapes inside it to build more complex patterns. The loop still runs the same way. Everything inside repeats for each value of i.

function setup() {
  createCanvas(600, 400);
  noLoop();
}
function draw() {
  background(0);
  strokeWeight(6);
  for (let i = 0; i < 5; i++) {
    noFill();
    stroke(200, 50, 50);
    ellipse(110 + i * 95, 200, 150, 150);

    noStroke();
    fill(200, 50, 50);
    ellipse(110 + i * 95, 160, 30, 30);
    ellipse(110 + i * 95, 195, 25, 25);
    ellipse(110 + i * 95, 225, 20, 20);
    ellipse(110 + i * 95, 250, 15, 15);

    fill(0, 0, 0);
    ellipse(110 + i * 95, 165, 15, 15);
    ellipse(110 + i * 95, 200, 12.5, 12.5);
    ellipse(110 + i * 95, 230, 10, 10);
    ellipse(110 + i * 95, 255, 6, 6);
  }
}

One loop, many shapes per iteration. The single value of i positions everything in the group at once.

Five groups of nested circles on a black canvas, each with a large red outline and a column of smaller red and black circles inside

Nested loops

A single loop repeats something in a line. Two loops nested together repeat it across a grid: the outer loop moves down the rows, the inner loop moves across each column.

function setup() {
  createCanvas(400, 400);
  noLoop();
  rectMode(CENTER);
}
function draw() {
  background(0);
  let cellSize = 80;
  for (let row = 0; row < 5; row++) {
    for (let col = 0; col < 5; col++) {
      let x = 40 + col * cellSize;
      let y = 40 + row * cellSize;
      fill(200, 50, 50);
      noStroke();
      rect(x, y, 60, 60);
      fill(255);
      ellipse(x, y, 40, 40);
      fill(0);
      ellipse(x, y, 20, 20);
    }
  }
}
  • cellSize controls the spacing between cells.
  • x and y are calculated fresh for each cell.
  • Inside each cell: a red square, a white ring, and a black centre, all stacked at the same point using rectMode(CENTER) from Session 3.

Every cell is identical, the grid handles the repetition.

A 5 by 5 grid of red squares each containing a white ring and a black centre

Size variation

The loop variable does not have to control position. Here row drives the size of the black circle: the further down the canvas, the bigger it grows inside its cell.

function setup() {
  createCanvas(400, 400);
  rectMode(CENTER);
  noLoop();
}
function draw() {
  background(0);
  let spacing = 80;
  for (let row = 0; row < 5; row++) {
    for (let col = 0; col < 5; col++) {
      let x = 40 + col * spacing;
      let y = 40 + row * spacing;
      let sizeA = 10 + row * 12;
      fill(200, 50, 50);
      noStroke();
      rect(x, y, 70, 70);
      fill(0, 0, 0);
      noStroke();
      ellipse(x, y, sizeA, sizeA);
    }
  }
}

The top row has circles barely visible. By the bottom row they nearly fill the cell. Every cell in the same row is identical. One variable, sizeA, drives the whole variation across the grid.

A 5 by 5 grid of red squares with black circles that grow larger from the top row to the bottom row


In Session 5 we look at while loops: a different way to repeat, useful when you do not know in advance how many times something should run.

p5.js Session 3: More Shapes

The Japan flag needed just one shape. This session we draw another flag, and for that we will add three more shapes to your toolkit.

The rect() function

rect(x, y, width, height) draws a rectangle. By default, x and y are the top-left corner. Width and height set how far it stretches from there.

function setup() {
  createCanvas(400, 400);
  noLoop();
}
function draw() {
  background(220);
  fill(200, 50, 50);
  noStroke();
  rect(75, 75, 250, 150);
}

A 250 by 150 rectangle, starting 75 pixels from the left edge and 75 from the top.

A red rectangle on a grey canvas

rectMode(CENTER)

By default, rect() positions from the top-left corner. rectMode(CENTER) changes that: x and y become the centre point, the same way ellipse() works. That makes it easier to position several shapes relative to each other on the canvas. To switch back: rectMode(CORNER).

function setup() {
  createCanvas(400, 400);
  noLoop();
  rectMode(CENTER);
}
function draw() {
  background(220);
  noStroke();

  fill(255, 255, 255);
  rect(200, 100, 250, 75);

  fill(200, 50, 50);
  rect(200, 175, 250, 75);
}

Two rectangles centred on the same vertical axis. White on top, red below. The flag is starting to take shape.

Two rectangles centred on the same axis, white on top and red below

The line() function

line(x1, y1, x2, y2) draws a straight line between two points. A line has no fill, only a stroke. stroke() sets its colour and strokeWeight() sets its thickness.

function setup() {
  createCanvas(400, 400);
  noLoop();
  rectMode(CENTER);
}
function draw() {
  background(220);
  noStroke();

  fill(255, 255, 255);
  rect(200, 100, 250, 75);

  fill(200, 50, 50);
  rect(200, 175, 250, 75);

  stroke(0, 0, 0);
  strokeWeight(10);
  line(75, 60, 75, 350);
}

The flagpole is a vertical black line, 10 pixels wide, running along the left edge of the flag.

Two rectangles with a vertical black flagpole on the left edge

The triangle() function

triangle(x1, y1, x2, y2, x3, y3) connects three points. Each pair of numbers is one corner.

function setup() {
  createCanvas(400, 400);
  noLoop();
  rectMode(CENTER);
}
function draw() {
  background(220);
  noStroke();

  fill(255, 255, 255);
  rect(200, 100, 250, 75);

  fill(200, 50, 50);
  rect(200, 175, 250, 75);

  stroke(0, 0, 0);
  strokeWeight(10);
  line(75, 60, 75, 350);

  fill(17, 69, 126);
  noStroke();
  triangle(75, 62.5, 75, 212.5, 200, 137.5);
}

The blue wedge has three corners: top-left at (75, 62.5), bottom-left at (75, 212.5), and the tip pointing right at (200, 137.5). Almost a flag. But the pole has disappeared behind the triangle, because the triangle was drawn after it. p5.js executes draw() in sequence, top to bottom. Whatever is written last appears on top.

The flag with a blue triangle added, the flagpole hidden behind it

Draw order

Moving the line to the end of draw() puts it on top of everything else.

function setup() {
  createCanvas(400, 400);
  noLoop();
  rectMode(CENTER);
}
function draw() {
  background(220);
  noStroke();

  fill(255, 255, 255);
  rect(200, 100, 250, 75);

  fill(200, 50, 50);
  rect(200, 175, 250, 75);

  fill(17, 69, 126);
  noStroke();
  triangle(75, 62.5, 75, 212.5, 200, 137.5);

  stroke(0, 0, 0);
  strokeWeight(10);
  line(75, 60, 75, 350);
}

The pole now sits in front of the flag, exactly where it should be.

The completed Czech flag with the flagpole drawn on top

Czech flag

As scripts grow it gets easy to lose track of what each part does. Comments let you add notes directly in the code. p5.js ignores them when the sketch runs. They are there for you, not the computer. Put // before any text to make it a comment. A comment on its own line is enough to label a whole block.

function setup() {
  createCanvas(400, 400);
  noLoop();
  rectMode(CENTER);
}
function draw() {
  background(220);
  noStroke();

// White top stripe
  fill(255, 255, 255);
  rect(200, 100, 250, 75);

// Red bottom stripe
  fill(200, 50, 50);
  rect(200, 175, 250, 75);

  // Blue triangle
  fill(17, 69, 126);
  noStroke();
  triangle(75, 62.5, 75, 212.5, 200, 137.5);

  // Flagpole
  stroke(0, 0, 0);
  strokeWeight(10);
  line(75, 60, 75, 350);
}

In Session 4 we look at loops: how to repeat a shape across the canvas without writing the same line of code a hundred times.

p5.js Session 2: Your First Sketch

Session 1 set up the editor and introduced the default template. In this session you write something real: a shape, a colour, a canvas that is yours.

Canvas size

You already know the canvas is your piece of paper. Those two numbers inside createCanvas() are its width and height in pixels. Change them and the paper changes size.

function setup() {
  createCanvas(600, 300);
  noLoop();
}

function draw() {
  background(220);
}

A wider canvas, 600 pixels across and 300 tall. We need to change the default canvas size: the Japan flag we are drawing in this session is wider than it is tall. Let’s use 600 by 300.

The p5.js preview panel showing a 600 by 300 grey canvas

How coordinates work

Every point on the canvas has an x position and a y position. x goes left to right. y goes top to bottom. The top left corner is (0, 0).

Think of a grid pinned at the top left of your piece of paper. As you move right, x increases. As you move down, y increases. The centre of a 600 by 300 canvas is (300, 150).

Diagram showing the canvas coordinate system with (0,0) at the top left and axes labeled

Colour: fill and stroke

Before you draw a shape, you tell p5.js what colour to use. fill() sets the colour inside the shape. stroke() sets the colour of the outline. Both take RGB values: three numbers between 0 and 255, one for red, one for green, one for blue.

fill(220, 0, 0);   // red fill
stroke(0);         // black outline

To remove the outline entirely:

noStroke();

These settings stay in effect until you change them. Set the colour before the shape you want to colour.

Your first shape: ellipse

ellipse(x, y, w, h) draws an ellipse. x and y are the centre point. w is the width, h is the height. When width and height are equal, you get a circle.

The Japan flag is a useful shape to aim for: a red circle, centred on a white canvas. It uses everything covered above.

function setup() {
  createCanvas(600, 300);
  noLoop();
}

function draw() {
  background(255);
  fill(220, 0, 0);
  noStroke();
  ellipse(300, 150, 180, 180);
}
  • background(255) fills the canvas white.
  • fill(220, 0, 0) sets the colour to red before the ellipse is drawn.
  • noStroke() removes the outline.
  • ellipse(300, 150, 180, 180) places a 180 by 180 circle at the centre of the 600 by 300 canvas.

Run it. That is your first real sketch.

The p5.js preview panel showing the finished Japan flag sketch

Saving your sketch

File > Save, or Cmd/Ctrl + S. Give the sketch a name by clicking the pencil icon next to the title at the top. Once saved, it appears in your account under File > Open.


In Session 3 we add more shapes and look at what happens when you combine them on the canvas.

p5.js Session 1: Introduction to p5.js

If you have ever wondered whether you need to be a developer to make generative art, you do not. This series goes through p5.js step by step, a tool built for designers and creatives, not computer scientists. A second series covers more complex territory, and from there the work is yours to take wherever you want.

p5.js is a JavaScript library for creative coding. It was created by Lauren Lee McCarthy in 2013 as a reinterpretation of Processing, the Java-based environment that Ben Fry and Casey Reas built in 2001 to make programming accessible to artists and designers. p5.js brings the same idea to the browser.

You can use p5.js in two ways: through the web editor at editor.p5js.org, or by setting up a local environment on your computer. This series uses the web editor throughout. No installation, no configuration - open the browser and start.

The web editor

Go to editor.p5js.org. The editor loads with a default sketch already open.

The p5.js web editor showing a new sketch with the default template

You can run sketches without an account, but you cannot save them. To save your work, click “Sign up” in the top right. You can create a free account with a username and password, or log in directly with GitHub or Google.

The p5.js web editor sign up page

Once you have an account, use File > Save (or Cmd/Ctrl + S) to save. Each saved sketch gets a name - you can rename it by clicking the pencil icon next to the sketch name at the top.

To start a fresh sketch, go to File > New. The File menu is also where you save your work, duplicate an existing sketch, or download it to your computer.

The p5.js web editor File menu with New highlighted

The default template

Every new sketch opens with this code:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
}

Think of it like sitting down to draw. Before anything else, you reach for a piece of paper. That is what createCanvas(400, 400) does: it puts a 400 by 400 pixel canvas in front of you. setup() is where that happens. It runs once when the sketch starts, and it is where you get everything ready before the drawing begins.

draw() is the drawing itself. It runs on a loop after setup, 60 times per second by default. Right now the only thing in it is background(220), which paints the whole canvas grey. The number is a greyscale value: 0 is black, 255 is white, 220 is a light grey.

Press play and you will see exactly that: a plain grey square, your blank page, ready.

The p5.js web editor showing the preview panel with a grey square canvas

The noLoop() function

Because draw() loops by default, the sketch keeps running even when nothing is changing. For a static generative art you usually want the sketch to run once and stop. Add noLoop() inside setup():

function setup() {
  createCanvas(400, 400);
  noLoop();
}

Every sketch in this series uses noLoop() unless the post is specifically about animation.

You could also put all your drawing code inside setup() and skip draw() entirely for static work, since setup is executed only once. I prefer not to. I keep setup() for configuration and draw() for the artwork, whether it moves or not. That consistency makes it easier to read your own code later, and noLoop() costs nothing.

Variables

A variable is a container. It holds a piece of information so you can use it later, or use it in multiple places. Think of it like storing your name or your age: the label stays the same, the value inside it can change.

For drawing, variables are how you store things you want to reuse. The diameter of a circle, the position of a shape on the canvas, a colour value. Instead of typing 400 in five different places, you store it once:

let canvasSize = 400;

Now you can use canvasSize anywhere in the sketch. Change the number once and it updates everywhere.

Data types

p5.js uses the same basic types as JavaScript. Three are enough for now. Numbers are any numeric value: 400, 3.14, 0. Strings are text wrapped in quotes: "hello", "red". Booleans are true or false, used for on/off decisions. You do not need to declare the type when you create a variable - JavaScript works it out from the value you assign.


That is the foundation. In Session 2 we write the first real sketch from scratch: a shape, a colour, and a canvas that is yours.