Script 08 - Loops


The 10 Print Pattern

ten_print

What do we see? Can you find the underlying algorithm?

Algorithm

  • Go row by row
  • Place a / or a \ randomly

ten_print_commodore

The original BASIC program for the Commodore 64 from the 1980s:

10 PRINT CHR$(205.5+RND(1)); : GOTO 10

Bash:

f="╱╲";while :;do print -n ${f[(RANDOM % 2) + 1]};done

This is pattern is considered a phenomenon of creative coding through its simplicity and visual appeal There is a whole book dedicated to this line of code. You can watch the author talk about his work on youtube

How could we modify the pattern? Which parameters do we have?

Spacing

ten_print_quadratic ten_print_spacing

Probability

E.g. 90% probability for a backslash

ten_print_prob

As we are repeatedly placing a / or a \ we need to learn how to repeat code for creating such a pattern. And that is coincidentally what you are going to learn in this script 😁.


Topics

  • while Loops
  • for-Loops
  • 2D Loops
    • Filling the 2D canvas
  • Grid-based Pattern

Learning Objectives

With this topic you

  • understand how to repeat code,
  • know how to implement a while and a for-loop,
  • know how to implement a 2D for-loop, and
  • understand how to create grid-based pattern.

Looping

For repeating the same code multiple times, there are two types of loops, namely while- and for-loops.

Let’s first have a look into while-loops.

while-Loop

The while loop is closely related to the if-statement. However, the if-statement block is only executed once, if the condition is true. For a while-statement, its code block is executed as long as (while) a condition is true.

// Pseudo code

while (condition) {  
    statement;
}

You have to make sure that the condition becomes false at some point!!

Otherwise you have created an infinity loop 🤬

// While Loop

let counter = 0;    // Iteration counter
while (counter < 3) { // Loop condition
  
    print('wow');     // Loop activity

    counter += 1;       // Increase counter
}
print('All done…');

ch02_04
[michaelkipp]

// Loop Inside Draw

...

function draw() {

    let counter = 0; // Iteration counter
    while (counter < 30) // Loop condition
    {
        ellipse(mouseX + random(30), mouseY + random(30), 2, 2);
        counter += 1; // Increase counter
    }
}

...

What is exactly happening here? What is repeated when?

  • The draw() function is called by default 60 times per second.
    • For each draw() call, we execute the loop, hence we draw 30 random ellipses 60 times per second.

Even though the while-loop works just fine with setting up a counter and increasing it manually, there is another way of creating loops which gives us more control and which is less error prone.

Before we have a look into for-loops, let’s review the general loop logic.

Loop Logic

Independently from the specific syntax both loops follow the same logic and include the same components.

For repeating code you need to

  • Create a variable (e.g. i) as iterator
    • Initialize the iterator with a start value
  • Give a termination condition
  • Increase the iterator in each loop
  • Check for the termination condition

The last two points can also happen in reversed order (hence, first check the termination condition and then increase the iterator).

For-Loop

The for-loop also executes a block of code the given number of times. It is used more often than the while loop because it gives you more control.

Let’s first review the above loop logic in the while-loop syntax:

let counter = 0;    // INITIALIZATION
while (counter < 3) // CONDITION
{ 
    // code
    counter += 1;   // STEP
}

Now, let’s see how the logic is arranged in a within the for-loop syntax:

// Pseudo Code

for(INITIALIZATION; CONDITION; STEP)
{
    // code
}

Here an example with the correct syntax:

for(let i = 0; i < numberOfTimes; i++)
{

    // code

}
  • You can chose any variable name as iterator
    • i as short for iterator is just typical
  • You can chose any start value
    • Most times it will be 0 though
  • You can chose any end value numberOfTimes
    • This can be a direct value or a variable
  • i++ is a shortcut for i=i+1 and represents the step size, meaning how the value of the iterator changes from iteration to iteration
    • You can chose any step size you want, e.g. i+=12

Here, the same examples that we had before with a while-loop but with a for-loop:

let counter = 0;
while (counter < 3)
{ 
    print('wow');
    counter += 1;
}

becomes:


for(let i = 0; i < 3; i++){
    print('wow');
}

or with a different variable name:

for(let counter = 0; counter < 3; counter++){
    print('wow');
}

The above follow the same logic as the while loop:

ch02_04
[michaelkipp]

// The For-Loop Inside Draw

...

function draw() {

    // let counter = 0; // Iteration counter
    // while (counter < 30) // Loop condition
    // {
    //     ellipse(mouseX + random(30), mouseY + random(30), 2, 2);
    //     counter += 1; // Increase counter
    // }

    for (let i = 0; i < 30; i++) {
        ellipse(mouseX + random(30), mouseY + random(30), 2, 2);
    }
}

What is the scope of the iteration variable?

The scope is local within the loop code block, meaning it is only visible inside the loop:

// Pseudo Code

for(let i = 0; i < numberOfTimes; i++)
{

    // i only exists here

}
print(i); //gives an error

