[orx-delegate-magic] Add PropertyFollower (#313)

This commit is contained in:
Abe Pazos
2023-05-30 20:17:22 +00:00
committed by GitHub
parent 57b2720fd0
commit 5fa45572cd
2 changed files with 222 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
@file:Suppress("PackageDirectoryMismatch")
package org.openrndr.extra.delegatemagic.smoothing
import org.openrndr.Clock
import org.openrndr.math.EuclideanVector
import org.openrndr.math.LinearType
import org.openrndr.math.clamp
import org.openrndr.math.map
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.sign
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0
class DoublePropertyFollower(
private val clock: Clock,
private val property: KProperty0<Double>,
private val maxAccel: Double,
private val maxAccelProperty: KProperty0<Double>?,
private val maxSpeed: Double,
private val maxSpeedProperty: KProperty0<Double>?,
private val dampDist: Double,
private val dampDistProperty: KProperty0<Double>?
) {
private var current: Double? = null
private var lastTime: Double? = null
private var velocity = 0.0
operator fun getValue(any: Any?, property: KProperty<*>): Double {
if (lastTime != null) {
val dt = clock.seconds - lastTime!!
if (dt > 1E-10) {
val maxAccel = maxAccelProperty?.get() ?: maxAccel
val maxSpeed = maxSpeedProperty?.get() ?: maxSpeed
val dampDist = dampDistProperty?.get() ?: dampDist
var offset = this.property.get() - current!!
val len = abs(offset)
val dist = min(dampDist, len) // 0.0 .. dampDist
// convert dist to desired speed
offset = offset.sign *
dist.map(0.0, dampDist, 0.0, maxSpeed)
val acceleration = clamp(
offset - velocity,
-maxAccel, maxAccel
)
velocity = clamp(
velocity + acceleration,
-maxSpeed, maxSpeed
)
current = current!! + velocity
}
} else {
current = this.property.get()
}
lastTime = clock.seconds
return current ?: error("no value")
}
}
class PropertyFollower<T>(
private val clock: Clock,
private val property: KProperty0<T>,
private val maxAccel: Double,
private val maxAccelProperty: KProperty0<Double>?,
private val maxSpeed: Double,
private val maxSpeedProperty: KProperty0<Double>?,
private val dampDist: Double,
private val dampDistProperty: KProperty0<Double>?
) where T : LinearType<T>, T : EuclideanVector<T> {
private var current: T? = null
private var lastTime: Double? = null
private var velocity = property.get().zero
operator fun getValue(any: Any?, property: KProperty<*>): T {
if (lastTime != null) {
val dt = clock.seconds - lastTime!!
if (dt > 1E-10) {
val maxAccel = maxAccelProperty?.get() ?: maxAccel
val maxSpeed = maxSpeedProperty?.get() ?: maxSpeed
val dampDist = dampDistProperty?.get() ?: dampDist
var offset = this.property.get() - current!!
val len = offset.length
val dist = min(dampDist, len) // 0.0 .. dampDist
// convert dist to desired speed
offset = offset.normalized *
dist.map(0.0, dampDist, 0.0, maxSpeed)
var acceleration = offset - velocity
if (acceleration.length > maxAccel) {
acceleration = acceleration.normalized * maxAccel
}
velocity += acceleration
if (velocity.length > maxSpeed) {
velocity = velocity.normalized * maxSpeed
}
current = current!! + velocity
}
} else {
current = this.property.get()
}
lastTime = clock.seconds
return current ?: error("no value")
}
}
/**
* Create a property follower delegate
* @param property the property to smooth
* @param cfg the simulation parameters
* @since 0.4.3
*/
fun Clock.following(
property: KProperty0<Double>,
maxAccel: Double = 0.1,
maxAccelProperty: KProperty0<Double>? = null,
maxSpeed: Double = 10.0,
maxSpeedProperty: KProperty0<Double>? = null,
dampDist: Double = 400.0,
dampDistProperty: KProperty0<Double>? = null
) = DoublePropertyFollower(
clock = this,
property = property,
maxAccel = maxAccel,
maxAccelProperty = maxAccelProperty,
maxSpeed = maxSpeed,
maxSpeedProperty = maxSpeedProperty,
dampDist = dampDist,
dampDistProperty = dampDistProperty
)
/**
* Create a property follower delegate
* @param property the property to smooth
* @param cfg the simulation parameters
* @since 0.4.3
*/
fun <T> Clock.following(
property: KProperty0<T>,
maxAccel: Double = 0.1,
maxAccelProperty: KProperty0<Double>? = null,
maxSpeed: Double = 10.0,
maxSpeedProperty: KProperty0<Double>? = null,
dampDist: Double = 400.0,
dampDistProperty: KProperty0<Double>? = null
) where T : LinearType<T>, T : EuclideanVector<T> =
PropertyFollower(
clock = this,
property = property,
maxAccel = maxAccel,
maxAccelProperty = maxAccelProperty,
maxSpeed = maxSpeed,
maxSpeedProperty = maxSpeedProperty,
dampDist = dampDist,
dampDistProperty = dampDistProperty
)

View File

@@ -0,0 +1,59 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.delegatemagic.smoothing.following
import org.openrndr.extra.delegatemagic.smoothing.smoothing
import org.openrndr.math.Vector2
import kotlin.random.Random
/**
* Demonstrates using delegate-magic tools with
* [Double] and [Vector2].
*
* The white circle's position uses [following].
* The red circle's position uses [smoothing].
*
* `following` uses physics (velocity and acceleration).
* `smoothing` eases values towards the target.
*
* Variables using delegates (`by`) interpolate
* toward target values, shown as gray lines.
*
* The behavior of the delegate-magic functions can be configured
* via arguments that affect their output.
*
* The arguments come in pairs of similar name:
* The first one, often of type [Double], is constant,
* The second one contains `Property` in its name and can be
* modified after its creation and even be linked to a UI
* to modify the behavior of the delegate function in real time.
* The `Property` argument overrides the other.
*/
fun main() = application {
program {
val target = object {
var pos = drawer.bounds.center
}
val spos by smoothing(target::pos)
val fpos by following(target::pos)
extend {
if (frameCount % 90 == 0) {
target.pos = Vector2(
Random.nextDouble(0.0, width.toDouble()),
Random.nextDouble(10.0, height.toDouble())
)
}
drawer.fill = ColorRGBa.WHITE
drawer.circle(fpos, 15.0)
drawer.fill = ColorRGBa.RED
drawer.circle(spos, 10.0)
drawer.fill = null
drawer.stroke = ColorRGBa.GRAY.opacify(0.5)
drawer.lineSegment(0.0, target.pos.y, width.toDouble(), target.pos.y)
drawer.lineSegment(target.pos.x, 0.0, target.pos.x, height.toDouble())
}
}
}