Learning the P5 Drawing Library With ES6 and Webpack
P5 is a fantastic library for working with the canvas. I’ve used it a bunch for small coding experiments. Mostly I’ve worked with it in one file and referenced it through a script tag in the HTML, but that doesn’t scale too well. I recently had a larger project I wanted to use P5 for and decided to get it up-and-running with file bundling and modern Javascript ES6 features. This is my implementation, a simple boilerplate and project to build off of.
The best part of P5 is how quickly you can apply what you’ve learned here. When you get done You’ll be positioned to make your own interesting sketches.
The Idea
Moire patterns are interference patterns. You take a shape, duplicate it, then move, rotate and scale the duplicate around. Because the points are still close to the original location, our brains tend to group them together. Thus, rotating a duplicate of a random field of dots reads as rings to our brain.
That’s one of the simplest types of moire patterns, but there are many more. Here’s one I made with concentric rings:
Yes, we’re going to learn how to get P5 set up in a modern Javascript development environment. But the fun part will be making these patterns ourselves. So let’s get to it.
Setup
If you know a little bit about Javascript development, you can skip most of this section. To you: know that we’re using webpack-dev-server and Babel with ES6 stage 2. We’ll also need the P5 library installed. If you have no clue what I just said: good news! This section is for you.
I’m going to assume you have Node installed and can use the console at least a bit. If not, follow this tutorial to install Node/NPM and this interactive CodeAcademy course for the terminal. Trust me, even if you only ever plan on doing creative coding, doing this is well worth the effort.
Ok, let’s get started. Create a folder for your project. Next, navigate into it in your console and run npm init. You will be asked a bunch of questions. Fill it out as much as you can, but keep in mind that the details don’t matter too much. You can alter them later and some things, like the entry point, we won’t be using at all.
Now we can use the Node Package Manager (NPM) to install our project’s dependencies. We need a few: Webpack, webpack-dev-server, babel, babel-core, babel-loader, babel-preset-2015, and babel-preset-stage-2.
Let’s get those installing first, then I’ll explain what they do:
npm install -s webpack webpack-dev-server babel babel-core babel-loader babel-preset-2015 babel-preset-stage-2Okay, let’s take those one at a time.
Webpack is a Javascript bundler, among other things. We’ll use it to combine different Javascript files into one. This will let us break out our code in future articles and keep our scripts easy to read and follow.
Webpack-dev-server is a webserver with hot reloading that automatically bundles code and runs your webpack configuration file. It will make our lives easier as we develop. You can also run Webpack in listen mode manually with webpack --colors --progress --watch , but using the dev server is both more convenient and helps combat CORS issues when developing locally.
Babel converts a codebase from one language/language standard to another. In this case, it takes code written in modern Javascript and converts it into code that almost any browser can understand. Babel is a fantastic tool that frequently pops up in Javascript development. In React development, for example, you frequently use .JSX files. These files are mostly pure Javascript, but they have HTML embedded into them. All JSX code can be converted to pure Javascript, but writing the equivalent code would be messy. That’s why we let Babel do it for us.
Both babel-preset-2015 and babel-preset-stage-2 are Babel definitions for ES6. The first is ES6 by the yearly standard, while the second is for language features that didn’t quite get finalized by the time the ES6 standard came out. We’ll mostly use if for the spread operator (which looks like an ellipses, or …) which makes cloning data objects simple. If you move into React development, you’ll see the ellipses a lot.
Setting up the Project
Okay, so you have everything installed and ready. There’s a few files we need to set up before we get to the P5 development. First up, let’s get the structure done. Here’s how I have mine set up:
project
--+ index.HTML
--+ main.js
--+ package.json
--+ webpack.config.jsSimple enough. If your project grows, you can easily move the code into a js directory.
Let’s go ahead and populate our HTML page:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Moire Patterns</title>
<link rel="stylesheet" href="./css.css">
</head>
<body>
<div id="test"></div>
</body>
<script src="bundle.js"></script>
</html>Again, simple. You might notice the bundle.js file that doesn’t actually exist yet. That’s because we won’t be creating it. Rather, that will be the output file for Webpack. Speaking of, we need to fill in the the Webpack configuration file.
module.exports = {
entry: "./main.js",
output: {
path: __dirname,
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015', 'stage-2']
}
}
}
]
}
};Okay, let’s take this slow.
Entry is the main Javascript file of your project. I’m using a main.js, but you can call it whatever you like as long as it exists. The output property holds an object with a path, which should be kept as __dirname. That just gives you the local path directory to work with.
The filename is the name of the bundled code that Webpack will output. It’s also what we’ll reference in index.html. Next up are the module and loaders. Modules are bundles of code that do things. Think of them like you would Node packages, but for Webpack.
Loaders handle reading different languages. What we care about is loading Babel, so our code gets transpiled from modern Javascript into something even Internet Explorer can run. Test is a way to compare different files to see which rule should be applied. Note the exclude command: that stops it from testing files in the node_modules directory, which would take a very long time. Finally, we provide some options to babel-loader, which is what gets used. Those options are two presets: es2015 and stage-2. es2015 is ES6, along with all the language features brought in with it. Some of those features weren’t quite finalized by the time ES6 officially released, however, so there’s a bit of a hotfix called stage-2. I mostly included that because the spread operator is insanely powerful. You’ll see it used once we get to the coding, but know that React development features it heavily.
One last line. We’re going to add a custom command to the package.json file to run webpack-dev-server. You can skip this and run the server directly instead, but its handy to know how to make a command. Open up your package.json file and find the scripts object:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}Let’s add a line for our command.
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "echo 'Starting Webpack dev server! \n' && webpack-dev-server"
}Okay, so what we’re doing here is providing a name property that holds a string. That string gets interpreted by the console. If you try typing echo ‘hello world!’ into the console, you’ll see the console print out hello world! The && command tells the console to also run the next command, which in this case is the execution of webpack-dev-server.
One last thing. Let’s add some simple CSS to make the canvas anchor into the upper-left corner:
canvas{
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0;
}With that done, we’re finally ready to code up our moire pattern!
The boilerplate
Okay, we need some basic HTML to support our sketch. Nothing fancy, just your classic boilerplate:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Moire Patterns</title>
<link rel="stylesheet" href="./css.css">
</head>
<body>
</body>
</html>We also need a link to our CSS bundled code. We’ll be writing our stuff in a main.js file, but we need to add the bundle.js file to the site. Let’s do that down at the bottom:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Moire Patterns</title>
<link rel="stylesheet" href="./css.css">
</head>
<body>
</body>
<script src="bundle.js"></script>
</html>We also have a reference to a CSS file that hasn’t been created yet. So let’s make it and give it one selector, targeting the canvas element:
canvas{
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0;
}This will make sure that the canvas is positioned at the absolute corner no matter what else we add to the page. I use fixed here so its always visible, but feel free to mix it up as you see fit.
Now let’s set up our main.js file. First, importing P5:
import * as p5 from './p5.js';Then, create a new P5 object:
import * as p5 from './p5.js';
const P5 = new p5();Done this way, P5 will expect a function to be passed in. So we need to create one and then pass it into the new p5 object.
import * as p5 from './p5.js';let s = (sk) => {
}
const P5 = new p5(s);
Okay, now we get to the fun part, making sketches. Most of our development is going to happen inside that function we created. When running, P5 will check to see if certain functions are present which, if they are, it will call. There are lots of different functions you can provide (you can see them all by heading over to the P5 documentation and checking out the structure category) but the two we’ll be working with are setup and draw. Let’s create them now.
let s = (sk) => {
sk.setup = () =>{
}
sk.draw = () =>{
}
}Setup is used to establish your sketch. It only runs once, when the script starts. Draw, on the other hand, is called many times a second. If you wish, you can alter the rate by calling sk.frameRate() and passing in the frames you want per second.
Okay, so let’s create a canvas. You do this with createCanvas(). Let’s throw that in there, along with some dimensions:
sk.setup = () =>{
sk.createCanvas(window.innerWidth,window.innerHeight);
}The two references to the window get the dimensions of the displayed browser page. That’s why we gave it an absolute value in the CSS. If you want to put that canvas as a child of another element, you can chain another function onto it: sk.createCanvas(window.innerWidth,window.innerHeight).parent('elementId');
But we don’t need that.
Now, if you inspect your page, you will see a full-screen canvas element. Awesome! Now we can draw things to it. Of course, you’re still looking at a white screen. Let’s fix that:
sk.setup = () =>{
sk.createCanvas(window.innerWidth,window.innerHeight);
sk.background(40);
}And like that, we have a background. Drawing to the canvas really is that easy. If you want to draw a box, you can do sk.rect(x,y,width,height);. If you want to draw a line, instead do sk.line(x1,y1,x2,y2);. Drawing a point is done via sk.point(x,y);.
If you tried those out, you might have noticed that the line and point were hard to see. they’re a bit hard to see. They’re both really thin, and black on dark grey isn’t the most discernable combination. So let fix it:
sk.setup = () =>{
sk.createCanvas(window.innerWidth,window.innerHeight);
sk.background(40);
sk.stroke(200);
sk.strokeWeight(3);
}If you tested it now, you’ll see things have much improved. The point and line are thicker and lighter. The box, though, is still filled with white. You can fix that with sk.fill(0); to fill it with black.
In the case of sk.fill(0);, I am passing in a grayscale value into the function. In much the same way that CSS has shorthand properties, P5 does too. This is equivalent to passing in three values to the function as the Red, Green, and Blue values like sk.fill(0,0,0);. If you want a different color, you can pass in values to each of those between 0 and 255. A fourth argument can be added to handle transparency.
You can also change how colors are handled, if you prefer other color modes:
//Puts the sketch into Hue, Saturation and Brightness mode
sk.colorMode(HSB);
sk.stroke(200,100,0);If you run these you’ll notice that the first one is yellowish, while the second is purple.
P5 also accepts CSS color values as strings. That means you can call sk.stroke('#a97'); for setting a hex value, or even sk.stroke('rgba(255,204,0,0.5)'); .
Get Zachary Williams’s stories in your inbox
Join Medium for free to get updates from this writer.
Feel free to choose a color for your moire pattern!
With colors out of the way, let’s work on making our first moire pattern. We’ll keep it simple with a field of random dots. It will look a little something like this when we’re done:
To make a random point, we run sk.point(window.innerWidth,window.innerHeight);. If we want more than one dot, we stick it in a loop:
for(let i=0;i<1000;i++){
sk.point(window.innerWidth,window.innerHeight);
}And now there are a thousand random points on the screen. pretty cool, right? However, this isn’t a moire pattern yet. We need to duplicate the pattern and then offset it. When setting the position of the dots, we used random numbers without a seed. Because of that, we won’t be able to get the same field of dots just by calling the loop again. So how should we duplicate the pattern? There’s many ways go about this. We could change the for loop to push a coordinate into an array, perhaps like:
let arr [];
for(let i=0;i<1000;i++){
arr.push({x:window.innerWidth,y:window.innerHeight})
}And then to render we could do a forEach loop:
arr.forEach(el=>sk.point(el.x,el.y)); Which is equivalent to:
arr.forEach(function(el){
sk.point(el.x,el.y);
});That would let us draw all our points as many times as we want, and we’ll end up doing this in a later article. But for now, there’s a better approach. As an example of why, let’s assume first we want to translate our dots a bit to the right:
arr.forEach(el=>sk.point(el.x,el.y));
arr.forEach(el=>sk.point(el.x+5,el.y));That works. But what if we want to rotate our dots? Then the math gets more complicated. There’s a much better way to handle this: we take all the dots and turn them into an image. Then you can manipulate all the dots as if they were a single thing using tools P5 already provides you.
Let’s trash the loops and start again. First we’re going to create a graphic:
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);Notice we called it similarly to how we created the canvas in the first place. That’s because there are a lot of similarities. Think of graphics as undrawn virtual canvases. You can draw to them, set their background, their fill color, stroke etc. But they only get drawn when you tell them to, in the position you set.
Let’s change it so our dots render on the graphic rather than the canvas proper:
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);
for(let i=0;i<1000;i++){
gfx.point(
Math.random()
*window.innerWidth,Math.random()
*window.innerHeight
);
}You probably noticed there’s nothing on the screen now. We need to render it. We can do that with the image function:
for(let i=0;i<1000;i++){
gfx.point(
Math.random()
*window.innerWidth,Math.random()
*window.innerHeight
); ///The new lines
sk.image(gfx,0,0);
And there you go. You have a bunch of black dots. But shouldn’t they be lighter than that? Remember: we applied our stroke and strokeWeight to the canvas. The graphic we created acts like its own canvas. So, to style our dots, we need to change what we’re calling the stroke and strokeWeight on, something like this:
sk.setup = () =>{
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);
sk.createCanvas(window.innerWidth,window.innerHeight)
sk.background(40); //Changed stroke and strokeWeight reference from sk to gfx
gfx.stroke(200);
gfx.strokeWeight(3);
for(let i=0;i<1000;i++){
gfx.point(
Math.random()
*window.innerWidth,Math.random()
*window.innerHeight
);
}
sk.image(gfx,0,0);
}
And there we go, we have our field of dots.
Okay, now for the fun part: let’s make our first moire pattern. To do that, we want a new graphic that exactly matches our last one. We can do this by duplicating the object with Object.assign(), like so: Object.assign({},gfx);. The first argument is a new object. The second is the object to copy into it. Let’s go ahead an add this into our code:
sk.setup = () =>{
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);
let gfx2;
sk.createCanvas(window.innerWidth,window.innerHeight)
sk.background(40);
gfx.stroke(200);
gfx.strokeWeight(3);
for(let i=0;i<1000;i++){
gfx.point(
Math.random()
*window.innerWidth,Math.random()
*window.innerHeight
);
} //creating the cloned object
gfx2 = {...gfx};
sk.image(gfx,0,0);
sk.image(gfx2,2,2);
}
That gets you a little something like this:
But here’s the thing: in ES6, there’s a better way to duplicate the object than Object.assign. Remember the spread operator we talked about? Here’s our chance to use it. Change that line to read gfx2 = {...gfx};. This also copies the object into another object and gets you the same result, only it is shorter and, in the long run, more useful. Spread also works on arrays, so keep that in mind.
You might notice the second image call offsets the position by 2 in the X and Y. That is us changing the origin of the image. It is how we have those points offset. It might be a bit more obvious if you change it to a bigger value, feel free to experiment.
A Variable Detour
Okay, a quick detour about how Javascript handles variables. You might have noticed that I moved the variable declaration up to the top of the code. This isn’t strictly necessary in Javascript because Javascript is a bit of a strange beast.
Before executing the code, the interpreter runs through the code and moves all variable declarations up to the top, a bit like I did manually. The browser then interprets the code line-by-line. Because of this, the variables exist before you ever declare them, just with a value of undefined. So this:
let foo = () =>{
aFunction1();
aFunction2();
let bar = "Hey!";
}becomes this to the browser:
let foo = () =>{
let bar = undefined;
aFunction1();
aFunction2();
bar = "Hey!";
}Most of the time, this doesn’t matter. But it does matter when if comes to functions. Declaring a function via function(){} and let x = function(){} (or let x = () => {} if you prefer ES6) are not identical. Give these two snippets a try:
x();
function x(){
console.log("it works!")
}And:
x();
let x = () =>{
console.log("it works!")
}Did you notice how the first works while the second didn’t? The first moves the function declaration to the top, while the second moves an undefined variable to the top that then gets assigned down in the code. When the browser goes looking for the first, there is a reference to the function already mapped. When it looks for it in the second, it sees a variable instead, one that hasn’t been assigned yet. Thus, calling it like a function breaks.
Back to the Code
Okay, we’re really close to having our first moire pattern. Instead of offsetting by a value, why not rotate the graphic? That’s easy in P5. Let’s call l sk.rotate(1); before we render the second image.
Notice we are calling rotate on the sketch there, not the graphic. We want to rotate the whole image, not just a part of it. Calling gfx2.rotate(1); would rotate any shapes drawn onto that graphic, not the graphic itself.
Our new code looks like this:
sk.setup = () =>{
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);
let gfx2;
sk.createCanvas(window.innerWidth,window.innerHeight)
sk.background(40);
gfx.stroke(200);
gfx.strokeWeight(3);
gfx.line(0,0,window.innerWidth,0);
for(let i=0;i<1000;i++){
gfx.point(Math.random()*window.innerWidth, Math.random()*window.innerHeight);
}
gfx2 = {...gfx};
sk.image(gfx,0,0);
sk.rotate(1);
sk.image(gfx2,0,0);
}But wait, that doesn’t look right. If you ran this, you should see a problem. It doesn’t really look like the points rotated:
Diagnostic time. Let’s try to see what went wrong by throwing a line across the top of the graphic:
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);
let gfx2;
sk.createCanvas(window.innerWidth,window.innerHeight)
sk.background(40);
gfx.stroke(200);
gfx.strokeWeight(3);//Diagnostic line, remove after troubleshooting
gfx.line(0,0,window.innerWidth,0);
And there we go, we have two big problems. One, it’s rotating far too much. Two, it’s rotating from the corner. Let’s fix the rotation first.
Okay, so you probably know you can describe a circle using either degrees or radians. A circle is 360 degrees, but it can also be described with radians as 2π. To convert between them, know that 1 radian = 180 degrees/π. That’s roughly 57.3 degrees and, looking at our sketch, that looks to be about how much we rotated it by.
While radians are incredibly useful, for us rotating in degrees will be easier to use. We need to change the angleMode P5 is operating in. Call sk.angleMode(sk.DEGREES);. Note here that sk.DEGREES simply returns a string of 'degrees'. You can confirm this with a console.log, if you wish. This is an included constant in P5. You will see these pop up from time to time.
Adding that into our code, we have:
sk.setup = () =>{
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);
let gfx2;
sk.createCanvas(window.innerWidth,window.innerHeight);
//added an angleMode
sk.angleMode(sk.DEGREES);
sk.background(40);
gfx.stroke(200);
gfx.strokeWeight(3);
gfx.line(0,0,window.innerWidth,0);
for(let i=0;i<1000;i++){
gfx.point(Math.random()*window.innerWidth, Math.random()*window.innerHeight);
}
gfx2 = {...gfx};
sk.image(gfx,0,0);
sk.rotate(1);
sk.image(gfx2,0,0);
}Better, but it is still rotating from the corner. Time to fix that. The upper-left corner of the image is its origin, where the image is drawn from and all relative points are relative to. What we really want is the origin to be in the center of the graphic instead. Right above the angleMode, add in an imageMode:
sk.imageMode(sk.CENTER);
sk.angleMode(sk.DEGREES);
sk.background(40);
gfx.stroke(200);
gfx.strokeWeight(3);We feed a constant into imageMode too, just like we did with angleMode. There’s many constants you can work with. You can see them all in the P5 documentation.
Running the code now will show that the dots no longer cover the whole screen. The canvas has an origin point, too, and that’s where we are drawing the image from. We could fix this by moving each image half a screen to the right, but let’s be consistent and change where we’re drawing from instead using translate:
sk.imageMode(sk.CENTER);
sk.angleMode(sk.DEGREES);//Translate half a screen in X and Y
sk.translate(window.innerWidth/2,window.innerHeight/2);sk.background(40);
gfx.stroke(200);
gfx.strokeWeight(3);
And there it is, a simple moire pattern!
By the end, your code should look a little something like this:
import * as p5 from './p5.js';
//a P5 moire pattern.
let s = (sk) => {
let layers = [];
// sk.translate(window.innerWidth/2,window.innerHeight/2);
sk.setup = () =>{
let gfx = sk.createGraphics(window.innerWidth,window.innerHeight);
let gfx2;
sk.createCanvas(window.innerWidth,window.innerHeight);
sk.angleMode(sk.DEGREES);
sk.imageMode(sk.CENTER);
sk.translate(window.innerWidth/2,window.innerHeight/2);
sk.background(40);
gfx.stroke(200);
gfx.strokeWeight(3);
gfx.line(0,0,window.innerWidth,0);
for(let i=0;i<1000;i++){
gfx.point(Math.random()*window.innerWidth, Math.random()*window.innerHeight);
}
gfx2 = {...gfx};
sk.image(gfx,0,0);
sk.rotate(1);
sk.image(gfx2,0,0);
}
}
const P5 = new p5(s);This is about as simple a moire pattern as there is, but it gives us a good to work off of in the next articles. There’s still a bunch more we have to cover, too. Did you notice we never put anything in the draw function? We’re going to be dealing with that soon, when we start drawing things according to values passed in from UI elements. First, though, I want to make more interesting patterns than what you see here.
Definitely try experimenting with our code. There’s a lot you can do with just this simple boilerplate you have here, and already the functions covered can do some amazing things. Best of all, knowing what we used here, you’re well-positioned to delve into the P5 documentation yourself and peruse the example code. If you create something cool, please do share! That’s it this time for me. See you in the next one.










