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