From 32dc9352ad254223fec4f972ebe29631d8b37426 Mon Sep 17 00:00:00 2001 From: Abe Pazos Date: Wed, 17 May 2023 17:05:03 +0200 Subject: [PATCH] [orx-delegate-magic] Add PropertyFollower --- .../kotlin/smoothing/PropertyFollower.kt | 163 ++++++++++++++++++ .../src/jvmDemo/kotlin/DemoFollowing01.kt | 59 +++++++ 2 files changed, 222 insertions(+) create mode 100644 orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertyFollower.kt create mode 100644 orx-delegate-magic/src/jvmDemo/kotlin/DemoFollowing01.kt diff --git a/orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertyFollower.kt b/orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertyFollower.kt new file mode 100644 index 000000000..041998f72 --- /dev/null +++ b/orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertyFollower.kt @@ -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, + private val maxAccel: Double, + private val maxAccelProperty: KProperty0?, + private val maxSpeed: Double, + private val maxSpeedProperty: KProperty0?, + private val dampDist: Double, + private val dampDistProperty: KProperty0? +) { + 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( + private val clock: Clock, + private val property: KProperty0, + private val maxAccel: Double, + private val maxAccelProperty: KProperty0?, + private val maxSpeed: Double, + private val maxSpeedProperty: KProperty0?, + private val dampDist: Double, + private val dampDistProperty: KProperty0? +) where T : LinearType, T : EuclideanVector { + 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, + maxAccel: Double = 0.1, + maxAccelProperty: KProperty0? = null, + maxSpeed: Double = 10.0, + maxSpeedProperty: KProperty0? = null, + dampDist: Double = 400.0, + dampDistProperty: KProperty0? = 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 Clock.following( + property: KProperty0, + maxAccel: Double = 0.1, + maxAccelProperty: KProperty0? = null, + maxSpeed: Double = 10.0, + maxSpeedProperty: KProperty0? = null, + dampDist: Double = 400.0, + dampDistProperty: KProperty0? = null +) where T : LinearType, T : EuclideanVector = + PropertyFollower( + clock = this, + property = property, + maxAccel = maxAccel, + maxAccelProperty = maxAccelProperty, + maxSpeed = maxSpeed, + maxSpeedProperty = maxSpeedProperty, + dampDist = dampDist, + dampDistProperty = dampDistProperty + ) diff --git a/orx-delegate-magic/src/jvmDemo/kotlin/DemoFollowing01.kt b/orx-delegate-magic/src/jvmDemo/kotlin/DemoFollowing01.kt new file mode 100644 index 000000000..63d93ac45 --- /dev/null +++ b/orx-delegate-magic/src/jvmDemo/kotlin/DemoFollowing01.kt @@ -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()) + } + } +}