Circle Packing
About This Sketch
Watch as circles populate the canvas and grow organically until they fill the available space. This algorithm mimics natural packing phenomena seen in biological cells, soap bubbles, and crystal formations.
The emergent pattern is never the same twiceโrandomness in initial placement leads to unique final configurations. This demonstrates how simple local rules (don't overlap) can create complex global patterns.
Algorithm
Circles are randomly placed and grow until they touch each other or the canvas edge. This creates organic, efficient packing patterns similar to bubble formations.
**Key Concepts:**
- **Collision Detection**: Checking distance between circle centers
- **Constrained Growth**: Circles expand until touching neighbors
- **Space Filling**: Emergent tessellation from simple rules
- **Iterative Refinement**: Gradual system evolution toward equilibrium
**How it works:**
1. Attempt to place new circle at random position
2. Check if it overlaps any existing circles
3. If no overlap, add to collection
4. For each existing circle, check if it can grow
5. Grow if not touching any other circle or boundary
6. Continue until space is efficiently packed
7. Color by age/order to show growth sequence
Pseudocode
SETUP:
circles = empty list
DRAW (every frame):
IF circles.count < max:
new_circle = random position, radius 3
overlapping = FALSE
FOR EACH existing circle:
IF distance < sum of radii:
overlapping = TRUE
IF NOT overlapping:
ADD new_circle to list
FOR EACH circle:
can_grow = TRUE
FOR EACH other circle:
IF distance < sum of radii + buffer:
can_grow = FALSE
IF can_grow AND within bounds:
radius = radius + 0.5
FOR EACH circle:
DRAW circle with color based on age
Source Code
let sketch = function(p) {
let circles = [];
let maxCircles = 500;
p.setup = function() {
p.createCanvas(400, 300);
p.colorMode(p.RGB);
};
p.draw = function() {
const colors = getThemeColors();
p.background(...colors.bg);
// Try to add new circles
if (circles.length < maxCircles) {
let newCircle = {
x: p.random(p.width),
y: p.random(p.height),
r: 3
};
let overlapping = false;
for (let other of circles) {
let d = p.dist(newCircle.x, newCircle.y, other.x, other.y);
if (d < newCircle.r + other.r) {
overlapping = true;
break;
}
}
if (!overlapping) {
circles.push(newCircle);
}
}
// Grow circles
for (let circle of circles) {
let canGrow = true;
for (let other of circles) {
if (circle === other) continue;
let d = p.dist(circle.x, circle.y, other.x, other.y);
if (d < circle.r + other.r + 2) {
canGrow = false;
break;
}
}
if (canGrow &&
circle.x - circle.r > 0 &&
circle.x + circle.r < p.width &&
circle.y - circle.r > 0 &&
circle.y + circle.r < p.height) {
circle.r += 0.2;
}
}
// Draw circles
p.noFill();
for (let i = 0; i < circles.length; i++) {
let circle = circles[i];
let t = (i / circles.length);
let colorChoice = t < 0.33 ? colors.accent1 :
t < 0.66 ? colors.accent2 : colors.accent3;
p.stroke(...colorChoice, 180);
p.strokeWeight(1.5);
p.circle(circle.x, circle.y, circle.r * 2);
}
};
};