複雑なアニメーションをプログラムする 〜Reanimate入門〜
Reanimateはアニメーションを作成するためのライブラリです。
ReanimateはHaskellのライブラリとして実装されているのでプログラムによってアニメーションを記述することができます。ライブラリに実装されている機能も多く、ドキュメントも豊富ですし、オンラインのPlaygroundまで用意されていてかなり完成度の高いライブラリになっています。さらにLaTeXや物理エンジン(Chipmonk 2D), POV-Ray, Blenderなど外部ツールとの連携もサポートされています。アニメーションの各フレームはSVGで書き出されるようになっており、幾何学的な図形やSVGフォントを使った文字などから構成されたアニメーションを作るのが得意です。作ったアニメーションは最終的にMP4, GIF, WebMに出力することができます(中間生成物である各フレームのSVGを取り出すことも可能です)。
公式サイトにExampleが多く載っているので、まずはReanimateを使ってどの様なアニメーションが作れるのか確認してみると良いでしょう。
この記事ではReanimateを使ってアニメーションを作るために必要な基本的な概念を解説していきたいと思います。
Reanimate 入門
Reanimateを使ったアプリケーションは以下のreanimate
関数を使ってmain
を実装するのがゴールになります。
reanimate :: Animation -> IO ()
この関数によって実装されるアプリケーションの使い方は後で見るとして、まずは引数となっているAnimation
型について見ていきましょう。
Animation
これは言わずもがなアニメーションを表す型です。
mkAnimation :: Duration -> (Time -> SVG) -> Animation
という関数から作ることができ、全体の長さと各時間におけるSVGを指定するとアニメーションになることが分かります。
Duration
もTime
もDouble
のエイリアスです。
type Duration = Double
type Time = Double
SVG
SVG
はその名の通りSVGを表す型です。以下のような関数から基本的な図形を作成することができます。
-- | 円
mkCircle :: Double -> SVG
-- | 楕円
mkEllipse :: Double -> Double -> SVG
-- | 長方形
mkRect :: Double -> Double -> SVG
-- | 線分
mkLine :: (Double, Double) -> (Double, Double) -> SVG
-- | 折れ線
mkLinePath :: [(Double, Double)] -> SVG
-- | パス
mkPath :: [PathCommand] -> SVG
-- | 色名から背景を塗りつぶすSVGを作成する
mkBackground :: String -> SVG
-- | 複数のSVGを組み合わせて一つのSVGにする
mkGroup :: [SVG] -> SVG
実はHaddock上では上記関数の返り値の型はTree
になっているのですが、SVG
はTree
の型エイリアスなので問題ありません。
type SVG = Tree
上述のような関数で単純に生成したSVGは画面の真ん中が中心になるように配置されます。SVGの位置などのプロパティはSVG -> SVG
型の関数によって変更することができます。
-- | SVGを x, y 方向に移動させる
translate :: Double -> Double -> SVG -> SVG
-- | SVGを回転させる
rotate :: Double -> SVG -> SVG
-- | SVGを拡大縮小する
scale :: Double -> SVG -> SVG
-- | 線の色を変える
withStrokeColor :: String -> SVG -> SVG
-- | 塗りつぶしの色を変える
withFillColor :: String -> SVG -> SVG
これだけで色んなアニメーションが作れそうですが、実はアニメーションを作るなら後述するObject
型をTweenを使って操作するほうが簡単な場合もあります。
簡単なアニメーション
まだまだ便利な機能はありますが、ここまでの概念で一度簡単なアニメーションを作成してみましょう。
drawBox :: Animation
drawBox = mkAnimation 2 $ \t ->
partialSvg t $ pathify $
mkRect (screenWidth/2) (screenHeight/2)
これは四角形を描くアニメーションになります。screenWidth, screenHeight
は画面の大きさを取得する関数、pathify
はmkCircle
などで作られたSVGをパスによる表現に変換する関数で、partialSvg
はパスを途中まで描いたSVGに変換する関数です。それぞれの型は以下のようになっています。
screenWidth :: Fractional a => a
screenHeight :: Fractional a => a
pathify :: SVG -> SVG
partialSvg :: Double -> SVG -> SVG
drawBox
が記述しているアニメーションを見てみましょう。
イメージ通りですね👏
同様に円を描くdrawCircle
を実装することもできます。
drawCircle :: Animation
drawCircle = mkAnimation 2 $ \t ->
partialSvg t $ pathify $
mkCircle (screenHeight/3)
作成したアニメーションは以下のようなAnimation -> Animation
型の関数を使って変化させることができます。
-- | アニメーションを反転させる
reverseA :: Animation -> Animation
-- | アニメーションを再生してから反転させる
playThenReverseA :: Animation -> Animation
-- | アニメーションが始まる前に動かない時間を作る
pauseAtBeginning :: Duration -> Animation -> Animation
-- | アニメーションが終わった後に動かない時間を作る
pauseAtEnd :: Duration -> Animation -> Animation
-- | 各フレームのSVGを一様に変換する
mapA :: (SVG -> SVG) -> Animation -> Animation
例えば playThenReverseA
を使えば drawBox
は以下のようになります。
このアニメーションはdrawBox
とreverseA drawBox
を順次つなげたような形になっていますよね。次はアニメーションを合成してより複雑なアニメーションを作る方法を見てみましょう。
-- | 1つ目のアニメーションを再生した後に2つ目のアニメーションを再生する
seqA :: Animation -> Animation -> Animation
-- | 1つ目のアニメーションを再生した後に、その結果を残しつつ2つ目のアニメーションを再生する
andThen :: Animation -> Animation -> Animation
-- | 2つのアニメーションを同時に再生する
parA :: Animation -> Animation -> Animation
-- |2つのアニメーションを同時に再生しつつ、短い方のアニメーションをループする
parLoopA :: Animation -> Animation -> Animation
-- |2つのアニメーションを同時に再生しつつ、短い方のアニメーションが終わったらそこで終了する
parDropA :: Animation -> Animation -> Animation
seqA
と andThen
の違いが分かりにくいかと思うので実際に見てみましょう。
まずは drawBox `seqA` drawCircle
です。
次に drawBox `andThen` drawCircle
です。
最初に描かれた四角形が残り続けていることが分かります。
ところでこのように順番に再生されるアニメーションを記述するのであれば、モナドを使った手続き型のインターフェースがあればより直感的に書くことができそうです。ReanimateではそのためにScene s a
というモナドのインスタンスとなっている型が用意されています。
Scene s a
Scene s a
は手続き的に扱えるアニメーション(シーン)を表す型です。以下のplay
関数を使ってAnimation
型から作成することができます。
play :: Animation -> Scene s ()
反対にScene s a
からAnimation
型を作ることも可能です。
scene :: (forall s. Scene s a) -> Animation
(Scene s a
には Var s a
という読み書き可能な変数の機能があり s
が存在型となっていることで ST s a
同様、変数の値を外から取り出せないようになっています)
早速これらを使ってdrawBox
とdrawCircle
を順番に実行するアニメーションを作成してみましょう。
scene do
play drawBox
play drawCircle
(do
の前の$
を省略するにはBlockArguments
拡張を有効にする必要があります)
bindによりアニメーションはシーケンシャルに再生されますが fork :: Scene s a -> Scene s a
を使うことで並列に再生することも可能です。
これはparA
を使った時と同じ挙動になります。
Sprite s a
ここまではAnimation
型でもできることでしたが、Scene s a
の機能としてアニメーションの対象をSprite s
として切り出し直接操作することができます。
newSpriteA :: Animation -> Scene s (Sprite s)
これらの関数は Sprite s
を作成すると同時にシーンの中でアニメーションを再生することも行います。
Sprite s
には以下のような操作が用意されています。
-- | Sprite を破壊して画面から消す
destroySprite :: Sprite s -> Scene s ()
-- | Sprite に対して時間に応じたSVGの変換を適用する
spriteTween :: Sprite s -> Duration -> (Double -> SVG -> SVG) -> Scene s ()
-- | Sprite にエフェクトを適用する
spriteE :: Sprite s -> Effect -> Scene s ()
まずは destroySprite
を試してみましょう。
scene do
s <- fork $ newSpriteA drawBox
fork $ wait 1 >> destroySprite s
play drawCircle
まず fork
を使って四角形を描き始めた後に、再び fork
で1秒後に四角形を描いているSpriteを消す処理を走らせます。最後に円を描き始めることで、2秒かけて円を描いている途中の1秒目で四角形が消えるというアニメーションを作ることができました(ここでwait
はScene s a
の中で指定された秒数だけ待つ関数です)。
次にエフェクトを使ったアニメーションを見てみましょう。
scene do
s <- fork $ newSpriteA drawCircle
spriteE s $ overBeginning 1 fadeInE
spriteE s $ overEnding 0.5 fadeOutE
円を描く drawCircle
アニメーションが、開始一秒でフェードイン overBeginning 1 fadeInE
し、終了する0.5秒でフェードアウト overEnding 0.5 fadeOutE
しているのが分かります。Effect
はこれだけでなく豊富な種類が用意されていますが、ここではObject s a
を使ったモーションの説明に移りたいので気になる人は以下のHaddockを参考にしてください。
Object s a
Sprite s
はアニメーションを伴う対象を Scene s a
の中で扱うための型でしたが、Object s a
はSVGのようなオブジェクトを Scene s a
の中で扱うための型です。
oNew :: Renderable a => a -> Scene s (Object s a)
oShow :: Object s a -> Scene s ()
SVG
は Renderable
のインスタンスになっているので oNew
を使って Object s a
を作ることができます。oNew
しただけでは Object s a
は表示されず明示的に oShow
を呼ぶ必要があります。
Object s a
は以下のような関数を使って操作することができます。
-- | オブジェクトの持つプロパティを変更する
oModifyS :: Object s a -> State (ObjectData a) b -> Scene s ()
-- | 時間をかけてオブジェクトの持つプロパティを変更する
oTweenS :: Object s a -> Duration -> (Double -> State (ObjectData a) b) -> Scene s ()
oTweenS
を使えば以下のように終了時の座標を指定するだけで Object s a
を動かすことができます。
scene do
obj <- oNew $ mkCircle 2
oShow obj
oTweenS obj 2 (\t -> oRightX %= \origin -> fromToS origin screenRight t)
oRightX
はレンズになっていて %=
を使うことで State
の中でその値だけ書き換えるという操作を行っています。
oRightX :: Lens' (ObjectData a) Double
(%=) :: MonadState s m => ASetter s s a b -> (a -> b) -> m ()
つまりオブジェクトの右端のx座標oRightX
を元の位置origin
から画面の右端screenRight
まで刻みながらfromToS
変化させるという事を行っているのです。
ところで今回はオブジェクトの右端のx座標を変化させましたがObject s a
の座標はどのように表現されているのでしょうか?もし左端を変化させたいと思った場合、オブジェクトの幅を考慮して右端の座標が変化する量を計算する必要があるのでしょうか?こういう内部表現は描画エンジン毎に異なってくるので普通は注意する必要がありますが、実は全く気にする必要はないのです。左端を変化させたければoLeftX
というレンズが用意されているのでそれを使えば思ったとおりに動くでしょう。つまりオブジェクト位置の内部表現がどうなっていたとしても操作したい性質を表すレンズさえ用意されていれば、あたかもその性質を直接操作しているようにプログラムを書くことができるのです。これはレンズの非常に強力で便利な性質だと思います。
例として大きさの違う3つのオブジェクトを左揃えにすることを考えます。通常であれば原点の位置からそれぞれの幅を考慮して左に移動する距離をプログラマーが計算する必要がありますが。レンズを使えば"左端を揃えるだけ"で実装できてしまうのです。
scene do
obj1 <- oNew $ translate 0 0 $ mkCircle 2.34
obj2 <- oNew $ translate 0 1 $ mkRect 1.34 1.61
obj3 <- oNew $ translate 0 (-1) $ mkCircle 0.45
oShow obj1
oShow obj2
oShow obj3
fork $ oTweenS obj1 2 (\t -> oLeftX %= \origin -> fromToS origin (-2) t)
fork $ oTweenS obj2 2 (\t -> oLeftX %= \origin -> fromToS origin (-2) t)
oTweenS obj3 2 (\t -> oLeftX %= \origin -> fromToS origin (-2) t)
wait 1
大きさの違う3つのオブジェクトを想定通り左揃えにできていると思います。大きくて揃える左端が外側になっているオブジェクトは正しく右に移動していますね。
oRightX, oLeftX
以外にもObject s a
に対して多くのレンズが用意されています。
-- | x軸方向の移動
oTranslateX :: Lens' (ObjectData a) Double
-- | y軸方向の移動
oTranslateY :: Lens' (ObjectData a) Double
-- | 上端のy座標
oTopY :: Lens' (ObjectData a) DoubleSource
-- | 下端のy座標
oBottomY :: Lens' (ObjectData a) DoubleSource
-- | 左端のx座標
oLeftX :: Lens' (ObjectData a) DoubleSource
-- | 右端のx座標
oRightX :: Lens' (ObjectData a) DoubleSource
-- | 中心のx座標
oCenterX :: Lens' (ObjectData a) DoubleSource
-- | 中心のy座標
oCenterY :: Lens' (ObjectData a) DoubleSource
-- | 上方向のマージン
oMarginTop :: Lens' (ObjectData a) DoubleSource
-- | 右方向のマージン
oMarginRight :: Lens' (ObjectData a) DoubleSource
-- | 下方向のマージン
oMarginBottom :: Lens' (ObjectData a) DoubleSource
-- | 左方向のマージン
oMarginLeft :: Lens' (ObjectData a) Double
-- | 透明度
oOpacity :: Lens' (ObjectData a) Double
-- | 拡大縮小倍率
oScale :: Lens' (ObjectData a) DoubleSource
reanimate関数の使い方
Animation
型を作った後は reanimate
関数に適用しmain
を実装します。
main :: IO ()
main = reanimate animation
このようにすればアプリケーションに引数を与えて実行することで動作ファイルを作成したりできるようになっています。
$ stack run -- --help
Usage: reanimate [COMMAND | [-v|--verbose] [--ghc PATH]
[-G|--ghc-opt ARG] [--self PATH]]
This program contains an animation which can either be viewed in a web-browser
or rendered to disk.
Available options:
--ghc PATH Path to GHC binary
-G,--ghc-opt ARG Additional option to pass to ghc
--self PATH Source file used for live-reloading
-h,--help Show this help text
Available commands:
check Run a system's diagnostic and report any missing
external dependencies.
view Play animation in browser window.
render Render animation to file.
raw Output raw SVGs for animation at 60 fps. Used
internally by viewer.
mp4ファイルを作成するには以下のようにします。
$ stack run render
Animation options:
fps: 60
width: 2560
height: 1440
fmt: mp4
target: output.mp4
raster: RasterRSvg
Starting render of animation: 2.0
Frames generated: 120/120, time spent: 0s
Frames rastered: 120/120, time spent: 9s
Frames rendered: 120/120, time spent: 4s
mp4を作成するには ffmpeg が必要です。
その他の機能にも必要なツールが入ってるかどうかは以下のように確認することができます。
$ stack run check
reanimate checks:
Has ffmpeg: 4.3.1
Has ffmpeg(rsvg): no
Has dvisvgm: /Library/TeX/texbin/dvisvgm
Has povray: /usr/local/bin/povray
Has blender: no
Has rsvg-convert: 2.50.2
Has inkscape: no
Has imagemagick: no
Has LaTeX: /Library/TeX/texbin/latex
Has LaTeX package 'babel': OK
Has LaTeX package 'preview': OK
Has LaTeX package 'amsmath': OK
Has XeLaTeX: /Library/TeX/texbin/xelatex
Has XeLaTeX package 'ctex': OK
入っていないツールがあるからといって必ずしもアプリケーションを実行できないわけでは有りません。必要なものがあれば大丈夫です。
mp4ではなくGIFやWebMを書き出す時はフォーマットを指定します。
$ stack run render -- --format gif
Animation options:
fps: 25
width: 320
height: 180
fmt: gif
target: output.gif
raster: RasterRSvg
Starting render of animation: 2.0
Frames generated: 50/50, time spent: 0s
Frames rastered: 50/50, time spent: 2s
Frames rendered: 50/50, time spent: 1s
これでGIFファイルが作成されます。
reanimateにはファイルを生成しなくてもアニメーションをブラウザで確認できる機能があります。これは.hs
ファイルを直接実行しないと使えないので例えば以下のようにしましょう。
$ stack runghc app/Main.hs
おわりに
reanimateは機能が豊富なのでここで紹介しきれなかったものはたくさんあります。例えばLaTexと連携すれば数式や英文に対して細やかなアニメーションを作成することが可能です。公式サイトにチュートリアルがあるので見てみてください。
他にもChipmonk 2DやBlenderとの連携などまだまだ機能はありますが、入門記事ということでここで筆を置きたいと思います。最後にこの記事の冒頭に貼ったタイトルのアニメーションのコードを参考までに貼り付けておきます。
scene do
s <- fork $ newSpriteA $ staticFrame 2 titleSvg
spriteE s $ overBeginning 0.5 fadeInE
spriteE s $ overEnding 0.4 fadeOutE
spriteTween s 0.4 (flip const)
spriteTween s 0.1 (\t -> scale (1+(0.1*t)))
spriteTween s 1.1 (flip const)
spriteTween s 0.4 (\t -> scale (1+(5*t)))
where
titleSvg = mkGroup
[ scale 0.5 $ translate 0 3 $ mkText "複雑なアニメーションを"
, scale 0.5 $ translate 0 0 $ mkText "プログラムする"
, scale 0.5 $ translate 0 (-3) $ mkText "〜Reanimate入門〜"
]
\読んでいただきありがとうございました!/
この記事が面白かったら いいね♡ をいただけると嬉しいです☺️
バッジを贈っていただければ次の記事を書くため励みになります🙌
Discussion