---
title: Build an SVG Circle Grid with p5.js
description: Make a configurable SVG graphic of a grid of circles in random colors and sizes with p5.js.
date: 2023-06-18
tags:
  - Processing
  - p5.js
  - Code Tutorial
  - SVG
synopsis: In this tutorial, we'll learn how to make a configurable SVG graphic of a grid of circles in random colors and sizes with p5.js.
imageURL: /img/circle-grid-post-image.webp
imageAlt: A grid of multicolored circles.
---
Processing is a fantastic language for creative programming and learning how to code, allowing programmers of all skill levels to quickly and simply create complex graphics, data visualizations, and generative art. Its Javascript implementation, p5.js, is perfect for those already familiar with Javascript, or who want to use processing to make graphics for the web without the complexity of SVG, or the insanity of using CSS for complex graphics. Today we're going to build [a simple but pretty graphic using P5](#et-voila).

## Our goal
First, let's define exactly what we're going to be making. We want to make a grid of circles that:
1. Fills the viewport
2. Randomly assigns a size to each circle
3. Randomly assigns a color to each circle
4. Is flexible and avoids hardcoded values, allowing us to get a variety of different looks depending on our parameters
5. Allows us to download our generated image as an SVG

With that nailed down, let's go ahead and get set up.

## Setup
Processing has a handy <abbr title="Integrated Development Environment">IDE</abbr>, much like Arduino, that we can use to get started quickly. Processing IDE is available as a Flatpak, so it should be simple to install no matter what distro you're running. If, sadly, you're on Windows or MacOS, I'm sure there is also a simple way to install it on your machine that can be worked out with a quick search on the internet.

Once you've installed and launched the IDE, at the top right you'll notice a dropdown that says "Java." This is the Processing IDE's mode selector. Click the dropdown, and choose "Manage Modes." In the new window, install "p5.js Mode," and switch to p5.js using the mode selector.

## Getting started
If you've done everything correctly up to now, you should see two tabs in your IDE: a .js file, and index.html. Our .js file should look like this:
``` javascript
function setup() {

}


function draw() {

}
```
and our index.html should look like this:
``` html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- PLEASE NO CHANGES BELOW THIS LINE (UNTIL I SAY SO) -->
  <script language="javascript" type="text/javascript" src="libraries/p5.min.js"></script>
  <script language="javascript" type="text/javascript" src="sketch_230615e.js"></script>
  <!-- OK, YOU CAN MAKE CHANGES BELOW THIS LINE AGAIN -->

  <style>
    body {
      padding: 0;
      margin: 0;
    }
  </style>
</head>

<body>
</body>
</html>
```
In any processing sketch, there are two main functions, as you can see by looking at our .js file. `setup()` will run once when the sketch is loaded, and `draw()` will loop repeatedly. For our purposes, we don't need `draw()`, so we'll just leave it empty.

Now that we have our boilerplate, the next thing we need to do is set up the canvas for our sketch. As you may recall, we want our sketch to fill the viewport, so let's set up a canvas inside of our `setup()` function like so:
``` javascript
function setup() {
  createCanvas(window.innerWidth, window.innerHeight);
  background(0);
  noStroke();
}
```
Now if we click the play icon at the top left of the Processing IDE, a browser window should open and load a page showing our sketch so far. We ought to see a viewport entirely covered with one large black canvas.

[![The LibreWolf web browser opened to localhost. The viewport is entirely black.](../../img/circle-grid-canvas.webp "A good start!")](../../img/circle-grid-canvas.webp)
## Random results
To meet conditions two and three of our goal, we'll need a way to get random numbers. Let's write a quick function inside `setup()` to provide us with random integers:
``` javascript
  const getRandomInt = (min, max) => {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min) + min);
  }
```

## Let's make some circles
To make a circle in p5, we need to define three parameters: x position, y position, and its diameter. With that in mind, we can define a circle like so:
``` javascript
circle(10,10,10);
```
Add one to the bottom of `setup()` and refresh your browser tab if you'd like to test it out. Be sure and delete it afterwards.

We're going to be making a grid of circles, however, so we are going to need a little more information. First, let's start by generating a line of circles. Although we only need to generate a line of circles on one axis for the purpose of making a grid, a row or a column, for the sake of future flexibility, we'll assume that our line might be horizontal, vertical, or diagonal. To do this, we'll need the following information:
* Line start position x
* Line start position y
* Distance between circles on x
* Distance between circles on y
* Minimum circle diameter
* Maximum circle diameter
* Quantity of circles
* The axis upon which our line extends
* An array of colors for our circles

