Elaborate tutorial.

Former-commit-id: 31267dbb1675723b3b9afde806d215b358a7abac
This commit is contained in:
David Himmelstrup 2019-12-03 16:01:18 +08:00
commit 4283793c5a
6 changed files with 283 additions and 13 deletions

View file

@ -181,3 +181,33 @@ This file is auto-generated by docs/render_all.sh. DO NOT EDIT.
<br/><hr><br/>
## tut_glue_keyframe
<details>
<summary>View tut_glue_keyframe.hs</summary>
<pre><code class="haskell">
{!examples/tut_glue_keyframe.hs!}
</code></pre>
</details>
<br/>
<video width="640" height="360" autoplay loop>
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_keyframe.mp4">
</video>
<br/><hr><br/>
## tut_glue_fourier
<details>
<summary>View tut_glue_fourier.hs</summary>
<pre><code class="haskell">
{!examples/tut_glue_fourier.hs!}
</code></pre>
</details>
<br/>
<video width="640" height="360" autoplay loop>
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_fourier.mp4">
</video>
<br/><hr><br/>

View file

@ -3,7 +3,7 @@
ROOT=`stack path --project-root`
EXAMPLES='boundingbox colormaps goo latex_basic latex_color latex_draw
latex_wheel raster sphere blender_default_cube
tut_glue_svg tut_glue_animate'
tut_glue_svg tut_glue_animate tut_glue_keyframe tut_glue_fourier'
WIDTH=640
HEIGHT=$((WIDTH*9/16))

View file

@ -25,11 +25,13 @@ SVG features, as demonstrated in the below animation:
</details>
<br/>
<video width="640" height="360" autoplay loop>
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_svg.mp4">
<source src="../rendered/tut_glue_svg.mp4">
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_svg.mp4">
</video>
## Animation = Time 🡢 SVG
## Animation = Time ➞ SVG
Animations can be defined as SVG images over time (plus a bit of bookkeeping such as their duration). With this approach, the time variable can determine SVG properties such as radius, path lengths, rotation, and color. Reanimate ships with a bunch of combinators for composing and arranging animations.
<details>
<summary>Toggle source code.</summary>
@ -39,27 +41,59 @@ SVG features, as demonstrated in the below animation:
</details>
<br/>
<video width="640" height="360" autoplay loop>
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_animate.mp4">
<source src="../rendered/tut_glue_animate.mp4">
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_animate.mp4">
</video>
Reanimate is not an opinionated framework, though, and also offers a more traditional keyframing tools. The example below uses an imperative API to schedule the various sub animations, transitions, and effects.
<details>
<summary>Toggle source code.</summary>
<pre><code class="haskell">
{!examples/tut_glue_keyframe.hs!}
</code></pre>
</details>
<br/>
<video width="640" height="360" autoplay loop>
<source src="../rendered/tut_glue_keyframe.mp4">
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_keyframe.mp4">
</video>
## Pillar I: Haskell
TODO: Write about fourier drawings. About how advanced math is doable with Haskell.
A large part of Reanimate's expressive power comes from using Haskell as the scripting language. Haskell tends to favor expressiveness and correctness over raw performance and that is exactly what is needed from a glue language. All the heavy-lifting of rendering frames and encoding videos is handled by external tools and Reanimate merely needs to concern itself with finding intuitive ways of describing animations.
The following examples shows how something as seemingly complicated as fourier series can be expressed and animated in Haskell. Most noteworthy is the layered structure of the code:
1. The first layer handles the mathematics of fourier series without worrying about graphics or how properties should be animated,
2. the second layer describes how to draw a single frame given the length of the fourier series and the degree of rotation,
3. the third layer deals with time: order of animations, durations, number of rotations, transition timing functions, pauses, etc.
<details>
<summary>Toggle source code.</summary>
<pre><code class="haskell">
{!examples/tut_glue_fourier.hs!}
</code></pre>
</details>
<br/>
<video width="640" height="360" autoplay loop>
<source src="../rendered/tut_glue_fourier.mp4">
<source src="https://github.com/Lemmih/reanimate/raw/master/docs/rendered/tut_glue_fourier.mp4">
</video>
## Pillar II: Libraries
TODO: Write about how lots of libraries are available for Haskell. Use chiphunk
as an example. Show SVG primitives with 2D physics.
## Pillar III: LaTeX
## Pillar II: LaTeX
TODO: Show that LaTeX is a provider of SVG graphics. It has the type
'latex :: Text -> SVG'. Caching is automatic and it plays well with
other SVG functions (partialSvg, center, etc).
## Pillar IV: potrace
## Pillar III: potrace
## Pillar V: Povray
## Pillar IV: Povray
## Pillar VI: Blender
## Pillar V: Blender

