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.record
— Functionrecord(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 .gif
s 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 Observable
s, 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 Point
s 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!