You'll notice that we have min-max values for circle diameter, and an array for our circle colors. This is so that a size and color can be chosen randomly by our program.

With that out of the way, let's start writing a function to generate our line of circles inside `setup()`:
``` javascript
const generateCircleLine = (startX, startY, distX, distY, minD, maxD, qty, axis, fillArr) => {
}
```

We are going to need to increment our starting coordinates, so let's assign two variables to those parameters:
``` javascript
let x = startX;
let y = startY;
```

And we can write our loop inside of `generateCircleLine()`:
``` javascript
for (let i = 0; i < qty; i++) {

  const diameter = getRandomInt(minD-1, maxD);

  if (!i) {
     fill(...fillArr[getRandomInt(0, fillArr.length)]);
     circle(x,y,diameter);

     continue;
  }

  switch (axis) {
    case 'x':
      x = !distX ? x + startX : x + distX;
    break;
    case 'y':
      y = !distY ? y + startY : y + distY;
    break;
    case 'xy':
      x = !distX ? x + startX : x + distX;
      y = !distY ? y + startY : y + distY;
    break;
  }

  fill(...fillArr[getRandomInt(0, fillArr.length)]);
  circle(x,y,diameter);
}
```
In the loop above:
* We start by assigning `diameter` to a random integer between `minD` and `maxD`
* Then, if `i` is false / 0, indicating that we're on our first iteration, we randomly choose a fill color value from `fillArr`, draw our first circle, and continue on to our next iteration.
* Now that we're on iteration 1 / circle 2, we need to figure out what axis or axes our line is on, and calculate the new starting coordinates for the circle about to be drawn. For this, we use a switch statement, so if we are operating on the x asis, only the x starting coordinate will be incremented, et cetera. We have used the ternary operator here to indicate that if a distance is not specified along any given axis, the program should increment by a distance equal to its starting coordinate.
* As we now have our coordinates, we choose a random fill color from fillArr, draw our next circle, and repeat until `i < qty`.

That was a lot, so let's look at our code and make sure everything is alright. This is what our code should look like at the moment:
``` javascript
function setup() {
  createCanvas(window.innerWidth, window.innerHeight);
  background(0);
  noStroke();

  const getRandomInt = (min, max) => {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min) + min);
  }

  const generateCircleLine = (startX, startY, distX, distY, minD, maxD, qty, axis, fillArr) => {

    let x = startX;
    let y = startY;

    for (let i = 0; i < qty; i++) {

      const diameter = getRandomInt(minD-1, maxD);

      if (!i) {
         fill(...fillArr[getRandomInt(0, fillArr.length)]);
         circle(x,y,diameter);

         continue;
      }

      switch (axis) {
        case 'x':
          x = !distX ? x + startX : x + distX;
        break;
        case 'y':
          y = !distY ? y + startY : y + distY;
        break;
        case 'xy':
          x = !distX ? x + startX : x + distX;
          y = !distY ? y + startY : y + distY;
        break;
      }

      fill(...fillArr[getRandomInt(0, fillArr.length)]);
      circle(x,y,diameter);
    }
  }
}


function draw() {

}
```

We can check that everything is working by calling our new function at the bottom of `setup()`:
``` javascript
generateCircleLine(10, 10, 20, 0, 5, 10, 10, 'x', [[255,255,255]]);
```

