p5.js is a fun JavaScript library for drawing on an HTML5 canvas, and it has some of the clearest tutorials I have seen. It gives you functionality for things like image manipulation, drawing lines and shapes, displaying images, working with trigonometry, and more. And it is especially popular for generative art, such as fractals.
In this tutorial, I will show you how to use p5.js to convert an image like this:
to a mosaic of dots like this:
This tutorial assumes a working knowledge of JavaScript and familiarity with pre-algebra, but prior knowledge of p5.js isn't strictly necessary. You can follow along on this by creating an account on the p5.js online editor and logging in. The finished product can be found here.
📝 Making a first canvas
As a basic p5.js program, let's start by making a canvas and drawing a single small dot there. We would do that by taking this code to the p5.js editor:
function setup() {
createCanvas(300, 200);
}
function draw() {
ellipse(50, 60, 15, 15);
}
We are starting with basic implementations two of the major functions in a p5.js program: setup
and draw
.
The setup
function runs at the beginnng of a p5.js program, and what we're doing in it is calling createCanvas, a built-in function from p5.js, to create a small HTML5 <canvas>
element of width 300 and height 200.
The draw
function runs repeatedly in the JavaScript event loop, and what we're doing is calling ellipse
to put a circle on the canvas, with a diameter of 15 pixels and its center at point (50, 60)
of that canvas. Remember at school plotting points on Cartesian coordinate grids in math class? That is the same concept here with drawing on a canvas. In fact, a lot of concepts from math class can be used as tools to make cool art!
Now that we've got our setup and draw functions, press play on the p5.js editor, and you should see something like this:
One key difference between the Cartesian grids in math class, and the ones in an HTML5 canvas, though, is that as you can see, point (50, 60)
is at the top-left of the canvas, not the bottom-left. Unlike in the graphs from math class, the y-axis on an HTML5 canvas goes from top to bottom, not bottom to top. The x-axis, though, still goes left to right.
By the way, since we're only drawing our picture once rather than repeatedly (like if we were making an animated p5.js sketch), it's kind of pointless to call draw
repeatedly. So let's make it so we're only calling draw
once.
function setup() {
createCanvas(300, 200);
+ noLoop();
}
By adding a call to noLoop, now after the first time we call draw
, we don't call draw
again unless our code calls redraw.
Before we move on to loading an image, one other thing worth noting, circles/ellipses are not the only shape you can draw in p5. You can find code to draw other shapes, like lines, curves, rectangles, and more, in the links at this reference.
📷 Loading an image
We've got our canvas made, but now we need a way of loading the image we're editing.
First, in the p5 editor, left of the sketch.js
filename, click the right arrow to pop our the "sketch files" panel, click the down triangle on the line that says "sketch files", select "upload file" in the dropdown, and then upload your image.
Now, to use the image, add the following code to the p5.js editor, adding a preload
function and replacing the setup
function:
let img;
function preload() { img = loadImage('./beach.jpg'); }
function setup() {
createCanvas(img.width, img.height);
noLoop();
}
The preload
function runs before setup
to load any assets needed for our p5.js program. What we're doing in our preload function is calling p5.js's loadImage function to load an image, represented in JavaScript as a p5.Image object, that we can manipulate. We store that Image in the img
global variable. Note that if you're using an image besides beach.jpg
, you'll want to change the name of the image you're loading in loadImage
.
Now, in setup
, we call createCanvas
like before, but now we use the Image
object to load the image. We then retrieve the image's width and height so the canvas we make is now the same size as the image.
Now that we've got the image's width and height, and a canvas made in that size, we're going to switch over to drawing the dots on our mosaic.
🐆 Plotting the dots
Circling back to our draw
function, let's replace that function's entire code with this:
function draw() { drawMosaic(5) }
function drawMosaic(dotRadius) {
// [TODO] Add code to put the dots on the mosaic!
}
Just like how in programming languages like Go, it's a good idea to have the main
relatively simple, I like having my draw
function be just a one-liner that calls the function that does the bulk of the action. We're going to have drawMosaic
be the central function of this program; it takes in the radius we want each dot to be, and it will be in charge of drawing all our dots.
We want dots all over the picture, so let's break up the image into columns; each column will be about 1.5 times the width of a dot (3 times the radius), and will be filled top to bottom with dots. So we'll need to know:
- How many columns the image will have
- With that knowledge, how to draw a column.
Let's start by just displaying a vertical line for each column. We'll get rid of the line later, but for now this is helpful as scaffolding, so if something is off about how we render the dots, such as what size they are, or where the dots are drawn, we can figure out what's being drawn in a given column relative to that column's lines.
So let's add these functions:
const columnWidth = (dotRadius) => dotRadius * 3;
const numberOfColumns = (dotRadius) =>
Math.ceil(width / columnWidth(dotRadius));
function drawColumnDots(dotRadius, offsetX) {
// [TODO] Replace the line with a column of dots
line(offsetX, 0, offsetX, height);
}
function drawMosaic(dotRadius) {
for (let i = 0; i < numberOfColumns(dotRadius); i++) {
offsetX = i * columnWidth(dotRadius);
drawColumnDots(dotRadius, offsetX);
}
}
Here's our functions so far:
-
columnWidth
is a helper function to get the width of a column. We have a column be triple the radius of a dot, so that we give each dot a bit of wiggle room as to where it will be drawn. -
numberOfColumns
tells us how many columns of dots we can fit in the picture. Which is the width of the image divided by the width of a column. -
drawColumnDots
will be in charge of adding all the dots to a given column, starting at the x-coordinateoffsetX
we pass in and ending atoffsetX + dotRadius
. For now, as scaffolding, we will just draw a straight vertical line at the left edge of the column. -
drawMosaic
draws every column; we loop over the number of columns we have, and for each one we create a column that starts at the x-coordinatei
times the width of a column. For example, if we have a column width of 15, then the sixth column of dots (zero indexed, so i = 5) of the mosaic starts at anoffsetX
of 75 pixels.
Press play on the p5.js editor, and you should see something like this:
But we didn't come here to draw some vertical lines, we came here to draw some dots, so let's do that!
function drawColumnDots(dotRadius, offsetX) {
// [TODO] Replace the line with a column of dots
line(offsetX, 0, offsetX, height);
let dotDiameter = 2 * dotRadius;
let dotHeightWithPadding = dotDiameter + 2;
let numDotsInColumn = Math.floor(height / dotHeightWithPadding);
for (let i = 0; i < numDotsInColumn; i++) {
let centerX = Math.floor(random(
offsetX + dotRadius,
offsetX + columnWidth(dotRadius) - dotRadius,
))
let centerY = i * dotHeightWithPadding + dotRadius;
ellipse(centerX, centerY, dotDiameter, dotDiameter);
}
}
Here's what happens:
- First, we declare variables for the diameter of a dot, and the height of each dot, with two pixels of padding so the dots aren't touching each other. We then divide the height of the image by
dotHeightWithPadding
to get the number of dots in the column. - Then, in the for loop, we will draw all the dots, from the top of the column to the bottom. First, we calculate the coordinates of the pixel at the center of the dot.
- For the x-coordinate, the leftmost position a dot can be is
dotRadius
pixels to the right of the start of the column. And the rightmost column isdotRadius
pixels to the left of the end of the column. So if a column is 15 pixels wide with a 5-pixel dot radius, we would randomly select an x-coordinate between 5 and 10 pixels to the right of the start of a column. - For the y-coordinate, each dot is
dotHeightWithPadding
pixels lower than the dot above it. We place the top dot's center atdotRadius
pixels below the top of the pixel, so that the top dots don't get cut off.
- For the x-coordinate, the leftmost position a dot can be is
Looks good, but we could use some randomness vertically too to so the dots aren't necessarily at the same height as the ones to the left and right of each other.
+ let topY = Math.floor(random(10));
for (let i = 0; i < numDotsInColumn; i++) {
let centerX = Math.floor(random(
offsetX + dotRadius,
offsetX + columnWidth(dotRadius) - dotRadius,
))
- let centerY = i * dotHeightWithPadding + dotRadius;
+ let centerY = topY + i * dotHeightWithPadding + dotRadius;
ellipse(centerX, centerY, dotDiameter, dotDiameter);
}
Looks good! Before we go on to fill in the colors of the columns, remove the call to line
, since we no longer need that piece of scaffolding.
🎨 Giving the dots their colors
The last step of drawing our mosaic is to color the dots. Each dot will be the same color as the color of the pixel at the center of the dot. Here's how we would do that:
let dotColor = img.get(centerX, centerY);
noStroke()
fill(dotColor);
ellipse(centerX, centerY, dotDiameter, dotDiameter);
Here's what happens:
- First, we use
Image.get
to retrieve the color of the pixel at the coordinates(centerX, centerY)
. This is represented as an array of 4 numbers: red, green, blue, and alpha-transparency (how see-through a pixel is). - We call noStroke to remove the outline on the dots, and we call fill to set the color of a dot.
- Finally, calling
ellipse
draws the dot in the color we selected.
Press play on the p5.js editor, and now the canvas will look like this:
Cool! One other thing I'd like to add though. This picture has a lot of light-colored pixels, so the dots would stand out better on a dark-colored background. So let's refactor drawMosaic
so that you can pick the color of the background.
function draw() { drawMosaic(10, color(30, 30, 30)); }
function drawMosaic(dotRadius, backgroundColor) {
background(backgroundColor);
// ... rest of the code in the function ...
}
We add a new parameter backgroundColor
to our drawMosaic
function, and we pass that into background to draw a background. In draw
, I picked the color 30, 30, 30
; since red/green/blue go from 0 to 255, this gives us a charcoal-black background color. I also made the dot radius 10 pixels instead of 5 to make the picture feel more abstract. Run the play button on the sketch, and now the mosaic looks like this!
We've made a cool piece of artwork with just 46 lines of code, but we've only scratched the surface of the kinds of art you can do with p5.js. If you had fun with this, you should check out the docs for more of p5's code, other people's sketches and YouTube videos for ideas on how you can work with p5 concepts, and check out your old notes from math class to see what other kinds of math, like trigonometry, can be used to make cool artwork!
Top comments (0)