An introduction to orx-time-operators

Introduction

orx-time-operators are a (small) suite of tools to deal with controlling raw data over-time. This means generating values which are frame dependent or smoothing out noisy data.

Included since ORX 0.3.51

Time Operators

Time Operators is in itself an Extension which provides an interface to develop in sync tools. It can be used like so:

extend(TimeOperators()) {
    track(envelope, lfo)
}

A look at Time Operators under the hood

Envelope

Envelope is an Attack/Decay based envelope which takes into account the elapsed time to change a given value over time.

Here are two examples where you can see the benefit of the Envelope in action. We start with a random set of values for the square’s size and we move to shape them to a set of smooth curves, including passing through a scenario where the values are held for a longer time.

Usage

This how you would use an Envelope starting with it’s function signature.

Envelope(
    restValue: Double = 0.0
    targetValue: Double = 1.0
    attack: Double = 0.3
    decay: Double = 0.5
    easingFactor: Double = 0.3
    reTrigger: Boolean = false
)
  • restValue : The value which the envelope gravitates to when approaching the resting phase
  • targetValue : The target value which the envelope approaches when it’s triggered
  • attack : The attack duration, how long after being triggered it will take to reach the target value
  • decay : The decay duration, how long after it reached the target value it will take to reach the resting value
  • easingFactor : A value between [0.0, 1.0] which allows shaping the curvature of the attack and decay phases
  • reTrigger: Forces the envelope to start from the restValue when it’s triggered during an ongoing attack/decay cycle

Trimmed down code example:

val size = Envelope(50.0, attack = 1.5, decay = 1.0)

extend(TimeOperators()) {
    track(size)
}
extend {
    // you can pass a new target value when triggering it
    if (frameCount % 120 == 0) size.trigger(Random.double(50.0, 350.0))

    drawer.circle(0.0, 0.0, size.value)
}

LFO

In the following video you can see the LFO in action. The essence of the animation is sampling the same LFO at different phases, done like so:

val droppedChars = lfo.sample(1.0, i / 8.0) * 8
val text = "OPENRNDR".dropLast(droppedChars.toInt())

Usage

It’s important to mention that the LFO always outputs values between [0.0, 1.0].

This how you would use a LFO starting with it’s function signature.

LFO(
    wave: LFOWave = LFOWave.Saw
)
  • wave : The type of wave to be used

Available waves:

enum class LFOWave {
    Saw, Sine, Square, Triangle
}

The LFO can either be used by calling a method after a wave’s name or through sample, which relies on the set wave type via lfo.wave:

lfo.saw()

// or

lfo.sample()

Trimmed down code example:

val lfo = LFO()

extend(TimeOperators()) {
    track(lfo)
}
extend {
    // you can pass a new target value when triggering it
    if (frameCount % 120 == 0) size.trigger(350.0)

    drawer.circle(0.0, 0.0, 40.0 + lfo.sine() * 20.0)
}

A look at Time Operators under the hood

Underneath it simply makes sure that before each draw loop the time operators are being ticked, allowing them to advance in time.

The interface of the ticking function is the following:

interface TimeTools {
    fun tick(seconds: Double, deltaTime: Double, frameCount: Int)
}

This simple interface also allows the operators to move forward in time without activating the TimeOperators extension via extend(TimeOperators()). This opens the possibility of trying different timing scenarios which would make the operators behave differently or just to gather the results ahead of time, without needing the draw loop to move the time forward.

Hope you enjoy using orx-time-operators. Please share your doodles in here.

2 Likes