DEV Community

Cover image for Convert images to mosaics in p5.js
Andy Haskell
Andy Haskell

Posted on

Convert images to mosaics in p5.js

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:

Photograph of large boulders on the sand at Singing Beach in Manchester By The Sea. Behind them is the Atlantic Ocean, in many shades of blue

to a mosaic of dots like this:

The same photograph of Singing Beach, converted to a mosaic of dots, rather than pixels. Each dot is the color of the pixel of the original image that's located at the center of that dot

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

Blank white canvas in the p5.js editor, with a single white circle with a black outline in its top left corner

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();
  }
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. How many columns the image will have
  2. 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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-coordinate offsetX we pass in and ending at offsetX + 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-coordinate i 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 an offsetX of 75 pixels.

Press play on the p5.js editor, and you should see something like this:

p5.js editor showing an HTML5 canvas displaying several vertical 1-pixel-wide black lines, indicating the left edge of each column of dots

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 is dotRadius 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 at dotRadius pixels below the top of the pixel, so that the top dots don't get cut off.

Our HTML5 canvas. Now, in each column marked by the lines, there are black circle outlines a couple pixels apart from each other.

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);
  }
Enter fullscreen mode Exit fullscreen mode

Our HTML5 canvas. Now, the columns of dots start at different pixel positions

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);
Enter fullscreen mode Exit fullscreen mode

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:

Our canvas, now showing all the dots from earlier, but each dot's color is taken from the image

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 ...
}
Enter fullscreen mode Exit fullscreen mode

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!

Our canvas, now showing all the dots from earlier, but each dot's color is taken from the image, and the background the dots are on is charcoal black.

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)