iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🎬

Programming Complex Animations: An Introduction to Reanimate

に公開

Reanimate is a library for creating animations.

Since Reanimate is implemented as a Haskell library, you can describe animations through programming. The library comes with many features, extensive documentation, and even an online Playground, making it a very polished tool. Furthermore, it supports integration with external tools such as LaTeX, physics engines (Chipmunk 2D), POV-Ray, and Blender. Each frame of the animation is exported as an SVG, so it is well-suited for creating animations composed of geometric shapes or text using SVG fonts. The final animations can be output as MP4, GIF, or WebM (it is also possible to extract the intermediate SVG files for each frame).

There are many examples on the official website, so it is a good idea to check them out first to see what kind of animations you can create with Reanimate.

https://reanimate.github.io/

In this article, I will explain the basic concepts required to create animations using Reanimate.

Introduction to Reanimate

The goal of an application using Reanimate is to implement main using the following reanimate function.

reanimate :: Animation -> IO ()

We will look at how to use the application implemented by this function later, but first, let's look at the Animation type used as the argument.

Animation

Needless to say, this type represents an animation.

mkAnimation :: Duration -> (Time -> SVG) -> Animation

You can see that it can be created from a function that takes the total length and an SVG for each point in time.

Both Duration and Time are aliases for Double.

type Duration = Double
type Time = Double

SVG

SVG is, as the name suggests, a type representing an SVG. You can create basic shapes using the following functions:

-- | Circle
mkCircle :: Double -> SVG

-- | Ellipse
mkEllipse :: Double -> Double -> SVG

-- | Rectangle
mkRect :: Double -> Double -> SVG

-- | Line segment
mkLine :: (Double, Double) -> (Double, Double) -> SVG

-- | Polyline
mkLinePath :: [(Double, Double)] -> SVG

-- | Path
mkPath :: [PathCommand] -> SVG

-- | Creates an SVG that fills the background with a given color name
mkBackground :: String -> SVG

-- | Combines multiple SVGs into a single SVG
mkGroup :: [SVG] -> SVG

Actually, on Haddock, the return type of the above functions is Tree, but since SVG is a type alias for Tree, there is no problem.

type SVG = Tree

SVGs simply generated with the functions mentioned above are positioned so that their center is at the middle of the screen. Properties such as the position of the SVG can be modified using functions of type SVG -> SVG.

-- | Moves the SVG in the x and y directions
translate :: Double -> Double -> SVG -> SVG

-- | Rotates the SVG
rotate :: Double -> SVG -> SVG

-- | Scales the SVG
scale :: Double -> SVG -> SVG

-- | Changes the stroke color
withStrokeColor :: String -> SVG -> SVG

-- | Changes the fill color
withFillColor :: String -> SVG -> SVG

With just these, it seems possible to create many different animations, but when creating animations, it is sometimes easier to manipulate the Object type using tweens, which will be described later.

A Simple Animation

There are still many useful features, but let's try to create a simple animation using the concepts we have covered so far.

drawBox :: Animation
drawBox = mkAnimation 2 $ \t ->
  partialSvg t $ pathify $
  mkRect (screenWidth/2) (screenHeight/2)

This is an animation that draws a rectangle. screenWidth and screenHeight are functions that retrieve the screen dimensions, pathify is a function that converts an SVG created with mkCircle or similar into a path-based representation, and partialSvg is a function that converts a path into an SVG drawn up to a certain point. Their types are as follows:

screenWidth :: Fractional a => a
screenHeight :: Fractional a => a
pathify :: SVG -> SVG
partialSvg :: Double -> SVG -> SVG

Let's take a look at the animation described by drawBox.

It looks just as imagined! 👏

Similarly, you can implement drawCircle, which draws a circle.

drawCircle :: Animation
drawCircle = mkAnimation 2 $ \t ->
  partialSvg t $ pathify $
  mkCircle (screenHeight/3)

You can transform created animations using functions of type Animation -> Animation like the following:

-- | Reverses the animation
reverseA :: Animation -> Animation

-- | Plays the animation and then reverses it
playThenReverseA :: Animation -> Animation

-- | Creates a duration of no movement before the animation starts
pauseAtBeginning :: Duration -> Animation -> Animation

-- | Creates a duration of no movement after the animation ends
pauseAtEnd :: Duration -> Animation -> Animation

-- | Uniformly transforms the SVG of each frame
mapA :: (SVG -> SVG) -> Animation -> Animation

For example, if you use playThenReverseA, drawBox becomes as follows:

