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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ include 'orx-camera',
|
|||||||
'orx-syphon',
|
'orx-syphon',
|
||||||
'orx-temporal-blur',
|
'orx-temporal-blur',
|
||||||
'orx-timer',
|
'orx-timer',
|
||||||
|
'orx-time-operators',
|
||||||
'orx-kinect-common',
|
'orx-kinect-common',
|
||||||
'orx-kinect-v1',
|
'orx-kinect-v1',
|
||||||
'orx-kinect-v1-natives-linux-arm64',
|
'orx-kinect-v1-natives-linux-arm64',
|
||||||
|
|||||||
Reference in New Issue
Block a user