Beyond Random: Adding Character to Your Circle Packing Algorithms

Circle packing on flow field

Circle packing is a staple of generative art, but its "vibe" can change dramatically with just a few tweaks. Below, I’ll share some tips on how to move beyond pure randomness and give your packing patterns a distinct personality.

[Japanese version / 日本語版はこちら]

 

Same Algorithm, Different Vibes

Take a look at these two images. Both use the same underlying circle packing logic, yet they feel completely different. Why? Because the way we "seed" our circles matters just as much as how we pack them.

 

What is Circle Packing?

Circle packing is a classic example of a mathematical packing problem.

Packing problems : Wikipedia

In the world of creative coding, we usually favor "Place and Grow" over strict mathematical packing. The logic is beautifully simple:

  1. Place: Pick a random spot. If it's empty, drop a tiny circle.
  2. Grow: Slowly increase the radius of every circle.
  3. Stop: If a circle hits a neighbor or a boundary, it stops growing.
  4. Repeat: Do this thousands of times.

Daniel Shiffman has a fantastic breakdown of this on The Coding Train.


Interestingly, using only "Place" without the "Grow" step creates its own unique aesthetic.


And artists like Takawo Shunsuke(@takawo) and Jeff++(@ippsketch) have pushed this concept into "Non-Circular Packing" territories.


👉 The article of the original idea "Non Circular Packing" by Jeff++(@ippsketch).


Here is the example p5.js code of the circle packing with only 'Place' without 'Grow'.


const w = 640;
const h = 640;
const num = 2000; // trial number

function setup() {
  createCanvas(w, h);

  // circle packing
  const circles = getRandomCircles(num, w * 0.4, h * 0.4);

  // drawing
  translate(w * 0.5, h * 0.5);
  background(100);
  circles.forEach((c) => circle(c.x, c.y, c.z));
}

function getRandomCircles(_num, _w, _h) {
  let circles = [];
  for (let i = 0; i < _num; i++) {
    let x = random(-1, 1) * _w;
    let y = random(-1, 1) * _h;
    let z = random(10, 50); // z axis as radius of the circle
    if (circles.every((c) => dist(x, y, c.x, c.y) > (z + c.z) * 0.5)) {
      circles.push(createVector(x, y, z));
    }
  }
  return circles;
}


'Only place without grow' method will create many open gaps if you set a small trial number.



You can consider these gaps as negative space as a design element.

 

The Power of Constraints: Packing on a Grid

What happens if we stop picking "completely" random coordinates? By snapping the center of each circle to a grid, we introduce a structured, architectural feel to the chaos.

// Completely random
let x = random(-1, 1) * _w;
let y = random(-1, 1) * _h;

// Snap to grid logic
const gridDiv = 15;
let x = floor(random(-gridDiv, gridDiv)) / gridDiv * _w;
let y = floor(random(-gridDiv, gridDiv)) / gridDiv * _h;


Compare the organic scatter of a pure random distribution versus the rhythmic alignment of grid-based packing. Even a high-density grid creates a subtle "order" that the eye can sense.



You'll get a different impression if you change circle size or grid gap.



 

Creative Seeding: Diagonals, Circles, and Swirls

We can take this even further by placing circles along specific paths, such as diagonals, circumferences, or even Perlin noise flow fields.

let rnd = random(-1, 1);
let x = rnd * _w;
let y = rnd * _h;

let rnd = random(TWO_PI);
let x = cos(rnd) * _w;
let y = sin(rnd) * _h;

for (let i = 0; i < _num; i++) {
  let rnd = 3 * TWO_PI * i / _num;
  let x = cos(rnd) * _w * i / _num;
  let y = sin(rnd) * _h * i / _num;

 

Flow Fields and Paths

This example demonstrates circle packing applied to a flow field (also known as a vector or noise field).

By calculating the noise field and the circle positions simultaneously, the packing follows the invisible "current" of the noise, creating a sense of organic movement.

The marriage of two popular techniques: circle packing and flow fields p5.js example

It's a drawing of the flow field.

Flow field lines

The combination looks nice also.

Circle packing on the flow field lines

 

The p5.js Implementation

This code is released under the GPL license. To see other works based on my code is my pleasure. And my honor.


const w = 570;
const h = 570;

function setup() {
  createCanvas(w, h);
  translate(w * 0.5, h * 0.5);
  background(100);

  // draw flow field and circle pack
  noFill();
  stroke(250);
  strokeWeight(3);
  const circles = getNoiseField(100, w * 0.4, h * 0.4, min(w, h) * 0.15, 5);

  // draw the circles
  fill(250);
  stroke(0);
  strokeWeight(1);
  circles.forEach((c) => {
    circle(c.x, c.y, c.z);
    if (c.z > 10) {
      circle(c.x, c.y, c.z * 0.5);
    }
  });
}

function getNoiseField(_num, _w, _h, _iDiv, _step) {
  const circles = [];
  for (let iX = -_w; iX < _w; iX += _iDiv) {
    for (let iY = -_h; iY < _h; iY += _iDiv) {
      let x = iX;
      let y = iY;
      beginShape();
      for (let i = 0; i < _num; i++) {
        let nVal = noise(x * 0.001, y * 0.001) * 2;
        x += _step * cos(nVal * TWO_PI);
        y += _step * sin(nVal * TWO_PI);
        vertex(x, y);
        let z = random(_step, _step * 5);
        if (circles.every((c) => dist(x, y, c.x, c.y) > (z + c.z) * 0.5)) {
          circles.push(createVector(x, y, z));
        }
      }
      endShape();
    }
  }
  return circles;
}

/*
Copyright (C) 2021- deconbatch

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/


In the getNoiseField function above, the code handles both the flow lines and the packing logic at once. It’s a bit of a "quick and dirty" hack—I’d love to see how you refactor this into something more elegant! 😌

Next Post Previous Post
No Comment
Add Comment
comment url