[![The LibreWolf web browser opened to localhost. The viewport is black, with a single row of ten white circles in varying sizes in the top-left corner."](../../img/circle-grid-line.webp "We have a line!")](../../img/circle-grid-line.webp)

And look at that! We have a line!

## Repeating rows
As we now have a function built to generate lines of circles, all we need to do is repeat the process and we'll have a grid. In `setup()`, let's go ahead and create `generateCircleGrid()` to do just that. Because we'll be passing our parameter values from `generateCircleGrid()` to `generateCircleLine()`, we'll need all of the same information, plus a few new parameters:
``` javascript
  const generateCircleGrid = (rowStartX, rowStartY, rowDistX, minD, maxD, rowCircleQty, numRows, rowSpacing, fillArr) => {
}
```
Inside our new function, all we need to do is call `generateCircleLine()` in a loop and we'll have ourselves a grid:
``` javascript
for (let i = 0; i < numRows; i++) {
      rowYPosition = !i ? rowStartY : rowStartY + rowSpacing*i;
      generateCircleLine(rowStartX, rowYPosition, rowDistX, 0, minD, maxD, rowCircleQty, 'x', fillArr);
}
```
In the first line of our for loop, we're checking whether we're in the first iteration (`i == 0`). If so, `rowYPosition` is assigned to rowStartY; otherwise, `rowYPosition` is assigned to `rowStartY + rowSpacing*i`. That means that on our second+ iterations, our row starting coordinate is incremented by our row spacing value multiplied by the current iteration number, ensuring that each iteration draws a row at the correct y coordinate.

We've hardcoded 'x' in the `generateCircleLine()` parameter, so that we only have to worry about incrementing one axis as we generate our grid: `generateCircleLine()` will create columns, and `generateCircleGrid()` loops them to generate rows. To this end, we're also passing 0 to the `distY` in generateCircleLine()`.

## The perfect fit
So, we can create lines of circles, and use those to make a grid, but we have a problem. Right now, our sketch needs to be explicitly told how many circles to draw in each row. We need a way to determine how many circles ought to be drawn in order to fill the canvas on a given axis.

Fortunately, this is an easy fix. If write a function inside `setup()` to subtract our starting coordinate from the window dimension for a given axis, divide the result by the distance between circles, and return the result, we should wind up with just the right nimber of circles to fit within that axis. We'll also give our function a parameter called `axis`. Using this parameter, we'll specify whether the operation should use `window.innerWidth` to return a result for the x axis, or `window.innerHeight` to return a result for the y axis.

``` javascript
const getMaxCircles = (distance, axis, startDistance)=> {
  return Math.floor(axis == 'x' ? (window.innerWidth - startDistance) / distance :
  (window.innerHeight - startDistance) / distance);
}
```

## Specifying the grid
We're now all set up to begin actually generating grids, so let's make one! To keep things neat, let's specify our grid in an object called `myGrid` that contains all the parameters we'll need. Here's what mine looks like:
``` javascript
const myGrid = {
  rowStartX: 20,
  rowStartY: 20,
  rowDistX: 20,
  minDiameter: 5,
  maxDiameter: 15,
  rowSpacing: 20,
  rowCircleQty: function() {return getMaxCircles(this.rowDistX, 'x', this.rowStartX)},
  numRows: function() {return getMaxCircles(this.rowSpacing, 'y', this.rowStartY)},
  fillArray: [
    [100, 50, 255],
    [100, 100, 255],
    [100, 150, 255],
    [100, 200, 255],
    [100, 250, 255]
  ]
}
```
You may notice a couple of things:
* I've taken advantage of the `getMaxCircles()` function we wrote earlier to calculate ideal values for `myGrid.rowCircleQty`, and  `myGrid.numRows`.
* `myGrid.fillArray` is using RGB values. Check the [p5.js documentation](https://p5js.org/reference/#/p5/fill) for other color values you can use.

Feel free to customize `myGrid` to make your ideal circle grid, and call `generateCircleGrid()`...

``` javascript
generateCircleGrid(
  myGrid.rowStartX,
  myGrid.rowStartY,
  myGrid.rowDistX,
  myGrid.minDiameter,
  myGrid.maxDiameter,
  myGrid.rowCircleQty(),
  myGrid.numRows(),
  myGrid.rowSpacing,
  myGrid.fillArray
);
```
...to see the result:

[![The LibreWolf web browser opened to localhost. The viewport is filled with circles of varying sizes in various shades of blue.](../../img/circle-grid-grid.webp "Beautiful!")](../../img/circle-grid-grid.webp)

## SVG export
Looking back on our goals, we've accomplished all but one: SVG export. Happily, there's [a library](https://github.com/zenozeng/p5.js-svg) that makes this trivial. All we need to do is link the script in the `<head>` of index.html,
``` html
<script language="javascript" type="text/javascript" src="https://unpkg.com/p5.js-svg@1.5.1"></script>
```
add a button in `<body>`,
``` html
<button onclick="save()">Save SVG</button>
```
style it in `<style>`,
``` css
button {
  position: fixed;
  bottom: 3rem;
  right: 3rem;
  width: 7rem;
  height: 3rem;
}
```
and make one small tweak in our Javascript file. We simply need to modify our `createCanvas()` function like so:
``` javascript
createCanvas(window.innerWidth, window.innerHeight, SVG);
```

## Our finished code
Here's what everything should look like at this point:
### *.js
``` javascript
function setup() {
  createCanvas(window.innerWidth, window.innerHeight, SVG);
  background(0);
  noStroke();

  const getRandomInt = (min, max) => {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min) + min);
  }

  const getMaxCircles = (distance, axis, startDistance)=> {
    return Math.floor(axis == 'x' ? (window.innerWidth - startDistance) / distance :
    (window.innerHeight - startDistance) / distance);
  }

  const generateCircleLine = (startX, startY, distX, distY, minD, maxD, qty, axis, fillArr) => {

    let x = startX;
    let y = startY;

    for (let i = 0; i < qty; i++) {

      const diameter = getRandomInt(minD-1, maxD);

      if (!i) {
         fill(...fillArr[getRandomInt(0, fillArr.length)]);
         circle(x,y,diameter);

         continue;
      }

      switch (axis) {
        case 'x':
          x = !distX ? x + startX : x + distX;
        break;
        case 'y':
          y = !distY ? y + startY : y + distY;
        break;
        case 'xy':
          x = !distX ? x + startX : x + distX;
          y = !distY ? y + startY : y + distY;
        break;
      }

      fill(...fillArr[getRandomInt(0, fillArr.length)]);
      circle(x,y,diameter);
    }
  }
  const generateCircleGrid = (rowStartX, rowStartY, rowDistX, minD, maxD, rowCircleQty, numRows, rowSpacing, fillArr) => {
    for (let i = 0; i < numRows; i++) {
      rowYPosition = !i ? rowStartY : rowStartY + rowSpacing*i;
      generateCircleLine(rowStartX, rowYPosition, rowDistX, 0, minD, maxD, rowCircleQty, 'x', fillArr);
    }
  }

    const myGrid = {
    rowStartX: 20,
    rowStartY: 20,
    rowDistX: 20,
    minDiameter: 5,
    maxDiameter: 15,
    rowSpacing: 20,
    rowCircleQty: function() {return getMaxCircles(this.rowDistX, 'x', this.rowStartX)},
    numRows: function() {return getMaxCircles(this.rowSpacing, 'y', this.rowStartY)},
    fillArray: [
      [100, 50, 255],
      [100, 100, 255],
      [100, 150, 255],
      [100, 200, 255],
      [100, 250, 255]
    ]
  }

  generateCircleGrid(
    myGrid.rowStartX,
    myGrid.rowStartY,
    myGrid.rowDistX,
    myGrid.minDiameter,
    myGrid.maxDiameter,
    myGrid.rowCircleQty(),
    myGrid.numRows(),
    myGrid.rowSpacing,
    myGrid.fillArray
  );
}


