Basics of p5.js

What is p5.js?

p5.js is a javascript library which you can basically think of as Processing's javascript version. It allows users to do graphical programming easily. In this workshop, we are going to use p5.js to create visuals iteratively and make multiples do the same, or make them each do something similar but different.

Getting started with p5.js

The first thing that we need to do is downloading the library files and importing it to the directory that we are going to be using in.

The library files can be downloaded from here. There is also an option to use the library files without downloading through CDN.

If you have downloaded p5.js library files, conventionally, you would create a folder inside the directory of the folder you are working in called "libraries" or "libs" and copy the library files, in this cas files called p5.js inside of that folder. Next we need to reference this file so we can use it in our html pages. We need to put something like this inside of our <head> tag.

<script language="javascript" src="libraries/p5.js"></script>
                
If you are going with the CDN option, which means you are referencing to a file on the web, you should be doing this instead:
<script language="javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.8/p5.js"></script>
                
And now, we are ready to code using p5.js!

Creating rectangles using for-loops

First, we need to create a new javascript file to do our coding using p5.js in. If you haven't done this already, create a folder for your javascript files to be in, and create a new javascript file.

Before we do any serious coding, let's create the basic structure of p5.js - creating setup and draw.

        function setup(){

        }
        function draw(){

        }
      
The code you put inside setup section will run only once at the very beginning of the load - hence the name "setup". Here is where you would usually define the size of your sketch, background color and initialize values, arrays, objects, etc,. The code you put in draw runs continuously over time - running 60 frames per second by default. We will put code that is to do with animating our visual elements in here.

Let's create a simple sketch of a rectangle traveling from one end of the sketch to another, and repeat this process continuously.

      function setup() {
        createCanvas(640, 140);
        rectMode(CENTER);
      }
      function draw(){
        background(0);
        stroke(255);
        fill(100);
        rect(frameCount % width, height / 2, 100, 100);
      }
      
There are some things to note in this sketch. p5.js has some values you can use that are pre-defined by the library itself. width and height are set by p5 library based on the size you give when you first create the canvas. frameCount is also another one of the predefined variables that keep track of how many frames the draw loop has displayed since the program has started to run. This value is useful when we are trying to animate something repeatedly as long as the frameCount is increasing.

% operand stands for the modulo operand. Modulus is basically a calculation of the remainder of a division. In the case of our example, frameCount % width will return the remainder of the operation frameCount / width. frameCount % width essentially returns values from 0 to width - 1, in this order and in repetition.

Now, instead of trying to make the rectangle move from left to right, we are going to try to rotate the rectangle.

      function setup() {
        createCanvas(windowWidth, 140);
        rectMode(CENTER);
        noStroke();
      }

      function draw() {
        background(0);
        translate(width / 2, height / 2);
        rotate(frameCount);
        rect(0, 0, 100, 100);
      }
      

The important things to note in this example are the arguments in the translate() function and the x and y coordinates in rect(). You will see that I've put the actual coordinates of the rectangle in translate instead of in rect(). This is because in order for us to rotate a rectangle in its place, we are essentially translating our canvas to let (width / 2, height / 2) be our new origin. This is why, in this new translated canvas, if we draw our rect at (0, 0), it renders as if it is at (width / 2, height / 2).

Next, we are going to put some iteration to our program to create multiple rotating rectangles.

      var col = 5;
      var c_width;

      function setup() {
        createCanvas(windowWidth, 140);
        rectMode(CENTER);
        noStroke();
        background(0);

        c_width = width / col;
      }

      function draw() {
        background(0);

        for (var i = 0; i < col; i++) {
          push();
          translate(i * c_width + c_width / 2, height / 2);
          rotate(frameCount);
          fill(255);
          rect(0, 0, 100, 100);
          pop();
        }
      }
      

In this example, we create a variable called col to indicate the number of columns we want to have, which we use to divide up our canvas in to 5 columns. Then we can calculate the width of each column by doing width / col. I've saved this value to a variable called c_width, which stands for cell width. We use a for-loop to create rectangles iteratively. Because our rectMode is set to CENTER, we have to calculate the center point of these divided columns. We do this by i * c_width + c_width / 2. The rest is the same, except for the fact that we surround our code inside of the for-loop with push() and pop().

If you are familiar with processing, this is equivalent to pushMatrix() and popMatrix(). This makes each of our iterated operations contained and prevents transformations of an iteration from affecting others. Without this push() and pop(), the operations will add on from the previous iterations, meaning the first translation will be at 1 * c_width + c_width / 2, but the second translation will be at 1 * c_width + c_width / 2 PLUS 2* c_width + c_width / 2.

Now, with this knowledge as a base, let's use a nested for-loop to create a matrix of rotating rectangles.

      var col = 10;
      var row = 8;

      var c_width, c_height;

      function setup() {
        createCanvas(500, 500);
        rectMode(CENTER);
        noStroke();
        background(0);

        c_width = width / col;
        c_height = height / row;
      }

      function draw() {
        background(0);

        for (var j = 0; j < row; j++) {
          for (var i = 0; i < col; i++) {
            push();
            translate(i * c_width + c_width / 2, j * c_height + c_height / 2);
            rotate(frameCount);
            fill(random(255), random(255), random(255));
            rect(0, 0, 30, 30);
            pop();
          }
        }
      }
      

Here, we need to add variable row and c_height in order to set the number of rows and calculate the cell's height. We need to put another for-loop outside of our exising loop. In regards to the order of events of the nested for-loop, in our case, when j = 0, we will be at our 1st row, and we start drawing one rect per column until we are done with our iteration at when i < col (i = col - 1). When one row is done being drawn, it moves down to the next row and so on. We can calculate our center point for our height in the same way that we do for width by doing j * c_height + c_height / 2.

Before we move on in terms of learning more about iterative methods, let's look at ways that we can make this graphics a little easier on our eyes.

      var col = 10;
      var row = 5;

      var c_width, c_height;

      function setup() {
        createCanvas(windowWidth, 240);
        rectMode(CENTER);
        colorMode(HSB);
        noStroke();
        background(0);

        c_width = width / col;
        c_height = height / row;
      }

      function draw() {
        background(0, 30);

        for (var j = 0; j < row; j++) {
          for (var i = 0; i < col; i++) {
            push();
            translate(i * c_width + c_width / 2, j * c_height + c_height / 2);
            rotate(frameCount * 0.05);
            fill(frameCount % 360, 360, 360);
            rect(0, 0, 30, 30);
            pop();
          }
        }
      }
      

The first thing to note is that our rotation has slowed down. This is because, inside the rotate() function, it now has * 0.05 after the frameCount. This decreases the speed of the rotation.

Secondly, we've changed the colorMode to HSB, which stands for Hue, Saturation, Brightness. This color mode makes it easy for us to go through the primary colors, as we only need to change one number, hue. In this example, saturation and brightness is set to their maximum values, 360. We can also use frameCount variable in our color value. Which will make it change from red to blue, and on repeat.

The last thing to look at is the second number in our background() function. This second number indicates the opacity of the background, and using this will create this "fading" or "trailing" visual.

Mouse Interactions

Just like setup() and draw() functions for p5.js, the code inside of the mousePressed() function will run whenever a mouse press is detected.

    function mousePressed(){
        //run when mouse is pressed
    }
                

There are also environment variables of p5.js that make it easy to use mouse positions to create visuals.

  • mouseX: x position of mouse ( = event.clientX)
  • mouseY: y position of mouse ( = event.clientY)
  • pmouseX: x position of mouse in the PREVIOUS frame
  • pmouseY: y position of mouse in the PREVIOUS frame
    function setup() {
        createCanvas(windowWidth, windowHeight);
    }

    function draw() {
        stroke(random(255), random(255), random(255));
        strokeWeight(5);
        line(pmouseX, pmouseY, mouseX, mouseY);
    }

    function mousePressed() {
        fill(random(255), random(255), random(255));
        ellipse(mouseX, mouseY, 50, 50);
    }
                

