This morning I was playing around with kotlin’s Extensions, in view of a future episode in the series @abe and I have started (if you missed it, you can find it here ).
Here’s the code
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extensions.Screenshots
import org.openrndr.extra.noise.Random
import org.openrndr.math.Vector2
import org.openrndr.shape.*
import kotlin.math.*
fun main() = application {
configure {
width = 1000
height = 1000
}
fun <T> Collection<T>.randomWeighted(w: Collection<Double>): Pair<T, Int> {
val items = this.toList()
var weights = w.toList()
if (items.size != weights.size){
throw Exception("Items size and weights size are different!")
}
val prefixSum = weights.sum()
weights = weights.map{it * 1.0/prefixSum}
val t = Random.double(0.0, 1.0)
var s = 0.0
for (i in weights.indices){
val it = weights[i]
s += it
if (t < s){
return Pair(items[i], i)
break
}
}
return Pair(items.last(), items.size - 1)
}
fun Rectangle.split(how:Int, p:Double = 0.3):List<Rectangle>{
val corner = this.corner
val w = this.width
val h = this.height
val t = if (p < 0.5) Random.double(p, 1.0 - p) else Random.double(1.0 - p, p)
val result = if (how == 1){
arrayListOf<Rectangle>(Rectangle(corner, t * w, h ), Rectangle(corner + Vector2(t * w, 0.0), (1.0 -t) * w, h))
}
else {
arrayListOf<Rectangle>(Rectangle(corner, w, t * h ), Rectangle(corner + Vector2(0.0, t * h), w, (1.0 -t) * h))
}
return result
}
fun Rectangle.hatch(dir:Vector2, w: Double = 5.0):List<Segment>{
val corner = if (dir.x > 0) this.corner else this.corner + Vector2.UNIT_X * this.width
val l = (this.center - corner).length * 2.0
val offset = Vector2(-dir.y, dir.x) * l * sign(dir.x)
val steps = (offset.length/w).toInt()
val segments = mutableListOf<Segment>()
repeat(steps){
val t = it * 1.0/(steps - 1)
val seg = Segment(corner - dir * l + offset * t , corner + dir * l + offset * t )
val inter = intersections(this.contour, seg.contour)
if ( inter.size == 2){
segments.add(Segment(inter[0].position, inter[1].position))
}
}
return segments.toList()
}
program {
var rects = mutableListOf<Rectangle>(drawer.bounds)
val palette = arrayListOf("#DB2B39", "#29335C", "#F3A712", "#F0CEA0", "#534D41").map { ColorRGBa.fromHex(it) }
repeat(50) {
val w = rects.map {
it.area
}
val (sh, id) = rects.randomWeighted(w)
val noise = if (Random.bool(0.2)) 1 else 0
val cut = if (sh.width > sh.height) 1 else 0
val sp = sh.split((cut + noise) % 2)
val newRects = mutableListOf<Rectangle>()
for (i in rects.indices) {
if (i == id) {
sp.forEach {
newRects.add(it)
}
} else {
newRects.add(rects[i])
}
}
rects = newRects
}
val colors = rects.map {
palette.random()
}
val hatches = rects.map {
it.hatch(Vector2(0.5 * (Random.int(0, 2) * 2 - 1), -0.5).normalized, Random.double(5.0, 20.0))
}.flatten()
extend(Screenshots())
extend {
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 0.5
rects.forEachIndexed { id, it ->
drawer.fill = colors[id]
drawer.rectangle(it)
}
drawer.segments(hatches)
drawer.fill = null
drawer.stroke = ColorRGBa.BLACK
drawer.rectangles(rects)
}
}
}
The extensions are the following
- An extension of the Collection class to sample from a collection of type T according to (not necessarily normalized ) weights. I used this to randomly choose the rectangle to split according to area. (If I were more disciplined, I would have added a throwing exception in case of negative weights ). I don’t know if this is already implemented somewhere, but it was easy enough to show how cool extensions functions are.
- An extension of the Rectangle class to split the rectangle either horizontally or vertically, according to the longer side.
- An extension to perform some very basic hatchings from a given direction. It requires some more tweaking to make it into a working tool for any direction vector, unless you are into weird glitches, then welcome on the boat
If you run the code you shoud get something like this