How to Cluster Circles of Different Sizes While Preventing Overlap
2008 November 07
That’s not the catchiest headline, but it’s what this post is about. I’ve finally solved the problem I’ve been working on since September: how to take a bunch of circles of different sizes and group them around a central point, while preventing any overlap. See this illustration:

This turned out to be much more complicated than I had expected. If all the circles were of the same diameter, it would be easy to calculate regular positions and space them evenly. But I have a project with circles of all different sizes, and I needed them to be near each other, maybe even touching, but not overlapping. I knew it was possible, because I’d seen it done before, such as in visualizations on Many Eyes.
I started by trying to solve the problem using trigonometry. Measuring radii, tangents, and such seemed to be the answer, but got very complicated very quickly. Then I turned to the Traer Physics library for Processing. My idea was to have a central point of “gravity” that would attract the circle objects, but then make each circle repel the others, so they would sort of come together and then space out. JohnG, on the Processing boards figured out a way to do that, but his example used objects of uniform size and mass. With a variety of sizes, again, it got more complicated, and was using more computation power than I needed. After all, I didn’t need cool, fluid-like motion. I just needed to arrange things properly.
My answer came from Mike G. (thanks, Mike!), who suggested using a square-spiral approach, no trigonometry involved. Take circle A, placed at center. Then place circle B at center as well, but move it to the right one pixel. Then check to see if it’s colliding (overlapping) circle A. If so, move it down one, then left one, two, then up one, two, then right one, two, three, and so on. Keep “spiralling” outward along a progressively larger square path, and eventually you will find a point where the two circles no longer overlap.
This was a major breakthrough, and I got pretty close to figuring it out myself, but had to call in Mike for help. He sent me some code this morning for spacing out three circles. Things got more complicated the more circles I added, but I managed to get it working, finally, this afternoon. Give it a shot, and play around with adding/subtracting circles and seeing how the cluster is recalculated. As a bonus, I factored in a spacing variable, so you can have the circles sit farther apart or closer together. The next step is to integrate this into my iTunes visualization.

Made a new version of your applet. Written in a hurry… Bit messy, but … Instead of using the square spiral it swings each sphere around the circle at different distances. It seems to do a better job of avoiding putting a lot of spheres on the diagonals. I also added a break in the collision count loop (which is applicable to your applet as well) because we don’t care to know how many collisions we have. After the first we can break out.
// // Circle clustering // using a square spiral technique // // by Scott Murray // 2008-11-06 //
int numDiscs = 5; int spacing = 0; int minRadius = 10; int maxRadius = 50; Object[] objects = new Object[1]; //Initially just create one -- we’ll add more later, once data is loaded PFont font;
void setup() { //size(screen.width, screen.height); size(500, 500); smooth(); ellipseMode(CENTER); noStroke(); //font = loadFont(“AlteHaasGrotesk-20.vlw”);
//Initialize the first object objects[0] = new Object(0);
//Create all additional objects and append them to the array for(int i = 1; i < numDiscs; i++) { Object objectInstance = new Object(i); objects = (Object[]) append(objects, objectInstance); } }
void draw() { background(255); pushMatrix(); translate(width/2, height/2);
//Draw each object for(int i = 0; i < objects.length; i++) { objects[i].display(); }
popMatrix(); }
class Object { int id; float x; float y; int radius; int diameter; float armLength = 0.0; int spiralPhase = 1; //Which way we’re moving around the spiral --- 1=right, 2=down, 3=left, 4=up float spiralMaxDistance = 1; //The max x or y distance from center
//Constructor method Object(int _id) { id = _id; x = int(random(-width/2, width/2)); y = int(random(-height/2, height/2)); radius = int(random(minRadius, maxRadius)); diameter = radius * 2; }
void display() { pushMatrix(); translate(x, y); fill(150, 50); ellipse(0, 0, diameter, diameter); fill(0); // textFont(font, 20); text(id, -5, 7); popMatrix(); }
void resetObject() { x = 0; y = 0; spiralPhase = 1; spiralMaxDistance = 1; armLength = 1; }
void moveInSpiral() { float rotation = (6.18f / 360.0f); float oldx; float oldy;
}
void newRandomPositionAndSize() { x = int(random(-width/2, width/2)); y = int(random(-height/2, height/2)); radius = int(random(minRadius, maxRadius)); diameter = radius * 2; } }
// // Checks for collisions between any two (circular) objects // boolean isCollision(Object object1, Object object2) { float minDistance = object1.radius + object2.radius + spacing; float actualDistance = dist(object1.x, object1.y, object2.x, object2.y); boolean isCollision; if (actualDistance < minDistance) { isCollision = true; } else { isCollision = false; } return isCollision; }
void clusterObjects() { //First reset all positions to 0,0 for(int i = 0; i < objects.length; i++) { objects[i].resetObject(); } //Then resolve collisions --- test current object (i) against all prior objects (j) //i starts at 1, though, in order to skip over the first object, which will just remain at 0,0 for(int i = 1; i < objects.length; i++) { boolean collisionsOccurring = true; int collisionTally = 0; while(collisionsOccurring) { for(int j = 0; j < i; j++) { //Test i vs. all prior j’s if(isCollision(objects[i], objects[j])) { collisionTally++; //If there is a collision, then increment the tally. break; } } if(collisionTally > 0) { //If any collisions happened, objects[i].moveInSpiral(); //then move object i to a new position collisionTally = 0; //and start the loop over again. } else { //But if no collsions happened, collisionsOccurring = false; //then this loop is done, and we can move on to the next object i. } } } }
void mousePressed() { clusterObjects(); }
void keyPressed() { if (key == ‘r’) { //Randomize positions and sizes for (int i = 0; i < objects.length; i++) { objects[i].newRandomPositionAndSize(); } } else if (key == ’]’) { //Increase spacing spacing++; clusterObjects(); } else if (key == ‘[‘) { //Decrease spacing spacing--; clusterObjects(); } else if (key == ‘+’ || key == ‘=’) { //Create a new object numDiscs++; Object objectInstance = new Object(objects.length); objects = (Object[]) append(objects, objectInstance); } else if (key == ‘-’ || key == ‘_’) { //Remove the newest object if(numDiscs > 0) { numDiscs--; objects = (Object[]) shorten(objects); } } }
Comment by M — 2012 March 29 @ 6:05 pm