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
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, -π/2 - (eased_n * 2π)), 80, :fill)
text(string("frame $framenumber of $(scene.framerange.stop)"),
Point(O.x, O.y-190),
halign=:center)
text(scene.opts,
boxbottomcenter(BoundingBox()),
halign=:center,
valign=:bottom)
end
animate(demo, [
Scene(demo, backdrop, 0:359),
Scene(demo, frame, 0:359,
easingfunction=easeinoutcubic,
optarg="made with Julia")
],
creategif=true)
In this example, the movie uses two scenes, each specifying a function to draw frames from 0 to 359. For each frame numbered 0 to 359, the graphics are drawn by both the backdrop()
and frame()
functions, in that order. A drawing is automatically created (in PNG format) and centered (origin()
) so you can start drawing immediately. The finish()
function is automatically called when all the drawing functions in the scenes have completed, and the process starts afresh for the next frame. The second scene, calling the frame()
function, shows how you can pass optional information to the function.
Luxor.Movie
— TypeThe 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.Scene
— TypeThe 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 - the optional
opts
which is a single argument of an abstract type which can be accessed within the framefunction
Luxor.animate
— Functionanimate(movie::Movie, scenelist::Array{Scene, 1};
creategif=false,
framerate=30,
pathname="",
tempdirectory="",
usenewffmpeg=true)
Create the movie defined in movie
by rendering the frames define in the array of scenes in scenelist
.
If creategif
is true
, the function attempts to call the ffmpeg
utility 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. ffmpeg
should be installed and available, of course, if this is to work.
In suitable environments (eg Juno), the resulting animation is displayed in the Plots window.
Example
animate(bang, [
Scene(bang, backdrop, 0:200),
Scene(bang, frame1, 0:200, easingfunction=easeinsine)],
creategif=true,
pathname="/tmp/animationtest.gif")
The usenewffmpeg
option, true
by default, uses single-pass palette generation and more complex filtering provided by recent versions of the ffmpeg
utility, mainly to cope with transparent backgrounds. If set to false
, the behavior is the same as in previous versions of Luxor.
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 selection, framerate adjustment and color palette tweaking. The creategif
option for the animate
function makes an attempt at running ffmpeg
and assumes that it's already 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 (animated GIFs like constant backgrounds):
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) # every frame
nightsky = Scene(sun24demo, nightskyfunction, 0:6) # midnight to 06:00
nightsky1 = Scene(sun24demo, nightskyfunction, 17:23) # 17:00 to 23:00
daysky = Scene(sun24demo, dayskyfunction, 5:19) # 05:00 to 19:00
sun = Scene(sun24demo, sunfunction, 6:18) # 06:00 to 18:00
ground = Scene(sun24demo, groundfunction, 0:23) # every frame
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)
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 is important.
An alternative approach is to use the incoming framenumber as the master parameter that determines the position and appearance of all the graphics.
function frame(scene, framenumber)
background("black")
n = rescale(framenumber, scene.framerange.start, scene.framerange.stop, 0, 1)
n2π = rescale(n, 0, 1, 0, 2π)
sethue(n, 0.5, 0.5)
box(BoundingBox(), :fill)
if 0.25 < n < 0.75
sethue("yellow")
circle(polar(150, n2π + π/2), 20, :fill)
end
if n < 0.25 || n > 0.75
sethue("white")
circle(polar(150, n2π + π/2), 20, :fill)
end
end
Passing information to the frame() function
If you want to pass information to the frame function, such as an array of values, try these:
function frame(scene, framenumber, datapoints)
...
end
somedata = Datapoints[...]
animate(demo, [
Scene(demo, (s, f) -> frame(s, f, somedata),
0:100,
optarg=somedata)
],
creategif=true,
pathname="...")
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.
function draweasingfunction(f, pos, w, h)
@layer begin
translate(pos)
setline(0.5)
sethue("black")
box(O, w, h, :stroke)
sethue("purple")
for i in 0:0.005:1.0
circle(Point(-w/2, h/2) + Point(w * i, -f(i, 0, h, 1)), 1, :fill)
end
sethue("black")
text(replace(string(f), "Luxor." => ""), Point(0, h/2 - 20), halign=:center)
end
end
t = Tiler(650, 650, 5, 5)
margin=5
fontsize(10)
fontface("Menlo")
for (pos, n) in t
n > length(Luxor.easingfunctions) && continue
draweasingfunction(Luxor.easingfunctions[n], pos,
t.tilewidth-margin, t.tileheight-margin)
end
In these graphs, the horizontal axis is time (between 0 and 1), and the vertical axis is the parameter value (between 0 and 1).
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.
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:
t
is the current time (framenumber) of the transitionb
is the beginning value of the propertyc
is the change between the beginning and destination value of the propertyd
is the total length of the transition
The easeinoutbezier()
function accepts two additional arguments, two normalized control points of a normalized Bezier curve from Point(0, 0) to Point(1, 1). You can use these to define the shape of a custom easing transition. The Bezier curve's y
coordinate determines the acceleration. [?]
Luxor.easingflat
— Functioneasingflat(t, b, c, d)
A flat easing function, same as lineartween()
.
For all easing functions, the four parameters are:
t
time, ie the current framenumberb
beginning position or bottom value of the rangec
total change in position or top value of the ranged
duration, ie a framecount
t/d
ort/=d
normalizest
to between 0 and 1... * c
scales up to the required range value... + b
adds the initial offset
Luxor.lineartween
— Functiondefault linear transition - no easing, no acceleration
Luxor.easeinquad
— Functioneaseinquad(t, b, c, d)
quadratic easing in - accelerating from zero velocity
Luxor.easeoutquad
— Functioneaseoutquad(t, b, c, d)
quadratic easing out - decelerating to zero velocity
Luxor.easeinoutquad
— Functioneaseinoutquad(t, b, c, d)
quadratic easing in/out - acceleration until halfway, then deceleration
Luxor.easeincubic
— Functioneaseincubic(t, b, c, d)
cubic easing in - accelerating from zero velocity
Luxor.easeoutcubic
— Functioneaseoutcubic(t, b, c, d)
cubic easing out - decelerating to zero velocity
Luxor.easeinoutcubic
— Functioneaseinoutcubic(t, b, c, d)
cubic easing in/out - acceleration until halfway, then deceleration
Luxor.easeinquart
— Functioneaseinquart(t, b, c, d)
quartic easing in - accelerating from zero velocity
Luxor.easeoutquart
— Functioneaseoutquart(t, b, c, d)
quartic easing out - decelerating to zero velocity
Luxor.easeinoutquart
— Functioneaseinoutquart(t, b, c, d)
quartic easing in/out - acceleration until halfway, then deceleration
Luxor.easeinquint
— Functioneaseinquint(t, b, c, d)
quintic easing in - accelerating from zero velocity
Luxor.easeoutquint
— Functioneaseoutquint(t, b, c, d)
quintic easing out - decelerating to zero velocity
Luxor.easeinoutquint
— Functioneaseinoutquint(t, b, c, d)
quintic easing in/out - acceleration until halfway, then deceleration
Luxor.easeinsine
— Functioneaseinsine(t, b, c, d)
sinusoidal easing in - accelerating from zero velocity
Luxor.easeoutsine
— Functioneaseoutsine(t, b, c, d)
sinusoidal easing out - decelerating to zero velocity
Luxor.easeinoutsine
— Functioneaseinoutsine(t, b, c, d)
sinusoidal easing in/out - accelerating until halfway, then decelerating
Luxor.easeinexpo
— Functioneaseinexpo(t, b, c, d)
exponential easing in - accelerating from zero velocity
Luxor.easeoutexpo
— Functioneaseoutexpo(t, b, c, d)
exponential easing out - decelerating to zero velocity
Luxor.easeinoutexpo
— Functioneaseinoutexpo(t, b, c, d)
exponential easing in/out - accelerating until halfway, then decelerating
Luxor.easeincirc
— Functioneaseincirc(t, b, c, d)
circular easing in - accelerating from zero velocity
Luxor.easeoutcirc
— Functioneaseoutcirc(t, b, c, d)
circular easing out - decelerating to zero velocity
Luxor.easeinoutcirc
— Functioneaseinoutcirc(t, b, c, d)
circular easing in/out - acceleration until halfway, then deceleration
Luxor.easeinoutinversequad
— Functioneaseinoutinversequad(t, b, c, d)
ease in, then slow down, then speed up, and ease out
Luxor.easeinoutbezier
— Functioneaseinoutbezier(t, b, c, d, cpt1, cpt2)
This easing function takes six arguments, the usual t
, b
, c
, and d
, but also two points. These are the normalized control points of a Bezier curve drawn between Point(0, 0)
to Point(1.0, 1.0)
. The y
value of the Bezier is the eased value for t
.
In your frame()
generating function, if a Scene specifies the easeinoutbezier
easing function, you can use this:
...
lineareasing = rescale(framenumber, 1, scene.framerange.stop)
beziereasing = scene.easingfunction(lineareasing, 0, 1, 1,
Point(0.25, 0.25), Point(0.75, 0.75))
...
These two control points lie on the line between 0/0
and 1/1
, so it's equivalent to a linear easing (lineartween()
or easingflat
).
However, in the next example, the two control points define a wave-like curve that changes direction before changing back. When animating with this easing function, an object will 'go retrograde' for a while.
lineareasing = rescale(framenumber, 1, scene.framerange.stop)
beziereasing = scene.easingfunction(lineareasing, 0, 1, 1,
Point(0.01, 1.99), Point(0.99, -1.5))