Animation

Animation helper functions

Luxor provides some functions to help you create animations—at least, it provides some assistance in creating lots of individual frames that can later be stitched together to form a moving animation, such as a GIF or MP4.

There are four steps to creating an animation.

1 Use Movie to create a Movie object which determines the title and dimensions.

2 Define some functions that draw the graphics for specific frames.

3 Define one or more Scenes that call these functions for specific frames.

4 Call the animate(movie::Movie, scenes) function, passing in the scenes. This creates all the frames and saves them in a temporary directory. Optionally, you can ask for ffmpeg (if it's installed) to make an animated GIF for you.

Example

using Luxor

demo = Movie(400, 400, "test")

function backdrop(scene, framenumber)
    background("black")
end

function frame(scene, framenumber)
    sethue(Colors.HSV(framenumber, 1, 1))
    eased_n = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    circle(polar(100, -pi/2 - (eased_n * 2pi)), 80, :fill)
    text(string("frame $framenumber of $(scene.framerange.stop)"),
        Point(O.x, O.y-190),
        halign=:center)
end

animate(demo, [
    Scene(demo, backdrop, 0:359),
    Scene(demo, frame, 0:359, easingfunction=easeinoutcubic)
    ],
    creategif=true)

animation example

Luxor.MovieType.

The Movie and Scene types and the animate() function are designed to help you create the frames that can be used to make an animated GIF or movie.

1 Provide width, height, title, and optionally a frame range to the Movie constructor:

demo = Movie(400, 400, "test", 1:500)

2 Define one or more scenes and scene-drawing functions.

3 Run the animate() function, calling those scenes.

Example

bang = Movie(400, 100, "bang")

backdrop(scene, framenumber) =  background("black")

function frame1(scene, framenumber)
    background("white")
    sethue("black")
    eased_n = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    circle(O, 40 * eased_n, :fill)
end

animate(bang, [
    Scene(bang, backdrop, 0:200),
    Scene(bang, frame1, 0:200, easingfunction=easeinsine)],
    creategif=true,
    pathname="/tmp/animationtest.gif")
Luxor.SceneType.

The Scene type defines a function to be used to render a range of frames in a movie.

  • the movie created by Movie()
  • the framefunction is a function taking two arguments: the scene and the framenumber.
  • the framerange determines which frames are processed by the function. Defaults to the entire movie.
  • the optional easingfunction can be accessed by the framefunction to vary the transition speed
Luxor.animateFunction.
animate(movie::Movie, scenelist::AbstractArray{Scene, 1};
        creategif=false,
        pathname=""
        framerate=30,
        tempdirectory=".")

Create the movie defined in movie by rendering the frames define in the array of scenes in scenelist.

If creategif is true, the function tries to call ffmpeg on the resulting frames to build a GIF animation. This will be stored in pathname (an existing file will be overwritten; use a ".gif" suffix), or in (movietitle).gif in a temporary directory.

Example

animate(bang, [
    Scene(bang, backdrop, 0:200),
    Scene(bang, frame1, 0:200, easingfunction=easeinsine)],
    creategif=true,
    pathname="/tmp/animationtest.gif")
animate(movie::Movie, scene::Scene; creategif=false, framerate=30)

Create the movie defined in movie by rendering the frames define in scene.

Making the animation

For best results, you'll have to learn how to use something like ffmpeg, with its hundreds of options, which include codec selction, framerate adjustment and color palette tweaking. The creategif option for the animate function makes an attempt at running ffmpeg and assumes that it's installed. Inside animate(), the first pass creates a GIF color palette, the second builds the file:

run(`ffmpeg -f image2 -i $(tempdirectory)/%10d.png -vf palettegen -y $(seq.stitle)-palette.png`)

run(`ffmpeg -framerate 30 -f image2 -i $(tempdirectory)/%10d.png -i $(seq.stitle)-palette.png -lavfi paletteuse -y /tmp/$(seq.stitle).gif`)

Many movie editing programs, such as Final Cut Pro, will also let you import sequences of still images into a movie timeline.

Using scenes

Sometimes you want to construct an animation that has different components, layers, or scenes. To do this, you can specify scenes that are drawn only for specific frames.

As an example, consider a simple example showing the sun for each hour of a 24 hour day.

sun24demo = Movie(400, 400, "sun24", 0:23)

The backgroundfunction() draws a background that's used for all frames:

function backgroundfunction(scene::Scene, framenumber)
    background("black")
end

A nightskyfunction() draws the night sky:

function nightskyfunction(scene::Scene, framenumber)
    sethue("midnightblue")
    box(O, 400, 400, :fill)
end

A dayskyfunction() draws the daytime sky:

function dayskyfunction(scene::Scene, framenumber)
    sethue("skyblue")
    box(O, 400, 400, :fill)
end

The sunfunction() draws a sun at 24 positions during the day:

function sunfunction(scene::Scene, framenumber)
    i = rescale(framenumber, 0, 23, 2pi, 0)
    gsave()
    sethue("yellow")
    circle(polar(150, i), 20, :fill)
    grestore()
end

Finally a groundfunction() draws the ground:

function groundfunction(scene::Scene, framenumber)
    gsave()
    sethue("brown")
    box(Point(O.x, O.y + 100), 400, 200, :fill)
    grestore()
    sethue("white")
end

Now define a group of Scenes that make up the movie. The scenes specify which functions are to be used, and for which frames:

backdrop  = Scene(sun24demo, backgroundfunction, 0:23)
nightsky  = Scene(sun24demo, nightskyfunction, 0:6)
nightsky1 = Scene(sun24demo, nightskyfunction, 17:23)
daysky    = Scene(sun24demo, dayskyfunction, 5:19)
sun       = Scene(sun24demo, sunfunction, 6:18)
ground    = Scene(sun24demo, groundfunction, 0:23)

Finally, the animate function scans the scenes in the scenelist for a movie, and calls the functions for each frame to build the animation:

animate(sun24demo, [backdrop, nightsky, nightsky1, daysky, sun, ground],
    framerate=5,
    creategif=true)

sun24 animation

Notice that for some frames, such as frame 0, 1, or 23, three of the functions are called: for others, such as 7 and 8, four or more functions are called. Also notice that the order of scenes and the use of backgrounds can be important.

Easing functions

Transitions for animations often use non-constant and non-linear motions, and these are usually provided by easing functions. Luxor defines some of the basic easing functions and they're listed in the (unexported) array Luxor.easingfunctions. Each scene can have one easing function.

Most easing functions have names constructed like this:

ease[in|out|inout][expo|circ|quad|cubic|quart|quint]

and there's an easingflat() linear transition.

In these graphs, the horizontal axis is time (between 0 and 1), and the vertical axis is the parameter value (between 0 and 1).

easing function summary

One way to use an easing function in a frame-making function is like this:

function moveobject(scene, framenumber)
    background("white")
    ...
    easedframenumber = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    ...

This takes the current frame number, compares it with the end frame number of the scene, then adjusts it.

In the next example, the purple dot has sinusoidal easing motion, the green has cubic, and the red has quintic. They all traverse the drawing in the same time, but have different accelerations and decelerations.

animation easing example

fastandfurious = Movie(400, 100, "easingtests")
backdrop(scene, framenumber) =  background("black")
function frame1(scene, framenumber)
    sethue("purple")
    eased_n = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    circle(Point(-180 + (360 * eased_n), -20), 10, :fill)
end
function frame2(scene, framenumber)
    sethue("green")
    eased_n = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    circle(Point(-180 + (360 * eased_n), 0), 10, :fill)
end
function frame3(scene, framenumber)
    sethue("red")
    eased_n = scene.easingfunction(framenumber, 0, 1, scene.framerange.stop)
    circle(Point(-180 + (360 * eased_n), 20), 10, :fill)
end
animate(fastandfurious, [
    Scene(fastandfurious, backdrop, 0:200),
    Scene(fastandfurious, frame1,   0:200, easingfunction=easeinsine),
    Scene(fastandfurious, frame2,   0:200, easingfunction=easeinoutcubic),
    Scene(fastandfurious, frame3,   0:200, easingfunction=easeinoutquint)
    ],
    creategif=true)

Here's the definition of one of the easing functions:

function easeoutquad(t, b, c, d)
   t /= d
   return -c * t * (t - 2) + b
end

Here:

Luxor.easingflatFunction.
easingflat(t, b, c, d)

A flat easing function, same as lineartween().

For all easing functions, the four parameters are:

  • t time, ie the current framenumber
  • b beginning position or bottom value of the range
  • c total change in position or top value of the range
  • d duration, ie a framecount
  1. t/d or t/=d normalizes t to between 0 and 1
  2. ... * c scales up to the required range value
  3. ... + b adds the initial offset
Luxor.lineartweenFunction.

default linear transition - no easing, no acceleration

Luxor.easeinquadFunction.
easeinquad(t, b, c, d)

quadratic easing in - accelerating from zero velocity

Luxor.easeoutquadFunction.
easeoutquad(t, b, c, d)

quadratic easing out - decelerating to zero velocity

Luxor.easeinoutquadFunction.
easeinoutquad(t, b, c, d)

quadratic easing in/out - acceleration until halfway, then deceleration

Luxor.easeincubicFunction.
easeincubic(t, b, c, d)

cubic easing in - accelerating from zero velocity

Luxor.easeoutcubicFunction.
easeoutcubic(t, b, c, d)

cubic easing out - decelerating to zero velocity

Luxor.easeinoutcubicFunction.
easeinoutcubic(t, b, c, d)

cubic easing in/out - acceleration until halfway, then deceleration

Luxor.easeinquartFunction.
easeinquart(t, b, c, d)

quartic easing in - accelerating from zero velocity

Luxor.easeoutquartFunction.
easeoutquart(t, b, c, d)

quartic easing out - decelerating to zero velocity

Luxor.easeinoutquartFunction.
easeinoutquart(t, b, c, d)

quartic easing in/out - acceleration until halfway, then deceleration

Luxor.easeinquintFunction.
easeinquint(t, b, c, d)

quintic easing in - accelerating from zero velocity

Luxor.easeoutquintFunction.
easeoutquint(t, b, c, d)

quintic easing out - decelerating to zero velocity

Luxor.easeinoutquintFunction.
easeinoutquint(t, b, c, d)

quintic easing in/out - acceleration until halfway, then deceleration

Luxor.easeinsineFunction.
easeinsine(t, b, c, d)

sinusoidal easing in - accelerating from zero velocity

Luxor.easeoutsineFunction.
easeoutsine(t, b, c, d)

sinusoidal easing out - decelerating to zero velocity

Luxor.easeinoutsineFunction.
easeinoutsine(t, b, c, d)

sinusoidal easing in/out - accelerating until halfway, then decelerating

Luxor.easeinexpoFunction.
easeinexpo(t, b, c, d)

exponential easing in - accelerating from zero velocity

Luxor.easeoutexpoFunction.
easeoutexpo(t, b, c, d)

exponential easing out - decelerating to zero velocity

Luxor.easeinoutexpoFunction.
easeinoutexpo(t, b, c, d)

exponential easing in/out - accelerating until halfway, then decelerating

Luxor.easeincircFunction.
easeincirc(t, b, c, d)

circular easing in - accelerating from zero velocity

Luxor.easeoutcircFunction.
easeoutcirc(t, b, c, d)

circular easing out - decelerating to zero velocity

Luxor.easeinoutcircFunction.
easeinoutcirc(t, b, c, d)

circular easing in/out - acceleration until halfway, then deceleration

easeinoutinversequad(t, b, c, d)

ease in, then slow down, then speed up, and ease out