From 9119e4a95a8144437b18b71ca5777a459e25fa40 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Fri, 21 Apr 2023 12:32:59 +0200 Subject: [PATCH] [orx-delegate-magic, orx-envelopes] Add orx-delegate-magic, orx-envelopes --- orx-delegate-magic/README.md | 38 ++++++ orx-delegate-magic/build.gradle.kts | 25 ++++ .../kotlin/dynamics/PropertySpringForcer.kt | 128 ++++++++++++++++++ .../kotlin/smoothing/PropertySmoother.kt | 102 ++++++++++++++ .../kotlin/tracking/PropertyTracker.kt | 39 ++++++ .../src/jvmDemo/kotlin/DemoSmoothing01.kt | 29 ++++ .../src/jvmDemo/kotlin/DemoSpring01.kt | 33 +++++ orx-envelopes/README.md | 7 + orx-envelopes/build.gradle.kts | 25 ++++ orx-envelopes/src/commonMain/kotlin/ADSR.kt | 37 +++++ .../src/commonMain/kotlin/Envelope.kt | 7 + .../src/commonMain/kotlin/Tracker.kt | 69 ++++++++++ .../src/jvmDemo/kotlin/DemoADSRTracker01.kt | 35 +++++ settings.gradle.kts | 2 + 14 files changed, 576 insertions(+) create mode 100644 orx-delegate-magic/README.md create mode 100644 orx-delegate-magic/build.gradle.kts create mode 100644 orx-delegate-magic/src/commonMain/kotlin/dynamics/PropertySpringForcer.kt create mode 100644 orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertySmoother.kt create mode 100644 orx-delegate-magic/src/commonMain/kotlin/tracking/PropertyTracker.kt create mode 100644 orx-delegate-magic/src/jvmDemo/kotlin/DemoSmoothing01.kt create mode 100644 orx-delegate-magic/src/jvmDemo/kotlin/DemoSpring01.kt create mode 100644 orx-envelopes/README.md create mode 100644 orx-envelopes/build.gradle.kts create mode 100644 orx-envelopes/src/commonMain/kotlin/ADSR.kt create mode 100644 orx-envelopes/src/commonMain/kotlin/Envelope.kt create mode 100644 orx-envelopes/src/commonMain/kotlin/Tracker.kt create mode 100644 orx-envelopes/src/jvmDemo/kotlin/DemoADSRTracker01.kt diff --git a/orx-delegate-magic/README.md b/orx-delegate-magic/README.md new file mode 100644 index 00000000..e9dd3bde --- /dev/null +++ b/orx-delegate-magic/README.md @@ -0,0 +1,38 @@ +# orx-delegate magic + +Collection of magical property delegators + +## Delegated properties + +[Kotlin documentation](https://kotlinlang.org/docs/delegated-properties.html) + +## Property smoothing + +```kotlin +val state = object { + var radius = 10.0 +} + +val smoothRadius by smoothing(state::radius) +``` + + +## Property dynamics + +```kotlin +val state = object { + var radius = 10.0 +} + +val dynamicRadius by springForcing(state::radius) +``` + +## Property tracking + +```kotlin +val state = object { + var radius = 10.0 +} + +val radiusHistory by tracking(state::radius) +``` diff --git a/orx-delegate-magic/build.gradle.kts b/orx-delegate-magic/build.gradle.kts new file mode 100644 index 00000000..b104cf60 --- /dev/null +++ b/orx-delegate-magic/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + org.openrndr.extra.convention.`kotlin-multiplatform` +} + +kotlin { + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + implementation(project(":orx-parameters")) + implementation(libs.openrndr.application) + implementation(libs.openrndr.draw) + implementation(libs.openrndr.filter) + implementation(libs.kotlin.reflect) + } + } + + @Suppress("UNUSED_VARIABLE") + val jvmDemo by getting { + dependencies { + implementation(project(":orx-shapes")) + } + } + } +} \ No newline at end of file diff --git a/orx-delegate-magic/src/commonMain/kotlin/dynamics/PropertySpringForcer.kt b/orx-delegate-magic/src/commonMain/kotlin/dynamics/PropertySpringForcer.kt new file mode 100644 index 00000000..18a0f4c3 --- /dev/null +++ b/orx-delegate-magic/src/commonMain/kotlin/dynamics/PropertySpringForcer.kt @@ -0,0 +1,128 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.openrndr.extra.delegatemagic.dynamics + +import org.openrndr.Program +import org.openrndr.math.LinearType +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty0 + +class DoublePropertySpringForcer( + private val program: Program, + private val property: KProperty0, + private val k: Double, + private val kProperty: KProperty0?, + private val decay: Double, + private val decayProperty: KProperty0? + +) { + private var output: Double? = null + private var lastTime: Double? = null + private var velocity = 0.0 + operator fun getValue(any: Any?, property: KProperty<*>): Double { + val k = kProperty?.get() ?: k + val decay = decayProperty?.get() ?: decay + + val anchor = this.property.get() + if (lastTime != null) { + val dt = program.seconds - lastTime!! + if (dt > 0.0) { + val sfY = -k * (output!! - anchor) + velocity = velocity * decay + sfY * dt * 10.0 + output = output!! + velocity * dt * 10.0 + } + } else { + output = this.property.get() + } + lastTime = program.seconds + return output ?: error("no value") + } +} + +class LinearTypePropertySpringForcer>( + private val program: Program, + private val property: KProperty0, + private val k: Double, + private val kProperty: KProperty0?, + private val decay: Double, + private val decayProperty: KProperty0? +) { + private var output: T? = null + private var lastTime: Double? = null + private var velocity: T? = null + operator fun getValue(any: Any?, property: KProperty<*>): T { + val k = kProperty?.get() ?: k + val decay = decayProperty?.get() ?: decay + + val anchor = this.property.get() + if (lastTime != null) { + val dt = program.seconds - lastTime!! + if (dt > 0.0) { + val sfY = (output!! - anchor) * -k + + velocity = if (velocity != null) { + velocity!! * decay + sfY * dt * 10.0 + } else { + sfY * dt * 10.0 + } + output = output!! + velocity!! * dt * 10.0 + } + } else { + output = this.property.get() + } + lastTime = program.seconds + return output ?: error("no value") + } +} + +/** + * Create a property spring force delegate + * @param property the property that is used as the spring anchor + * @param k the spring stiffness + * @param kProperty the spring stiffness property, overrides [k] + * @param decay velocity decay, best to set to < 1 + * @param decayProperty velocity decay property, overrides [decay] + * @since 0.4.3 + */ +fun Program.springForcing( + property: KProperty0, + k: Double = 1.0, + kProperty: KProperty0? = null, + decay: Double = 0.9, + decayProperty: KProperty0? = null +): DoublePropertySpringForcer { + return DoublePropertySpringForcer( + program = this, + property = property, + k = k, + kProperty = kProperty, + decay = decay, + decayProperty = decayProperty + ) +} + +/** + * Create a property spring force delegate + * @param property the property that is used as the spring anchor + * @param k the spring stiffness + * @param kProperty the spring stiffness property, overrides [k] + * @param decay velocity decay, best to set to < 1 + * @param decayProperty velocity decay property, overrides [decay] + * @since 0.4.3 + */ +fun > Program.springForcing( + property: KProperty0, + k: Double = 1.0, + kProperty: KProperty0? = null, + decay: Double = 0.9, + decayProperty: KProperty0? = null +): LinearTypePropertySpringForcer { + return LinearTypePropertySpringForcer( + program = this, + property = property, + k = k, + kProperty = kProperty, + decay = decay, + decayProperty = decayProperty + ) +} \ No newline at end of file diff --git a/orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertySmoother.kt b/orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertySmoother.kt new file mode 100644 index 00000000..f191bc6f --- /dev/null +++ b/orx-delegate-magic/src/commonMain/kotlin/smoothing/PropertySmoother.kt @@ -0,0 +1,102 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.openrndr.extra.delegatemagic.smoothing + +import org.openrndr.Program +import org.openrndr.math.LinearType +import kotlin.math.pow +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty0 + +class DoublePropertySmoother( + private val program: Program, + private val property: KProperty0, + private val factor: Double = 0.99, + private val factorProperty: KProperty0? +) { + private var output: Double? = null + private var lastTime: Double? = null + operator fun getValue(any: Any?, property: KProperty<*>): Double { + if (lastTime != null) { + val dt = program.seconds - lastTime!! + if (dt > 1E-10) { + val steps = dt * 60.0 + val ef = (factorProperty?.get() ?: factor).pow(steps) + output = output!! * ef + this.property.get() * (1.0 - ef) + } + } else { + output = this.property.get() + } + lastTime = program.seconds + return output ?: error("no value") + } +} + +class PropertySmoother>( + private val program: Program, + private val property: KProperty0, + private val factor: Double = 0.99, + private val factorProperty: KProperty0? +) { + private var output: T? = null + private var lastTime: Double? = null + operator fun getValue(any: Any?, property: KProperty<*>): T { + if (lastTime != null) { + val dt = program.seconds - lastTime!! + if (dt > 1E-10) { + val steps = dt * 60.0 + val ef = (factorProperty?.get() ?: factor).pow(steps) + + val target = this.property.get() + output = output!! * ef + target * (1.0 - ef) + } + } else { + output = this.property.get() + } + lastTime = program.seconds + return output ?: error("no value") + } +} + +/** + * Create a property smoother delegate + * @param property the property to smooth + * @param factor the smoothing factor + * @since 0.4.3 + */ +fun Program.smoothing(property: KProperty0, factor: Double = 0.99): DoublePropertySmoother { + return DoublePropertySmoother(this, property, factor, null) +} + +/** + * Create a property smoother delegate + * @param property the property to smooth + * @param factor the smoothing factor property + * @since 0.4.3 + */ +fun Program.smoothing( + property: KProperty0, + factor: KProperty0 +): DoublePropertySmoother { + return DoublePropertySmoother(this, property, 1E10, factor) +} + +/** + * Create a property smoother delegate + * @param property the property to smooth + * @param factor the smoothing factor + * @since 0.4.3 + */ +fun > Program.smoothing(property: KProperty0, factor: Double = 0.99): PropertySmoother { + return PropertySmoother(this, property, factor, null) +} + +/** + * Create a property smoother delegate + * @param property the property to smooth + * @param factor the smoothing factor property + * @since 0.4.3 + */ +fun > Program.smoothing(property: KProperty0, factor: KProperty0): PropertySmoother { + return PropertySmoother(this, property, 1E10, factor) +} \ No newline at end of file diff --git a/orx-delegate-magic/src/commonMain/kotlin/tracking/PropertyTracker.kt b/orx-delegate-magic/src/commonMain/kotlin/tracking/PropertyTracker.kt new file mode 100644 index 00000000..e19ee610 --- /dev/null +++ b/orx-delegate-magic/src/commonMain/kotlin/tracking/PropertyTracker.kt @@ -0,0 +1,39 @@ +@file:Suppress("PackageDirectoryMismatch") + +package org.openrndr.extra.delegatemagic.tracking + +import org.openrndr.Program +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty0 + +class PropertyTracker(private val program: Program, private val property: KProperty0, val length: Int = 30) { + private val track = mutableListOf() + private var lastTime: Double? = null + + operator fun getValue(any: Any?, property: KProperty<*>): List { + if (lastTime != null) { + val dt = program.seconds - lastTime!! + if (dt > 1E-10) { + track.add(this.property.get()) + } + } else { + track.add(this.property.get()) + } + if (track.size > length) { + track.removeAt(0) + } + lastTime = program.seconds + return track + } +} + +/** + * Create a property tracker + * @param property the property to track + * @param length the maximum length of the tracked history + * @return a property tracker + * @since 0.4.3 + */ +fun Program.tracking(property: KProperty0, length: Int = 30): PropertyTracker { + return PropertyTracker(this, property, length) +} \ No newline at end of file diff --git a/orx-delegate-magic/src/jvmDemo/kotlin/DemoSmoothing01.kt b/orx-delegate-magic/src/jvmDemo/kotlin/DemoSmoothing01.kt new file mode 100644 index 00000000..9708374f --- /dev/null +++ b/orx-delegate-magic/src/jvmDemo/kotlin/DemoSmoothing01.kt @@ -0,0 +1,29 @@ +import org.openrndr.application +import org.openrndr.extra.delegatemagic.smoothing.smoothing +import kotlin.random.Random + +fun main() = application { + program { + val state = object { + var x = width / 2.0 + var y = height / 2.0 + var radius = 5.0 + } + + val sx by smoothing(state::x) + val sy by smoothing(state::y) + val sradius by smoothing(state::radius) + extend { + if (Random.nextDouble() < 0.01) { + state.radius = Random.nextDouble(10.0, 200.0) + } + if (Random.nextDouble() < 0.01) { + state.x = Random.nextDouble(0.0, width.toDouble()) + } + if (Random.nextDouble() < 0.01) { + state.y = Random.nextDouble(10.0, height.toDouble()) + } + drawer.circle(sx, sy, sradius) + } + } +} \ No newline at end of file diff --git a/orx-delegate-magic/src/jvmDemo/kotlin/DemoSpring01.kt b/orx-delegate-magic/src/jvmDemo/kotlin/DemoSpring01.kt new file mode 100644 index 00000000..91072c4d --- /dev/null +++ b/orx-delegate-magic/src/jvmDemo/kotlin/DemoSpring01.kt @@ -0,0 +1,33 @@ +import org.openrndr.application +import org.openrndr.extra.delegatemagic.dynamics.springForcing +import org.openrndr.extra.delegatemagic.smoothing.smoothing +import kotlin.random.Random + +fun main() = application { + program { + val state = object { + var x = width / 2.0 + var y = height / 2.0 + var radius = 5.0 + } + + val sx by springForcing(state::x, k = 10.0) + val sy by springForcing(state::y) + val sradius by springForcing(state::radius) + extend { + if (Random.nextDouble() < 0.01) { + state.radius = Random.nextDouble(10.0, 200.0) + } + + if (Random.nextDouble() < 0.01) { + state.x = Random.nextDouble(0.0, width.toDouble()) + } + + if (Random.nextDouble() < 0.01) { + state.y = Random.nextDouble(10.0, height.toDouble()) + } + + drawer.circle(sx, sy, sradius) + } + } +} \ No newline at end of file diff --git a/orx-envelopes/README.md b/orx-envelopes/README.md new file mode 100644 index 00000000..abb1656f --- /dev/null +++ b/orx-envelopes/README.md @@ -0,0 +1,7 @@ +# orx-envelopes + +ADSR envelopes and tools + +## ADSR + +Attack, decay, sustain, release \ No newline at end of file diff --git a/orx-envelopes/build.gradle.kts b/orx-envelopes/build.gradle.kts new file mode 100644 index 00000000..b104cf60 --- /dev/null +++ b/orx-envelopes/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + org.openrndr.extra.convention.`kotlin-multiplatform` +} + +kotlin { + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + implementation(project(":orx-parameters")) + implementation(libs.openrndr.application) + implementation(libs.openrndr.draw) + implementation(libs.openrndr.filter) + implementation(libs.kotlin.reflect) + } + } + + @Suppress("UNUSED_VARIABLE") + val jvmDemo by getting { + dependencies { + implementation(project(":orx-shapes")) + } + } + } +} \ No newline at end of file diff --git a/orx-envelopes/src/commonMain/kotlin/ADSR.kt b/orx-envelopes/src/commonMain/kotlin/ADSR.kt new file mode 100644 index 00000000..dd391d86 --- /dev/null +++ b/orx-envelopes/src/commonMain/kotlin/ADSR.kt @@ -0,0 +1,37 @@ +package org.openrndr.extra.envelopes + +import org.openrndr.math.mix +import kotlin.math.min + +data class ADSR( + val attackDuration: Double, + val decayDuration: Double, + val sustainValue: Double, + val releaseDuration: Double +) : Envelope{ + override fun value(t: Double, tOff: Double): Double { + return adsr(attackDuration, decayDuration, sustainValue, releaseDuration, t, tOff) + } + + override fun isActive(t: Double, tOff: Double): Boolean { + return !(t - tOff > releaseDuration) + } +} + +fun adsr( + attackDuration: Double, + decayDuration: Double, + sustainValue: Double, + releaseDuration: Double, + t: Double, + tOff: Double = 1E10 +): Double { + + val da = t / attackDuration + val dc = (t - attackDuration) / decayDuration + + val vOn = mix(min(1.0, da), sustainValue, dc.coerceIn(0.0..1.0)) + + return mix(vOn, 0.0, ((t - tOff) / releaseDuration).coerceIn(0.0..1.0)) + +} \ No newline at end of file diff --git a/orx-envelopes/src/commonMain/kotlin/Envelope.kt b/orx-envelopes/src/commonMain/kotlin/Envelope.kt new file mode 100644 index 00000000..1886af2f --- /dev/null +++ b/orx-envelopes/src/commonMain/kotlin/Envelope.kt @@ -0,0 +1,7 @@ +package org.openrndr.extra.envelopes + +interface Envelope { + fun value(t: Double, tOff: Double): Double + + fun isActive(t: Double, tOff:Double): Boolean +} \ No newline at end of file diff --git a/orx-envelopes/src/commonMain/kotlin/Tracker.kt b/orx-envelopes/src/commonMain/kotlin/Tracker.kt new file mode 100644 index 00000000..d117c50f --- /dev/null +++ b/orx-envelopes/src/commonMain/kotlin/Tracker.kt @@ -0,0 +1,69 @@ +@file:Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED") + +package org.openrndr.extra.envelopes + +import org.openrndr.Program +import org.openrndr.animatable.Clock +import org.openrndr.extra.parameters.DoubleParameter + +class Trigger(val on: Double, var off: Double, val envelope: Envelope) + +class TrackerValue(val time: Double, val value: Double) +abstract class Tracker(val program: Program) { + + val triggers = mutableListOf() + + + protected abstract fun createEnvelope(): T + + fun triggerOn() { + val t = program.seconds + triggers.removeAll { !it.envelope.isActive(t - it.on, it.off - it.on) } + triggers.add(Trigger(program.seconds, 1E30, createEnvelope())) + } + + fun triggerOff() { + val t = program.seconds + triggers.removeAll { !it.envelope.isActive(t - it.on, it.off - it.on) } + triggers.lastOrNull()?.let { + it.off = program.seconds + } + } + + fun values(): List { + val t = program.seconds + return triggers.mapNotNull { + val tOn = t - it.on + val tOff = it.off - it.on + + if (it.envelope.isActive(tOn, tOff)) { + val v = it.envelope.value(tOn, tOff) + + TrackerValue(t, v) + } else { + null + } + } + } + + fun value(): Double { + return values().sumOf { it.value } + } + +} + +class ADSRTracker(program: Program): Tracker(program) { + + @DoubleParameter("attack", 0.0, 20.0, order = 1) + var attack: Double = 0.1 + @DoubleParameter("decay", 0.0, 20.0, order = 2) + var decay: Double = 0.1 + @DoubleParameter("sustain", 0.0, 1.0, order = 3) + var sustain: Double = 0.9 + @DoubleParameter("release", 0.0, 20.0, order = 4) + var release: Double = 0.9 + + override fun createEnvelope(): ADSR { + return ADSR(attack, decay, sustain, release) + } +} \ No newline at end of file diff --git a/orx-envelopes/src/jvmDemo/kotlin/DemoADSRTracker01.kt b/orx-envelopes/src/jvmDemo/kotlin/DemoADSRTracker01.kt new file mode 100644 index 00000000..96725a3d --- /dev/null +++ b/orx-envelopes/src/jvmDemo/kotlin/DemoADSRTracker01.kt @@ -0,0 +1,35 @@ +import org.openrndr.application +import org.openrndr.draw.loadFont +import org.openrndr.extra.envelopes.ADSRTracker + +fun main() { + application { + program { + val tracker = ADSRTracker(this) + tracker.attack = 1.0 + tracker.decay = 0.2 + tracker.sustain = 0.8 + tracker.release = 2.0 + + keyboard.keyDown.listen { + if (it.name == "t") + tracker.triggerOn() + } + keyboard.keyUp.listen { + if (it.name == "t") + tracker.triggerOff() + } + extend { + tracker.values().forEach { + drawer.circle(40.0, 40.0, 20.0 * it.value) + drawer.translate(40.0, 0.0) + } + drawer.defaults() + drawer.circle(drawer.bounds.center, 100.0 * tracker.value()) + + drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0) + drawer.text("press and hold 't'", 20.0, height - 20.0) + } + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a56461b7..d42f913f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,8 +23,10 @@ include( "orx-compositor", "orx-compute-graph", "orx-compute-graph-nodes", + "orx-delegate-magic", "orx-jvm:orx-dnk3", "orx-easing", + "orx-envelopes", "orx-jvm:orx-expression-evaluator", "orx-jvm:orx-file-watcher", "orx-parameters",