Julia におけるアニメーションのしくみ

# はじめに

アニメーションでやってみたいことがあるので、既存のコードでどのような実装がなされているのかを調査する。

具体的には、Plots.jl[^plots] の animation.jl を読んでいく。
https://github.com/JuliaPlots/Plots.jl/blob/master/src/animation.jl

[^plots]: Copyright (c) 2015: Thomas Breloff.

# まずは概要から

## Animation の使われ方

### 一般的な使われ方

```julia
anim = @animate for i in 1:10
    plot(t->sinpi(t+i/5), range(0, 2, length=100))
end

#gif(anim, "sin.gif", fps=10)
```

![sin.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202534/7636c7bd-2c47-3daf-9a8a-ee872e9504e4.gif)


### マクロを展開すると？

意味的には次のコードになる。

```julia
anim = Animation()
for i in 1:10
    plt = plot(t->sinpi(t+i/5), range(0, 2, length=100))
    frame(anim, plt)
end
```

より正確には、 `@animate` の部分を `@macroexpand` するとマクロ展開後のコードが得られるので確認しよう。

```julia
# gensym を使って変数名を生成した結果、 var"##tag#番号" という変数が得られる？
var"##anim#576" = Animation()
global var"##counter#577" = 1

for i = 1:10
    plot((t->begin
            sinpi(t + i / 5)
        end), range(0, 2, length=100))
    
    # 例えば 10 フレームごとにプロットする場合は var"##counter#577" を使って条件分岐する
    if true
        frame(var"##anim#576")
    end
    global var"##counter#577" += 1
end

var"##anim#576"
```

`plt` が明示されていないが、現在のプロットを保持する変数 `CURRENT_PLOT` が暗黙的に使われる（`current()` を通して呼び出される）。
https://github.com/JuliaPlots/Plots.jl/blob/master/src/plot.jl

## 処理の流れ

1. 連番 PNG のパスを記憶するために anim を作成する
2. フレームごとに plot から PNG を出力する
3. ffmpeg を呼び出して、連番 PNG から mp4 に変換する

# 主な関数をもっと詳しく

## Animation

連番 PNG を管理する

```julia:animation.jl
"Represents an animation object"
struct Animation
    dir::String
    frames::Vector{String}
end

function Animation()
    tmpdir = convert(String, mktempdir())
    Animation(tmpdir, String[])
end
```

1. 一時ディレクトリを作成する
2. 一時ディレクトリのパスとフレーム数を記憶する構造体を返す

`frames::Vector{String}` がフレーム数を記憶するわけなのだが、なぜこれが単なる整数ではなく、文字列の配列になっているかは疑問に思った。試してみると、（デフォルトで immutable となる）構造体だと内部変数を変更できないが、内部配列ならその要素を変更できるようなのである。immutable の制限を回避するためにこのような方法をとっているのだろうか。`dir` は変更されないほうがいいのは理解できるし、まあそういうものかととりあえずは納得した。

## frame

PNG を１フレーム出力する

```julia:animation.jl
"""
    frame(animation[, plot])
Add a plot (the current plot if not specified) to an existing animation
"""
function frame(anim::Animation, plt::P=current()) where P<:AbstractPlot
    i = length(anim.frames) + 1
    filename = @sprintf("%06d.png", i)
    png(plt, joinpath(anim.dir, filename))
    push!(anim.frames, filename)
end
```

1. anim と plt を受け取る
2. anim から現在のフレーム数を取得する
3. 現在のフレーム数を６桁まで０埋めして PNG のファイル名にする（000001.png など）
4. Plots.png で plt を PNG に出力する
5. anim のフレーム数を加算する

ここで見ればわかるように、 `Animation.frames` は連番 PNG のファイル名の配列になっている。

## mp4

連番 PNG から mp4 を作成する

ごちゃごちゃしているが、本質的にはこの２行に集約される。`fn` は動画のファイル名。

```julia:animation.jl
run(`ffmpeg -v 0 -framerate $fps -loop $loop -i $(animdir)/%06d.png -pix_fmt yuv420p -y $fn`)

AnimatedGif(fn)
```

ffmpeg に PNG 連番ファイルを渡して mp4 を作成している。
その後で返ってくる `AnimatedGif` は後述。

## Base.show

```julia:animation.jl
"Wraps the location of an animated gif so that it can be displayed"
struct AnimatedGif
    filename::String
end

# write out html to view the gif... note the rand call which is a hack so the image doesn't get cached
function Base.show(io::IO, ::MIME"text/html", agif::AnimatedGif)
    ext = file_extension(agif.filename)
    write(io, if ext == "gif"
        "<img src=\"$(relpath(agif.filename))?$(rand())>\" />"
    elseif ext in ("mov", "mp4")
        "<video controls><source src=\"$(relpath(agif.filename))?$(rand())>\" type=\"video/$ext\"></video>"
    else
        error("Cannot show animation with extension $ext: $agif")
    end)
end
```

mp4 作成の後に返される `AnimatedGif` は、作成した動画のパスを保持している。

Jupyter などでオブジェクトが呼ばれると、その型に合った `Base.show` が呼ばれる。上のコードはその時の動作を記述している。要するに、 mp4 関数が呼ばれると作成した動画を Jupyter 上に表示するような HTML タグを出力する。

Julia / Jupyter での画像表示を実装する
https://qiita.com/Lirimy/items/b5604b41247ba8ca7a00

アドレスに乱数を付加しているのは、キャッシュにアクセスさせず、毎回直に動画ファイルを読み込みにいかせるためのテクニックのようだ。