Programming with p5.js

p5.js is a JavaScript library based on the Processing programming language

p5.js provides many easy-to-use functions for doing things like:

  • drawing 2-dimensional shapes, such as ellipses, rectangles, lines, polygons, curves, etc.
  • display images and text in a way that lets them be modified in any way imaginable
  • easily read input from the mouse and keyboard
  • play sounds and music
  • play and process video

we’ll use p5.js to create some nice pictures and interesting programs as a way to learn more about the features of JavaScript

Our First p5.js Program

to use p5.js you need a web page and some JavaScript code

our web basic p5.js web page will look like this:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">

    <title>p5.js Starter Site</title>

    <!-- load jQuery -->
    <!-- <script src="scripts/jquery-3.2.1.min.js"></script> -->

    <!-- load your JavaScript code -->
    <script src="scripts/main.js"></script>

    <!-- if you want p5.js, uncomment the following line -->
    <script src="scripts/p5/p5.min.js"></script>

    <!-- if you want p5.js sound functions, uncomment the following line -->
    <!-- <script src="scripts/p5/p5.sound.minjs"></script> -->

    <!-- load CSS styles from styles/mystyle.css -->
    <link rel="stylesheet" type="text/css" href="styles/mystyle.css">

</head>

<body>

<h1>p5.js Starter Site</h1>

<div id="p5canvas"></div>

</body>

</html>

notice there is a div element with the id p5canvas

this is where the output of our p5.js code will be displayed

now in main.js lets add this code:

'use strict';

function setup() {
    // create the p5.js canvas and attach it to the HTML element whose id is
    // p5canvas
    createCanvas(500, 500).parent('p5canvas');
}

function draw() {
    // draw an ellipse
    // with center=(250, 250)
    // width=200, height=80
    ellipse(250, 250, 200, 80);

    // draw a rectangle
    // with upper left corner=(250, 250)
    // width=200, height=80
    rect(250, 250, 200, 80);
}

first, all p5.js that we write will have at least these two functions: setup() and draw()

the setup() functions is always called first, and it is called exactly one time; it’s purpose is to do whatever set-up is necessary for the program, such as loading image files, setting initial values of variables, etc.

after setup() finishes, the draw() function is called again and again at approximately a rate of 60 times per second; the idea is that draw() will draw one frame of animation on the canvas

the particular draw() function above draws exactly the same thing every time, and so the image never changes

mouseX and mouseY

here’s a p5.js program that lets you move an ellipse around the screen:

function setup() {
    createCanvas(500, 500).parent('p5canvas');
}

function draw() {
    ellipse(mouseX, mouseY, 200, 80);
}

mouseX and mouseY return the coordinates of the mouse pointer in the p5canvas

if you move the mouse pointer around, then the values of mouseX and mouseY usually change on each call to draw()

thus, the ellipse moves around the screen and leaves a trail

Clearing the Canvas

if you don’t want the ellipse to leave a trail as you move it around, then you need to “clear the screen” at the start of each call to draw()

to do that, we’ll use the background function to set the color of the background:

function setup() {
    createCanvas(500, 500).parent('p5canvas');
}

function draw() {
    background(200, 200, 200);

    ellipse(mouseX, mouseY, 200, 80);
}

background(200, 200, 200) sets the entire canvas background to the RGB color (200, 200, 200), i.e. a shade of gray

now the ellipse leaves no trail

The fill and stroke of a shape

the fill(r, g, b) function sets the fill-color of the shapes that follow it to be the RGB color (r, g, b)

the stroke(0, 0, 255) function sets the color of the stroke, i.e. the color of the line around the shape

function setup() {
    createCanvas(500, 500).parent('p5canvas');
}

function draw() {
    background(200, 200, 200);

    fill(255, 0, 0);
    stroke(0, 0, 255);
    ellipse(mouseX, mouseY, 200, 80);
}

now the ellipse is red in the middle with a thin blue line (the stroke) around the edge

two other useful functions you should know about are noStroke() and noFill()

noStroke() turns off the stroke completely

noFill() turns off the fill completely, so that the whatever color is under the ellipse will be seen

Other basic drawing functions

p5.js has a number of other functions for drawing shapes that you can find in the P5 reference

for example:

  • line(a, b, c, d) draws a line between the points (a, b) and (c, d)
  • triangle(a, b, c, d, e, f) draws a triangle whose corner points are (a, b), (c, d), and (e, f)
  • quad(a, b, c, d, e, f, g, h) draws a 4-sided quadrilateral whose corner points are (a, b), (c, d), (e, f), and (g, h)
  • using beginShape, endShape, and vertex, you can create any sequence of connected points; see the reference for beginShape for examples of how this works
  • arc(x, y, width, height, startAngle, stopAngle) draws a portion of the ellipse whose center is (x, y) and dimensions are width and height. The ellipse is draw just from the startAngle to the stopAngle
  • bezier and curve are a little more involved, but both can be used to draw different kinds of curves between points

another sometimes useful function is smooth(), which, after it is called, will try to draw curves in a way that makes their edges look smoother than usual

Screen Coordinates

the p5.js canvas uses a standard 2-dimensional coordinate system for specifying the location of pixels

recall that the screen is essentially a big rectangle of pixels, where a pixel is the smallest element of the screen we can name

suppose we have a p5.js canvas that is 500 pixels wide and 500 pixels high

the columns of pixels are numbered, from left to right, 0, 1, 2, ..., 499 (not 500!)

the rows of pixels are numbered, from top to bottom, 0, 1, 2, ..., 499 (not 500!)

the coordinates of a pixel are specified by its column number x followed by its row number y

for example, the pixel (175, 388) is the pixel in column 175 and row 388

here are a few important pixels coordinates to know:

  • (0, 0) is the upper-left-most pixel
  • (499, 0) is the upper-right-most pixel
  • (0, 499) is the lower-left-most pixel
  • (499, 499) is the lower-right-most pixel

this coordinate system is very similar to the usual one from mathematics, except for a couple of important differences:

  • in p5.js, the origin point (0, 0) is at the upper-left-most corner; in mathematics, the origin point is usually in the center
  • in p5.js, the y-values get bigger as you go down; in mathematics, the y-values get bigger as you go up

Example: Symmetry

the following examples shows how you can draw the same shape in pleasing symmetrical patterns

'use strict';

function setup() {
    createCanvas(500, 500).parent('p5canvas');
    smooth();
}

function draw() {
    background(150);   // try commenting out this statement

    noStroke();
    fill(255, 0, 0);

    ellipse(mouseX, mouseY, 100, 100);
    ellipse(500 - mouseX, mouseY, 100, 100);
    ellipse(mouseX, 500 - mouseY, 100, 100);
    ellipse(500 - mouseX, 500 - mouseY, 100, 100);

    fill(0, 255, 0);
    ellipse(mouseY, mouseX, 100, 100);
    ellipse(500 - mouseY, mouseX, 100, 100);
    ellipse(mouseY, 500 - mouseX, 100, 100);
    ellipse(500 - mouseY, 500 - mouseX, 100, 100);
}

Random Numbers

JavaScript has a standard random number generating function called Math.random() that returns a random number in the range [0, 1)

p5.js provides a handy function called random(lo, hi) that returns a random number in the range [lo, hi)

if lo is 0, then p5.js lets you write just random(hi)

for example, here’s a program that changes the color of circle randomly on each call to draw() so that it appears to flash:

'use strict';

function setup() {
    createCanvas(500, 500).parent('p5canvas');
}

function draw() {
    background(150);   // try commenting out this statement

    noStroke();
    fill(random(256), random(256), random(256));
    ellipse(mouseX, mouseY, 100, 100);
}

another interesting trick is to add a small random number to, say, the position of the circle so that it appears to jiggle:

function draw() {
    background(220);

    noStroke();
    fill(255, 0, 0);
    ellipse(mouseX + random(-2, 2), mouseY + random(-2, 2), 100, 100);
}

Reading Mouse Clicks

let’s write a p5.js program that draws a circle wherever you click the mouse

to do this we’ll use the mousePressed() function

mousePressed is automatically called every time you press a mouse button

'use strict';

function setup() {
    createCanvas(500, 500).parent('p5canvas');
    background(200);
}

function draw() {
    // do nothing
}

function mousePressed() {
    fill(255, 0, 0);
    noStroke();
    ellipse(mouseX, mouseY, 100, 100);
}

draw() does nothing in this program; the only time anything is drawn on the screen is when a mouse button is pressed

Example: Simple Drawing

another useful feature of p5.js are the pmouseX and the pmouseY variables

pmouseX is the x-location of the mouse pointer in the previous call to draw()

pmouseY is the y-location of the mouse pointer in the previous call to draw()

here is a program that draws a line wherever you move the mouse:

'use strict';

function setup() {
    createCanvas(500, 500).parent('p5canvas');
    background(200);
}

function draw() {
    // draw a blue line between the current mouse location
    // and the mouse location in the previous call to draw
    stroke(0, 0, 255);
    line(mouseX, mouseY, pmouseX, pmouseY);
}

one way to make this program better is to draw only when a mouse button is pressed

we can do that using the pre-defined p5.js variable mouseIsPressed and if-statement like this:

'use strict';

function setup() {
    createCanvas(500, 500).parent('p5canvas');
    background(200);
}

function draw() {
    stroke(0, 0, 255);
    if (mouseIsPressed) {
        line(mouseX, mouseY, pmouseX, pmouseY);
    }
}

now the line is only draw when you hold down a mouse button, which is a big improvement

another nice little feature is to change the mouse when the a line is being drawn

you can do this with the p5.js cursor() function

function draw() {
    stroke(0, 0, 255);
    if (mouseIsPressed) {
        cursor(ARROW);
        line(mouseX, mouseY, pmouseX, pmouseY);
    } else {
        cursor(CROSS);
    }
}

notice that we need the else part of the if-statement to restore the mouse pointer to the style we want it to be when a line is not being drawn

Reading the Keyboard

the p5.js pre-defined function keyPressed() is called automatically whenever you press a key on the keyboard

for example, this program draws a random circle whenever you press a key

'use strict';

function setup() {
    createCanvas(500, 500).parent('p5canvas');
    background(200);
}

function draw() {
    // do nothing
}

function keyPressed() {
    // choose a random fill color
    fill(random(256), random(256), random(256));
    noStroke();

    let diam = random(10, 100);
    let randX = random(500);
    let randY = random(500);
    ellipse(randX, randY, diam, diam);
}

after you press a few keys the screen will fill up with randomly colored circles

let’s add the ability to clear the screen when the user presses the C key

to do this, we will check the value of the pre-defined p5.js variable key inside an if-statement; if key is a 'c' or 'C', then we’ll clear the screen instead of drawing a circle

'use strict';

function setup() {
    createCanvas(500, 500).parent('p5canvas');
    background(200);
}

function draw() {
    // do nothing
}

function keyPressed() {
    if (key === 'c' || key === 'C') {
        background(200);
    } else {
        // choose a random fill color
        fill(random(256), random(256), random(256));
        noStroke();

        let diam = random(10, 100);
        let randX = random(500);
        let randY = random(500);
        ellipse(randX, randY, diam, diam);
    }
}

Example: A Bouncing Ball

let’s write a p5.js program that makes a ball bounce around the screen

this is quite a useful little program, as it shows some of the basic techniques for simple animation

we will represent our bouncing ball with four variables:

  • x and y represent the location of the center of the ball at any time
  • dx and dy represent how far (and in what direction) the ball moves on each call to draw(); (dx, dy) is called the velocity of the ball

the idea is that the ball will start at, say, the center of the screen, and then on each call to draw() we’ll draw the ball and then change it’s x-location by the amount dx, and its y-location by the amount dy

this will cause the ball to move in a straight line at a constant speed

'use strict';

let x, y;    // position of the ball
let dx, dy;  // velocity of the ball

function setup() {
    createCanvas(500, 500).parent('p5canvas');

    // set the initial value of the ball's position and velocity
    x = 250;
    y = 250;
    dx = -1.5;  // try setting these
    dy = 1.1;   // randomly
}

function draw() {
    background(220);

    fill(255, 0, 0);
    noStroke();
    ellipse(x, y, 100, 100);

    // update the position of the ball
    x += dx;
    y += dy;
}

this works, but there is an obvious problem: the ball quickly disappears off the screen and never returns

so lets modify it to “bounce” off the edges of the screen

the screen has four edges, so we will use four if-statements to check to see if the ball is off the screen

'use strict';

let x, y;    // position of the ball
let dx, dy;  // velocity of the ball

function setup() {
    createCanvas(500, 500).parent('p5canvas');

    // set the initial value of the ball's position and velocity
    x = 250;
    y = 250;
    dx = -1.5;  // try setting these
    dy = 1.1;   // randomly
}

function draw() {
    background(220);

    fill(255, 0, 0);
    noStroke();
    ellipse(x, y, 100, 100);

    // update the position of the ball
    x += dx;
    y += dy;

    // if the ball has gone off the screen, reverse its direction

    // gone off left?
    if (x < 0) {
        dx = -dx;
    }

    // gone off right?
    if (x >= 500) {
        dx = -dx;
    }

    // gone off top?
    if (y < 0) {
        dy = -dy;
    }

    if (y >= 500) {
        dy = -dy;
    }
}

the statement dx = -dx; causes the ball to reverse direction along the x-axis

the statement dy = -dy; causes the ball to reverse direction along the y-axis

some other interesting things to try:

  • instead of making the ball bounce on an edge, you can make it “wrap-around” of the other side of the screen; for example, when the ball hits the right edge, running x = 0 instead dx = -dx will cause wrapping
  • you might prefer to make the ball bounce off an edge as soon as any part of hits the edge; in the above program the ball goes half-way into the edge, and doesn’t reverse until the center hits the edge; you can do this by checking the four points at the top, bottom, left, and right of the ball
  • it’s easy to experiment with gravity-like effects by changing dx and dy a little bit in each call to draw(); for example, try putting dy += 0.01; right after the statement y += dy — this will cause the ball to move differently, as if it is being pulled downwards by gravity
  • commenting-out the background statement will cause the ball to leave a trail, which can draw some interesting patterns

Global Variables and Local Variables

in the program above, we used four global variables: x, y, dx, and dy

a variable is said to be global if it was declared outside of any function

any function can read, or modify, a global variable

in contrast, a local variable is a variable declared inside some function

a local variable can only be read, or modified, inside the function where it was declared

p5.js uses, and encourages, the use of a lot of global variables

this is one of the reasons it is easy to use

however, most experienced programmers believe that global variables are generally a bad thing, and you should avoid them whenever possible

the problem with global variables is that it can be very hard to keep track of how they change within a large program since any part of the program could potentially modify it

it contrast, local variables can only be modified inside the function they were declared within, and so it is usually easier to understand how they are changing and being used

Hit Detection with Circles

hit detection is the name of the general problem of determining if a given point “hits”, or is inside of, some other shape

for example, how can you tell if the mouse pointer is inside, or outside, of a circle?

this turns out to be not too hard (at least compared to other kinds of hit detection!)

suppose the center of your circle is (x, y), and it has diameter diam

then inside of draw() you could use an if-statement like this to do something when the most pointer is in the circle:

if (dist(x, y, mouseX, mouseY) < diam/2) {
    // the mouse pointer is in the circle
    // do something!
}