How to create the following?

ch05_04

2D Loops

In the following, we are not learning any new syntax but only apply what we have learned above.

As we are working on a 2D canvas in x and y, often times a 2D loop is used to fill a space, for example the canvas. You can imagine this as the filling of a grid.

This usually follows the logic of:

For every row, look at every element…

ch01_05
ch01_06
ch01_07
ch01_08
ch01_09

For implementing this, we nest two loop as follows

// Pseudo Code

For every row {
    For every column {

    }
}

With the correct syntax, this looks as follows:

// 2D For-Loop

// Nested loop to run through the grid
for (let gridY = 0; gridY < numberRows; gridY++) {
    for (let gridX = 0; gridX < numberColumns; gridX++) {
        print("Row: " + gridY + " Column: " + gridX);
    }
}

But as said before, you normally used shorter names. The following is the same as this above but with different variable names. If the variables refer to x,y coordinate, you often also use x and y as iterator variable names:

// 2D For-Loop

// Nested loop to run through the grid
for (let y = 0; y < numberRows; y++) {
    for (let x = 0; x < numberColumns; x++) {
        print("Row: " + y + " Column: " + x);
    }
}

A Grid

Now, we can, for example, draw a rect at each x,y coordinate within the 2D loop. What do we need to change in the following example first?

// A Grid

let numberRows = 3;
let numberColumns = 4;

let rectSize = 50;

function setup() {
    createCanvas(1000, 1000);
    background(255);

    noLoop();
}

function draw() {

    // Nested loop to run through the grid
    for (let y = 0; y < numberRows; y++) {
        for (let x = 0; x < numberColumns; x++) {
            rect(x, y, rectSize, rectSize);
        }
    }
}
  1. We need a higher number of rows and columns
    • For example, simply the width and height of the canvas
  2. We need to take larger steps to draw fewer rectangles.
    • Here, we can use the size of the rectangle for the step size of the loop for creating a continuos grid.
// A Grid

let rectSize = 50;

function setup() {
    createCanvas(1000, 1000);
    background(255);

    noLoop();
}

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    // We are using the rectSize variable
    // to control the step size of the
    // loop
    for (let y = 0; y < height; y+=rectSize) {
        for (let x = 0; x < width; x+=rectSize) {
            rect(x, y, rectSize, rectSize);
        }
    }
}

Now, we can for example also draw ellipses along the grid.

// A Grid

let rectSize = 50;

function setup() {
    createCanvas(1000, 1000);
    background(255);

    noLoop();
    noFill();
}

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    // We are using the rectSize variable
    // to control the step size of the
    // loop
    for (let y = 0; y < height; y+=rectSize) {
        for (let x = 0; x < width; x+=rectSize) {

            rect(x, y, rectSize, rectSize);
            ellipse(x, y, rectSize, rectSize);
        }
    }
}

Why is there this strange offset around the border?

Because we are drawing the ellipses at the left upper corner (0,0) of each cell.

For that we add to the above example rectangle with stepSize as size to visualize the grid structure we are working with:

twodloop_01

We could simply shift the drawing of the ellipse to the center of the current grid cell.

// A Grid

let rectSize = 50;

function setup() {
    createCanvas(1000, 1000);
    background(255);

    noLoop();
    noFill();
}

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    // We are using the rectSize variable
    // to control the step size of the
    // loop
    for (let y = 0; y < height; y+=rectSize) {
        for (let x = 0; x < width; x+=rectSize) {

            // rect(x, y, rectSize, rectSize);
            ellipse(x + rectSize*0.5, y + rectSize*0.5, rectSize, rectSize);
        }
    }
}

For making this more interesting, we could control the width and height of the ellipses with mouseX and mouseY. For that we need to map the possible value range of 0..1000 that mouseX and mouseY can have to a smaller range, e.g., 5..500 for having a reasonable sizes for the ellipse:

// A Grid

let rectSize = 50;

function setup() {
    createCanvas(1000, 1000);
    background(255);

    // For interaction don't forget
    // to turn on the loop again!
    //noLoop();
    noFill();
}

function draw() {

    // Clearing the background in each
    // draw call
    background(255);

    // Mapping the value range mouseX, and mouseY
    // can have to 5..500 with the map function
    // https://p5js.org/reference/#/p5/map
    let diameterX = map(mouseX, 0, width, 5, 250);
    let diameterY = map(mouseY, 0, height, 5, 250);

    // Nested loop to run over
    // all pixels of the canvas
    // We are using the rectSize variable
    // to control the step size of the
    // loop
    for (let y = 0; y < height; y+=rectSize) {
        for (let x = 0; x < width; x+=rectSize) {

            // rect(x, y, rectSize, rectSize);
            ellipse(x + rectSize*0.5, y + rectSize*0.5, diameterX, diameterY);
        }
    }
}

The 10 PRINT Example

Remember this intro? Now we know how to implement the 10 PRINT example!

