Add orx-time-operators with Envelope & LFO
This commit is contained in:
42
orx-time-operators/README.md
Normal file
42
orx-time-operators/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# orx-time-operators
|
||||
|
||||
A collection of time-sensitive functions aimed at controlling raw data over-time.
|
||||
|
||||
## Usage
|
||||
|
||||
Use the TimeOperators extension to `tick` the operators, making them advance in time.
|
||||
|
||||
```kotlin
|
||||
extend(TimeOperators()) {
|
||||
track(envelope, lfo)
|
||||
}
|
||||
```
|
||||
|
||||
### Envelope
|
||||
|
||||
```kotlin
|
||||
val size = Envelope(50.0, 400.0, 0.5, 0.5)
|
||||
|
||||
if (frameCount % 80 == 0) {
|
||||
size.trigger() // also accepts a new target value
|
||||
}
|
||||
|
||||
drawer.circle(0.0, 0.0, size.value)
|
||||
```
|
||||
|
||||
### LFO
|
||||
|
||||
```kotlin
|
||||
val size = LFO(LFOWave.SINE) // default LFOWave.SAW
|
||||
|
||||
val freq = 0.5
|
||||
val phase = 0.5
|
||||
|
||||
drawer.circle(0.0, 0.0, size.sample(freq, phase))
|
||||
|
||||
// or
|
||||
|
||||
drawer.circle(0.0, 0.0, size.sine(freq, phase))
|
||||
```
|
||||
|
||||
|
||||
19
orx-time-operators/build.gradle
Normal file
19
orx-time-operators/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
sourceSets {
|
||||
demo {
|
||||
java {
|
||||
srcDirs = ["src/demo/kotlin"]
|
||||
compileClasspath += main.getCompileClasspath()
|
||||
runtimeClasspath += main.getRuntimeClasspath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":orx-parameters")
|
||||
|
||||
demoImplementation(project(":orx-camera"))
|
||||
demoImplementation("org.openrndr:openrndr-core:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
|
||||
demoImplementation(sourceSets.getByName("main").output)
|
||||
}
|
||||
40
orx-time-operators/src/demo/kotlin/DemoEnvelope.kt
Normal file
40
orx-time-operators/src/demo/kotlin/DemoEnvelope.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.isolated
|
||||
import org.openrndr.extra.timeoperators.Envelope
|
||||
import org.openrndr.extra.timeoperators.TimeOperators
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
program {
|
||||
val size = Envelope(50.0, 400.0, 0.5, 0.5)
|
||||
val rotation = Envelope(easingFactor = 0.4)
|
||||
|
||||
extend(TimeOperators()) {
|
||||
track(size, rotation)
|
||||
}
|
||||
extend {
|
||||
if (frameCount % 80 == 0) {
|
||||
size.trigger()
|
||||
}
|
||||
|
||||
if (frameCount % 50 == 0) {
|
||||
rotation.trigger(180.0)
|
||||
}
|
||||
|
||||
drawer.isolated {
|
||||
drawer.stroke = ColorRGBa.PINK
|
||||
drawer.fill = ColorRGBa.PINK
|
||||
|
||||
drawer.translate(width / 2.0, height / 2.0)
|
||||
drawer.rotate(rotation.value)
|
||||
|
||||
val side = size.value / 2.0
|
||||
val offset = side / 2.0
|
||||
|
||||
drawer.rectangle(0.0 - offset, 0.0 - offset, side, side)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
orx-time-operators/src/demo/kotlin/DemoLFO.kt
Normal file
34
orx-time-operators/src/demo/kotlin/DemoLFO.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.isolated
|
||||
import org.openrndr.extra.timeoperators.Envelope
|
||||
import org.openrndr.extra.timeoperators.LFO
|
||||
import org.openrndr.extra.timeoperators.LFOWave
|
||||
import org.openrndr.extra.timeoperators.TimeOperators
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
program {
|
||||
val size = LFO()
|
||||
val rotation = LFO(LFOWave.SINE)
|
||||
|
||||
extend(TimeOperators()) {
|
||||
track(size, rotation)
|
||||
}
|
||||
extend {
|
||||
drawer.isolated {
|
||||
drawer.stroke = ColorRGBa.PINK
|
||||
drawer.fill = ColorRGBa.PINK
|
||||
|
||||
drawer.translate(width / 2.0, height / 2.0)
|
||||
drawer.rotate(rotation.sample() * 180.0)
|
||||
|
||||
val side = (size.sample(0.5) * 400.0) / 2.0
|
||||
val offset = side / 2.0
|
||||
|
||||
drawer.rectangle(0.0 - offset, 0.0 - offset, side, side)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
orx-time-operators/src/main/kotlin/Envelope.kt
Normal file
138
orx-time-operators/src/main/kotlin/Envelope.kt
Normal file
@@ -0,0 +1,138 @@
|
||||
package org.openrndr.extra.timeoperators
|
||||
|
||||
import org.openrndr.extra.parameters.BooleanParameter
|
||||
import org.openrndr.extra.parameters.DoubleParameter
|
||||
import org.openrndr.math.clamp
|
||||
import org.openrndr.math.mix
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
|
||||
enum class EnvelopePhase {
|
||||
Rest, Attack, Decay
|
||||
}
|
||||
|
||||
// Exponential Ease-In and Ease-Out by Golan Levin
|
||||
// http://www.flong.com/texts/code/shapers_exp/
|
||||
private fun exponentialEasing(x: Double, a: Double): Double {
|
||||
var a = a
|
||||
val epsilon = 0.00001
|
||||
val minParamA = (0.0 + epsilon)
|
||||
val maxParamA = (1.0 - epsilon)
|
||||
a = max(minParamA, min(maxParamA, a))
|
||||
|
||||
return if (a < 0.5) {
|
||||
// emphasis
|
||||
a = (2.0 * a)
|
||||
x.pow(a)
|
||||
} else {
|
||||
// de-emphasis
|
||||
a = (2.0 * (a - 0.5))
|
||||
x.pow(1.0 / (1 - a))
|
||||
}
|
||||
}
|
||||
|
||||
class Envelope(
|
||||
var restValue: Double = 0.0,
|
||||
var targetValue: Double = 1.0,
|
||||
@DoubleParameter("Attack Duration", 0.0, 5.0, 3, 0)
|
||||
var attack: Double = 0.3,
|
||||
@DoubleParameter("Decay Duration", 0.0, 5.0, 3, 1)
|
||||
var decay: Double = 0.5,
|
||||
@DoubleParameter("Easing Factor", 0.0, 1.0, 3, 2)
|
||||
var easingFactor: Double = 0.3,
|
||||
@BooleanParameter("Re-trigger", 3)
|
||||
var reTrigger: Boolean = false
|
||||
) : TimeTools
|
||||
{
|
||||
var phase = EnvelopePhase.Rest
|
||||
set(value) {
|
||||
if (value == EnvelopePhase.Rest) {
|
||||
initialTime = Double.NEGATIVE_INFINITY
|
||||
current = initialRestValue
|
||||
}
|
||||
|
||||
field = value
|
||||
}
|
||||
|
||||
val value: Double
|
||||
get() {
|
||||
return current
|
||||
}
|
||||
|
||||
private var initialTime = Double.NEGATIVE_INFINITY
|
||||
private var current = restValue
|
||||
private var initialRestValue = restValue
|
||||
private val cycleDuration: Double
|
||||
get() {
|
||||
return attack + decay
|
||||
}
|
||||
|
||||
override fun tick(seconds: Double, deltaTime: Double, frameCount: Int) {
|
||||
if (phase == EnvelopePhase.Rest) return
|
||||
|
||||
// TODO: what happens if deltaTime < cycleDuration?
|
||||
|
||||
if (initialTime == Double.NEGATIVE_INFINITY) {
|
||||
initialTime = seconds
|
||||
}
|
||||
|
||||
val cycleTime = seconds - initialTime
|
||||
|
||||
if (cycleTime < 0) {
|
||||
phase = EnvelopePhase.Rest
|
||||
return
|
||||
}
|
||||
|
||||
if (cycleTime <= attack) {
|
||||
phase = EnvelopePhase.Attack
|
||||
} else if (cycleTime > attack && cycleTime < cycleDuration) {
|
||||
phase = EnvelopePhase.Decay
|
||||
} else {
|
||||
phase = EnvelopePhase.Rest
|
||||
return
|
||||
}
|
||||
|
||||
if (phase == EnvelopePhase.Attack) {
|
||||
current = if (attack == 0.0) {
|
||||
targetValue
|
||||
} else {
|
||||
val t = clamp(cycleTime / attack, 0.0, 1.0)
|
||||
|
||||
mix(restValue, targetValue, exponentialEasing(t, easingFactor))
|
||||
}
|
||||
}
|
||||
|
||||
if (phase == EnvelopePhase.Decay) {
|
||||
current = if (decay == 0.0) {
|
||||
initialRestValue
|
||||
} else {
|
||||
val t = clamp((cycleTime - attack) / decay, 0.0, 1.0)
|
||||
|
||||
mix(targetValue, initialRestValue, exponentialEasing(t, easingFactor))
|
||||
}
|
||||
}
|
||||
|
||||
if (current.isNaN()) {
|
||||
println("current is NaN, $phase")
|
||||
}
|
||||
}
|
||||
|
||||
fun trigger(value: Double = targetValue) {
|
||||
restValue = if (initialTime != Double.NEGATIVE_INFINITY) {
|
||||
current
|
||||
} else {
|
||||
initialRestValue
|
||||
}
|
||||
|
||||
if (reTrigger) {
|
||||
restValue = initialRestValue
|
||||
}
|
||||
|
||||
initialTime = Double.NEGATIVE_INFINITY
|
||||
phase = EnvelopePhase.Attack
|
||||
|
||||
targetValue = value
|
||||
}
|
||||
}
|
||||
60
orx-time-operators/src/main/kotlin/LFO.kt
Normal file
60
orx-time-operators/src/main/kotlin/LFO.kt
Normal file
@@ -0,0 +1,60 @@
|
||||
package org.openrndr.extra.timeoperators
|
||||
|
||||
import org.openrndr.math.clamp
|
||||
import org.openrndr.math.mod
|
||||
import kotlin.math.*
|
||||
|
||||
internal const val TAU = 2.0 * PI
|
||||
|
||||
// TODO: When there's a @DropdownParameter switch from Int to String
|
||||
enum class LFOWave(val wave: Int) {
|
||||
SAW(0), SINE(1), SQUARE(2), TRIANGLE(3)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class LFO(var wave: LFOWave = LFOWave.SAW) : TimeTools {
|
||||
private var current = 0.0
|
||||
set(value) {
|
||||
field = clamp(value, 0.0, 1.0)
|
||||
}
|
||||
|
||||
private var initialTime = Double.NEGATIVE_INFINITY
|
||||
private var dt = 0.0
|
||||
private var time = 0.0
|
||||
|
||||
override fun tick(seconds: Double, deltaTime: Double, frameCount: Int) {
|
||||
time += deltaTime
|
||||
}
|
||||
|
||||
fun sample(frequency: Double = 1.0, phase: Double = 0.0): Double {
|
||||
return when(wave) {
|
||||
LFOWave.SAW -> saw(frequency, phase)
|
||||
LFOWave.SINE -> sine(frequency, phase)
|
||||
LFOWave.SQUARE -> square(frequency, phase)
|
||||
LFOWave.TRIANGLE -> triangle(frequency, phase)
|
||||
}
|
||||
}
|
||||
|
||||
fun saw(frequency: Double = 1.0, phase: Double = 0.0): Double {
|
||||
val cycleFreq = 1.0 / frequency
|
||||
val cycleTime = mod(time + (phase * frequency), cycleFreq)
|
||||
current = (cycleTime) / cycleFreq
|
||||
return current
|
||||
}
|
||||
|
||||
fun sine(frequency: Double = 1.0, phase: Double = 0.0): Double {
|
||||
current = sin((phase * TAU) + time * frequency * TAU) * 0.5 + 0.5
|
||||
return current
|
||||
}
|
||||
|
||||
fun square(frequency: Double = 1.0, phase: Double = 0.0): Double {
|
||||
current = max(sign(sin((phase * TAU) + time * frequency * TAU)), 0.0)
|
||||
return current
|
||||
}
|
||||
|
||||
fun triangle(frequency: Double = 1.0, phase: Double = 0.0): Double {
|
||||
val t = (time * frequency) + (phase * frequency)
|
||||
current = 1.0 - 2.0 * abs(mod(t, 1.0) - 0.5)
|
||||
return current
|
||||
}
|
||||
}
|
||||
23
orx-time-operators/src/main/kotlin/TimeOperators.kt
Normal file
23
orx-time-operators/src/main/kotlin/TimeOperators.kt
Normal file
@@ -0,0 +1,23 @@
|
||||
package org.openrndr.extra.timeoperators
|
||||
|
||||
import org.openrndr.Extension
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.draw.Drawer
|
||||
|
||||
interface TimeTools {
|
||||
fun tick(seconds: Double, deltaTime: Double, frameCount: Int)
|
||||
}
|
||||
|
||||
class TimeOperators : Extension {
|
||||
override var enabled: Boolean = true
|
||||
|
||||
private val operators = mutableSetOf<TimeTools>()
|
||||
|
||||
fun track(vararg tools: TimeTools) {
|
||||
operators.addAll(tools)
|
||||
}
|
||||
|
||||
override fun beforeDraw(drawer: Drawer, program: Program) {
|
||||
operators.forEach { it.tick(program.seconds, program.deltaTime, program.frameCount) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user