dist is a pre-defined p5.js function that returns the distance between two points, i.e. dist(x, y, a, b) is the distance between points (x, y) and (a, b)

it’s entirely up to you to decide what to when the mouse pointer is inside the circle

for example, you could make it stop by doing this:

if (dist(x, y, mouseX, mouseY) < diam/2) {
    dx = 0;
    dy = 0;
}

A Trick for Stopping Everything

the following trick is sometimes used in video games to stop all the animation

let gameOver = false;  // global variable

// ...

function draw() {
    if (gameOver) {
        // do something, e.g. print "game over" on the canvas
        return;
    }
    // ...
}

function keyPressed() {
    gameOver = true;
}

in this example, when any key is pressed gameOver is set to true, which causes the draw() to call the code in the if-statement at the top

Using Text in p5.js

you display text in the p5.js canvas using the functions textFont, textSize, and text

please see the p5.js documentation page for text to see an example of how to use fonts

also, the sample program at the end of these notes has more examples

Example: A Dodging Game

here’s a sample program that combines many of the small features discussed above

it’s a game where the goal is to avoid touching the red ball for as long as possible

you get 1 point every second that you survive; notice how the p5.js function millis() is used

millis() returns the number of milliseconds (a millisecond is 1/1000th of a second) that have elapsed since the start of the program

millis()/1000 returns the number of seconds since the start of the program

Math.floor(millis()/1000) returns the number of seconds since the start of the program, rounded-down to the nearest integer

'use strict';

// global variables
let x1, y1;
let dx1, dy1;
let diam1;
let fillColor1;

let x2, y2;
let dx2, dy2;
let diam2;
let fillColor2;

let gameOver;

function setup() {
    createCanvas(500, 500).parent('p5canvas');
    textFont('Georgia');
    textSize(32);

    let gameOver = false;

    // initialize ball1
    x1 = 250;
    y1 = 250;
    dx1 = random(-5, 5);
    dy1 = random(-5, 5);
    diam1 = random(50, 150);
    fillColor1 = color(255, 0, 0);

    // initialize ball 2
    x2 = 250;
    y2 = 250;
    dx2 = random(-5, 5);
    dy2 = random(-5, 5);
    diam2 = random(50, 150);
    fillColor2 = color(random(256), random(256), random(256));
}

function draw() {
    if (gameOver === true) {
        textSize(90);
        text('Game over', 10, 250);
        return;
    }

    background(220);

    if (dist(x1, y1, mouseX, mouseY) < diam1 / 2) {
        gameOver = true;
    }

    updateBall1();
    updateBall2();

    fill(0);
    text('Score: ' + Math.floor(millis() / 1000), 20, 30);
}

function updateBall1() {
    fill(fillColor1);
    noStroke();
    ellipse(x1, y1, diam1, diam1);

    // update the position of the circle
    x1 += dx1;
    y1 += dy1;

    let radius = diam1 / 2;

    // off right?
    if (x1 + radius >= 500) {
        dx1 = -dx1;
    }

    // off bottom?
    if (y1 + radius >= 500) {
        dy1 = -dy1;
    }

    // off left?
    if (x1 - radius < 0) {
        dx1 = -dx1;
    }

    // off top?
    if (y1 - radius < 0) {
        dy1 = -dy1;
    }
}

function updateBall2() {
    fill(fillColor2);
    noStroke();
    ellipse(x2, y2, diam2, diam2);

    // update the position of the circle
    x2 += dx2;
    y2 += dy2;

    let radius = diam2 / 2;

    // off right?
    if (x2 + radius >= 500) {
        dx2 = -dx2;
    }

    // off bottom?
    if (y2 + radius >= 500) {
        dy2 = -dy2;
    }

    // off left?
    if (x2 - radius < 0) {
        dx2 = -dx2;
    }

    // off top?
    if (y2 - radius < 0) {
        dy2 = -dy2;
    }
}