- Two Strategies
- Strategy 1: Deriving the x and y positions from the index
- Review: Arranging items in a line
- Deriving the column (x position)
- Deriving the row (y position)
- Varying the items
- Using a function to draw each cell
- Strategy 2: Nested loops
- Drawing different items at different grid positions
- Extracting grid() into a function
- Appendix I: Higher-Order Programming
- Appendix II: Why push and pop
Two Strategies
There are two strategies for arranging items in a grid:
- Use a single loop to iterate over the number of items; derive the x and y position from the item number.
- 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).
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);
}
}
The shape position is derived from the loop index, using the function 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);
}
}
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);
}
}
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.
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
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);
}
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);
}
}
}
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);
}
}
}
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();
}
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();
}
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);
}
}
}
}
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();
}
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);
}
}
}
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);
}
}
}
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);
}
}
}
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);
}
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);
}
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);
}
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();
}
©2020–2022 by Oliver Steele.