never executed always true always false
    1 {-|
    2 Module      : Reanimate.Animation
    3 Copyright   : Written by David Himmelstrup
    4 License     : Unlicense
    5 Maintainer  : lemmih@gmail.com
    6 Stability   : experimental
    7 Portability : POSIX
    8 
    9 Declarative animation API based on combinators. For a higher-level interface,
   10 see 'Reanimate.Scene'.
   11 
   12 -}
   13 module Reanimate.Animation
   14   ( Duration
   15   , Time
   16   , SVG
   17   , Animation
   18   -- * Creating animations
   19   , mkAnimation
   20   , animate
   21   , staticFrame
   22   , pause
   23   -- * Querying animations
   24   , duration
   25   , frameAt
   26   -- * Composing animations
   27   , seqA
   28   , andThen
   29   , parA
   30   , parLoopA
   31   , parDropA
   32   -- * Modifying animations
   33   , setDuration
   34   , adjustDuration
   35   , mapA
   36   , takeA
   37   , dropA
   38   , lastA
   39   , pauseAtEnd
   40   , pauseAtBeginning
   41   , pauseAround
   42   , repeatA
   43   , reverseA
   44   , playThenReverseA
   45   , signalA
   46   , freezeAtPercentage
   47   , addStatic
   48   -- * Misc
   49   , getAnimationFrame
   50   , Sync(..)
   51   -- * Rendering
   52   , renderTree
   53   , renderSvg
   54   ) where
   55 
   56 import           Control.Arrow              ()
   57 import           Data.Fixed                 (mod')
   58 import           Graphics.SvgTree
   59 import           Graphics.SvgTree.Printer
   60 import           Reanimate.Constants
   61 import           Reanimate.Ease
   62 import           Reanimate.Svg.Constructors
   63 import           Text.XML.Light.Output
   64 
   65 -- | Duration of an animation or effect. Usually measured in seconds.
   66 type Duration = Double
   67 -- | Time signal. Goes from 0 to 1, inclusive.
   68 type Time = Double
   69 
   70 -- | SVG node.
   71 type SVG = Tree
   72 
   73 -- | Animations are SVGs over a finite time.
   74 data Animation = Animation Duration (Time -> SVG)
   75 
   76 -- | Construct an animation with a given duration.
   77 mkAnimation :: Duration -> (Time -> SVG) -> Animation
   78 mkAnimation = Animation
   79 
   80 -- | Construct an animation with a duration of @1@.
   81 animate :: (Time -> SVG) -> Animation
   82 animate = Animation 1
   83 
   84 -- | Create an animation with provided @duration@, which consists of stationary frame displayed for its entire duration.
   85 staticFrame :: Duration -> SVG -> Animation
   86 staticFrame d svg = Animation d (const svg)
   87 
   88 -- | Query the duration of an animation.
   89 duration :: Animation -> Duration
   90 duration (Animation d _) = d
   91 
   92 -- | Play animations in sequence. The @lhs@ animation is removed after it has
   93 --   completed. New animation duration is '@duration lhs + duration rhs@'.
   94 --
   95 --   Example:
   96 --
   97 --   @'Reanimate.Builtin.Documentation.drawBox' `'seqA'` 'Reanimate.Builtin.Documentation.drawCircle'@
   98 --
   99 --   <<docs/gifs/doc_seqA.gif>>
  100 seqA :: Animation -> Animation -> Animation
  101 seqA (Animation d1 f1) (Animation d2 f2) =
  102   Animation totalD $ \t ->
  103     if t < d1/totalD
  104       then f1 (t * totalD/d1)
  105       else f2 ((t-d1/totalD) * totalD/d2)
  106   where
  107     totalD = d1+d2
  108 
  109 -- | Play two animation concurrently. Shortest animation freezes on last frame.
  110 --   New animation duration is '@max (duration lhs) (duration rhs)@'.
  111 --
  112 --   Example:
  113 --
  114 --   @'Reanimate.Builtin.Documentation.drawBox' `'parA'` 'adjustDuration' (*2) 'Reanimate.Builtin.Documentation.drawCircle'@
  115 --
  116 --   <<docs/gifs/doc_parA.gif>>
  117 parA :: Animation -> Animation -> Animation
  118 parA (Animation d1 f1) (Animation d2 f2) =
  119   Animation (max d1 d2) $ \t ->
  120     let t1 = t * totalD/d1
  121         t2 = t * totalD/d2 in
  122     mkGroup
  123     [ f1 (min 1 t1)
  124     , f2 (min 1 t2) ]
  125   where
  126     totalD = max d1 d2
  127 
  128 -- | Play two animation concurrently. Shortest animation loops.
  129 --   New animation duration is '@max (duration lhs) (duration rhs)@'.
  130 --
  131 --   Example:
  132 --
  133 --   @'Reanimate.Builtin.Documentation.drawBox' `'parLoopA'` 'adjustDuration' (*2) 'Reanimate.Builtin.Documentation.drawCircle'@
  134 --
  135 --   <<docs/gifs/doc_parLoopA.gif>>
  136 parLoopA :: Animation -> Animation -> Animation
  137 parLoopA (Animation d1 f1) (Animation d2 f2) =
  138   Animation totalD $ \t ->
  139     let t1 = t * totalD/d1
  140         t2 = t * totalD/d2 in
  141     mkGroup
  142     [ f1 (t1 `mod'` 1)
  143     , f2 (t2 `mod'` 1) ]
  144   where
  145     totalD = max d1 d2
  146 
  147 -- | Play two animation concurrently. Animations disappear after playing once.
  148 --   New animation duration is '@max (duration lhs) (duration rhs)@'.
  149 --
  150 --   Example:
  151 --
  152 --   @'Reanimate.Builtin.Documentation.drawBox' `'parLoopA'` 'adjustDuration' (*2) 'Reanimate.Builtin.Documentation.drawCircle'@
  153 --
  154 --   <<docs/gifs/doc_parDropA.gif>>
  155 parDropA :: Animation -> Animation -> Animation
  156 parDropA (Animation d1 f1) (Animation d2 f2) =
  157   Animation totalD $ \t ->
  158     let t1 = t * totalD/d1
  159         t2 = t * totalD/d2 in
  160     mkGroup
  161     [ if t1>1 then None else f1 t1
  162     , if t2>1 then None else f2 t2 ]
  163   where
  164     totalD = max d1 d2
  165 
  166 -- | Empty animation (no SVG output) with a fixed duration.
  167 --
  168 --   Example:
  169 --
  170 --   @'pause' 1 `'seqA'` 'Reanimate.Builtin.Documentation.drawProgress'@
  171 --
  172 --   <<docs/gifs/doc_pause.gif>>
  173 pause :: Duration -> Animation
  174 pause d = Animation d (const None)
  175 
  176 -- | Play left animation and freeze on the last frame, then play the right
  177 --   animation. New duration is '@duration lhs + duration rhs@'.
  178 --
  179 --   Example:
  180 --
  181 --   @'Reanimate.Builtin.Documentation.drawBox' `'andThen'` 'Reanimate.Builtin.Documentation.drawCircle'@
  182 --
  183 --   <<docs/gifs/doc_andThen.gif>>
  184 andThen :: Animation -> Animation -> Animation
  185 andThen a b = a `parA` (pause (duration a) `seqA` b)
  186 
  187 -- | Calculate the frame that would be displayed at given point in @time@ of running @animation@.
  188 --
  189 -- The provided time parameter is clamped between 0 and animation duration.
  190 frameAt :: Time -> Animation -> SVG
  191 frameAt t (Animation d f) = f t'
  192   where
  193     t' = clamp 0 1 (t/d)
  194 
  195 -- | Helper function for pretty-printing SVG nodes.
  196 renderTree :: SVG -> String
  197 renderTree t = maybe "" ppElement $ xmlOfTree t
  198 
  199 -- | Helper function for pretty-printing SVG nodes as SVG documents.
  200 renderSvg :: Maybe Number -- ^ The number to use as value of the @width@ attribute of the resulting top-level svg element. If @Nothing@, the width attribute won't be rendered.
  201           -> Maybe Number -- ^ Similar to previous argument, but for @height@ attribute.
  202           -> SVG          -- ^ SVG to render
  203           -> String       -- ^ String representation of SVG XML markup
  204 renderSvg w h t = ppDocument doc
  205 -- renderSvg w h t = ppFastElement (xmlOfDocument doc)
  206   where
  207     width = 16
  208     height = 9
  209     doc = Document
  210       { _documentViewBox = Just (-width/2, -height/2, width, height)
  211       , _documentWidth = w
  212       , _documentHeight = h
  213       , _documentElements = [withStrokeWidth defaultStrokeWidth $ scaleXY 1 (-1) t]
  214       , _documentDescription = ""
  215       , _documentLocation = ""
  216       , _documentAspectRatio = PreserveAspectRatio False AlignNone Nothing
  217       }
  218 
  219 -- | Map over the SVG produced by an animation at every frame.
  220 --
  221 --   Example:
  222 --
  223 --   @'mapA' ('scale' 0.5) 'Reanimate.Builtin.Documentation.drawCircle'@
  224 --
  225 --   <<docs/gifs/doc_mapA.gif>>
  226 
  227 mapA :: (SVG -> SVG) -> Animation -> Animation
  228 mapA fn (Animation d f) = Animation d (fn . f)
  229 
  230 -- | Freeze the last frame for @t@ seconds at the end of the animation.
  231 --
  232 --   Example:
  233 --
  234 --   @'pauseAtEnd' 1 'Reanimate.Builtin.Documentation.drawProgress'@
  235 --
  236 --   <<docs/gifs/doc_pauseAtEnd.gif>>
  237 pauseAtEnd :: Duration -> Animation -> Animation
  238 pauseAtEnd t a = a `andThen` pause t
  239 
  240 -- | Freeze the first frame for @t@ seconds at the beginning of the animation.
  241 --
  242 --   Example:
  243 --
  244 --   @'pauseAtBeginning' 1 'Reanimate.Builtin.Documentation.drawProgress'@
  245 --
  246 --   <<docs/gifs/doc_pauseAtBeginning.gif>>
  247 pauseAtBeginning :: Duration -> Animation -> Animation
  248 pauseAtBeginning t a =
  249     Animation t (freezeFrame 0 a) `seqA` a
  250 
  251 -- | Freeze the first and the last frame of the animation for a specified duration.
  252 --
  253 --   Example:
  254 --
  255 --   @'pauseAround' 1 1 'Reanimate.Builtin.Documentation.drawProgress'@
  256 --
  257 --   <<docs/gifs/doc_pauseAround.gif>>
  258 pauseAround :: Duration -> Duration -> Animation -> Animation
  259 pauseAround start end = pauseAtEnd end . pauseAtBeginning start
  260 
  261 -- Freeze frame at time @t@.
  262 freezeFrame :: Time -> Animation -> (Time -> SVG)
  263 freezeFrame t (Animation d f) = const $ f (t/d)
  264 
  265 -- | Change the duration of an animation. Animates are stretched or squished
  266 --   (rather than truncated) to fit the new duration.
  267 adjustDuration :: (Duration -> Duration) -> Animation -> Animation
  268 adjustDuration fn (Animation d gen) =
  269   Animation (fn d) gen
  270 
  271 -- | Set the duration of an animation by adjusting its playback rate. The
  272 --   animation is still played from start to finish without being cropped.
  273 setDuration :: Duration -> Animation -> Animation
  274 setDuration newD = adjustDuration (const newD)
  275 
  276 -- | Play an animation in reverse. Duration remains unchanged. Shorthand for:
  277 --   @'signalA' 'reverseS'@.
  278 --
  279 --   Example:
  280 --
  281 --   @'reverseA' 'Reanimate.Builtin.Documentation.drawCircle'@
  282 --
  283 --   <<docs/gifs/doc_reverseA.gif>>
  284 reverseA :: Animation -> Animation
  285 reverseA = signalA reverseS
  286 
  287 -- | Play animation before playing it again in reverse. Duration is twice
  288 --   the duration of the input.
  289 --
  290 --   Example:
  291 --
  292 --   @'playThenReverseA' 'Reanimate.Builtin.Documentation.drawCircle'@
  293 --
  294 --   <<docs/gifs/doc_playThenReverseA.gif>>
  295 playThenReverseA :: Animation -> Animation
  296 playThenReverseA a = a `seqA` reverseA a
  297 
  298 -- | Loop animation @n@ number of times. This number may be fractional and it
  299 --   may be less than 1. It must be greater than or equal to 0, though.
  300 --   New duration is @n*duration input@.
  301 --
  302 --   Example:
  303 --
  304 --   @'repeatA' 1.5 'Reanimate.Builtin.Documentation.drawCircle'@
  305 --
  306 --   <<docs/gifs/doc_repeatA.gif>>
  307 repeatA :: Double -> Animation -> Animation
  308 repeatA n (Animation d f) = Animation (d*n) $ \t ->
  309   f ((t*n) `mod'` 1)
  310 
  311 
  312 -- | @freezeAtPercentage time animation@ creates an animation consisting of stationary frame,
  313 -- that would be displayed in the provided @animation@ at given @time@.
  314 -- The duration of the new animation is the same as the duration of provided @animation@.
  315 freezeAtPercentage :: Time  -- ^ value between 0 and 1. The frame displayed at this point in the original animation will be displayed for the duration of the new animation
  316                    -> Animation -- ^ original animation, from which the frame will be taken
  317                    -> Animation -- ^ new animation consisting of static frame displayed for the duration of the original animation
  318 freezeAtPercentage frac (Animation d genFrame) =
  319   Animation d $ const $ genFrame frac
  320 
  321 -- | Overlay animation on top of static SVG image.
  322 --
  323 --  Example:
  324 --
  325 --  @'addStatic' ('mkBackground' "lightblue") 'Reanimate.Builtin.Documentation.drawCircle'@
  326 --
  327 --  <<docs/gifs/doc_addStatic.gif>>
  328 addStatic :: SVG -> Animation -> Animation
  329 addStatic static = mapA (\frame -> mkGroup [static, frame])
  330 
  331 -- | Modify the time component of an animation. Animation duration is unchanged.
  332 --
  333 --   Example:
  334 --
  335 --   @'signalA' ('fromToS' 0.25 0.75) 'Reanimate.Builtin.Documentation.drawCircle'@
  336 --
  337 --   <<docs/gifs/doc_signalA.gif>>
  338 signalA :: Signal -> Animation -> Animation
  339 signalA fn (Animation d gen) = Animation d $ gen . fn
  340 
  341 -- | @takeA duration animation@ creates a new animation consisting of initial segment of
  342 --   @animation@ of given @duration@, played at the same rate as the original animation.
  343 --
  344 --  The @duration@ parameter is clamped to be between 0 and @animation@'s duration.
  345 --  New animation duration is equal to (eventually clamped) @duration@.
  346 takeA :: Duration -> Animation -> Animation
  347 takeA len (Animation d gen) = Animation len' $ \t ->
  348     gen (t * len'/d)
  349   where
  350     len' = clamp 0 d len
  351 
  352 -- | @dropA duration animation@ creates a new animation by dropping initial segment
  353 --   of length @duration@ from the provided @animation@, played at the same rate as the original animation.
  354 --
  355 --  The @duration@ parameter is clamped to be between 0 and @animation@'s duration.
  356 --  The duration of the resulting animation is duration of provided @animation@ minus (eventually clamped) @duration@.
  357 dropA :: Duration -> Animation -> Animation
  358 dropA len (Animation d gen) = Animation len' $ \t ->
  359     gen (t * len'/d + len/d)
  360   where
  361     len' = d - clamp 0 d len
  362 
  363 -- | @lastA duration animation@ return the last @duration@ seconds of the animation.
  364 lastA :: Duration -> Animation -> Animation
  365 lastA len a = dropA (duration a - len) a
  366 
  367 clamp :: Double -> Double -> Double -> Double
  368 clamp a b number
  369   | a < b     = max a (min b number)
  370   | otherwise = max b (min a number)
  371 
  372 -- (#) :: a -> (a -> b) -> b
  373 -- o # f = f o
  374 
  375 -- | Ask for an animation frame using a given synchronization policy.
  376 getAnimationFrame :: Sync -> Animation -> Time -> Duration -> SVG
  377 getAnimationFrame sync (Animation aDur aGen) t d =
  378   case sync of
  379     SyncStretch -> aGen (t/d)
  380     SyncLoop    -> aGen (takeFrac $ t/aDur)
  381     SyncDrop    -> if t > aDur then None else aGen (t/aDur)
  382     SyncFreeze  -> aGen (min 1 $ t/aDur)
  383   where
  384     takeFrac f = snd (properFraction f :: (Int, Double))
  385 
  386 -- | Animation synchronization policies.
  387 data Sync
  388   = SyncStretch
  389   | SyncLoop
  390   | SyncDrop
  391   | SyncFreeze