[orx-delegate-magic] Add PropertyFollower (#313)
This commit is contained in:
@@ -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
|
||||
)
|
||||
59
orx-delegate-magic/src/jvmDemo/kotlin/DemoFollowing01.kt
Normal file
59
orx-delegate-magic/src/jvmDemo/kotlin/DemoFollowing01.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user