Animation

Makie.jl has extensive support for animations; you can create arbitrary plots, and save them to:

  • .mkv (the default, doesn't need to convert)
  • .mp4 (good for Web, most supported format)
  • .webm (smallest file size)
  • .gif (largest file size for the same quality)

This is all made possible through the use of the ffmpeg tool, wrapped by FFMPEG.jl.

Have a peek at Interaction for some more information once you're done with this.

A simple example

Simple animations are easy to make; all you need to do is wrap your changes in the record function.

When recording, you can make changes to any aspect of the Scene or its plots.

Below is a small example of using record.

scene = lines(rand(10); linewidth=10)

record(scene, "out.mp4", 1:255; framerate = 60) do i
    scene.plots[2][:color] = RGBf0(i/255, (255 - i)/255, 0) # animate scene
    # `scene.plots` gives the plots of the Scene.
    # `scene.plots[1]` is always the Axis if it exists,
    # and `scene.plots[2]` onward are the user-defined plots.
end;

AbstractPlotting.recordFunction
record(func, scene, path; framerate = 24, compression = 20)
record(func, scene, path, iter;
        framerate = 24, compression = 20, sleep = true)

The first signature provides func with a VideoStream, which it should call recordframe!(io) on when recording a frame.

Records the Scene scene after the application of func on it for each element in itr (any iterator). func must accept an element of itr.

The animation is then saved to path, with the format determined by path's extension. Allowable extensions are:

  • .mkv (the default, doesn't need to convert)
  • .mp4 (good for Web, most supported format)
  • .webm (smallest file size)
  • .gif (largest file size for the same quality)

.mp4 and .mk4 are marginally bigger and .gifs are up to 6 times bigger with the same quality!

The compression argument controls the compression ratio; 51 is the highest compression, and 0 is the lowest (lossless).

When sleep is set to true (the default), AbstractPlotting will display the animation in real-time by sleeping in between frames. Thus, a 24-frame, 24-fps recording would take one second to record.

When it is set to false, frames are rendered as fast as the backend can render them. Thus, a 24-frame, 24-fps recording would usually take much less than one second in GLMakie.

Typical usage patterns would look like:

record(scene, "video.mp4", itr) do i
    func(i) # or some other manipulation of the Scene
end

or, for more tweakability,

record(scene, "test.gif") do io
    for i = 1:100
        func!(scene)     # animate scene
        recordframe!(io) # record a new frame
    end
end

If you want a more tweakable interface, consider using VideoStream and save.

Extended help

Examples

scene = lines(rand(10))
record(scene, "test.gif") do io
    for i in 1:255
        scene.plots[:color] = Colors.RGB(i/255, (255 - i)/255, 0) # animate scene
        recordframe!(io)
    end
end

or

scene = lines(rand(10))
record(scene, "test.gif", 1:255) do i
    scene.plots[:color] = Colors.RGB(i/255, (255 - i)/255, 0) # animate scene
end

In both cases, the returned value is a path pointing to the location of the recorded file.

Animation using time

To animate a scene, you can also create a Node, e.g.:

time = Node(0.0)
Observable{Float64} with 0 listeners. Value:
0.0

and use lift on the Node to set up a pipeline to access its value. For example:

scene = Scene()
time = Node(0.1)
myfunc(v, t) = sin.(v .* t)
positions = lift(t -> myfunc.(range(0, stop=2pi, length=50), t), time)
scene = lines!(scene, positions)
Scene (960px, 540px):
  2 Plots:
    ├ Axis2D{...}
    └ Lines{...}
  1 Child Scene:
    └ Scene (960px, 540px)

now, whenever the Node time is updated (e.g. when you push! to it), the plot will also be updated.

push!(time, Base.time());
1.600156242524156e9

You can also set most attributes equal to Observables, so that you need only update a single variable (like time) during your animation loop. A translation of the first example to this Observables paradigm is below:

"'Time' - an Observable that controls the animation"
t = Node(0)

"The colour of the line"
c = lift(t) do t
    RGBf0(t/255, (255 - t)/255, 0)
end

scene = lines(rand(10); linewidth=10, color = c)

record(scene, "out2.mp4", 1:255; framerate = 60) do i
    t[] = i # update `t`'s value
end

A more complicated example:

let
    scene = Scene()

    f(t, v, s) = (sin(v + t) * s, cos(v + t) * s, (cos(v + t) + sin(v)) * s)
    t = Node(Base.time()) # create a life signal
    limits = FRect3D(Vec3f0(-1.5, -1.5, -3), Vec3f0(3, 3, 6))
    p1 = meshscatter!(scene, lift(t-> f.(t, range(0, stop = 2pi, length = 50), 1), t), markersize = 0.05)[end]
    p2 = meshscatter!(scene, lift(t-> f.(t * 2.0, range(0, stop = 2pi, length = 50), 1.5), t), markersize = 0.05)[end]

    lines = lift(p1[1], p2[1]) do pos1, pos2
        map((a, b)-> (a, b), pos1, pos2)
    end
    linesegments!(scene, lines, linestyle = :dot, limits = limits)
    # record a video
    N = 150
    record(scene, "out3.mp4", 1:N) do i
        t[] = Base.time()
    end
end

Appending data to a plot

If you're planning to append to a plot, like a lines or scatter plot (basically, anything that's point-based), you will want to pass an Observable Array of Points to the plotting function, instead of passing x, y (and z) as separate Arrays. This will mean that you won't run into dimension mismatch issues (since Observables are synchronously updated).

TODO add more tips here

Animating a plot "live"

You can animate a plot in a for loop:

for i = 1:length(r)
    s[:markersize] = r[i]
    sleep(1/24)
end

Similarly, for plots based on functions:

scene = Scene()
v = range(0, stop=4pi, length=50)
f(v, t) = sin(v + t) # some function
s = lines!(
    scene,
    lift(t -> f.(v, t), time),
)[end];

for i = 1:length(v)
    time[] = i
    sleep(1/24)
end

If you want to animate a plot while interacting with it, check out the async_latest function, and the Interaction section.

Transforming a live loop to an animation

You can transform a live loop to a recording using the record function very simply. For example,

positions = Node(Point2f0.(rand(10), rand(10)))
scene = Scene()
scatter!(scene, positions)
for i in 1:10
    positions[] = Point2f0.(rand(10), rand(10))
    sleep(1/4)
end

can be recorded just by changing the for loop to a record-do "loop":

positions = Node(Point2f0.(rand(10), rand(10)))
scene = Scene()
scatter!(scene, positions)
record(scene, "name.mp4", 1:10) do i
    positions[] = Point2f0.(rand(10), rand(10))
    sleep(1/4)
end

More complex examples

scene = Scene();
function xy_data(x, y)
    val = sqrt(x^2 + y^2)
    val == 0.0 ? 1f0 : (sin(val)/val)
end
r = range(-2, stop = 2, length = 50)
surf_func(i) = [Float32(xy_data(x*i, y*i)) for x = r, y = r]
z = surf_func(20)
surf = surface!(scene, r, r, z)[end]

wf = wireframe!(scene, r, r, lift(x-> x .+ 1.0, surf[3]),
    linewidth = 2f0, color = lift(x-> to_colormap(x)[5], surf[:colormap])
)
N = 150
scene
record(scene, "out5.mp4", range(5, stop = 40, length = N)) do i
    surf[3] = surf_func(i)
end

You can see yet more complicated examples in the Example Gallery!