Arranging Items in a Grid

Two Strategies

There are two strategies for arranging items in a grid:

  1. Use a single loop to iterate over the number of items; derive the x and y position from the item number.
  2. image
  3. Use a nested loop to iterate over the x positions and, for each x, iterate over the y positions (left sketches). If the item number is required, derive it from the values of x and y (right sketch).
  4. image

Strategy 1: Deriving the x and y positions from the index

Review: Arranging items in a line

This builds on Arranging a Line of Items. Here is a sketch from that page:

function setup() {
	createCanvas(windowWidth, windowHeight);
}

function draw() {
	for (let i = 0; i < 30; i++) {
		let x = 20 + 50 * i;
		circle(x, 100, 40);
	}
}
image

The shape position is derived from the loop index, using the function x=10+50ix = 10 + 50i used here.

Note: From here on, the definition of setup() is not shown. Each of the following code samples assumes that the sketch also contains a setup() function:

function setup() {
	createCanvas(windowWidth, windowHeight);
}

Derive y from i as well.

function draw() {
	for (let i = 0; i < 30; i++) {
		let x = 30 + 50 * i;
		let y = 30 + 10 * i;
		circle(x, y, 40);
	}
}
image

Deriving the column (x position)

Let’s start with the angled line from above, and just look at the x position.

function draw() {
	for (let i = 0; i < 30; i++) {
		let x = 30 + 50 * i;
		let y = 30 + 50 * i;
		circle(x, y, 20);
	}
}
image

Instead of a diagonal line, where each x is greater than the previous x and each y is greater than the previous y, we would like to arrange the items on a grid. Here, each item is labelled with its index number. The x value increases from items 0 to 4, and then it resets back to the left side and increases again to 9, and so on.

image

We can use the modulo (remainder) operator % to chop it into pieces. For an input sequence i = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 etc., the modulo operator i % 5 produces 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, etc. The sketch then multiples this by 50 to make the result more visible: 0, 50, 100, 150, 200, 0, 50, 100, 150, 200, 0, 50, etc.

Note: A mnemonic for % is that it is like division / except that its value is what’s left over (the °s).

function draw() {
	for (let i = 0; i < 20; i++) {
    let column = i % 5;
		let x = 30 + 50 * column;
		let y = 30 + 50 * i;
		circle(x, y, 20);
	}
}
image

Deriving the row (y position)

Now let’s compute the y position. Since there are five items per row, the column is just the number of the item divided by five, and rounded down. For an input sequence i = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 etc., dividing by 5 produces 0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, etc. Rounding this down by using floor() produces 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, etc.

function draw() {
	for (let i = 0; i < 20; i++) {
    let row = floor(i / 5);
		let x = 30 + 50 * i;
		let y = 30 + 50 * row;
		circle(x, y, 20);
	}
}
image

Putting this together:

function draw() {
	background(100);

	for (let i = 0; i < 20; i++) {
    let row = floor(i / 5);
    let column = i % 5;

		let x = 30 + 50 * column;
		let y = 30 + 50 * row;

		circle(x, y, 20);
	}
}
image

Varying the items

We can use the row and column in other ways. This sketch gives each circle a size that is a function of its row and column.

function draw() {
	background(100);

	for (let i = 0; i < 30; i++) {
    let row = floor(i / 5);
    let column = i % 5;
		
		let x = 30 + 50 * column;
		let y = 40 + 50 * row;
		
		let size = map(row - column, 0, 10, 10, 50);
		circle(x, y, size);
	}
}
image

Using a function to draw each cell

Instead of calling circle, we can use our own function.

function draw() {
	background(100);

	for (let i = 0; i < 30; i++) {
    let row = floor(i / 5);
    let column = i % 5;
		
		let x = 30 + 50 * column;
		let y = 40 + 50 * row;
		
		myShape(x, y, row, column, i);
	}
}

function myShape(x, y, row, column, i) {
	let size = map(row - column, 0, 10, 10, 50);
	circle(x, y, size);
}
image