ch05_01
[10print.org]

We need to place randomly either a slash or backslash in each cell of our grid. In order to have a simple 50-50 decision maker, you can use the following:

let probability = 0.5;

if (random(1) < probability) //all random numbers between 0..0.49
{
    //...
}
else //all random numbers between 0.5..0.99
{
    //...
}

Using simple line drawing for the slashes and styling the output:

// 10Print Pattern

let cellSize = 50;

// Threshold for which
// slash to draw
let probability = 0.5;


function setup() {
    createCanvas(1000, 1000);
    background(255);

    noLoop();
}

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    // We are using the rectSize variable
    // to control the step size of the
    // loop
    for (let y = 0; y < height; y += cellSize) {
        for (let x = 0; x < width; x += cellSize) {


            // Switch which "character" to draw
            // based on probability
            if (random(1) < probability) {

                line(x, y, x + cellSize, y + cellSize);

            } else {

                line(x, y + cellSize, x + cellSize, y);

            }
        }
    }
}

More Examples

For a better understanding of the grid structure and also of operators, here a couple of examples.

How do you need to control the fill command to create the following examples?

ch05_10

// Grid Examples - STEP 2

let stepSize = 100;


function setup() {
    createCanvas(1000, 1000);
    background(100);

    noLoop();
    
    background(255);
    noFill();
}

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    for (let y = 0; y < height; y+=stepSize) {
        for (let x = 0; x < width; x+=stepSize) {


            fill(255);
            // Changing the fill color
            // only for the cells on the
            // diagonal
            if ( y == x)
            {
                    fill(0);
            }

            rect(x, y, stepSize, stepSize);
        }
    }
}

ch05_11

// Grid Examples

...

stroke(0);
fill(255);

if (x > y) {
    stroke(255);
    fill(0);
}

...

ch05_12

The overall logic to create a checkerboard is to fill every other cell black and to shift that every other row.

You could also say that in the even rows (meaning the 0., 2., 4. row…), the even columns (meaning the 0., 2., 4. column…) should be black, and in the uneven rows, the uneven cells should be black.

You can identify even numbers with the modulo operator.

The Modulo Operator

The modulo operator returns for a division with a whole number the rest of that division:

// Pseudo Code

5 / 2 is 2 (the quotient) with rest 1
7 / 5 is 1 with rest 2

x / y is quotient q with rest r
x = q * y + r

This comes in handy when testing for even numbers:

let number = 7;

if (number % 2 == 0) 
{
    print("even");
}

In our example, we can not work directly with the pixel coordinates, as by adding an even stepSize for the grid, we only have even pixel coordinates, such as 0, 100, 200,… We need to divide the coordinates by stepSize to get the indices of the cells, with which we then want to do the modulo operation. Hence coloring a checkerboard can be done as follows:

// Grid Examples

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    for (let y = 0; y < height; y += stepSize) {
        for (let x = 0; x < width; x += stepSize) {

            fill(255);

            // We need to devide by stepSize
            // to get the indices
            let row = y / stepSize;
            let column = x / stepSize;


            if ((row % 2 == 0) && (column % 2 == 0)) {
                fill(0);
            } 
            else if ((row % 2 != 0) && (column % 2 != 0)) {
                fill(0);
            }

            rect(x, y, stepSize, stepSize);
        }
    }
}

The following is a shorter versions of the above logic. It is left to the interested reader to figure it out:

// Grid Examples

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    for (let y = 0; y < height; y += stepSize) {
        for (let x = 0; x < width; x += stepSize) {

            fill(255);

            // We need to divide by stepSize
            // to get the indices
            let row = y / stepSize;
            let column = x / stepSize;


            if ((row + column) % 2 == 0) {
                fill(0);
            } 

            rect(x, y, stepSize, stepSize);
        }
    }
}

You could also utilize the grid logic for a tile-based coloring of the canvas in HSB mode, such as in the following example. Once again, it is left to the interested reader to figure it out. For that keep in mind that you can set the ranges of the HSB channels to whatever ranges you want:

ch05_13

// Grid Examples

let stepSize = 25;

function setup() {
    createCanvas(1000, 1000);

    noStroke();
    noLoop();

    colorMode(HSB, width, height, 100);
}

function draw() {

    // Nested loop to run over
    // all pixels of the canvas
    for (let y = 0; y < height; y += stepSize) {
        for (let x = 0; x < width; x += stepSize) {

            fill(x, height - y, 100);
            rect(x, y, stepSize, stepSize);
        }
    }
}

Summary

  • Loops
while(i < numberOfTimes)
for(int i = 0; i < numberOfTimes; i++)
  • 2D Loop
    • For every row look at every element…
for (let y = 0; y < numberRows; y++)
{
    for (let x = 0; x < numberColumns; x++)
    {
        print("Row: " + y + " Column: " + x);
    }
}

Use the reference 🚒


The End

🎨 🎡 🔗