On Tarbell's Circle Inversion

Jared Tarbell is a generative artist that always fascinated me since I first saw his piece Substrate many years ago.
I really like this period of generative art of the early 2000s, which was less based on human interaction, controllers, etc., and more concerned with complex structures emerging from simple rules repeated over and over.

I did a little study on one of his pieces, Circle Inversion. The little text description explains how the procedure work:

  • A series of circles of different radius are placed on the canvas
  • At each step a moving point randomly selects a circle from the list, and updates its position through circle inversion
  • A tiny transparent point is drawn at the position

Here is the code

import org.openrndr.Fullscreen
import org.openrndr.KEY_ESCAPE
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extensions.Screenshots
import org.openrndr.extra.noise.shapes.uniform
import org.openrndr.extra.noise.uniform
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
import kotlin.math.pow


fun main() = application {
    configure {
        width = 1000
        height = 1000
        fullscreen = Fullscreen.CURRENT_DISPLAY_MODE
    }

    class Source(pos: Vector2, var rad: Double) {
        /* Class to be used in the future allowing different shapes
         */
        val sh = Circle(pos, rad)

        fun getPos(): Vector2 {
            return sh.center
        }

        fun getRadius(): Double {
            return sh.radius
        }

        fun getNewPoint(p: Vector2): Vector2 {
            /*
             Return the displaced point
             */
            val center = getPos()
            val d = (p - center).length
            if (d > 0) {
                val dir = (p - center) / d
                return center + dir / d * getRadius().pow(2.0)
            } else {
                return p
            }
        }
    }

    program {

        keyboard.keyDown.listen {
            if (it.key == KEY_ESCAPE) {
                application.exit()
            }
        }
        // Create the list of sources
        // Notice: Different criteria for the sources will give rise to greatly different end results
        var startRad = 300.0
        val nSeeds = 20
        val sources = List(nSeeds) {
            startRad *= 0.95
            val dir = Polar(Double.uniform(0.0, 360.0), 1.0).cartesian
            val newRad = Double.uniform(50.0, startRad)
            Source(
                drawer.bounds.center + dir * (300.0 + Double.uniform(-newRad, newRad)), newRad
            )
        }
        //Initial position of the moving point
        var pos = drawer.bounds.uniform()

        val rt = renderTarget(width, height) {
            colorBuffer(type = ColorType.FLOAT32)
        }

        extend(Screenshots())
        val numRepeats = 10000

        extend {
            drawer.isolatedWithTarget(rt) {
                drawer.stroke = null
                drawer.fill = ColorRGBa.WHITE.opacify(0.005)
                drawer.drawStyle.blendMode = BlendMode.ADD
                repeat(numRepeats) { //Repeat the iteration per single frame to speed up the rendering
                        //Choose a source at random
                        val source = sources.random()
                        val newPoint = source.getNewPoint(pos)
                        //If the newPoint goes out of the canvas, leave it where it is
                        if (drawer.bounds.contains(newPoint)) {
                            pos = newPoint
                            //Use drawer.point(newPoint) for a slightly different effect
                            drawer.circle(newPoint, 0.5)
                        }
                    }
                }
            drawer.image(rt.colorBuffer(0))
        }
    }
}

The code creates a Source class, which works as an interface. Indeed, a variation I would try in the future considers sources which are not circles, but more general shapes: for each point P to be displaced, the radius r is then substituted by the distance between the “center” O of the shape S and the intersection point of S with the segment OP.

Notice also that the end result is heavily determined by the initial arrangement of the circles: different arrangements give vastly different results.