Circle Packing

Circle packing is such a fantastic effect, it looks infinitely complex, while also being mathematically beautiful. In this tutorial, we’re going to create a circle packing effect… Interestingly, this is a good example of an effect that isn’t particularly efficient to run, but at the same time, will still be very quick!

As usual, we’re going to begin with a small, clean canvas. We will also set the size of the canvas and adjust it based on the user’s device pixel ratio, or pixel density. This ensures that the final result is crisp on all monitors.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');

var size = window.innerWidth;
var dpr = window.devicePixelRatio;
canvas.width = size * dpr;
canvas.height = size * dpr;
context.scale(dpr, dpr);

context.lineWidth = 2;
  

Now, I’m going to explain a little about our process, so we know which variables we will need. You’ll be able to see here that it’s not the most efficient, but it really gets the job done.

Our steps will be:

  1. Create a new Circle
  2. Check to see if the circle collides with any other circles we have.
  3. If it doesn’t collide, we will grow it slightly, and then check again if it collides.
  4. Repeat these steps until we have a collision, or we reach a “max size”
  5. Create another circle and repeat x times.

So, we have an array of circles, a totalCircles, a min & max circleSize and a createCircleAttempts. Let’s get this in code.

12
13
14
15
16
17
18
var circles = [];
var minRadius = 2;
var maxRadius = 100;
var totalCircles = 500;
var createCircleAttempts = 500;
 

Now we will spec out our process. We will make a createCircle and doesCircleHaveACollision function, and then fill it in with our goals… including calling the createAndDrawCircle function once for each of our totalCircles variable.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function createAndDrawCircle() {
  
  // Loop through from 0 to createCircleAttempts
  // trying to create a circle.

  // Once we have a circle created, grow it until
  // it hits another, or reaches max size.

  // Draw the circle
}

function doesCircleHaveACollision(circle) {
  // Return true of false depending on whether the circle collides with another.

  // but return false for now
  return false;
}

for( var i = 0; i < totalCircles; i++ ) {  
  createAndDrawCircle();
}

This is the fun part, we can go through our functions and fill them in. If we tackle this in a step by step way, it will flow out really well.

First, we’ll start with creating a circle object, we’ll give it an x, y and a radius

21
22
23
24
25
26
  var newCircle = {
    x: Math.floor(Math.random() * size),
    y: Math.floor(Math.random() * size),
    radius: minRadius
  };

Now, we’ll add it to our list of circles, and draw it… we didn’t really need to do this just yet, but being able to see what we’re coding render out really helps with the process.

29
30
31
32
33
  circles.push(newCircle);
  context.beginPath();
  context.arc(newCircle.x, newCircle.y, newCircle.radius, 0, 2*Math.PI);
  context.stroke(); 

Awesome, and there we have it, tiny circles all over our screen. Next, we can look at growing them. We will do this 1 unit at a time, and when they collide, we’ll take one step back, and break out of the loop.

26
27
28
29
30
31
32
33
  for(var radiusSize = minRadius; radiusSize < maxRadius; radiusSize++) {
    newCircle.radius = radiusSize;
    if(doesCircleHaveACollision(newCircle)){
      newCircle.radius--;
      break;
    } 
  }

Wow, what a mess we’ve made! Of course we know the reason for this. Currently our doesCircleHaveACollision function always returns false … we’ll need to fill that in.

The way that we tell if circles have a collision, is a little bit of trigonometry. We’re going to loop through each of the circles that are drawn and compare them to the current circle being drawn. If their radii combined, is greater than the distance between each of their centers, then we know there’s a collision.

To get the distance between the two center points, we will use pythagoras' theorem (whoa, that high school math coming in handy!)

40
41
42
43
44
45
46
47
48
49
50
51
  for(var i = 0; i < circles.length; i++) {
    var otherCircle = circles[i];
    var a = circle.radius + otherCircle.radius;
    var x = circle.x - otherCircle.x;
    var y = circle.y - otherCircle.y;

    if (a >= Math.sqrt((x*x) + (y*y))) {
      return true;
    }
  }
  

Almost there! But another small gotcha, when we’re creating our circles, there’s also a chance that they’re appearing inside others.

We’re going to use a loop in the creation area now as well, its a little inefficient to randomly guess positions, but really at the end of the day, unless we were doing millions of circles, we won’t see any slow down.

If the circle doesn’t find a safe place to draw, the attempt is abandoned.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  var newCircle;
  var circleSafeToDraw = false;
  for(var tries = 0; tries < createCircleAttempts; tries++) {
    newCircle = {
      x: Math.floor(Math.random() * size),
      y: Math.floor(Math.random() * size),
      radius: minRadius
    }
    
    if(doesCircleHaveACollision(newCircle)) {
      continue;
    } else {
      circleSafeToDraw = true;
      break;
    }
  }

  if(!circleSafeToDraw) {
    return;
  }

Wow, now we’ve got some beautiful circles, all packed in! There’s only one little step more to do, and that is to trigger a collision when they hit the wall as well as each other. We’ll break that into two if statements, one checking the top and bottom, and one checking the left and right.

66
67
68
69
70
71
72
73
74
75
76
  if(circle.x + circle.radius >= size ||
     circle.x - circle.radius <= 0) {
    return true;
  }
    
  if(circle.y + circle.radius >= size ||
      circle.y - circle.radius <= 0) {
    return true;
  }
  

And there we have it! It’s not the prettiest code, but it’s a great example of how some complex things can be reasoned out, thought about, and stepped through with relative ease & a little math.

↪ You can edit the code above, and have it run by pressing the arrow between the code and demo, but if you like, you can also play around a little with this code