A simple bouquet print art generator made for the flower themed #WCCChallenge. The generated output is ready-to-plot vectors.

bouquet-01

bouquet-02

bouquet-03

bouquet-04

bouquet-05

Video

A 2 minute video that shows 120 non-cherry-picked bouquets.

Generator outline

The bouquets are generated by a process that depends on Hobby curves, Bezier polygon clipping, Poisson disk sampling and flow fields.

First we start with an initial guess for the contours of our bouquet:

  • generate 20 equidistant points on a circle
  • distort each of the points by adding uniform circular noise
  • find Hobby curve through the distorted points

Inside the initial contour we use Poisson disk sampling to find N pistil points that have at least 80 units distance between them.

For every pistil point we generate a corolla circle of radius 80 with the pistil point at its center. The corollae circles are unioned together to find our final bouquet contour.

We generate the contours for our vase as follows:

  • find the union of an elliptical foot, a circular belly and a rectangular neck
  • sample 60 equidistant points on the contour
  • slightly distort the points by adding uniform circular noise
  • find Hobby curve through the distorted points
  • subtract bouquet shape from Hobby curve

We calculate a flow field on isotropic Gaussians centered on the pistil points.

Generate stigmae circles centered on the pistil points and fill stigmae circles as follows:

  • use Poisson disk sampling to generate points
  • for each point sample flow field and draw a line segment

Fill the bouquet shape:

  • use Poisson disk sampling to generate points
  • for each point that is not inside a stigmae circle sample flow field and draw a line segment

We find a vignette shape by subtracting the the bouquet shape from the canvas shape. Fill the vignette shape:

  • use Poisson disk sampling to generate points
  • for each point that is not inside the vase shape sample flow field and draw a line segment

Source code

Here you find the source code to the bouquet generator. Note that this is one-off quality code that is not written with the intention to be read by the untrained eye.

// Bouquet generator by Edwin Jakobs (@voorbeeld)
// don't be a jerk license
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.noise.*
import org.openrndr.extra.shapes.hobbyCurve
import org.openrndr.ffmpeg.MP4Profile
import org.openrndr.ffmpeg.ScreenRecorder
import org.openrndr.math.Vector2
import org.openrndr.shape.*
import kotlin.math.*
import kotlin.random.Random

fun main() {
    application {
        configure {
            width = 720
            height = 1080
        }

        fun wobblyCircle(center: Vector2, radius: Double, random: Random = Random.Default): ShapeContour {
            return hobbyCurve(
                Circle(center, radius).contour.equidistantPositions(20).take(20).map {
                    it + Vector2.uniformRing(0.0, 50.0, random)
                }, closed = true
            )
        }

        fun vase(center: Vector2): Shape {
            val base = Circle(center, 150.0).shape
                .union(Ellipse(center + Vector2(0.0, 150.0), 100.0, 50.0).shape)
                .union(Rectangle.fromCenter(center - Vector2(0.0, 200.0), 200.0, 200.0).shape)
                .contours.first()

            val distorted = base.equidistantPositions(60).map {
                it + Vector2.uniformRing(0.0, 5.0)
            }
            return hobbyCurve(distorted, closed = true).shape
        }

        program {
            extend(ScreenRecorder()) {
                frameRate = 1
                maximumDuration = 120.0
                (profile as MP4Profile).apply {
                    userArguments = arrayOf("-tune", "animation")
                }
            }
            extend {
                val initialBouquet = wobblyCircle(Vector2(width / 2.0, height / 2.0 - 200.0), 300.0).shape
                val pistils = initialBouquet.scatter(80.0, distanceToEdge = 40.0)
                val corollae = pistils.map { Circle(it, 80.0).shape }
                val bouquet = corollae.drop(1).fold(corollae.first()) { a, b -> a.union(b) }
                val vignette = drawer.bounds.shape.difference(bouquet)
                val vase = vase(Vector2(width / 2.0, height - 300.0)).shape.difference(bouquet)

                val flowField = Array(width * height) { Vector2.ZERO }
                for (pistil in pistils) {
                    for (y in 0 until height) {
                        for (x in 0 until width) {
                            val d = Vector2(pistil.x - x, pistil.y - y)
                            val n = d.normalized.perpendicular() * exp(-d.length * 0.2)
                            flowField[y * width + x] += n
                        }
                    }
                }
                for (i in flowField.indices) {
                    flowField[i] = flowField[i].normalized
                }

                drawer.clear(ColorRGBa.WHITE)
                drawer.stroke = ColorRGBa.BLACK

                val r = Random(0)
                val stigmae = pistils.map { Circle(it, r.nextDouble(10.0, 30.0)) }

                drawer.lineSegments(stigmae.flatMap {
                    it.shape.scatter(r.nextDouble(4.0, 5.0), random = r, distanceToEdge = 2.0)
                }.map {
                    val x = it.x.roundToInt().coerceIn(0, width - 1)
                    val y = it.y.roundToInt().coerceIn(0, height - 1)
                    val n = flowField[y * width + x]
                    val start = Vector2(x * 1.0, y * 1.0)
                    val end = start + n * 1.5
                    LineSegment(start, end)
                })

                drawer.lineSegments(bouquet.scatter(Random.nextDouble(3.0, 8.0)).mapNotNull {
                    val x = it.x.roundToInt().coerceIn(0, width - 1)
                    val y = it.y.roundToInt().coerceIn(0, height - 1)
                    val n = flowField[y * width + x]
                    val start = Vector2(x * 1.0, y * 1.0)
                    val end = start + n * 4.5
                    if (stigmae.none { s -> s.contains(start) || s.contains(end) }) {
                        LineSegment(start, end)
                    } else {
                        null
                    }
                })

                drawer.lineSegments(vignette.scatter(Random.nextDouble(3.0, 12.0), distanceToEdge = 6.0).mapNotNull {
                    val x = it.x.roundToInt().coerceIn(0, width - 1)
                    val y = it.y.roundToInt().coerceIn(0, height - 1)
                    val n = if (y < 800.0) flowField[y * width + x] else flowField[y * width + x].perpendicular()
                    val start = Vector2(x * 1.0, y * 1.0)
                    val end = start + n * 4.5
                    if (!vase.contains(start) && !vase.contains(end)) {
                        LineSegment(start, end)
                    } else {
                        null
                    }
                })
            }
        }
    }
}