This animation looks like a sequential connection of drawBox and reverseA drawBox. Next, let's look at how to combine animations to create more complex ones.

-- | Plays the second animation after the first one completes
seqA :: Animation -> Animation -> Animation

-- | Plays the second animation after the first one, while keeping the result of the first
andThen :: Animation -> Animation -> Animation

-- | Plays two animations simultaneously
parA :: Animation -> Animation -> Animation

-- | Plays two animations simultaneously, looping the shorter one
parLoopA :: Animation -> Animation -> Animation

-- | Plays two animations simultaneously, ending when the shorter one finishes
parDropA :: Animation -> Animation -> Animation

The difference between seqA and andThen might be hard to grasp, so let's look at them in action.

First, here is drawBox `seqA` drawCircle:

Next is drawBox `andThen` drawCircle:

You can see that the rectangle drawn first remains on the screen.

By the way, if you are describing animations that play in order like this, it would feel more intuitive if there were a procedural interface using Monads. Reanimate provides a type called Scene s a which is an instance of a Monad for this purpose.

Scene s a

Scene s a is a type representing an animation (scene) that can be handled procedurally. It can be created from the Animation type using the play function below.

play :: Animation -> Scene s ()

Conversely, it is also possible to create the Animation type from Scene s a.

scene :: (forall s. Scene s a) -> Animation

(Scene s a has a feature for readable and writable variables called Var s a, and since s is an existential type, just like in ST s a, variable values cannot be extracted from the outside.)

Let's immediately use these to create an animation that executes drawBox and drawCircle in sequence.

scene do
  play drawBox
  play drawCircle

(To omit the $ before do, you need to enable the BlockArguments extension.)

Animations play sequentially via the bind operation, but they can also be played in parallel using fork :: Scene s a -> Scene s a.

This behavior is the same as when using parA.

Sprite s a

Up until now, we've seen things that can be done with the Animation type, but as a feature of Scene s a, we can extract targets for animation as Sprite s and manipulate them directly.

newSpriteA :: Animation -> Scene s (Sprite s)

These functions create a Sprite s while simultaneously starting its animation within the scene.

The following operations are available for Sprite s:

-- | Destroys the Sprite and removes it from the screen
destroySprite :: Sprite s -> Scene s ()

-- | Applies a time-dependent SVG transformation to the Sprite
spriteTween :: Sprite s -> Duration -> (Double -> SVG -> SVG) -> Scene s ()

-- | Applies an effect to the Sprite
spriteE :: Sprite s -> Effect -> Scene s ()

First, let's try destroySprite.

scene do
  s <- fork $ newSpriteA drawBox
  fork $ wait 1 >> destroySprite s
  play drawCircle

First, we use fork to start drawing the rectangle, then use fork again to run a process that destroys the Sprite drawing the rectangle after 1 second. By starting to draw the circle at the end, we've created an animation where the rectangle disappears at the 1-second mark while the circle is being drawn over 2 seconds (here, wait is a function that waits for the specified number of seconds within Scene s a).

Next, let's look at an animation using effects.

scene do
  s <- fork $ newSpriteA drawCircle
  spriteE s $ overBeginning 1 fadeInE
  spriteE s $ overEnding 0.5 fadeOutE

You can see that the drawCircle animation fades in for the first second (overBeginning 1 fadeInE) and fades out during the final 0.5 seconds (overEnding 0.5 fadeOutE). Many other types of Effect are available besides these; however, since I want to move on to explaining motion using Object s a, please refer to the following Haddock if you're interested.

https://hackage.haskell.org/package/reanimate-1.1.2.1/docs/Reanimate-Effect.html

Object s a

Sprite s was a type for handling targets with animation inside a Scene s a, but Object s a is a type for handling objects like SVGs within a Scene s a.

oNew :: Renderable a => a -> Scene s (Object s a)
oShow :: Object s a -> Scene s ()

Since SVG is an instance of Renderable, you can create an Object s a using oNew. Simply calling oNew will not display the Object s a; you must explicitly call oShow.

An Object s a can be manipulated using functions like the following:

-- | Modifies the properties held by the object
oModifyS :: Object s a -> State (ObjectData a) b -> Scene s ()

-- | Modifies the properties held by the object over time
oTweenS :: Object s a -> Duration -> (Double -> State (ObjectData a) b) -> Scene s ()

Using oTweenS, you can move an Object s a simply by specifying the target coordinates at the end, as shown below:

