Introduction to p5.js

Johannes Preis
Sep 3, 2019 · 15 min read

p5.js is a JavaScript library for graphics programming which, at its core, provides a simple API for drawing to an HTML <canvas> element. In the following article, you will get to know its most important functions and features — hopefully providing you with everything you need to start writing your own sketches and continue exploring the many creative applications of this library.

This article is aimed at programming beginners but it assumes basic knowledge of JavaScript and its syntax, specifically:

If you want to get started right away, feel free to skip ahead to the Setup section.

Some background

p5.js’ API and general design were inspired by the Processing project so it’s worth taking a look what this is all about. From the official Processing website:

Processing is a flexible software sketchbook and a language for learning how to code within the context of the visual arts. Since 2001, Processing has promoted software literacy within the visual arts and visual literacy within technology. (https://processing.org/)

This connection between coding and visual arts was something which immediately resonated with me when I first used Processing several years ago. In computer science classes at university assignments often consisted of writing contrived CLI applications. It was only after I discovered Processing and played around with it for some time that some programming concepts really “clicked” for me. Having a visual output (and not just some strings in the terminal) made it easier for me to reason about what the programs were doing.
Also, Processing helps to get something drawn to the screen with almost no boilerplate code and a straightforward API.
Programs written in Processing are called sketches. Just like a sketch on a piece of paper, they’re more about quickly exploring an idea than having everything well thought out and planned beforehand. A sketch can be discarded if the idea doesn’t seem feasible or can be iterated upon to explore different variations.
This sketchbook metaphor is what fundamentally influenced the design of the Processing language and this also transitioned over seamlessly to p5.js.

Aside from being a great tool for learning how to code, p5.js has further interesting applications, like data visualisation or generative design. There are also numerous libraries extending the basic functionality of p5.js e.g. by enabling DOM interactions, integrating hardware like webcams or microphones and visualizing geodata.

But first, let’s get comfortable with the basics!

Setup

The quickest and easiest way to get started with p5.js is the p5.js web editor where we can directly start writing our code and see the results. Alternatively, you can fork this Codesandbox template.

p5.js Web Editor

If you prefer using you own code editor, you can either follow the getting started guide, clone this simple p5.js starter project or use the live-p5 extension for VS Code.

A note on p5.js’ modes

p5.js supports two modes: Global and instance mode. In global mode, as the name suggests, all functions and constants of p5.js are declared on the global object (window, since we’re working in the browser).

Since we’re just getting started, global mode is ideal for us as it requires a little less boilerplate code which lets us focus on our actual sketch. Global mode is suitable for quickly prototyping ideas and playing around — if you start using p5.js in a productive environment, consider using instance mode to avoid polluting the global scope.

All code examples in this article assume that p5.js is run in global mode.

Our first sketch

We’re now ready to write our first sketch! Type the code below into your editor of choice (if you are using the web editor, I suggest enabling Auto refresh).

The declaration of the two functions setup() and draw() is the bedrock of every sketch as those functions are treated specially by p5.js. From the docs:

The setup() function is called once when the program starts. It’s used to define initial environment properties such as screen size and background color and to load media such as images and fonts as the program starts.

Called directly after setup() , the draw() function continuously executes the lines of code contained inside its block until the program is stopped […].

Our setup() function is pretty straightforward: We simply create our drawing area using the createCanvas() function, specifying its width and height in pixels (if you don’t create a canvas manually, p5.js will create a default one for you with a size of 100x100 pixels).

That’s all the boilerplate code that we need, so let’s finally draw something — on to our draw() function! Let’s first, set the background color of our sketch using the background() function. There are many different ways to specify the color (more on that in the next section), but for now we’re just going to use a HEX color code.

Next, let’s draw an ellipse using the aptly named ellipse() function.
It takes four parameters: The first two (x and y) determine the x- and y-coordinates of the center of the ellipse whereas the third and fourth parameters (w and h) determine the width and height respectively.

Since our sketch is 400x400 pixels wide, we should see a circle that sits exactly at the center of our canvas — and sure enough, that’s what we see:

Congratulations, you’ve written your first sketch! A rather simple one that is, but it goes to show how easy it is to get something displayed on the screen with p5.js.
There are many more functions for drawing a variety of shapes provided by p5.js — lines, rectangles, triangles, polygons, Bezier curves and many more. You can check them out in the Shapes section of the p5.js reference.

If you play around with the arguments for the ellipse() function, you will notice that increasing the value for y moves the center of the ellipse further down on the canvas — contrary to what we’re used to from math class, where the y-axis is usually pointing upwards. We should have a good understanding where things are drawn on our canvas, so let’s take a moment to examine p5.js’ coordinate system.

The origin of the coordinate system (0,0) is the top-left corner of our canvas. Since it’s 400 pixels high and 400 pixels wide the canvas’ bottom-right corner has the coordinates of (399,399).
Whenever you pass coordinates as arguments to p5.js’ functions, you can also use negative values as the coordinate system extends beyond the bounds of the canvas.
Note: p5.js is also able to utilize another renderer (WebGL) which allows us to draw in 3D-space. If you’re curious, check out this introduction.

Fills, strokes and color

We have at our disposal a few functions to change how shapes are drawn by p5.js (a white fill and a 1 pixel wide black stroke are the default settings, as we’ve seen in our first sketch).

  • fill() sets the color used to fill shapes
  • noFill() disables filling
  • stroke() sets the color used to draw lines and borders around shapes
  • noStroke() disables drawing the stroke
  • strokeWeight() sets the width of the stroke used for lines, points, and the border around shapes (in pixels)

Using one of the above functions sets the fill / stroke until they’re overridden by another function call — even across multiple calls of draw():

We might expect to see a white ellipse (default fill color) and a black ellipse, since we’re only specifying the fill color after drawing the first ellipse. But the next time draw() is called, the fill color is still set to black, which is why the first ellipse also has a black fill color.

If we reduce the frame rate, we can see that on the first frame, the ellipse on the left is indeed filled with the default color. We can do this using the frameRate() function:

To fix this issue, we can simply set the fill color for the first ellipse explicitly:

Once our sketches get more complex and we start extracting our drawing logic into individual functions, we need a better way to ensure that setting the drawing style in one function does not affect how something is drawn in another one (we could always change the order in which these functions are called!). The push() and pop() functions help us achieve exactly that — we won’t go into more detail here since our sketches are still very simple, but if you ever run into problems like the one above, remember that p5.js has got you covered.

As mentioned before, there are several ways to specify color in p5.js — so far, we’ve only used a HEX RGB color codes but we can also use CSS color strings or specify the RGB or HSB and alpha values using numbers (if you’re not yet familiar with the difference between the RGB and HSB color modes I suggest checking out this short explanation).
By default, p5.js is running in RGB color mode so calling fill(255, 0, 0) would result in a red fill color (red = 255, blue = 0, green = 0). We can change the color mode passing either RGB or HSB (which are global p5.js constants) to the colorMode() function.
Enabling the HSB color mode changes the way p5.js interprets the three numbers used to specify a color when calling a color related function. Calling fill(255, 0, 0) when in HSB color mode will result in a black fill color since the brightness is set to 0 (hue = 255, saturation = 0, brightness = 0). An advantage of the HSB color mode is that it better represents how we think about colors (“this color has a certain hue, a certain brightness and a certain saturation” vs. “this color has this much red, this much blue and this much green in it”) Also, it’s much easier to animate certain color transitions from one hue to another which we will see later.

We don’t always have to provide all values to color-changing functions directly. Using the color() function, we can also store colors as variables:

Notice how the currently active color mode determines how the color variable is stored internally. We can take peek behind the curtains if we log the color variables to the console:

Note: You might be wondering why we declared the color variables outside of setup() and draw() but didn’t assign any values. The reason is that p5.js functions can only be used inside those functions (or custom functions called by them). In this case, it wouldn’t have been a problem to do both the variable declaration and assignment inside of draw() but when we have more expensive operations (like loading a large dataset), that’s something we definitely want to do inside of setup() and then we have to use these global variables.

We can also specify an alpha value for a color to determine how transparent it is. This is usually done by providing a fourth argument when specifying a color:

While the minimum alpha value (i.e. fully transparent color) is always 0 the maximum values differ between color modes. By default it’s 255 for RGB and 1.0 for HSB color mode.

One last tip to wrap up this section: When in RGB color mode, you can call all color functions with one argument if you just want to specify a grayscale value:

Putting things in motion

Up until now, our sketches have been pretty…static, but we’re about to change that! In this section, we’re going to explore how to add animations to our sketches. Many of the concepts explained here are actually not specific to p5.js and can be applied to any graphics programming library or framework.

Let’s go back to our very first sketch:

For starters, let’s just move the ellipse from left to right. The horizontal position of the ellipse is determined by the first parameter of the ellipse() function (x), so we need to replace our hardcoded x-coordinate of 200 with a values that changes over time — and that of course, calls for a variable!

We declare a new variable x which will hold the current x-position of our ellipse and initialize it with a value of 200. We then use it as the first argument in the ellipse() function call:

All that’s missing is to increase the value of x over time, which we will do inside our draw() function:

As mentioned before, draw() is called repeatedly when the sketch runs, so we can leverage this drawing loop to manipulate variables in fixed intervals which is the basis for all animation in p5.js.

And just like that, our ellipse starts moving!

Tip: We’ve already learned that we can change the frame rate using the frameRate() function. This can be helpful when we want to debug animations in our sketch, but it can also be desired for artistic effects or performance optimizations. Keep in mind that this only sets a “target” frame rate, but if there’s too much to draw, the browser might not be able “keep up”, resulting in a lower frame rate. For smooth animations we’re usually aiming to hit 60 frames per second (fps) — you can always retrieve the actual frame rate by calling frameRate() without arguments.

By adding larger values to x , we can make the ellipse move faster:

We can think of the value added to x as the speed of the ellipse. Instead of kilometers or miles per hour the unit is pixels per frame. Let’s extract that value into an aptly named variable:

At first, it might feel unintuitive to add speed to a position — what we actually want to add to x is the distance covered in a certain time interval (distance = speed * time). Our time interval is 1 frame, since we’re doing our calculation inside of draw() which makes distance = speed.

As you’re reading this, the ellipse has probably called it a day and left the canvas, never to be seen again. That’s because we keep incrementing x without ever checking if the ellipse is still inside our drawing area.
Let’s fix that by having the ellipse bounce off the right edge of the canvas once it reaches it.
In terms of our sketch, this means we have to start decreasing the value of x once it has become become equal or greater than the width of the sketch. Another way to think of it is that the ellipse has to reverse its direction of movement. We can achieve this by simply changing the sign of our speed variable:

This works as intended, but now we’re getting the same problem when the ellipse reaches the left edge of the canvas. We can easily fix this by adding another check to our if-statement:

There we go! The ellipse will now stay “contained” inside our drawing area.

Vertical movement

Our ellipse is only moving from left to right and from right to left — we could make things a little more interesting by adding vertical movement to our sketch. Let’s refactor our code a bit to make things easier along the way:

Since we’re about to have our ellipse move in two dimensions, it makes more sense to represent our variables as vectors. Thus we should also use velocity instead of speed as the name for this object (speed vs. velocity).

Right now, they are one-dimensional, containing only the component for horizontal position and movement. With what we’ve learned so far it’s now very easy to also add vertical movement: We simply add another property to our position object, which will hold the vertical position of the ellipse (position.y). We also add a second property to our velocity object (the velocity is now a two-dimensional vector), which will hold the rate and direction of the vertical movement (velocity.y).

All we have to do now is to duplicate the logic we’ve implemented before — adjusting the position of the ellipse and checking if it’s out of bounds:

We’re getting there, but there’s still room for improvement! Right now, the ellipse changes its direction when its center hits one of the four edges of the canvas. To make our animation look more believeable, let’s try to make it look like the ellipse itself was bouncing of edges (like a pool ball). Currently, our ellipse is moving “too far”, so we have to restrict its movement to a smaller portion of the canvas:

When moving towards either edge of the canvas, the ellipse has to stop several pixels short while the amount of pixels equals the radius of the ellipse. Our ellipse has a diameter of 100 pixels and thus a radius of 50 pixels, so we can update our sketch accordingly:

Nice! Our ellipse finally moves as expected. But why stop at animating movement? Any value that can change over time can be animated, so let’s try and animate the fill color of the ellipse. To make things easier, let’s switch over to HSB color mode and provide an initial fill color for our ellipse:

Animating the color means we have to change one or more values passed to the fill() functions over time. If we want to animate the hue, we simply have to change the first argument (since we’re in HSB color mode).

Just like we did for the movement of the ellipse we extract a variable for the hue and increment its value inside our draw() function. The value for hue can be thought of as the angle on the color circle (this is also the reason why the default range of values for hue is 0 to 360), so we have to reset hue to 0 once it reaches 360.

If we want to be fancy, we could also replace the incrementation and value check (lines 20 to 24) with this one-liner using the remainder operator hue = (hue + 1) % 360.

Tip: You can often achieve neat effects by not repainting the background on every frame. Let’s see what happens if we only set the background once inside of setup():

Creating responsive sketches

At this point, we might decide to change the dimensions of our sketch, e.g. to make it adapt to different screen sizes. Easy, right? We simply have to adjust our createCanvas() call:

Wait, that doesn’t look quite right! The ellipse still moves like the canvas is only 400 by 400 pixels. This is because we hardcoded the canvas width and height in our position checks. To make our sketches “respond” to arbitrary canvas sizes, we should always use the global width and height variables provided by p5.js. They hold the current canvas dimensions set in createCanvas(). We can rewrite our sketch to make use of these variables like so:

With this small adjustment we can now can use (almost) any dimensions for the canvas and the ellipse will still behave correctly (things will get weird though if either the width or the height of the canvas is smaller than the diameter of the ellipse).

We can take this idea even further and replace another hardcoded value in our sketch, namely the diameter of the ellipse. If we were to simply change the diameter in our ellipse() call and didn’t update the out-of-bounds check accordingly, we would run into a similar problem as above. So, let’s extract the diameter as a variable and use it accordingly:

Now, we can change the diameter without affecting the behaviour of the ellipse. We could even make the diameter depend upon the canvas dimensions which is especially handy if we want to increase the resolution of our sketch while preserving the same overall layout.

Wrapping up

Congratulations you made it to the end of this article! Along the way, we’ve touched upon quite a few topics and you should now have a good understanding of the fundamentals of p5.js.

We’ve seen how easy it is to get something drawn to the screen and how the simple API of p5.js supports exploring creative ideas without getting in the way. I hope that this article encouraged you to do just that — continue experimenting with this library and start filling your own sketchbook!
As a motivation, I have created a final variation of our sketch from before. Aside from the inclusion of p5.js’ random() function and some refactoring to make the code more object-oriented, there’s nothing new going on here. Try playing around with the different values for diameters, ellipse count and colors — you might be surprised how a few changes can affect the entire look of the sketch.

Thank you for reading and have fun exploring p5.js!

Further reading & resources

Passion, friendship, honesty, curiosity. If this appeals to you, Comsysto may well be your future. Apply now to join our team!

This blogpost is published by Comsysto Reply GmbH

comsystoreply

Innovation through insight.

Thanks to Vizards

Johannes Preis

Written by

Frontend Engineer at Comsysto Reply

comsystoreply

Innovation through insight. Thinking lean and moving agile when delivering software products for the digital era.

Johannes Preis

Written by

Frontend Engineer at Comsysto Reply

comsystoreply

Innovation through insight. Thinking lean and moving agile when delivering software products for the digital era.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store