The following code replaces just the implementation of the myShape function. The code in draw() remains exactly the same.

function myShape(x, y, row, column, i) {
	let size = map(row - column, 0, 10, 30, 40);
	circle(x, y, size / 2);
	square(x, y, size);
	circle(x + size, y, size / 2);
	circle(x, y + size, size / 2);
	circle(x + size, y + size, size / 2);
}
image

Strategy 2: Nested loops

An alternative to deriving the row and column from the linear index i, is to use a nested loop (one loop inside of another loop) to step through the x and y values, or the row and column numbers, directly.

function draw() {
	background(100);
	
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
				let x = 30 + 50 * column;
				let y = 40 + 50 * row;

				circle(x, y, row, 20);
		}
	}
}

If we need i (the index number), we can derive it from the row and column numbers.

function draw() {
	background(100);
	
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
        let i = 5 * row + column;
				let x = 30 + 50 * column;
				let y = 40 + 50 * row;

				circle(x, y, 20);
				text(i, x, y);
		}
	}
}
image

Or we can initialize it 0, and increment it each time through the (inner) loop, to keep count of how many items we've drawn.

function draw() {
	background(100);
	
	let i = 0;
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			circle(x, y, row, 20);
      text(i, x, y);
		}
	}
}
image

Drawing different items at different grid positions

Above, the same shape is drawn at each grid position. We will add a second function, that draws a second shape. First, rename myShape to decoratedSquare. This doesn’t change the behavior of the sketch, but it will make it more readable once we add the second shape, since then we can use descriptive names instead of decoratedSquare1 and decoratedSquare2.

function draw() {
	background(100);
	
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			decoratedSquare(x, y, row, column);
		}
	}
}

function decoratedSquare(x, y, row, column, i) {
	push();
	translate(x, y);
	
	let size = map(row - column, 0, 10, 30, 40);
	circle(0, 0, size / 2);
	square(0, 0, size);
	circle(size, 0, size / 2);
	circle(0, size, size / 2);
	circle(size, size, size / 2);
	pop();
}
image

Add a second drawing function circles() and call it instead.

function draw() {
	background(200);
	
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			circles(x, y, row, column);
		}
	}
}

function circles(x, y) {
	push();
	translate(x, y);
	
	fill('white');
	circle(0, 10, 20);
	fill('yellow');
	circle(10, 10, 20);
	fill('green');
	circle(10, 0, 20);

	pop();
}
image

Now, we can call circles() for some cells and decoratedSquare() for others.

function draw() {
	background(100);
	
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			if ((row + column) % 3 > 0) {
				circles(x, y, row, column);
			} else {
				decoratedSquare(x, y, row, column);
			}
		}
	}
}
image

Animate the circles, by incorporating data from millis().

function circles(x, y) {
	push();
	translate(x, y);
	
	let dx = map(cos(millis() / 500), -1, 1, 0, 10);
	let size = 30;
	fill('white');
	circle(0, dx, 30);
	fill('yellow');
	circle(dx, dx, 30);
	fill('green');
	circle(dx, 0, 30);

	pop();
}
image

Extracting grid() into a function

Let’s extract the code that draws the grid into its own function, so that we can call it several times in order to create several grids.

function draw() {
	background(200);
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			circles(x, y, row, column);
		}
	}
}

function draw() {
	background(200);
	grid();
}

function grid() {
	for (let row = 0; row < 5; row++) {
		for (let column = 0; column < 6; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			circles(x, y, row, column);
		}
	}
}

Change rows and columns from literal numbers 5 to parameters. This allows the function that calls grid() to specify the number of rows and columns.

function draw() {
	background(200);
	grid(5, 6);
}

function grid(rows, columns) {
	for (let row = 0; row < rows; row++) {
		for (let column = 0; column < columns; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			circles(x, y, row, column);
		}
	}
}

Call grid() twice, to draw two grids of different sizes.

