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

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 (#1) - What Is Creative Coding? (And Why You Should Care)
- Learn Creative Coding (#2) - Your First Sketch: Shapes, Colors, and the Canvas
- Learn Creative Coding (#3) - Movement and Time: Making Things Animate
- Learn Creative Coding (#4) - Randomness: The Secret Ingredient
- Learn Creative Coding (#5) - Loops and Grids: Repetition as a Design Tool (this post)
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