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