function draw() {

}
```
### index.html
``` html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- PLEASE NO CHANGES BELOW THIS LINE (UNTIL I SAY SO) -->
  <script language="javascript" type="text/javascript" src="libraries/p5.min.js"></script>
  <script language="javascript" type="text/javascript" src="sketch_230615e.js"></script>
  <script language="javascript" type="text/javascript" src="https://unpkg.com/p5.js-svg@1.5.1"></script>
  <!-- OK, YOU CAN MAKE CHANGES BELOW THIS LINE AGAIN -->

  <style>
    body {
      padding: 0;
      margin: 0;
    }

    button {
      position: fixed;
      bottom: 3rem;
      right: 3rem;
      width: 7rem;
      height: 3rem;
    }
  </style>
</head>

<body>
<button onclick="save()">Save SVG</button>
</body>
</html>
```

## Et voilà
And we're done! Now you can tweak the parameters and make grids with all sorts of colors and size variations, and even export and edit them in Inkscape!

[![The LibreWolf web browser opened to localhost. The viewport is filled with circles of varying sizes in various shades of blue. There is a button reading "Save SVG" in the bottom right corner.](../../img/circle-grid-complete.webp "The finished article.")](../../img/circle-grid-complete.webp)

If you'd like to make this project even better, maybe consider implementing a GUI to adjust your grid paramaters, or adding some interactivity.
