-*- 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
.
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.
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.
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))
nil
(range 10)))
The source documentation is here.
0.6.0
,2016-01-27
- Release: ClojureScript tweaks to
:require
syntax inREADME
. 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
toautomate-in
. 0.4.1-SNAPSHOT
,2014-08-21
- A bit of wrapper code for
[[https://github.com/gstamp/tween-clj][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
- Deployment.
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.