Circle packing

서클 패킹

Circles that keep growing — never overlapping — until they fill every gap of empty space.

Also known as: Circle packing algorithm
···
js
const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const palette = [cs.getPropertyValue('--accent').trim(), cs.getPropertyValue('--accent-2').trim(), cs.getPropertyValue('--accent-3').trim()].map(x => x || '#5b5bf7');
let w, h;
function resize() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  w = innerWidth; h = innerHeight;
  c.width = w * dpr; c.height = h * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();

const MAX = 240;
const circles = [];
function tryAdd() {
  if (circles.length >= MAX) return;
  for (let i = 0; i < 10; i++) {
    const x = Math.random() * w, y = Math.random() * h;
    let ok = true;
    for (const cir of circles) if (Math.hypot(x - cir.x, y - cir.y) < cir.r + 3) { ok = false; break; }
    if (ok) { circles.push({ x, y, r: 1.5, col: palette[circles.length % palette.length] }); return; }
  }
}
function step() {
  for (const cir of circles) {
    let can = cir.x - cir.r > 0 && cir.x + cir.r < w && cir.y - cir.r > 0 && cir.y + cir.r < h;
    if (can) for (const o of circles) { if (o === cir) continue; if (Math.hypot(cir.x - o.x, cir.y - o.y) < cir.r + o.r + 0.6) { can = false; break; } }
    if (can) cir.r += 0.18;
  }
  tryAdd();
}
function draw() {
  ctx.fillStyle = '#0d0d12'; ctx.fillRect(0, 0, w, h);
  for (const cir of circles) {
    ctx.beginPath(); ctx.arc(cir.x, cir.y, cir.r, 0, 7);
    ctx.fillStyle = cir.col; ctx.globalAlpha = 0.85; ctx.fill();
    ctx.globalAlpha = 1; ctx.strokeStyle = '#0d0d12'; ctx.lineWidth = 1.5; ctx.stroke();
  }
}
for (let i = 0; i < 160; i++) step(); // 미리 패킹을 진행해서 처음부터 빼곡하게 보이도록
draw();
(function loop() { step(); draw(); requestAnimationFrame(loop); })();

The process is simple: drop a tiny circle at a random spot and grow its radius a little each step until it touches another circle or the edge of the canvas. Once it can't grow any further, it stops, and new tiny circles keep getting seeded into whatever gaps remain. Repeat this and small circles seep into the narrow gaps between big ones, producing a dense mosaic that looks a lot like a bubble chart.

Because checking for overlaps between every pair of circles scales quadratically with count, production implementations usually bucket circles into a spatial grid and only check nearby ones. This demo just caps the circle count so it stays real-time without that optimisation.

Use it for poster background textures, bubble charts that encode a quantity as circle size, and organic logo backgrounds. Colour each circle differently and the packing becomes decorative on its own.

When to use

Use it for decorative poster/background texture, or bubble charts that map a quantity to circle size. Add spatial partitioning once the circle count grows large.