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.

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

var size = window.innerWidth;
canvas.width = size;
canvas.height = size;

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 its 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. Lets get this in code.

10
11
12
13
14
15
16
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

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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

19
20
21
22
23
24
  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.

27
28
29
30
31
  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.

24
25
26
27
28
29
30
31
  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 use pythagoras theorem (woah, that high school math coming in handy!)

38
39
40
41
42
43
44
45
46
47
48
49
  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.

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  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.

64
65
66
67
68
69
70
71
72
73
74
  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! Its 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