Circle packing and development logic
written by Hevey
How to start
Whether it's to start a very complex project like Dencity or a simple circle packing project, when you don't know how to do it, you have to go step by step...
So how do you start a circle packing project? What is circle packing made of?
We can start by drawing circles of random size at random positions:
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
background(220);
// Loop drawing 100 random circles
for (let i = 0; i < 100; i++) {
// To simplify the next step in the tutorial, an object is created for the circle
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000),
radius: random(20, 100)
};
// Drawing of the circle
circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
}
}
The code here draws 100 random circles on the canvas, with a random radius between 20 and 100 px (to simplify the example, the canvas has fixed dimensions, 1000x1000 px):
From there, how to determine if a circle overlaps another one?
It can be difficult to imagine it the first time and sometimes taking 5 seconds to make an ugly sketch on paper can really help... In this case, we can draw 2 circles that touch each other (since it's the distance minimum that there must be between 2 circles):
After connecting the centers of the 2 circles (= the minimum distance so that the circles do not overlap), we can see that this distance corresponds to the radius of the first circle + the radius of the second circle.
It is therefore necessary to check before drawing a circle if this minimum distance (radius 1 + radius 2) is respected with all the circles already drawn:
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
background(220);
// This array will contain the drawn circles
let drawnCircles = [];
// Loop drawing 100 random circles
for (let i = 0; i < 100; i++) {
// A random circle
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000),
radius: random(20, 100)
};
let drawTheCircle = true;
// Testing with all circles already drawn
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
// If the distance between the 2 circles is less than the minimum distance, we do not draw it (drawTheCircle = false) and we leave the loop (break) because it is useless to test the other circles
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < randomCircle.radius + circleToTest.radius) {
drawTheCircle = false;
break;
}
}
// If the circle can be drawn, we draw it and add it to the array of drawn circles
if (drawTheCircle) {
circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
drawnCircles.push(randomCircle);
}
}
}
No circle now overlaps the others:
To add more circles, just change the boundary of the first loop:
for (let i = 0; i < 10000; i++) {
You can also change the radius range to get a different result:
radius: random(5, 200)
And often it is desirable that the circles do not touch each other and that there is a minimum space between them... In this case, in addition to the 2 radii of minimum distance, I will add for example 5:
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < randomCircle.radius + circleToTest.radius + 5) {
You can see the difference:
And then ?
As long as you understand your code and its logic well, you can do a lot with simple circle packing.
I'll show you several other examples derived from this same circle packing code (to shorten the code a bit, I'll remove the first comments).
Fill in tiny circles
A solution to add lots of tiny circles is to change for example the range of values for the radius from a certain number of iterations of the loop:
radius: i < 2000 ? random(5, 150) : random(2, 10)
To get :
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
background(220);
let drawnCircles = [];
for (let i = 0; i < 20000; i++) {
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000),
radius: i < 2000 ? random(5, 150) : random(2, 10)
};
let drawTheCircle = true;
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < randomCircle.radius + circleToTest.radius + 2) {
drawTheCircle = false;
break;
}
}
if (drawTheCircle) {
circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
drawnCircles.push(randomCircle);
}
}
}
Fill with circles against each other
One solution is to add a new loop and test all the allowed radii from largest to smallest to find what is the largest radius you can use:
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
background(220);
let drawnCircles = [];
for (let i = 0; i < 10000; i++) {
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000)
};
// Loop to try different radius
for (let k = 200; k > 4; k--) {
let drawTheCircle = true;
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < k + circleToTest.radius) {
drawTheCircle = false;
break;
}
}
if (drawTheCircle) {
circle(randomCircle.x, randomCircle.y, k * 2);
randomCircle.radius = k;
drawnCircles.push(randomCircle);
break;
}
}
}
}
Add margins
To add margins to the edge of the image, you can change the X and Y value range:
let randomCircle = {
x: random(100, 900),
y: random(100, 900),
radius: random(5, 100)
};
And you get:
If you want to have more regular margins, in this case you have to take the radius into account:
let radius = random(5, 200);
let randomCircle = {
x: random(100 + radius, 900 - radius),
y: random(100 + radius, 900 - radius),
radius: radius
};
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
background(220);
let drawnCircles = [];
for (let i = 0; i < 20000; i++) {
let radius = random(5, 200);
let randomCircle = {
x: random(100 + radius, 900 - radius),
y: random(100 + radius, 900 - radius),
radius: radius
};
let drawTheCircle = true;
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < randomCircle.radius + circleToTest.radius + 3) {
drawTheCircle = false;
break;
}
}
if (drawTheCircle) {
circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
drawnCircles.push(randomCircle);
}
}
}
Add circles within circles
To be able to add circles inside the drawn circles, without overlap between the circles, a second check must be added:
&& circleToTest.radius < randomCircle.radius + distance + space
The radius of the drawn circle must be smaller than the radius of the new circle + the distance between the centers of the circles + the space between the circles (again, making a sketch on paper can help find the logical test).
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
background(220);
let drawnCircles = [];
let space = 3; // Space between circles
for (let i = 0; i < 20000; i++) {
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000),
radius: random(5, 200)
};
let drawTheCircle = true;
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
let distance = dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y);
if (distance < randomCircle.radius + circleToTest.radius + space && circleToTest.radius < randomCircle.radius + distance + space) {
drawTheCircle = false;
break;
}
}
if (drawTheCircle) {
circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
drawnCircles.push(randomCircle);
}
}
}
Use as a layer mask
In the Trasiedoc project you can see circle packing textures on some mints:
To achieve this effect, I used a layer that contained a generative texture and then I deleted part of this texture at the locations of the circles so that in the end only the texture remained around the circles. And since at the locations of the erased circles it is transparent, we can then see the texture of the layer below:
To better understand, I have prepared a simple example for you...
Here you have a canvas with a gray background and a layer with vertical black lines:
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
let weight = 10;
background(220);
// Layer
let layer = createGraphics(1000, 1000);
layer.strokeWeight(weight);
layer.stroke(80);
// Drawing vertical lines on the layer
for (let i = 0; i < 50; i++) {
let position = (i * 2 + 0.5) * weight;
layer.line(position, 0, position, 1000);
}
// Copy of the layer on the canvas
image(layer, 0, 0);
}
By drawing the circles with the blendMode REMOVE, these are erased from the layer along with the vertical lines:
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
let weight = 10;
background(220);
let layer = createGraphics(1000, 1000);
layer.strokeWeight(weight);
layer.stroke(80);
for (let i = 0; i < 50; i++) {
let position = (i * 2 + 0.5) * weight;
layer.line(position, 0, position, 1000);
}
// REMOVE mode to erase instead of draw
layer.blendMode(REMOVE);
let drawnCircles = [];
for (let i = 0; i < 10000; i++) {
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000),
radius: random(5, 200)
};
let drawTheCircle = true;
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < randomCircle.radius + circleToTest.radius + 3) {
drawTheCircle = false;
break;
}
}
if (drawTheCircle) {
layer.circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
drawnCircles.push(randomCircle);
}
}
// Copy of the layer on the canvas
image(layer, 0, 0);
}
We could then for example add a circle inside those removed and reduce the width of the lines:
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
let weight = 4;
background(220);
let layer = createGraphics(1000, 1000);
layer.strokeWeight(weight);
layer.stroke(80);
for (let i = 0; i < 125; i++) {
let position = (i * 2 + 0.5) * weight;
layer.line(position, 0, position, 1000);
}
// REMOVE mode to erase instead of draw
layer.blendMode(REMOVE);
let drawnCircles = [];
for (let i = 0; i < 10000; i++) {
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000),
radius: random(7, 200)
};
let drawTheCircle = true;
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < randomCircle.radius + circleToTest.radius + 3) {
drawTheCircle = false;
break;
}
}
if (drawTheCircle) {
layer.circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
drawnCircles.push(randomCircle);
}
}
// Drawing circles inside
fill(80);
noStroke();
drawnCircles.forEach(i => circle(i.x, i.y, (i.radius - weight) * 2));
// Copy of the layer on the canvas
image(layer, 0, 0);
}
Of course here we could have just drawn circles with borders over the lines as well, but the goal here was to simplify the example.
So, to slightly complicate it, here's another version with horizontal lines on the canvas:
function setup() {
createCanvas(1000, 1000);
noLoop();
}
function draw() {
let weight = 5;
background(220);
strokeWeight(weight);
stroke(80);
let layer = createGraphics(1000, 1000);
layer.background(220);
layer.strokeWeight(weight);
layer.stroke(80);
for (let i = 0; i < 100; i++) {
let position = (i * 2 + 0.5) * weight;
layer.line(position, 0, position, 1000);
line(0, position, 1000, position);
}
// REMOVE mode to erase instead of draw
layer.blendMode(REMOVE);
let drawnCircles = [];
for (let i = 0; i < 10000; i++) {
let randomCircle = {
x: random(0, 1000),
y: random(0, 1000),
radius: random(7, 200)
};
let drawTheCircle = true;
for (let j = 0; j < drawnCircles.length; j++) {
let circleToTest = drawnCircles[j];
if (dist(randomCircle.x, randomCircle.y, circleToTest.x, circleToTest.y) < randomCircle.radius + circleToTest.radius + 3) {
drawTheCircle = false;
break;
}
}
if (drawTheCircle) {
layer.circle(randomCircle.x, randomCircle.y, randomCircle.radius * 2);
drawnCircles.push(randomCircle);
}
}
// Copy of the layer on the canvas
image(layer, 0, 0);
}
Experiment
I'll stop there for the code examples, but suppose I want to continue with the last example, what could I do?
The possibilities are as numerous as your imagination and do not hesitate to test everything you want...
Seeing this I wondered for example what it would look like if instead of a filled circle, I drew unfilled circles but with a border (to alternate the vertical and horizontal lines of the layers):
Then I thought that with a color palette and drawing the lines with one of the colors from the palette, maybe this would be interesting:
I also removed the spaces between the lines and tested with other thicknesses:
Then I thought that leaving a random margin to the lines might add something more:
The result being rather satisfactory, I added to finish a generative texture effect:
Just a quick example...
But do you think that before this test starting with the code of the last example I already had the idea of the final result?
Absolutely not, I even thought it wouldn't work, but that's okay, I like to experiment and I tried anyway, thinking it might be nice to have an example for the article 🙂
I hope this tutorial has given you ideas and desire to experiment more things 😉