Learn Creative Coding (#5) - Loops and Grids: Repetition as a Design Tool

in #stem4 days ago

Learn Creative Coding (#5) - Loops and Grids: Repetition as a Design Tool

banner

What will I learn

  • Nested for-loops for creating grids;
  • varying properties per grid cell;
  • patterns from simple rules;
  • modular design: building complex compositions from simple pieces;
  • the power of small changes across many elements.

Requirements

  • A modern web browser;
  • Episodes #1-4 of this series.

Difficulty

  • Beginner

Curriculum (of the Learn Creative Coding Series):


Learn Creative Coding (#5) - Loops and Grids: Repetition as a Design Tool

There's something deeply satisfying about grids.

A single shape is just a shape. Repeat it 400 times in a grid and suddenly it's a pattern, a texture, a visual rhythm. Add small variations and it becomes something alive. This is one of the most powerful ideas in generative art: repetition with variation.

The basic grid

A nested for-loop gives you a grid. Nothing fancy:

function setup() {
  createCanvas(500, 500);
  background(240);
  noStroke();
  
  let spacing = 25;
  let size = 18;
  
  for (let x = spacing; x < width; x += spacing) {
    for (let y = spacing; y < height; y += spacing) {
      fill(80, 130, 200);
      ellipse(x, y, size, size);
    }
  }
}

A grid of blue dots. Clean, orderly, boring. But this is our canvas — now we start breaking it.

Vary one property

The simplest trick: make one property depend on position.

function setup() {
  createCanvas(500, 500);
  background(20);
  noStroke();
  
  let spacing = 25;
  
  for (let x = spacing; x < width; x += spacing) {
    for (let y = spacing; y < height; y += spacing) {
      // size depends on distance from center
      let d = dist(x, y, 250, 250);
      let size = map(d, 0, 350, 20, 2);
      
      fill(255, 200);
      ellipse(x, y, size, size);
    }
  }
}

dist() calculates the distance from each dot to the center. map() converts that distance into a size — big in the center, small at the edges. Instantly the grid has a focal point.

map(value, fromLow, fromHigh, toLow, toHigh) is one of p5.js's most useful functions. It rescales a value from one range to another. You'll use it everywhere.

Add noise

Now let's make it organic:

function setup() {
  createCanvas(500, 500);
  background(20);
  noStroke();
  
  let spacing = 20;
  
  for (let x = spacing; x < width; x += spacing) {
    for (let y = spacing; y < height; y += spacing) {
      let n = noise(x * 0.01, y * 0.01);
      let size = n * 20;
      
      fill(n * 200 + 55, 100, 200 - n * 100, 180);
      ellipse(x, y, size, size);
    }
  }
}

The noise field controls both size and color. Clusters of large bright dots next to clusters of small dark dots. It looks geological — like a cross-section of rock under a microscope.

Rotation per cell

Instead of varying size, vary rotation. This is where push()/pop() from episode 3 becomes essential:

function setup() {
  createCanvas(500, 500);
  background(245);
  
  let spacing = 30;
  let size = 20;
  
  for (let x = spacing; x < width; x += spacing) {
    for (let y = spacing; y < height; y += spacing) {
      push();
      translate(x, y);
      
      let angle = noise(x * 0.01, y * 0.01) * TWO_PI;
      rotate(angle);
      
      stroke(40);
      strokeWeight(1.5);
      noFill();
      rect(-size/2, -size/2, size, size);
      
      pop();
    }
  }
}

A grid of rectangles, each rotated by a different amount based on noise. The rectangles near each other have similar rotations because noise is smooth. You get this flowing, fabric-like pattern from nothing but rotated squares.

Lines instead of shapes

Grids of small lines are one of the classic generative art motifs. Vera Molnar did this in the 1960s with a plotter and it still looks stunning.

function setup() {
  createCanvas(500, 500);
  background(250);
  
  let spacing = 15;
  let len = 12;
  
  for (let x = spacing; x < width; x += spacing) {
    for (let y = spacing; y < height; y += spacing) {
      push();
      translate(x, y);
      
      let angle = noise(x * 0.008, y * 0.008) * PI;
      rotate(angle);
      
      stroke(30, 150);
      strokeWeight(1.2);
      line(-len/2, 0, len/2, 0);
      
      pop();
    }
  }
}

Short lines following a noise flow field. This is essentially a simplified version of a technique called "flow fields" — we'll go much deeper on this later, but the basic idea is right here: direction controlled by noise.

Multiple layers

Real compositions layer multiple grids on top of each other:

function setup() {
  createCanvas(600, 600);
  background(15);
  noStroke();
  
  // Layer 1: large, sparse, semi-transparent circles
  for (let x = 40; x < width; x += 80) {
    for (let y = 40; y < height; y += 80) {
      let n = noise(x * 0.005, y * 0.005);
      fill(200, 80, 80, n * 80);
      ellipse(x + random(-10, 10), y + random(-10, 10), n * 70, n * 70);
    }
  }
  
  // Layer 2: medium, denser
  for (let x = 20; x < width; x += 40) {
    for (let y = 20; y < height; y += 40) {
      let n = noise(x * 0.01 + 100, y * 0.01 + 100);  // offset to get different noise
      fill(80, 150, 200, n * 60);
      ellipse(x, y, n * 25, n * 25);
    }
  }
  
  // Layer 3: small, very dense, like stars
  for (let x = 5; x < width; x += 10) {
    for (let y = 5; y < height; y += 10) {
      if (random() > 0.7) {  // only 30% of positions
        fill(255, random(30, 100));
        ellipse(x + random(-3, 3), y + random(-3, 3), 2, 2);
      }
    }
  }
}

Three layers with different densities, sizes, colors, and noise offsets. The random(-10, 10) offsets break the rigid grid alignment — the elements feel placed, not plotted.

Notice the + 100 offset on the second layer's noise input. Without it, both layers would follow the same noise pattern and overlap perfectly. By sampling a different region of the noise space, each layer has its own flow.

Animated grid

Make it move:

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

function draw() {
  background(20);
  noStroke();
  
  let spacing = 25;
  let t = frameCount * 0.02;
  
  for (let x = spacing; x < width; x += spacing) {
    for (let y = spacing; y < height; y += spacing) {
      let n = noise(x * 0.015, y * 0.015, t);
      let size = n * 22;
      
      let hue = map(n, 0, 1, 150, 300);
      fill(hue % 255, 100 + n * 100, 200, 180);
      ellipse(x, y, size, size);
    }
  }
}

Same grid, but the noise field evolves over time. Dots swell and shrink, colors shift. It breathes.

Design principles from grids

Working with grids taught me a few things about generative art in general:

Constraint breeds creativity. A rigid grid is a constraint. But within it you have infinite freedom — size, color, rotation, offset, shape, opacity. The grid gives structure. The variation gives life.

Small changes, big impact. You don't need ten different tricks per piece. One grid + one property varying smoothly = compelling work. Adding a second variable doubles the complexity.

Symmetry is boring, near-symmetry is beautiful. A perfectly symmetric grid is wallpaper. A grid with noise-driven variation feels handmade. The imperfection is the point.

Allez, wat hebben we geleerd?

  • Nested for-loops create grids — the backbone of many generative artworks
  • dist() for distance calculations, map() for rescaling values
  • Varying one property (size, rotation, color) per cell transforms a boring grid into art
  • Noise offsets (+ 100) let multiple layers use different noise patterns
  • push()/pop() are essential when rotating individual grid elements
  • Layering multiple grids at different densities creates rich compositions
  • Random position offsets break rigid grid alignment

Next episode: interactivity. We'll make sketches that respond to mouse, keyboard, and touch. Time to let the viewer become part of the art.

Sallukes! Thanks for reading.

X

@femdev