Looking at the demo code above, you will see that the lines are drawn whenever the mouse moves around. This is because the environment values (mouseX, mouseY, pmouseX and pmouseY) are used inside of the draw() function, meaning that it will update the values for every frame of the animation of the canvas. The ellipse is drawn only upon pressing of the mouse, because the code is inside of the mousePressed() function.

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

    function draw() {
        background(0, 30);

        stroke(255);
        strokeWeight(5);
        line(pmouseX, pmouseY, mouseX, mouseY);
    }

    function mousePressed() {
        fill(random(255), random(255), random(255));
        ellipse(mouseX, mouseY, 50, 50);
    }
                

The difference between the above demo code and the previous one is whether the background is redrawn for every frame or not. The opacity of the background color is what makes the "trailing" visual effect.

Keyboard Interactions

Similar to mousePressed() function, the keyPressed() function will detect and run code inside.

    function keyPressed(){
        //run when a key is pressed
    }
                

There are also environment variables that keep track of the last key on the user's keyboard that was pressed:

  • key: the value of last ASCII key pressed
  • keyCode: the value of last non-ASCII key pressed

The key variable keeps track of the last ASCII key that was pressed - meaning that any LETTER key on your keyboard. The keyCode variable stores the value of any other non-ASCII "special" keys - the conclusive list of these offered by p5.js are: BACKSPACE, DELETE, ENTER, RETURN, TAB, ESCAPE, SHIFT, CONTROL, OPTION, ALT, UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW.

    var xPos;
    var yPos;
    var redVal;
    var greenVal;
    var blueVal;

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

        xPos = width / 2;
        yPos = height / 2;

        redVal = random(255);
        greenVal = random(255);
        blueVal = random(255);

        noStroke();
    }

    function draw() {
        //background(0, 30);

        fill(redVal, greenVal, blueVal);
        ellipse(xPos, yPos, 100, 100);
    }

    function keyPressed() {
        redVal = random(255);
        greenVal = random(255);
        blueVal = random(255);

        if(keyCode == LEFT_ARROW){
            xPos -= 15;
        }else if(keyCode == RIGHT_ARROW){
            xPos += 15;
        }else if(keyCode == UP_ARROW){
            yPos -= 15;
        }else if(keyCode == DOWN_ARROW){
            yPos += 15;
        }
    }
                
Code from Workshop:
function setup(){
	//runs once before draw
	createCanvas(windowWidth, windowHeight);
	noStroke();

	// ellipseMode(CORNER);
	rectMode(CENTER);
}

//width, height
//windowWidth, windowHeight
//mouseX, mouseY
//pmouseX, pmouseY

function draw(){
	//runs on loop
	background(0, 30);

	noStroke();
	for(var y = 0; y < 10; y++){
		for(var x = 0; x < 10; x++){
			push();
			//fill(random(255), random(255), random(255));
			translate(x * (width / 10), y * (height / 10));
			rotate(frameCount * 0.01);
			fill(150, 150, 255, 100);
			rect(0, 0, 10, 150);
			push();
			rotate(frameCount * 0.05);
			noFill();
			stroke(random(255), random(100, 150), random(100, 150), 50);
			strokeWeight(5);
			ellipse(0, 0, 75, 50);
			pop();
			pop();
		}
	}

	stroke(255, 0, 0);
	strokeWeight(25);
	line(pmouseX, pmouseY, mouseX, mouseY);
	noStroke();
	fill(0, 0, 255);
	ellipse(mouseX, mouseY, 15, 15);
}

function windowResized(){
	resizeCanvas(windowWidth, windowHeight);
}