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) {
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.