function draw() {
	background(200);
	grid(5, 6);
	
	translate(width / 2, height / 2);
	grid(3, 4);
}

function grid(rows, columns) {
	for (let row = 0; row < rows; row++) {
		for (let column = 0; column < columns; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			circles(x, y, row, column);
		}
	}
}
image

Appendix I: Higher-Order Programming

Higher-Order Programming involves using a function as a parameter or other value.

Make the cell drawing function a parameter of grid().

function setup() {
	createCanvas(windowWidth, windowHeight);
}

function draw() {
	background(200);
	grid(5, 6, circles);
	
	translate(width / 2, height / 2);
	grid(3, 4, circles);
}

function grid(rows, columns, drawer) {
	for (let row = 0; row < rows; row++) {
		for (let column = 0; column < columns; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			drawer(x, y, row, column);
		}
	}
}
image

Now different calls to grid() can draw different cells.

function setup() {
	createCanvas(windowWidth, windowHeight);
}

function draw() {
	background(200);
	grid(5, 6, circles);
	
	translate(width / 2, height / 2);
	grid(3, 4, decoratedSquare);
}

function grid(rows, columns, drawer) {
	for (let row = 0; row < rows; row++) {
		for (let column = 0; column < columns; column++) {
			let x = 30 + 50 * column;
			let y = 40 + 50 * row;

			drawer(x, y, row, column);
		}
	}
}
image

Appendix II: Why push and pop

function draw() {
	background(100);

	for (let i = 0; i < 30; i++) {
		let row = i % 5;
		let column = floor(i / 5);
		
		let x = 30 + 50 * column;
		let y = 40 + 50 * row;
		
		myShape(x, y, row, column, i);
	}
}

function myShape(x, y, row, column, i) {
	translate(x, y);
	
	let size = map(row - column, 0, 10, 30, 40);
	circle(0, 0, size / 2);
	square(0, 0, size);
	circle(size, y, size / 2);
	circle(0, size, size / 2);
	circle(size, size, size / 2);
}
image

Draw just the first shape

function draw() {
	background(100);

	for (let i = 0; i < 30; i++) {
		let row = i % 5;
		let column = floor(i / 5);
		
		let x = 30 + 50 * column;
		let y = 40 + 50 * row;
		
		myShape(x, y, row, column, i);
		break;
	}
}

function myShape(x, y, row, column, i) {
	translate(x, y);
	
	let size = map(row - column, 0, 10, 30, 40);
	circle(0, 0, size / 2);
	square(0, 0, size);
	circle(size, 0, size / 2);
	circle(0, size, size / 2);
	circle(size, size, size / 2);
}
image

Draw just the fourth shape

function draw() {
	background(100);

	for (let i = 0; i < 30; i++) {
		let row = i % 5;
		let column = floor(i / 5);
		
		let x = 30 + 50 * column;
		let y = 40 + 50 * row;

		if (i === 3) {
			myShape(x, y, row, column, i);
		}
	}
}

function myShape(x, y, row, column, i) {
	translate(x, y);
	
	let size = map(row - column, 0, 10, 30, 40);
	circle(0, 0, size / 2);
	square(0, 0, size);
	circle(size, 0, size / 2);
	circle(0, size, size / 2);
	circle(size, size, size / 2);
}
image

Fixed: add push() and pop()

function draw() {
	background(100);

	for (let i = 0; i < 30; i++) {
		let row = i % 5;
		let column = floor(i / 5);
		
		let x = 30 + 50 * column;
		let y = 40 + 50 * row;

		myShape(x, y, row, column, i);
	}
}

function myShape(x, y, row, column, i) {
	push();
	translate(x, y);
	
	let size = map(row - column, 0, 10, 30, 40);
	circle(0, 0, size / 2);
	square(0, 0, size);
	circle(size, 0, size / 2);
	circle(0, size, size / 2);
	circle(size, size, size / 2);
	pop();
}
image

©2020–2022 by Oliver Steele.