18 KiB
title | description | date | tags | synopsis | imageURL | imageAlt | ||||
---|---|---|---|---|---|---|---|---|---|---|
Build an SVG Circle Grid with p5.js | Make a configurable SVG graphic of a grid of circles in random colors and sizes with p5.js. | 2023-06-18 |
|
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. | /img/terminal.svg | A stylized illustration of a terminal prompt. |
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.
Our goal
First, let's define exactly what we're going to be making. We want to make a grid of circles that:
- Fills the viewport
- Randomly assigns a size to each circle
- Randomly assigns a color to each circle
- Is flexible and avoids hardcoded values, allowing us to get a variety of different looks depending on our parameters
- 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 IDE, 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:
function setup() {
}
function draw() {
}
and our index.html should look like this:
<!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:
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.
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:
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:
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()
:
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:
let x = startX;
let y = startY;
And we can write our loop inside of generateCircleLine()
:
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 betweenminD
andmaxD
- Then, if
i
is false / 0, indicating that we're on our first iteration, we randomly choose a fill color value fromfillArr
, 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:
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()
:
generateCircleLine(10, 10, 20, 0, 5, 10, 10, 'x', [[255,255,255]]);
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:
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:
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.
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:
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 formyGrid.rowCircleQty
, andmyGrid.numRows
. myGrid.fillArray
is using RGB values. Check the p5.js documentation for other color values you can use.
Feel free to customize myGrid
to make your ideal circle grid, and call generateCircleGrid()
to see the result:
generateCircleGrid(
myGrid.rowStartX,
myGrid.rowStartY,
myGrid.rowDistX,
myGrid.minDiameter,
myGrid.maxDiameter,
myGrid.rowCircleQty(),
myGrid.numRows(),
myGrid.rowSpacing,
myGrid.fillArray
);
SVG export
Looking back on our goals, we've accomplished all but one: SVG export. Happily, there's a library that makes this trivial. All we need to do is link the script in the <head>
of index.html,
<script language="javascript" type="text/javascript" src="https://unpkg.com/p5.js-svg@1.5.1"></script>
add a button in <body>
,
<button onclick="save()">Save SVG</button>
style it in <style>
,
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:
createCanvas(window.innerWidth, window.innerHeight, SVG);
Our finished code
Here's what everything should look like at this point:
*.js
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
<!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!
If you'd like to make this project even better, maybe consider implementing a GUI to adjust your grid paramaters, or adding some interactivity.