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