Simple Clojure "nano-automation" library for animation systems
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

-*- mode: org; mode: visual-line; -*-



The Adventures of Twizzle: a Clojure library for simple timeline-based automation, useful for animation systems.

An automation state maps named channels to time-varying values. Each channel can be populated with automation segments, each of which has a start time, duration and target value for the channel at the end of the fade.

twizzle doesn’t have a specific notion of time: it just uses timeline values which can be milliseconds (for realtime animation), frames (for rendering), floats, or anything else numerical.

By default, channel values are rationals or floats (mainly because the default interpolator works in conventional arithmetic), but it’s possible to attach interpolation functions to allow automation over arbitrary data values (for example, vectors of floats for RGB colour mixing), or to implement unusual fade behaviours (such as an on/off gate for the period of a fade).



In project.clj:

In the code:

(ns example
  (:require [eu.cassiel.twizzle :as tw]))

Create a new automation state with

(def state (tw/initial))

For a state with non-default starting values, add an initialisation map:

(def state (tw/initial :init {:pitchbend 64
                              :starts-at-one 1.0}))

Note that default initial value is nil. The default interpolator will interpret a start point of nil as 0 in any calculations, so once you start fading you’ll get rationals or floats, but before any fades take effect the sampled value will come back as nil.

Adding Fades

Add an automation fade to a state:

(tw/automate-at state :my-param 200 10 1.0)

Arguments are: state, channel name, starting timestamp, duration, final (target) value. This returns a new state. The fade duration (which here has length 10) specifies that the fade terminates at 210; sampling here will return 1.0. Sampling at 209 will return a value slightly biased towards the previous value of :my-param.

Overlapping fades on the same channel is discouraged. (The behaviour is well-defined but probably not useful.)

Anywhere beyond a fade, sampling the value will return the final value of the fade (in the example above, 1.0).

See also automate-in, which starts a fade at a relative offset from the state’s current location.

A channel can be completely cleared of fades by

(tw/clear state :my-param)

All fades in front of the current location are applied; all those ahead of the current position are discarded. Any fade that is in progress is interpolated, and its intermediate value saved before the fade is removed. The clear function is useful when live coding (to prevent manually applied fades overlapping); it can also be used to smooth live controller input by taking each incoming value x and doing a clear followed by an automate-in at the current location with a short fade time.

Location and Sampling

Locate a state to a particular position:

(tw/locate state 300)

A call to locate returns a new state with any fades which lie totally in front of (earlier than) the specified timestamp (here, 300) to be removed, once they’ve been sampled: in other words, the fades are chased, so that the target values of purged fades are applied. Example:

(-> (tw/initial)
    (tw/automate-at :my-param 100 10 9.9)
    (tw/locate 150)
    (tw/sample :my-param))

This last example returns 9.9, the target of the purged fade. If we added a second locate point at 50 on the line after the first locate (say: in front of the original fade), the result would still be 9.9, since the first locate would have chased that fade and removed it.

If a fade is in scope (i.e. the locate timestamp lies within the fade), it is not purged, and the state’s position can still be shifted back and forth along it (although I don’t know why you’d want to do that).

Sample a state at its current timestamp:

(tw/sample state :pitchbend)

A call to sample just returns the sampled value at that timestamp; the state is not changed.


For automation over values more interesting than floats, provide an interpolation function:

(def state (tw/initial :interp {:foreground colour-mix
                                :background colour-mix}
                       :init {:foreground [1 1 1]
                              :background [0 0 0]}))

The interpolator (in this case, colour-mix) will be called with three arguments: start value, end value, and interpolation position (from 0.0 to 1.0). Unless nil works as a potential initial value, provide that value as well.

There’s no reason why the interpolator - or the automation channel - should actually be numeric at all. Channels can “automate” arbitrary values, so long as the interpolator handles them. Here’s an example (currently being used by us on stage):

(def state (tw/initial :init   {:text "---"}
                       :interp {:text (fn [_ to _] to}}))

This channel has an initial value of =”—”= and any fade to another value (of any type) takes effect immediately.

We have some interpolators (including the default) in namespace eu.cassiel.twizzle.interpolators - see the documentation.

Complex Keys

Since this is Clojure, there’s nothing stopping you using complex keys, like vectors, as channel names:

(-> (tw/initial :init {[:VOLUME 3] 127})
    (tw/sample [:VOLUME 3]))

This would allow groups of channels to be set up and indexed programmatically, while allowing common :init or :interp values to be set for them (if you don’t mind a bit of reduce action):

(tw/initial :init (reduce (fn [m k] (assoc m [:VOLUME k] 127))
                          (range 10)))


The source documentation is here.


0.6.0, 2016-01-27
Release: ClojureScript tweaks to :require syntax in README.
0.6.0-SNAPSHOT, 2015-08-10
Incorporating ClojureScript support: Clojure 1.7.0 dependency, .cljc source file extension, tweaks to :require syntax.
0.5.0, 2014-09-19
Breaking change (prior to public release): renamed automate-by to automate-in.
0.4.1-SNAPSHOT, 2014-08-21
A bit of wrapper code for [[][tween-clj]].
0.3.1-SNAPSHOT, 2014-08-12
Bug-fix (function reordering), not caught in tests (I hate you, Midje).
0.3.0-SNAPSHOT, 2014-08-12
Implemented `clear`.
0.2.0, 2014-08-03
0.2.0-SNAPSHOT, 2014-08-01
Default function for vector interpolator.
0.1.1-SNAPSHOT, 2014-07-31
Bug-fix (purging multiple fades).
0.1.0-SNAPSHOT, 2014-07-31
Internal release.


Copyright © 2014 Nick Rothwell.

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.