mathematical animations with haskell https://reanimate.github.io
  • Haskell 95.8%
  • Elm 3.9%
  • Nix 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
David 0f588e873d Add missing module to cabal file.
Former-commit-id: 17b88c6d6a5657eb1e2d00f495ac759912dc3524
2019-02-27 15:44:53 +01:00
server Use files rather than memory to store cached values. 2019-02-27 15:42:31 +01:00
src/Reanimate Remove the GTK viewer. 2019-02-27 10:08:34 +01:00
web Slap a dummy domain on top of the rendering server. 2019-02-27 13:39:33 +01:00
ChangeLog.md First stab at reanimate. 2019-02-13 21:08:49 +01:00
README.md Link to the live coding site in the README. 2019-02-27 14:46:31 +01:00
reanimate.cabal Add missing module to cabal file. 2019-02-27 15:44:53 +01:00
Setup.hs First stab at reanimate. 2019-02-13 21:08:49 +01:00
stack.yaml Work on a high-level API. Rewrite examples. 2019-02-18 22:37:54 +01:00

reanimate

Reanimate is a reactive framework for creating non-interactive animations from SVG images. This package consists of a set of arrow combinators, a renderer (using ffmpeg), and a web-based previewer. Inline latex code is supported when 'latex' and 'dvisvgm' are installed.

Nothing about the API is stable at this point.

Live coding playground: https://lemmih.github.io/reanimate/

TODO

  • bounding boxes
  • alignment and positioning combinators
  • website for live coding

Examples

The example gifs are displayed at 25 fps.

Drawing latex equations

latex_draw :: Ani ()
latex_draw = pauseAtEnd 1 $ proc () -> do
  emit -< toHtml $ mkBackground "black"
  drawText msg `andThen` fillText msg -< ()
  where
    msg = "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}"
    placement = translate (320/2) (180/2) . scale 5
    fillText txt = proc () -> do
      duration 1 -< ()
      s <- signal 0 1 -< ()
      emit -< toHtml $ placement $
          withFillColor "white" $ withFillOpacity s $
            center $ latexAlign txt
    drawText txt = proc () -> do
      duration 2 -< ()
      s <- signal 0 1 -< ()
      emit -< toHtml $ placement $
        withStrokeColor "white" $ withFillOpacity 0 $ withStrokeWidth (Num 0.1) $
          partialSvg s $ center $ latexAlign txt

Drawing LaTeX equations

Bounding boxes

bbox :: Ani ()
bbox = proc () -> do
  emit -< toHtml $ mkBackground "black"
  duration 5 -< ()
  annotate' bbox1 -< g_ [transform_ $ Lucid.translate (320/2-50) (180/2)]
  annotate' bbox2 -< g_ [transform_ $ Lucid.translate (320/2+50) (180/2)]

bbox1 :: Ani ()
bbox1 = proc () -> do
  s <- signal 0 1 -< ()
  emit -< do
    toHtml $ mkBoundingBox $ rotate (360*s) svg
    toHtml $ withFillColor "white" $ rotate (360*s) svg
  where
    svg = scale 3 $ center $ latexAlign "\\sum_{k=1}^\\infty"

bbox2 :: Ani ()
bbox2 = proc () -> do
  s <- signalOscillate 0 1 -< ()
  emit -< do
    toHtml $ mkBoundingBox $ partialSvg s heartShape
    toHtml $ withStrokeColor "white" $ withFillOpacity 0 $ partialSvg s heartShape

Bounding boxes

Colorful LaTeX

latex_color :: Ani ()
latex_color = proc () -> do
    duration 0.1 -< ()
    emit -< toHtml $ mkBackground "black"
    emit -< toHtml $ translate (320/2) (180/2) $ withStrokeWidth (Num 0.2) $
      withStrokeColor "white" $
      withSubglyphs [0] (withFillColor "blue") $
      withSubglyphs [1] (withFillColor "yellow") $
      withSubglyphs [2] (withFillColor "green") $
      withSubglyphs [3] (withFillColor "red") $
      withSubglyphs [4] (withFillColor "darkslategrey") $
      svg
  where
    svg = scale 10 $ center $ latex "\\LaTeX"

Colorful LaTeX

Bezier curves

Bezier curves

Sine wave

sinewave :: Ani ()
sinewave = proc () -> do
    duration 10 -< ()
    emit -< toHtml $ mkBackground "black"
    idx <- signalOscillate 0 1 -< ()
    emit -< do
      defs_ $ clipPath_ [id_ "clip"] $ toHtml $
        mkRect (Num 0, Num (-height)) (Num $ idx*width) (Percent 100)
      toHtml $ translate margin height $ withStrokeColor "white" $
        withClipPathRef (Ref "clip") $ mkPathText $ renderPathText $ approxFnData 1000 wave
      toHtml $ withStrokeColor "white" $
        mkLine (Num margin, Num 10) (Num margin, Num 170)
      toHtml $ withStrokeColor "white" $
        mkLine (Num margin, Num height) (Num (margin+width), Num height)
    let (circX, circY) = wave idx
    emit -< g_ [transform_ $ Lucid.translate margin height] $
      circle_ [num_ cx_ circX, num_ cy_ circY, r_ "3", fill_ "red"]
  where
    freq = 3; margin = 30; width = 260; height = 90
    wave idx = (idx*width, sin (idx*pi*2*freq) * 50)

