Add orx-time-operators with Envelope & LFO

This commit is contained in:
Ricardo Matias
2020-04-08 20:10:05 +02:00
parent fa2a2f54ba
commit bba7d255c4
8 changed files with 357 additions and 0 deletions

View 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))
```

View 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)
}

View 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)
}
}
}
}
}

View 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)
}
}
}
}
}

View 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
}
}

View 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
}
}

View 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) }
}
}