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.