
14) Fits Perfectly
Basically, we’ll pack as many circles in as we can!
retriesPerFrameis how many circles it will try to place before giving up and drawing the frameminDiameteris the smallest a circle can be (this should be 1 for a ‘perfect fit’)maxDiameteris the largest one can bespacingis how much space to leave between circles (this should be 0 a ‘perfect fit’)borderswill draw a black border on each circlefillInsidewill place circles inside of each other as well as outside, so long as there is still enoughspacingblackPercentis how many of the circles will be black rather than bright colors
Here is an example with diameter 1-200; spacing 0, and fillInside/black off. I did max out retriesPerFrame, but this still took a while, since even with 100/frame, the last few empty spots have a ~1/100,000 chance of being chosen.

let gui;
let params = {
retriesPerFrame: 10,
minDiameter: 3, minDiameterMin: 1, minDiameterMax: 100,
maxDiameter: 100, maxDiameterMin: 10, maxDiameterMax: 400,
spacing: 3, spacingMin: -10, spacingMax: 10,
borders: true,
fillInside: true,
blackPercent: 0.25, blackPercentMin: 0, blackPercentMax: 1, blackPercentStep: 0.01,
};
let lastParams;
let circles = [];
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100);
gui = createGuiPanel("params");
gui.addObject(params);
gui.setPosition(420, 0);
}
function intersectsAny(c1) {
return circles.some((c2) => {
let dx = c1.x - c2.x;
let dy = c1.y - c2.y;
let dist2 = dx * dx + dy * dy;
let outsideDist = (c1.d + c2.d) / 2 + params.spacing;
let insideDist = Math.abs(c2.d - c1.d) / 2 - params.spacing;
let outside = dist2 >= outsideDist * outsideDist;
let inside = insideDist >= 0 && dist2 <= insideDist * insideDist;
if (params.fillInside) {
// reject only partial overlaps
return !(outside || inside);
} else {
// reject any overlap
return !outside;
}
});
}
function draw() {
// Reset if any params change
if (lastParams == undefined || Object.keys(params).some(k => params[k] !== lastParams[k])) {
circles = [];
lastParams = {...params};
}
// Find a random point not in any previous circle
let foundOne = false;
for (let i = 0; i < params.retriesPerFrame; i++) {
let x = floor(random() * width);
let y = floor(random() * height);
// A 1 pixel circle would fit, so place the largest one we could
if (!intersectsAny({x, y, d: params.minDiameter})) {
for (let d = params.maxDiameter; d >= params.minDiameter; d--) {
if (!intersectsAny({x, y, d})) {
foundOne = true;
circles.push({
x,
y,
d,
h: floor(random() * 360),
b: random() < params.blackPercent,
});
break;
}
}
if (foundOne) {
break;
}
}
}
background("white");
for (let {x, y, d, h, b} of circles) {
if (params.borders) {
stroke("black");
} else {
noStroke();
}
if (b) {
fill("black");
} else {
fill(h, 100, 100);
}
circle(x, y, d);
}
}
Posts in Genuary 2026:
- Genuary 2026.14: Fits Perfectly
- Genuary 2026.13: Self Portrait
- Genuary 2026.12: Boxes
- Genuary 2026.11: Quine
- Genuary 2026.10: Polar coordinates
- Genuary 2026.09: Cellular automata
- Genuary 2026.08: A city
- Genuary 2026.07: Boolean algebra
- Genuary 2026.06: Lights on/off
- Genuary 2026.05: Write 'genuary'
- Genuary 2026.04: lowres
- Genuary 2026.03: Fibonacci forever
- Genuary 2026.02: Twelve principles of animation
- Genuary 2026.01: One color, one shape