Rectangles and Extension functions

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

If you run the code you shoud get something like this

1 Like

Very nice! :slight_smile:

I have similar methods in my collection and have played with hatching too for the pen plotter :slight_smile:

My remix:

imports
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.rgb
import org.openrndr.draw.rectangleBatch
import org.openrndr.extensions.Screenshots
import org.openrndr.extra.noise.Random
import org.openrndr.math.Polar
import org.openrndr.math.map
import org.openrndr.shape.Rectangle
import org.openrndr.shape.Segment
import org.openrndr.shape.Shape
import org.openrndr.shape.intersection
fun main() = application {
    configure {
        width = 1000
        height = 1000
    }

    /**
     * Usage: listOf('a', 'b', 'c').pickWeighted(listOf(0.1, 0.6, 0.3)) ---> 'b' (60% chance)
     */
    fun <E> Collection<E>.pickWeighted(weights: Collection<Double>): E {
        if (size != weights.size) {
            error("pickWeighted() requires two collections with the same number of elements")
        }
        val rnd = Random.double0(weights.sum())
        var sum = 0.0
        val index = weights.indexOfFirst { sum += it; sum > rnd }
        return toList()[index]
    }

    fun Rectangle.split(t: Double = 0.5, horizontal: Boolean) = if (horizontal) {
        Pair(
            sub(0.0, 0.0, t, 1.0),
            sub(t, 0.0, 1.0, 1.0)
        )
    } else {
        Pair(
            sub(0.0, 0.0, 1.0, t),
            sub(0.0, t, 1.0, 1.0)
        )
    }

    fun Rectangle.hatch(angle: Double, w: Double = 5.0): Shape {
        val l = this.dimensions.length
        val steps = (l / w).toInt()
        val dir = Polar(angle).cartesian
        val off = dir.perpendicular() * l

        val pattern = Shape(List(steps) {
            val t = it.toDouble().map(0.0, steps - 1.0, -l / 2, l / 2)
            val p = this.center + dir * t
            Segment(p - off, p + off).contour
        })

        return intersection(pattern, this.contour)
    }

    program {
        val rects = mutableListOf(drawer.bounds)
        repeat(50) {
            val w = rects.map { it.area }
            val (id, sh) = rects.withIndex().toList().pickWeighted(w)

            val noise = if (Random.bool(0.2)) 1 else 0
            val cut = if (sh.width > sh.height) 1 else 0
            val horizontal = (cut + noise) % 2 == 1

            val (firstHalf, secondHalf) = sh.split(Random.double(0.3, 0.7), horizontal)
            rects[id] = firstHalf
            rects.add(secondHalf)
        }

        val palette = arrayListOf("#DB2B39", "#29335C", "#F3A712", "#F0CEA0", "#534D41").map { rgb(it) }
        val coloredRects = drawer.rectangleBatch {
            rects.forEach { r ->
                fill = palette.random()
                rectangle(r)
            }
        }

        val hatches = rects.map { r ->
            r.hatch(listOf(-45.0, 45.0).random(), Random.double(5.0, 20.0))
        }

        extend(Screenshots())

        extend {
            drawer.stroke = ColorRGBa.WHITE
            drawer.strokeWeight = 0.5

            drawer.rectangles(coloredRects)
            drawer.shapes(hatches)

            drawer.fill = null
            drawer.stroke = ColorRGBa.BLACK
            drawer.rectangles(rects)
        }
    }
}

Another interesting method to try is split() to split shapes with LineSegment or ShapeContour :slight_smile:

1 Like

Very cool, I didn’t know one can avoid using this in extensions, very handy. I also like a lot indexOfFirst, it’s great that there’s such a native method.
Also, I always forget that intersection (no s) returns a Shape :sweat_smile:

Hi There,

Novice coder, new to Openrndr :upside_down_face:

I found this post because I’m looking for ways to achieve different hatch fill techniques for pen plotting designs.

I found this great library for Processing - which I am experimenting with…

wondering if similar library or collection of examples exists for Openrndr

Thanks!

Hi @ronenosity ! :slight_smile: welcome to the forum!

Inspired by your question I added a new reply to the thread about pen plotters:

About your question about PGS… That library seems to provide so many tools! I think OPENRNDR + ORX provide many of the same algorithms.

I would start by looking into the guide. Then there are OPENRNDR demos, demos in the ORX repository, and I think there are ORXes to keep anyone busy for a very long time :slight_smile: orx-shapes, orx-triangulation, orx-turtle, orx-marching-squares, orx-jvm/orx-boofcv…

If you are looking for a specific algorithm and can’t find it I can try to help.

Feel free to share images or links to your plots. Would be great to see what you create! :slight_smile:

1 Like