[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