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 :slight_smile:

 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) {
                 circleToAdd = null
                 break
             }
         }
         return circleToAdd
     }
 
     // Function to add a circle constrained by a shape
     fun newCircBound(bounds:Shape, colors:ColorBufferShadow, circles:MutableList<Circ>):Circ? {
         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) {
                 circleToAdd = null
                 break
             }
         }
         return circleToAdd
     }
 
 
     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
         val img = loadImage("path_to_silhouette_image.jpg")
         // Colored image
         val imgCol = loadImage("path_to_color_image.jpg")
 
         // Get shadows so we can get pixel brightness and colors
         val shadow = img.shadow
         shadow.download()
         val shadowCol = imgCol.shadow
         shadowCol.download()
 
         // Populate the spots list
         for (x in 0 until w){
             for (y in 0 until h){
                 if(shadow[x,y].b > 0.1){
                     spots.add(Pair(Vector2(x * 1.0, y * 1.0), shadowCol[x,y]))
                 }
             }
         }
 
         // 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 {
             rt.colorBuffer(0).saveToFile(File("screenshots/screenshot-${frameCount.toString().padStart(5, '0')}.png"), async = false)
         }
 
         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) {
                     circleList.add(newCircle)
                     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. :slight_smile:

2 Likes

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

packing

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 ...
// val img = loadImage(PATH_SILHOUETTE)
// val imgCol = loadImage(PATH_COLOR)
  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 :slight_smile:

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 :slight_smile: ) 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. :slight_smile:

1 Like