Circle packing: Once again

When I started with creative coding, circle packing algorithms where quite in vogue. It’s a simple procedure to which I look fondly, almost with nostalgia.
So here it is an implementation aimed at beginners, both with creative coding and with kotlin/openrndr. I have tried to use different facets of the framework (collections, classes, nullability, etc.), in the hope that someone who is looking at our beloved tool can find an easy way in to learn and explore more, maybe starting with this piece of code and improve it

`````` import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.noise.random
import org.openrndr.math.Vector2
import org.openrndr.shape.*
import java.io.File

fun main() = application {
configure {
width = 1772
height = 1181
}

class Circ(var pos:Vector2, var col:ColorRGBa, var scl:Double = 1.0){
var r:Double = 1.0
var speed:Double = random(1.0, 2.0) * scl
var growing = true

fun grow(){
if (growing) r += speed
}

fun draw():Circle{
return Circle(pos, r)
}

// Checks if circle is contained in shape. Approximate results but good enough :)
fun edges(bounds: Shape):Boolean{
val points = Circle(pos, r).contour.sampleEquidistant(16).segments.map{it.start}
var edged = false
for (p in points){
if (!bounds.contains(p)) {
edged = true
}
}
return edged
}
}

// Function to add a circle based on a silhouette image
fun newCirc(spots: MutableList<Pair<Vector2, ColorRGBa>>, circles:MutableList<Circ>):Circ? {
val p = spots.random()
//Define a Circ instance which can also hold null values
var circleToAdd: Circ? = Circ(p.first, p.second)

// Checks if circle touches another circle
for (c in circles) {
if (c.pos.distanceTo(p.first) < c.r) {
break
}
}
}

// Function to add a circle constrained by a shape
val p = bounds.randomPoints(1)[0]
val col = colors[p.x.toInt(), p.y.toInt()]
// Defiine a Circ instance which can also hold null values
var circleToAdd: Circ? = Circ(p, col)

// Checks if circles touches another circle
for (c in circles) {
if (c.pos.distanceTo(p) < c.r) {
break
}
}
}

program {

val circleList = mutableListOf<Circ>()
val spots = mutableListOf<Pair<Vector2, ColorRGBa>>()
val tot = 10
// Change this for higher canvases size
val w = 1772
val h = 1181

val bounds = Rectangle(0.0, 0.0, w * 1.0, h * 1.0)

// Silhouette image
// Colored image

// Get shadows so we can get pixel brightness and colors

// Populate the spots list
for (x in 0 until w){
for (y in 0 until h){
}
}
}

// Use shape to constrain the circles
val boundsContain = Rectangle(0.0, 0.0, w - 10.0, h - 10.0).shape

// Define render target for higher resolution
val rt = renderTarget(w, h){
colorBuffer()
}

// Take a screenshot of the render target by pressing any key
keyboard.keyUp.listen {
}

backgroundColor = ColorRGBa.BLACK

extend{

//Changing the number of circles per frame gives different textures
var count = 0
var attempts = 0
while (count < tot) {
val newCircle = newCirc(spots, circleList) // Change to newCircBound(boundsContain, shadowCol, circleList) to sample randomly inside the bounding shape and use image colors
if (newCircle != null) {
count += 1
}
attempts += 1
if (attempts > 2000){
// Save screenshot to a screenshot folder (make sure it exists!)
rt.colorBuffer(0).saveToFile(File("screenshots/screenshot-last.png"), async = false)
application.exit()
}
}
println(attempts)

//Check if a circle touches the edges of the bounding shape or another circle, and in case it makes it stop growing
circleList.filter{ it.growing }.forEach {
if (it.edges(boundsContain)) {
// The circle will not grow anymore
it.growing = false
}

for (other in circleList){
//Check if circle is not overlapping with any other circle
if ((it != other) && (it.pos.distanceTo(other.pos)) < it.r + other.r) {
it.growing = false
break
}
}
it.grow()
}

drawer.isolatedWithTarget(rt) {
drawer.clear(ColorRGBa.BLACK)
circleList.forEach {
drawer.stroke = ColorRGBa.WHITE
drawer.fill = null // Change to it.col if you want colored circles according to the provided image
drawer.ortho() // Fixes the correct aspect ratio
drawer.circle(it.draw())
}
}
drawer.image(rt.colorBuffer(0))
}
}
}
``````

Make sure that the variable `w` and `h` coincide with your image width and height.

2 Likes

Hi! Nice looking design! It’s fun to pack things

It took me a bit of time to make the program run. Two things would make it easier:

1. Declare these before `fun main()` to make it more obvious they need to be adjusted.
``````private const val PATH_SILHOUETTE = "/path/to/silhouette-image.png"
private const val PATH_COLOR = "/path/to/color-image.png"

//... and use them below ...
``````
1. Get the image sizes automatically to avoid errors when the images are smaller than expected.
``````import kotlin.math.min
...
val w = min(img.width, imgCol.width)
val h = min(img.height, imgCol.height)
``````

Funny that this is how I got started with Kotlin and Processing, later with OPENRNDR

I ported a non-animated version and there’s a looong thread in the Processing forum. Wow 4 years ago already!

ps. the `pickWeighted` function used in my program can be found here.

2 Likes

Nice! These type of algorithms were (and still are!) really fun to explore.

I agree with the positioning of the path variables upfront, yes, it is much more accessible like this.
The variables `w` and `h` I mainly use in relation to this to make higher resolution images. Probably a good idea would also be to have something like `map(0, w, 0, img.width, x * 1.0).toInt()` and the correspective for `y` when accessing the shadow. It would still be left to the user to adjust the aspect ratio of the final resulting canvas, but one could achieve larger designs. Another thing that would be fun (no pun intended ) is to draw the final circles with `drawer.composition` and save everything in an svg. Maybe something for the plotter episode of the OPENRNDR meetup.

1 Like