scene do
  obj <- oNew $ mkCircle 2
  oShow obj
  oTweenS obj 2 (\t -> oRightX %= \origin -> fromToS origin screenRight t)

oRightX is a lens, and by using %=, it performs an operation to rewrite only that value within the State.

oRightX :: Lens' (ObjectData a) Double
(%=) :: MonadState s m => ASetter s s a b -> (a -> b) -> m ()

In other words, it changes the x-coordinate of the right edge of the object, oRightX, from its original position origin to the right edge of the screen screenRight in increments using fromToS over time.

By the way, while we changed the x-coordinate of the right edge of the object this time, how are the coordinates of an Object s a represented? If you wanted to change the left edge, would you need to calculate the amount the right edge coordinate changes by considering the width of the object? Normally, you would need to be careful because these internal representations differ for each rendering engine, but in fact, you don't need to worry about it at all. If you want to change the left edge, a lens called oLeftX is provided, so using that will make it work as expected. In other words, no matter what the internal representation of the object's position is, as long as a lens representing the property you want to manipulate is provided, you can write your program as if you were directly manipulating that property. I think this is an extremely powerful and convenient property of lenses.

As an example, let's consider aligning three objects of different sizes to the left. Normally, a programmer would need to calculate the distance to move to the left from the origin while considering each object's width. However, using lenses, it can be implemented just by "aligning the left edges."

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

You can see that the three objects of different sizes are aligned to the left as intended. The objects that were larger and had their left edges further out have correctly moved to the right.

Many other lenses besides oRightX and oLeftX are provided for Object s a.

-- | Movement in the x-axis direction
oTranslateX :: Lens' (ObjectData a) Double

-- | Movement in the y-axis direction
oTranslateY :: Lens' (ObjectData a) Double

-- | y-coordinate of the top edge
oTopY :: Lens' (ObjectData a) DoubleSource

-- | y-coordinate of the bottom edge
oBottomY :: Lens' (ObjectData a) DoubleSource

-- | x-coordinate of the left edge
oLeftX :: Lens' (ObjectData a) DoubleSource

-- | x-coordinate of the right edge
oRightX :: Lens' (ObjectData a) DoubleSource

-- | x-coordinate of the center
oCenterX :: Lens' (ObjectData a) DoubleSource

-- | y-coordinate of the center
oCenterY :: Lens' (ObjectData a) DoubleSource

-- | Top margin
oMarginTop :: Lens' (ObjectData a) DoubleSource

-- | Right margin
oMarginRight :: Lens' (ObjectData a) DoubleSource

-- | Bottom margin
oMarginBottom :: Lens' (ObjectData a) DoubleSource

-- | Left margin
oMarginLeft :: Lens' (ObjectData a) Double

-- | Opacity
oOpacity :: Lens' (ObjectData a) Double

-- | Scaling factor
oScale :: Lens' (ObjectData a) DoubleSource

How to use the reanimate function

After creating the Animation type, you apply it to the reanimate function and implement main.

main :: IO ()
main = reanimate animation

By doing this, you can create operation files by providing arguments to the application when executing it.

$ 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.

To create an mp4 file, you can do the following:

$ 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

ffmpeg is required to create an mp4.

You can check whether other features have the necessary tools installed as follows:

$ 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

Just because some tools are missing doesn't necessarily mean you can't run the application. It's fine as long as you have the ones you need.

When exporting as a GIF or WebM instead of an mp4, specify the format:

$ 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

This will create a GIF file.

Reanimate also has a feature that allows you to check the animation in a browser without generating a file. This can only be used by executing the .hs file directly, for example, as follows:

$ stack runghc app/Main.hs

Conclusion

Reanimate is rich in features, so there are many things I couldn't cover here. For example, by integrating with LaTeX, you can create detailed animations for mathematical formulas or English text. Take a look at the tutorial on the official website.

https://reanimate.readthedocs.io/en/latest/tut_equation/

There are many other features such as integration with Chipmunk 2D and Blender, but I will wrap things up here as this is an introductory article. Finally, I'll provide the code for the title animation shown at the beginning of this article for your reference.

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 "Programming Complex Animations"
       , scale 0.5 $ translate 0 0    $ mkText "with Programs"
       , scale 0.5 $ translate 0 (-3) $ mkText "~Introduction to Reanimate~"
       ]

\Thank you for reading!/
If you enjoyed this article, I would be happy if you could give it a Like ♡ ☺️
Receiving badges is a great encouragement for me to write the next article! 🙌

Discussion