From 737f1bcf85a7ebbc3b63f0053af3aade72953752 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sun, 17 Oct 2021 17:26:39 +0200 Subject: [PATCH] [orx-noise] Add ShapeProvider.poissonDiskSampling --- .../src/commonMain/kotlin/PoissonDisk.kt | 19 +++++++----- orx-noise/src/commonMain/kotlin/ShapeNoise.kt | 31 +++++++++++++++++++ .../src/commonMain/kotlin/UniformRandom.kt | 8 +++++ .../demo/kotlin/DemoPoissonDiskSampling02.kt | 29 +++++++++++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 orx-noise/src/commonMain/kotlin/ShapeNoise.kt create mode 100644 orx-noise/src/demo/kotlin/DemoPoissonDiskSampling02.kt diff --git a/orx-noise/src/commonMain/kotlin/PoissonDisk.kt b/orx-noise/src/commonMain/kotlin/PoissonDisk.kt index 2443b64f..305fdceb 100644 --- a/orx-noise/src/commonMain/kotlin/PoissonDisk.kt +++ b/orx-noise/src/commonMain/kotlin/PoissonDisk.kt @@ -6,6 +6,7 @@ import org.openrndr.math.clamp import org.openrndr.shape.Rectangle import kotlin.math.ceil import kotlin.math.sqrt +import kotlin.random.Random /* * TODO v2 @@ -24,8 +25,10 @@ internal const val epsilon = 0.0000001 * @param height the height of the area * @param r the minimum distance between each point * @param tries number of candidates per point - * @param boundsMapper a custom function to check if a point is within bounds * @param randomOnRing generate random points on a ring with an annulus from r to 2r + * @param random a random number generator, default value is [Random.Default] + * @param boundsMapper a custom function to check if a point is within bounds + * @return a list of points */ fun poissonDiskSampling( @@ -34,7 +37,9 @@ fun poissonDiskSampling( r: Double, tries: Int = 30, randomOnRing: Boolean = false, - boundsMapper: ((w: Double, h: Double, v: Vector2) -> Boolean)? = null + random: Random = Random.Default, + initialPoint: Vector2 = Vector2(width/2.0, height/2.0), + boundsMapper: ((w: Double, h: Double, v: Vector2) -> Boolean)? = null, ): List { val disk = mutableListOf() val queue = mutableListOf() @@ -46,7 +51,7 @@ fun poissonDiskSampling( val rows = ceil(height / cellSize).toInt() val cols = ceil(width / cellSize).toInt() - val grid = List(rows * cols) { -1 }.toMutableList() + val grid = MutableList(rows * cols) { -1 } fun addPoint(v: Vector2) { val x = (v.x / cellSize).fastFloor() @@ -60,21 +65,21 @@ fun poissonDiskSampling( queue.add(disk.lastIndex) } - addPoint(Vector2(width / 2.0, height / 2.0)) + addPoint(initialPoint) val boundsRect = Rectangle(0.0, 0.0, width, height) while (queue.isNotEmpty()) { - val activeIndex = Random.pick(queue) + val activeIndex = queue.random(random) val active = disk[activeIndex] var candidateAccepted = false candidateSearch@ for (l in 0 until tries) { val c = if (randomOnRing) { - active + Random.ring2d(r, 2 * r) as Vector2 + active + Vector2.uniformRing(r, 2 * r, random) } else { - active + Polar(Random.double0(360.0), radius).cartesian + active + Polar(random.nextDouble(0.0, 360.0), radius).cartesian } if (!boundsRect.contains(c)) continue@candidateSearch diff --git a/orx-noise/src/commonMain/kotlin/ShapeNoise.kt b/orx-noise/src/commonMain/kotlin/ShapeNoise.kt new file mode 100644 index 00000000..b1acd9e6 --- /dev/null +++ b/orx-noise/src/commonMain/kotlin/ShapeNoise.kt @@ -0,0 +1,31 @@ +package org.openrndr.extra.noise + +import org.openrndr.math.Vector2 +import org.openrndr.shape.* +import kotlin.random.Random + +fun ShapeProvider.uniform(random: Random = Random.Default): Vector2 { + val shape = shape + return Vector2.uniformSequence(shape.bounds, random).first { + shape.contains(it) + } +} + +fun ShapeProvider.poissonDiskSampling( + r: Double, + tries: Int = 30, + random: Random = Random.Default +): List { + val shape = shape + val bounds = shape.bounds + val poissonBounds = Rectangle(0.0, 0.0, bounds.width, bounds.height) + + val initialPoint = this.uniform(random).map(bounds, poissonBounds) + + return poissonDiskSampling(bounds.width, bounds.height, r, tries, false, random, initialPoint) { _, _, point -> + val contourPoint = point.map(poissonBounds, bounds) + shape.contains(contourPoint) + }.map { + it.map(poissonBounds, bounds) + } +} \ No newline at end of file diff --git a/orx-noise/src/commonMain/kotlin/UniformRandom.kt b/orx-noise/src/commonMain/kotlin/UniformRandom.kt index 2c1e5bd5..d510bcd6 100644 --- a/orx-noise/src/commonMain/kotlin/UniformRandom.kt +++ b/orx-noise/src/commonMain/kotlin/UniformRandom.kt @@ -70,6 +70,14 @@ fun Vector2.Companion.uniforms(count: Int, random: Random = Random.Default): List = List(count) { Vector2.uniform(rect, random) } +fun Vector2.Companion.uniformSequence(rect: Rectangle, + random: Random = Random.Default): Sequence = + sequence { + while(true) { + yield(uniform(rect, random)) + } + } + fun Vector2.Companion.uniformsRing(count: Int, innerRadius: Double = 0.0, outerRadius: Double = 1.0, random: Random = Random.Default): List = diff --git a/orx-noise/src/demo/kotlin/DemoPoissonDiskSampling02.kt b/orx-noise/src/demo/kotlin/DemoPoissonDiskSampling02.kt new file mode 100644 index 00000000..2fdb8f57 --- /dev/null +++ b/orx-noise/src/demo/kotlin/DemoPoissonDiskSampling02.kt @@ -0,0 +1,29 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.noise.poissonDiskSampling +import org.openrndr.math.Vector2 +import org.openrndr.math.mod_ +import org.openrndr.shape.Circle +import org.openrndr.shape.Ellipse +import kotlin.math.cos + +fun main() { + application { + program { + extend { + val shape = Ellipse(Vector2(width/2.0, height/2.0), 200.0, 150.0 + cos(seconds)*125.0).shape + val points = shape.poissonDiskSampling(10.0) + drawer.clear(ColorRGBa.BLACK) + drawer.stroke = null + drawer.fill = ColorRGBa.PINK + drawer.circles(points, 4.0) + + if (seconds.mod_(2.0) < 1.0) { + drawer.stroke = ColorRGBa.PINK + drawer.fill = null + drawer.shape(shape) + } + } + } + } +} \ No newline at end of file