View file

@ -10,8 +10,8 @@ import Reanimate.Animation
import Reanimate.Effect
import Reanimate.Scene
bgColor :: String
bgColor = "white"
bgColor :: PixelRGBA8
bgColor = PixelRGBA8 252 252 252 0xFF
segmentDuration :: Double
segmentDuration = 3
@ -25,7 +25,7 @@ main = reanimate $ bg `parA`
[animateCircleR, animateCircleP, animateRectR, animateColor
,signalA (constantS 0) $ setDuration transitionTime animateCircleR]
where
bg = animate $ const $ mkBackground bgColor
bg = animate $ const $ mkBackgroundPixel bgColor
animateCircleR :: Animation
animateCircleR = mkSegment "radius" $ \t -> mkCircle (t*2)

122
examples/tut_glue_fourier.hs Executable file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env stack
-- stack runghc --package reanimate
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Data.Complex
import Graphics.SvgTree
import Linear.V2
import Reanimate
import Reanimate.Signal
import Codec.Picture
-- layer 3
main :: IO ()
main = reanimate $ parA bg $ sceneAnimation $ do
play $ fourierA (fromToS 0 15) -- Rotate 15 times
# setDuration 50
# signalA (reverseS . powerS 2 . reverseS) -- Start fast, end slow
# pauseAtEnd 2
play $ fourierA (constantS 0) -- Don't rotate at all
# setDuration 10
# reverseA
# signalA (powerS 2) -- Start slow, end fast
# pauseAtEnd 2
where
bg = animate $ const $ mkBackgroundPixel (PixelRGBA8 252 252 252 0xFF)
-- layer 2
fourierA :: (Double -> Double) -> Animation
fourierA genPhi = animate $ \t ->
let circles = setFourierLength (t*piFourierLen) piFourier
in mkGroup
[ drawCircles $ fourierCoefficients $ rotateFourier (genPhi t) circles
, withStrokeColor "green" $
withStrokeLineJoin JoinRound $
withFillOpacity 0 $
withStrokeWidth (defaultStrokeWidth*2) $
mkLinePath $ mkFourierOutline circles
]
drawCircles :: [Complex Double] -> SVG
drawCircles [] = mkGroup []
drawCircles ( x :+ y : xs) =
translate x y $ drawCircles' xs
drawCircles' :: [Complex Double] -> SVG
drawCircles' circles = mkGroup
[ worker circles
, withStrokeColor "black" $
withStrokeLineJoin JoinRound $
withFillOpacity 0 $
mkLinePath [ (x, y) | x :+ y <- scanl (+) 0 circles ] ]
where
worker [] = None
worker (x :+ y : rest) =
let radius = sqrt(x*x+y*y) in
mkGroup
[ withStrokeColor "dimgrey" $
withFillOpacity 0 $
mkCircle radius
, translate x y $ worker rest ]
-- layer 1
data Fourier = Fourier {fourierCoefficients :: [Complex Double]}
piFourier :: Fourier
piFourier = mkFourier $ lineToPoints 500 $
toLineCommands $ extractPath $ scale 15 $ center $ latexAlign "\\pi"
piFourierLen :: Double
piFourierLen = sum $ map magnitude $ drop 1 $ take 500 $ fourierCoefficients piFourier
pointAtFourier :: Fourier -> Complex Double
pointAtFourier = sum . fourierCoefficients
mkFourier :: [RPoint] -> Fourier
mkFourier points = Fourier $ findCoefficient 0 :
concat [ [findCoefficient n, findCoefficient (-n)] | n <- [1..] ]
where
findCoefficient :: Int -> Complex Double
findCoefficient n =
sum [ toComplex point * exp (negate (fromIntegral n) * 2 *pi * i*t) * deltaT
| (idx, point) <- zip [0::Int ..] points, let t = fromIntegral idx/nPoints ]
i = 0 :+ 1
toComplex (V2 x y) = x :+ y
deltaT = recip nPoints
nPoints = fromIntegral (length points)
setFourierLength :: Double -> Fourier -> Fourier
setFourierLength _ (Fourier []) = Fourier []
setFourierLength len0 (Fourier (first:lst)) = Fourier $ first : worker len0 lst
where
worker _len [] = []
worker len (c:cs) =
if magnitude c < len
then c : worker (len - magnitude c) cs
else [c * (realToFrac (len / magnitude c))]
rotateFourier :: Double -> Fourier -> Fourier
rotateFourier phi (Fourier coeffs) =
Fourier $ worker (coeffs) (0::Integer)
where
worker [] _ = []
worker (x:rest) 0 = x : worker rest 1
worker [left] n = worker [left,0] n
worker (left:right:rest) n =
let n' = fromIntegral n in
left * exp (negate n' * 2 * pi * i * phi') :
right * exp (n' * 2 * pi * i * phi') :
worker rest (n+1)
i = 0 :+ 1
phi' = realToFrac phi
mkFourierOutline :: Fourier -> [(Double, Double)]
mkFourierOutline fourier =
[ (x, y)
| idx <- [0 .. granularity]
, let x :+ y = pointAtFourier $ rotateFourier (idx/granularity) fourier
]
where
granularity = 500

