DisplayScript

Animation

DisplayScript has a built-in system for implicit animation. Let's start with a simple example:

state moved = false
var x = 10
if moved {
    x = 110
}
draw at(x, 10, 100, 100, fill(black))
draw clickHandler(function () {
    next moved = !moved
})

When you click, the moved state variable changes from false to true and back, moving the black box back and forth instantaneously: there's no animation yet. Now let's add some animation.

state moved = false
var x = 10
if moved {
    x = 110
}
draw at(x, 10, 100, 100, fill(black))
draw clickHandler(function () {
    animate(smoothCurve(0.985), function () {
        next moved = !moved
    })
})

The animate function takes two arguments. The first is the animation curve: here we use the built-in smoothCurve with a parameter that happens to look good. The second argument is a function specifying the state updates to be animated. The animation system keeps track of where everything is before and after the state updates: any changes are animated using the given curve.

Note that we don't have to change the box-drawing code to support animation or specify any kind of transition. Code using built-in functions like at or opacity will animate automatically.

To animate your own values, you can use the animatable function.

state moved = false
draw clickHandler(function () {
    animate(smoothCurve(0.985), function () {
        next moved = !moved
    })
})
var a = 0
if moved {
    a = 100
}
show(a)
show(animatable(a))

Note how a changes instantaneously but animatable(a) animates.

Implementation Details

Every animatable value is produced by a call to animatable: both at and opacity call animatable internally. When you call animate, the system goes through three phases: begin, apply, and commit.

In the begin phase, the entire program is run as if we were drawing a new frame. Every call to animatable notes its current value with no animations applied and stores this value in a state variable.

In the apply phase, the function passed to animate is run, and its state updates are applied.

In the commit phase, the entire program is run again. Every call to animatable notes the difference between its current value and the value it stored during the begin phase. If the values are different, it adds an animation using the curve passed to animatable.

After these phases are over, the program draws again normally. While drawing normally, the animatable function applies all the animations added during the commit phase.

Note that animations are additive: each is expressed as an offset from the destination value, and the final animatable value is a sum of all the active animations.

Animations are removed when their effect on the final animatable value is less than 0.001.

Custom Curves

The animation curve passed to animate is a regular function. It takes a single argument—a time offset in seconds—and returns a progress value from zero (no progress) to one (the animation has finished).

You can define your own curves by creating a function like this. Here's a function for a linear curve:

function linearCurve(time) {
    var duration = 0.4
    if time > duration {
        return 1
    } else {
        return time / duration
    }
}

You can see what it looks like in our earlier example:

state moved = false
var x = 10
if moved {
    x = 110
}
draw at(x, 10, 100, 100, fill(black))
function linearCurve(time) {
    var duration = 0.4
    if time > duration {
        return 1
    } else {
        return time / duration
    }
}
draw clickHandler(function () {
    animate(linearCurve, function () {
        next moved = !moved
    })
})

Explicit Animation

If implicit animations don't fit what you're trying to do, you can also build animations yourself. The most basic way to run your own animations is the tick function:

state x = 10
draw at(x, 10, 100, 100, fill(black))
tick(function (elapsedTime) {
    next x = x + 100 * elapsedTime
    if next x > @width {
        next x = -100
    }
})

tick calls its argument once every time the program is run. The elapsedTime argument is the amount of time elapsed since the last call. Here we move the box across the screen at a constant rate of 100 pixels per second, wrapping around when it crosses the edge of the screen.

An even simpler way to build animations is to use the elapsedSeconds function:

var t = elapsedSeconds()
draw at(cos(t) * 100 + 100, sin(t) * 100 + 100, 100, 100, fill(black))

elapsedSeconds returns the number of seconds that have elapsed since the first time it was called. Here we move the box in a circle using the sin and cos functions.