Introduction
This tutorial will guide you through the steps to set up a simple animation and teach you the basics of timing, positioning, and \LaTeX. It is assumed that you are familiar with Haskell but you don't have to know anything about reanimate or animation in general.
All of the code snippets are interactive and require JavaScript.
Tutorial Goal
By the end of this tutorial, we will have a short animation that shows an equation and then draws an explanation of each term:
Step 1: Blank canvas
To begin with, let's draw a light-blue background and make it easier to see our animation canvas. We can do this in the imperative scene context by creating a sprite containing the background color. All SVG color names are supported so give mistyrose
, aquamarine
or burlywood
a try. Don't try hex codes, though, as they are beyond the scope of this tutorial.
animation :: Animation animation = scene $ do newSpriteSVG_ $ mkBackground "lightblue"
API references: scene, newSpriteSVG_, mkBackground, Animation.
Step 2: Typesetting an equation
Let's start by refactoring the previous animation and create a separate function that defines our environment. This will make it easier to modify the environment in the future:
env :: Animation -> Animation
env = addStatic bg
bg :: SVG
bg = mkBackground "lightblue"
SVG has no support for typesetting equations but reanimate offers a convenient interface to LaTeX via latexAlign :: Text -> SVG
. To place the equation at the center of the screen with the right size, we'll use scale :: Double -> SVG -> SVG
and center :: SVG -> SVG
. Try using centerX :: SVG -> SVG
, changing the scaling factor, or using a different equation like \\frac{\\infty}{\\pi}
.
animation :: Animation animation = env $ scene $ do newSpriteSVG_ equation wait 1 equation :: SVG equation = scale 3 $ center $ latexAlign "E = mc^2" env :: Animation -> Animation env = addStatic bg bg :: SVG bg = mkBackground "lightblue"
API references: addStatic, center, scale, latexAlign.
Step 3: Accessing glyphs
So far, so good. We have an environment with a blue background color and we have an equation in SVG format. It's almost time to start animating but first we need to split the equation SVG into smaller pieces. We can use the splitGlyphs :: [Int] -> SVG -> (SVG, SVG)
to access a set of child nodes of an SVG by index.
animation :: Animation animation = env $ scene $ do play $ staticFrame 1 equation play $ staticFrame 1 symb_e play $ staticFrame 1 symb_eq play $ staticFrame 1 symb_m play $ staticFrame 1 symb_c2 symb_e :: SVG symb_e = snd $ splitGlyphs [0] equation symb_eq :: SVG symb_eq = snd $ splitGlyphs [1] equation symb_m :: SVG symb_m = snd $ splitGlyphs [2] equation symb_c2 :: SVG symb_c2 = snd $ splitGlyphs [3,4] equation equation :: SVG equation = scale 3 $ center $ latexAlign "E = mc^2" env :: Animation -> Animation env = addStatic bg bg :: SVG bg = mkBackground "lightblue"
API references: splitGlyphs, staticFrame, play.
Step 4: Moving objects
Objects in reanimate encapsulate SVG nodes and makes it easier to animate properties such as position and scale. In the example below, we create an object from the E
SVG node and 'tween' position properties to make it move around the canvas. Tweening (or inbetweening) is an animation concept that simply means moving smoothly from one value to another.
Reanimate exports many properties such as oLeftX
, oTopY
and oCenterXY
. These properties manipulate the object's position with respect to its bounding box and margins. The position of an object is ultimately captured in the oTranslate
property and resetting this property will undo any movements.
animation :: Animation animation = env $ scene $ do newSpriteSVG_ equation obj <- oNew symb_e oShow obj oTweenS obj 1 moveTopLeft oTweenS obj 1 moveTopRight oTweenS obj 1 moveBotRight oTweenS obj 1 moveBotLeft oTweenS obj 1 moveToOrigin moveTopLeft t = do oLeftX %= \origin -> fromToS origin screenLeft t oTopY %= \origin -> fromToS origin screenTop t moveTopRight t = do oRightX %= \origin -> fromToS origin screenRight t oTopY %= \origin -> fromToS origin screenTop t moveBotRight t = do oRightX %= \origin -> fromToS origin screenRight t oBottomY %= \origin -> fromToS origin screenBottom t moveBotLeft t = do oLeftX %= \origin -> fromToS origin screenLeft t oBottomY %= \origin -> fromToS origin screenBottom t moveToOrigin t = oTranslate %= lerp t (V2 0 0) symb_e :: SVG symb_e = snd $ splitGlyphs [0] equation equation :: SVG equation = scale 3 $ center $ latexAlign "E = mc^2" env :: Animation -> Animation env = addStatic bg bg :: SVG bg = mkBackground "lightblue"
API references: oTweenS, oNew, oShow, oLeftX, oTopY, oRightX, oBottomY, oTranslate, screenBottom, screenLeft, screenRight, screenTop.
Now that we know how to move around an object, let's move each symbol in the equation to the left of the screen and scale them down slightly to make it a better fit. The animation looks a lot better if moves overlap slightly and this can be achieved by running them in parallel with fork
and inserting a wait 0.3
in between them.
animation :: Animation animation = env $ scene $ do symbols <- mapM oNew [symb_e, symb_eq, symb_m, symb_c2] mapM_ oShow symbols forM_ (zip symbols yPositions) $ \(obj, yPos) -> do fork $ oTweenS obj 1 $ \t -> do oScale %= \origin -> fromToS origin scaleFactor t oLeftX %= \origin -> fromToS origin screenLeft t oCenterY %= \origin -> fromToS origin yPos t wait 0.3 symb_e :: SVG symb_e = snd $ splitGlyphs [0] equation symb_eq :: SVG symb_eq = snd $ splitGlyphs [1] equation symb_m :: SVG symb_m = snd $ splitGlyphs [2] equation symb_c2 :: SVG symb_c2 = snd $ splitGlyphs [3,4] equation equation :: SVG equation = scale 3 $ center $ latexAlign "E = mc^2" yPositions = [3,1,-1,-3] scaleFactor = 0.7 env :: Animation -> Animation env = addStatic bg bg :: SVG bg = mkBackground "lightblue"
Step 5: Drawing text
Objects are invisible by default and there are many built-in ways of showing them. Let's have a look at oFadeIn
, oDraw
, oScaleIn
, and oGrow
:
animation :: Animation animation = env $ scene $ do fadeIn <- newText "oFadeIn" oModify fadeIn $ oCenterY .~ 3 oShowWith fadeIn oFadeIn draw <- newText "oDraw" oModify draw $ oCenterY .~ 1 oShowWith draw oDraw scaleIn <- newText "oScaleIn" oModify scaleIn $ oCenterY .~ -1 oShowWith scaleIn $ setDuration 1 . oScaleIn grow <- newText "oGrow" oModify grow $ oCenterY .~ -3 oShowWith grow oGrow forM_ [fadeIn, draw, scaleIn, grow] $ \obj -> fork $ oHideWith obj oFadeOut newText = oNew . scale 1.5 . center . latex env :: Animation -> Animation env = addStatic bg . mapA (withStrokeWidth defaultStrokeWidth) . mapA (withStrokeColor "black") bg :: SVG bg = mkBackground "lightblue"
API references: oFadeIn, oDraw, oScaleIn, oGrow.
Let's use the oDraw
function to draw text on the screen and let's make sure the text is left-aligned:
animation :: Animation animation = env $ scene $ do ls <- mapM oNew [energy, equals, mass, speedOfLight] forM_ (zip ls yPositions) $ \(obj, nth) -> do oModifyS obj $ do oLeftX .= -4 oCenterY .= nth oShowWith obj oDraw forM_ ls $ \obj -> fork $ oHideWith obj oFadeOut energy = scale 1.5 $ centerX $ latex "Energy" equals = scale 1.5 $ centerX $ latex "equals" mass = scale 1.5 $ centerX $ latex "mass times" speedOfLight = scale 1.5 $ centerX $ latex "speed of light$^2$" yPositions = [3,1,-1,-3] env :: Animation -> Animation env = addStatic (mkBackground "lightblue") . mapA (withStrokeWidth defaultStrokeWidth) . mapA (withStrokeColor "black")
Step 6: Putting it all together
We've reached the end and have all the parts needed to re-create the GIF shown in the beginning. To recap:
- The
Scene
monad is useful for animating a sequence of events. - We can typeset equations with
latex :: Text -> SVG
. - SVG nodes are regular Haskell data and we can extract child nodes with
splitGlyphs
. - Objects wrap SVG nodes and make it easy to set positioning.
- Objects have many built-in ways of being shown on canvas.
Putting everything together gives us this short animation:
animation :: Animation animation = env $ scene $ do symbols <- mapM oNew [symb_e, symb_eq, symb_m, symb_c2] mapM_ oShow symbols wait 1 forM_ (zip symbols yPositions) $ \(obj, yPos) -> do fork $ oTweenS obj 1 $ \t -> do oScale %= \origin -> fromToS origin scaleFactor t oLeftX %= \origin -> fromToS origin screenLeft t oCenterY %= \origin -> fromToS origin yPos t wait 0.3 wait 1 ls <- mapM oNew [energy, equals, mass, speedOfLight] forM_ (zip ls yPositions) $ \(obj, nth) -> do oModifyS obj $ do oLeftX .= -4 oCenterY .= nth oShowWith obj oDraw wait 2 forM_ ls $ \obj -> fork $ oHideWith obj oFadeOut forM_ (reverse symbols) $ \obj -> do fork $ oTweenS obj 1 $ \t -> do oScale %= \origin -> fromToS origin 1 t oTranslate %= lerp t (V2 0 0) wait 0.3 wait 2 scaleFactor = 0.7 symb_e :: SVG symb_e = snd $ splitGlyphs [0] svg symb_eq :: SVG symb_eq = snd $ splitGlyphs [1] svg symb_m :: SVG symb_m = snd $ splitGlyphs [2] svg symb_c2 :: SVG symb_c2 = snd $ splitGlyphs [3,4] svg svg = scale 3 $ center $ latexAlign "E = mc^2" energy = scale 1.5 $ centerX $ latex "Energy" equals = scale 1.5 $ centerX $ latex "equals" mass = scale 1.5 $ centerX $ latex "mass times" speedOfLight = scale 1.5 $ centerX $ latex "speed of light$^2$" yPositions = [3,1,-1,-3] env :: Animation -> Animation env = addStatic (mkBackground "lightblue") . mapA (withStrokeWidth defaultStrokeWidth) . mapA (withStrokeColor "black")