Sine wave

Morphing wave

morph_wave_circle :: Ani ()
morph_wave_circle = proc t -> do
    duration 5 -< ()
    idx <- signalOscillate 0 1 -< ()
    emit -< toHtml $ withStrokeColor "white" $ mkGroup
      [ mkBackground "black"
      , translate 30 90 $ mkPathText $ renderPathText $ morphPath circle wave1 idx
      , mkLine (Num 30, Num 10) (Num 30, Num 170)
      , mkLine (Num 30, Num 90) (Num 290, Num 90) ]
  where
    freq = 5; width = 260; radius = 50
    wave1 = approxFnData 1000 $ \idx -> (idx*width, sin (idx*pi*2*freq) * 20)
    circle = approxFnData 1000 $ \idx ->
      (cos (idx*pi*2+pi/2)*radius + width/2, sin (idx*pi*2+pi/2)*radius)

Morphing wave

Morph wave to circle

morph_wave_circle :: Ani ()
morph_wave_circle = proc t -> do
  duration 5 -< ()
  emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"]

  idx <- signalOscillate 0 1 -< ()
  emit -< do
    g_ [transform_ $ translate 30 90] $
      renderPath $ morphPath circle wave1 idx
    line_ [x1_ "30", x2_ "30", y1_ "10", y2_ "170", stroke_ "white"]
    line_ [x1_ "30", x2_ "290", y1_ "90", y2_ "90", stroke_ "white"]
  where
    freq = 5; width = 260; radius = 50
    wave1 = approxFnData 1000 $ \idx -> (idx*width, sin (idx*pi*2*freq) * 20)
    circle = approxFnData 1000 $ \idx ->
      (cos (idx*pi*2+pi/2)*radius + width/2, sin (idx*pi*2+pi/2)*radius)

Morphing wave to circle

Speed modification

progressMeters :: Ani ()
progressMeters = proc () -> do
  emit -< rect_ [width_ "100%", height_ "100%", fill_ "black"]
  annotate' (adjustSpeed 1.0 progressMeter) -< g_ [transform_ $ translate 40 20]
  annotate' (adjustSpeed 2.0 progressMeter) -< g_ [transform_ $ translate 140 20]
  annotate' (adjustSpeed 0.5 progressMeter) -< g_ [transform_ $ translate 240 20]

  emit -< do
    text_ [x_ "55", y_ "150", font_size_ "20"
          , text_anchor_ "middle"
          , fill_ "white"] "1x"
    text_ [x_ "155", y_ "150", font_size_ "20"
          , text_anchor_ "middle"
          , fill_ "white"] "2x"
    text_ [x_ "255", y_ "150", font_size_ "20"
          , text_anchor_ "middle"
          , fill_ "white"] "0.5x"

Speed modification

Highlights

Highlight

Clipping

clip_rect :: Ani ()
clip_rect = proc () -> do
  emit -< toHtml $ mkBackground "black"
  annotate' $ follow
    [ sim
      [ sim [ paintStatic prev | prev <- [max 0 (n-4) .. n-1] ]
      , sim [ runAni "black" i | i <- [n-4], i>=0 ]
      , runAni "white" n ]
    | n <- [0..15]
    ] -< g_ [transform_ $ Lucid.translate (320/2) (180/2)]
  where
    paintStatic nth = proc () ->
      emit -< toHtml $ withStrokeColor "white" $
        square (20+nth*10)
    runAni color nth = circle_clip $ proc () -> do
      duration 1 -< ()
      emit -< toHtml $ withStrokeColor color $
        square (20+nth*10)
    square side = center $ withFillOpacity 0 $ withStrokeWidth (Num 2) $
      mkRect (Num 0, Num 0) (Num side) (Num side)

Clipping

scaling

Animations can be reused, scaled and stretched in time. This example reuse four different animations of different lengths, scales their runtime so they're all in sync, and increases the playback speed.

scaling :: Ani ()
scaling = adjustSpeed 2 $ syncAll
  [ proc () ->
    annotate' animation -< g_ [transform_ $ Lucid.translate x y <> " " <> Lucid.scale 0.5 0.5]
  | x <- [0,160]
  , y <- [0,90]
  | animation <- [sinewave, morph_wave, highlight, progressMeters]]

Scaling

Valentine's Day

Valentine's Day

Basic LaTeX

latex_basic :: Ani ()
latex_basic = proc () -> do
  duration 2 -< ()
  s <- signalOscillate 0 1 -< ()
  emit -< toHtml $ mkGroup
    [ mkBackground "black"
    , translate (320/2) (180/2) $ mkGroup
      [ withStrokeColor "white" $ withFillOpacity 0 $ withStrokeWidth (Num 0.1) text
      , withFillColor "white" $ withFillOpacity s text] ]
  where
    text = scale 4 $ center $ latexAlign
      "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}"

Basic LaTeX