View file

@ -0,0 +1,84 @@
#!/usr/bin/env stack
-- stack runghc --package reanimate
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecursiveDo #-}
module Main (main) where
import Control.Monad (forM_)
import Graphics.SvgTree (Tree)
import Reanimate
import Reanimate.Driver (reanimate)
import Reanimate.Effect
import Codec.Picture
main :: IO ()
main = reanimate $ bg `parA` mainScene
where
bg = animate $ const $ mkBackgroundPixel (PixelRGBA8 252 252 252 0xFF)
mainScene :: Animation
mainScene = sceneAnimation $ mdo
play $ drawCircle
# setDuration drawCircleT
# applyE (constE flipXAxis)
# signalA (curveS 2)
fork $ play $ drawCircle
# freezeAtPercentage 1
# setDuration rotDur
rotDur <- withSceneDuration $ waitAll $
forM_ svgs $ \svg -> do
fork $ play $ drawTick
# setDuration rotateT
# repeatA rotateN
# applyE (overBeginning 0.5 drawInE)
# applyE (overEnding 0.5 drawOutE)
fork $ play $ drawSVG svg
# setDuration rotateT
# repeatA rotateN
# applyE (overBeginning rotateT drawInE)
# applyE (delayE rotateT $ overBeginning 1 fillInE)
# applyE (overEnding 0.5 fadeOutE)
wait (rotateT / fromIntegral (1+length svgs))
play $ drawCircle
# setDuration drawCircleT
# reverseA
# signalA (curveS 2)
return ()
where
drawCircleT = 2.5
rotateT = 5
rotateN = 3
svgCAF = center $ latex "\\LaTeX"
getNth n = snd (splitGlyphs [n] svgCAF)
svgs = [
withStrokeWidth 0.01 $
scale 2 $
translate 0 (tickLength*2) $
withStrokeColor "black" $
withFillColor "black" $
center $ getNth n
| n <- [0..4]]
radius, tickLength :: Double
radius = 1.25
tickLength = 0.25
drawCircle :: Animation
drawCircle = animate $ \t ->
withFillOpacity 0 $
withStrokeColor "black" $
rotate (-90) $
partialSvg t circPath
where
circPath = pathify $ mkCircle radius
drawTick :: Animation
drawTick = drawSVG $ mkLine (0, 0) (0, tickLength)
drawSVG :: Tree -> Animation
drawSVG svg = animate $ \t ->
withStrokeColor "black" $
rotate (t*360) $
translate 0 radius $
svg