Skip to content

Instantly share code, notes, and snippets.

@matthieubulte
Last active January 21, 2022 08:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matthieubulte/b1f979220ed6db8e1b4debd1c319bc7f to your computer and use it in GitHub Desktop.
Save matthieubulte/b1f979220ed6db8e1b4debd1c319bc7f to your computer and use it in GitHub Desktop.
Animations in Gadfly
using Gadfly
import Cairo, Fontconfig
using Printf
using FFMPEG
#################################### This is pretty much the same code as in https://github.com/JuliaPlots/Plots.jl/blob/master/src/animation.jl
struct Animation
dir::String
frames::Vector{String}
kwargs::Iterators.Pairs
end
function Animation(; kwargs...)
tmpdir = convert(String, mktempdir())
Animation(tmpdir, String[], kwargs)
end
struct AnimatedGif
filename::String
end
file_extension(fn) = Base.Filesystem.splitext(fn)[2][2:end]
default_fn() = tempname() * ".gif"
gif(anim::Animation, fn=default_fn()) = buildanimation(anim, fn; anim.kwargs...)
ffmpeg_framerate(fps) = "$fps"
ffmpeg_framerate(fps::Rational) = "$(fps.num)/$(fps.den)"
function frame!(anim::Animation, p; dpi=nothing, width=Compose.default_graphic_width, height=Compose.default_graphic_height, kwargs...)
i = length(anim.frames) + 1
filename = @sprintf("%06d.png", i)
draw(PNG(joinpath(anim.dir, filename), width, height, dpi=dpi), p)
push!(anim.frames, filename)
end
function buildanimation(
anim::Animation,
fn::String;
fps::Real = 20,
loop::Integer = 0,
verbose = false,
show_msg::Bool = true,
kwargs...
)
if length(anim.frames) == 0
throw(ArgumentError("Cannot build empty animations"))
end
fn = abspath(expanduser(fn))
animdir = anim.dir
framerate = ffmpeg_framerate(fps)
verbose_level = (verbose isa Int ? verbose : verbose ? 32 : 16) # "error"
# generate a colorpalette first so ffmpeg does not have to guess it
ffmpeg_exe(
`-v $verbose_level -i $(animdir)/%06d.png -vf "palettegen=stats_mode=diff" -y "$(animdir)/palette.bmp"`,
)
# then apply the palette to get better results
ffmpeg_exe(
`-v $verbose_level -framerate $framerate -i $(animdir)/%06d.png -i "$(animdir)/palette.bmp" -lavfi "paletteuse=dither=sierra2_4a" -loop $loop -y $fn`,
)
show_msg && @info("Saved animation to ", fn)
AnimatedGif(fn)
end
function Base.show(io::IO, ::MIME"text/html", agif::AnimatedGif)
ext = file_extension(agif.filename)
if ext == "gif"
html =
"<img src=\"data:image/gif;base64," *
base64encode(read(agif.filename)) *
"\" />"
elseif ext in ("mov", "mp4", "webm")
mimetype = ext == "mov" ? "video/quicktime" : "video/$ext"
html =
"<video controls><source src=\"data:$mimetype;base64," *
base64encode(read(agif.filename)) *
"\" type = \"$mimetype\"></video>"
else
error("Cannot show animation with extension $ext: $agif")
end
write(io, html)
return nothing
end
Base.showable(::MIME"image/gif", agif::AnimatedGif) = file_extension(agif.filename) == "gif"
function Base.show(io::IO, ::MIME"image/gif", agif::AnimatedGif)
open(fio -> write(io, fio), agif.filename)
end
function _animate(forloop::Expr, callgif=false; kwargs...)
if forloop.head ∉ (:for, :while)
error("@animate macro expects a for- or while-block. got: $(forloop.head)")
end
animsym = gensym("anim")
p = gensym("p")
block = forloop.args[2]
forloop.args[2] = quote
$p = $block
frame!($animsym, $p; $kwargs...)
end
retval = callgif ? :(gif($animsym)) : anymsim
esc(quote
$animsym = Animation(; $kwargs...)
$forloop
$retval
end)
end
macro animate(forloop::Expr, args...)
_animate(forloop; eval(args...)...)
end
macro gif(forloop::Expr, args...)
_animate(forloop, true; eval(args...)...)
end
#################################### Example usage. The for loop inner-block should return a plot for the current frame
@gif for i=0:0.01:1
p = plot(
x=[cos(2pi*i)*(1-i)], y=[sin(2pi*i)*(1-i)], Geom.point,
Coord.cartesian(xmin=-1, xmax=1, ymin=-1, ymax=1),
)
p = vstack(p, p)
hstack(p, p)
end (fps=50, dpi=100)
# Note that kwargs are passed through the macro. This is done to be able to pass arguments to the PNG function when creating frames. Otherwise you are not able to change the resolution of the PNGs and the GIF ends up looking 96dpi bad...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment