let's make animations simple

976 views
Skip to first unread message

Hassan Hayat

unread,
Jun 20, 2015, 10:26:30 AM6/20/15
to elm-d...@googlegroups.com
This is a follow-up of https://groups.google.com/forum/#!topic/elm-discuss/Cl6_stwjTnY

I've codified my ideas for animation here: https://github.com/TheSeamau5/simple-animations/tree/master

The idea is to represent animation as pure data as opposed to functions, and thus include them in the state of a component if necessary. 

For example, suppose you have the following component:

type alias Model =
 
{ position : Vector }
 
 
and you'd like to animate the position given some action, you'd change your type as follows

type alias Model =  
 
{ position : Animation Vector }
 

And then, in your update function:

update : Action -> Model -> Model
update action model
=
 
case action of
   
Move -> { model | position <- Animation.play position }
 

An Animation is basically a list of frames and play just moves to the next frame. So, when you want to get the current position to render it, you just use ".current"

view : Model -> Form
view
model =
  circle
20
 
|> filled red
 
|> move (model.position.current.x, model.position.current.y)
 

The Animation data structure is the same as the UndoList is elm-undo-redo

type alias Animation a =
 
{ previous : List a
 
, current : a
 
, next : List a
 
}

The cool thing about this approach is that Animations are totally mappable. So, out of the box, they work with all of your types, provided you can go from, say, floats to your type.

For example, suppose you want to animate from { x = 0, y = 0 } to { x = 10, y = 30 } linearly. You can use the linear function which is included

linear : Int -> Animation Float

which, given a number of frames, will produce an animation from 0 to 1 at regular intervals. linear 4 would give you 0 - 0.25 - 0.5 - 0.75 - 1. 

You then use map to do your bidding (map2 in this case)

myAnimation =
  map2
Vector
       
(map ((*) 10) (linear 20))
       
(map ((*) 30) (linear 20))

linear goes from 0 - 1, so for the x's we need to scale everything by 10 and for the y's we need to scale everything by 30. In this example we just assume that there will be 20 frames in the animation (which is a good number for an animation at 60 frames per second). And, that's it, there's your animation.



You'll see that the animations used are not much more complicated than the one above. The only difference is that not all animate linearly. Some use a quadratic function, others use a cubic function. But, remember that the animations are mappable. If you like the square root function, you can have your animation follow that curve by doing this:

map sqrt (linear 20)

Really. That simple. 

This approach should be benchmarked but the performance looks decent (on my laptop and my phone). The idea is that if you have one time animations (animations where the values are known beforehand, then the animation gets created whenever you produce your initial state and that's it. Going to the next frame is just a cons away. 


I'll try to release it as soon as I can, but tell me what do think of the approach. I don't know much about animations and I've never heard of this approach before.




Aaron VonderHaar

unread,
Jun 20, 2015, 11:17:49 AM6/20/15
to elm-d...@googlegroups.com
I like where this is going in that more people are thinking about the complexities here.

I just got back from vacation, so I haven't had a chance yet to follow up on the previous thread, and I'm sure I will have more thoughts on this one as well, but for now I have just a few to contribute:

I noticed you implemented all the different interpolation (animation curve) types.  I think you could easily integrate with [Dandandan/Easing], and we could benefit from reuse and consistency there.  I think you could cut out a bit of your code just by writing a `Easing.Interpolation a -> Int -> Animation.Animation a` function.


More general thoughts:

 - I'm skeptical about precomputing the frames, but I haven't played around with that approach yet.  I do understand the desire to make the model not contain functions, but it just feels incorrect.  Maybe my concern about this is irrelevant in practice.
 - I think you'll have trouble duplicating this effect [Move,Move.elm].  Note that if you click while the square is still moving, it smoothly redirects to the new target location.  In your FollowMouse example, the redirections keep the position continuous, but has discontinuities in the velocity.  The Move demo itself may seem frivolous, but this hints at a whole class of "interruptible" interactions that are partially animated and partially controlled real-time by user input.


 - I also suspect you'll have trouble implementing more sophisticated animations like these [physics], which I intend to attempt to duplicate next.  We'll need to make things like that easy as well if we want to compete with UIKit (iOS).


I've also been trying to reconsider a completely different approach for animations (and one that Evan originally proposed) where we carefully identify a set of things that can be handled in native-space instead of in elm-space if we can find a simple but still flexible way to do that.

In any case, those are a few thoughts since I only have a brief amount of free time this weekend.  Thanks for sharing what you've started.  I'm curious to see where your path leads.

--Aaron V.


--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Max Goldstein

unread,
Jun 20, 2015, 11:29:09 AM6/20/15
to elm-d...@googlegroups.com
As code review, you don't need to explicitly import List, and what the heck does reset do? The current frame is not the first frame, as the docs claim. If I understand it correctly, it undoes history, then jumps to the current frame, and then continues forward. Also, use cons rather than appending a singleton list.

As concept review, I really like that the library is much simpler than the one in the previous thread. I like the emphasis on mapping; from what I understand you map over the result of linear (or cubic, quadratic, etc) with your type. But I'm concerned with the notion of generating all frames explicitly up front. Besides the space cost, frames are not guaranteed to be equally spaced in time. This is why Time.fps and AnimationFrame.frame are a signal of numbers, and those numbers tell you the elapsed time. Sampling on the signals and ignoring the number will lead to unpredictable animations as the framerate changes. Besides, it's much more natural to say "I want a 3 second animation" than "I want a 180 frame animation". Yes, this means keeping functions around instead of applying them up front. But I think that you can still get all the mapping goodness using function composition.

I like the simplicity of linear : Int -> Animation Float so let's see if we can write linear' : Time -> Animation Float as something we can work with. How about animate : Animation a -> Time -> (a, Animation a)  which resembles random seeds a bit. Basically, you get a model for the current time, and the new animation saves the time delta so it can compute the next frame. An animation knows the desired run time and the time elapsed, and can interpolate from there. I think this would go well with foldp in a model. Also like random seeds, storing the animation in the model would technically be storing a function or two but it's an opaque type so that's okay.

Hopefully that's helpful. I am resisting the urge to fork your library since I was hoping to finish up other side projects today :)

PS: If you want to dive into the deep end of animation, I recommend spending a few hours on this article.

Hassan Hayat

unread,
Jun 20, 2015, 12:42:33 PM6/20/15
to elm-d...@googlegroups.com
Respoding to Max: 
 
what the heck does reset do? The current frame is not the first frame, as the docs claim. If I understand it correctly, it undoes history, then jumps to the current frame, and then continues forward.

Reset takes you back to the first frame. This is useful if you want to replay an animation. Say you have an animation that goes 1 - 2 - 3 - 4 and you're currently at 3, reset will take you back to 1.  


But I'm concerned with the notion of generating all frames explicitly up front.

That's why we'll need to benchmark it. 

  Besides the space cost, frames are not guaranteed to be equally spaced in time. This is why Time.fps and AnimationFrame.frame are a signal of numbers, and those numbers tell you the elapsed time. Sampling on the signals and ignoring the number will lead to unpredictable animations as the framerate changes. Besides, it's much more natural to say "I want a 3 second animation" than "I want a 180 frame animation". Yes, this means keeping functions around instead of applying them up front. But I think that you can still get all the mapping goodness using function composition.

This is a good point. The current model assumes constant framerate, which is untrue in real life. So, instead of playing animations from some "NextFrame" action, it would maybe be preferable to have the action be like "NextFrame Float" where the Float is the timeDelta. I'll have to think how it changes things though, but I agree, it would be preferable to say that you want a 3 second animation. Currently, I use numbers like 15 or 20 frames cuz they feel like good numbers, but I do end up multiplying them by 17 in my head to see how many milliseconds that gives me. 


I like the simplicity of linear : Int -> Animation Float so let's see if we can write linear' : Time -> Animation Float as something we can work with. How about animate : Animation a -> Time -> (a, Animation a)  which resembles random seeds a bit. Basically, you get a model for the current time, and the new animation saves the time delta so it can compute the next frame. An animation knows the desired run time and the time elapsed, and can interpolate from there. I think this would go well with foldp in a model. Also like random seeds, storing the animation in the model would technically be storing a function or two but it's an opaque type so that's okay.

Super interesting. I think that the big benefit of this is that you only need to compute the next state, which you're doing anyways in the update function. And we already know that random generators are mappable... But remember, functions are not mappable. 

Example: 

type alias Function a = a -> a
 
map
: (a -> b) -> Function a -> Function b
map f func b
=
 
-- how do I convert b to a to then apply func then f ?
 

So, handle these things with care. 


Hopefully that's helpful. I am resisting the urge to fork your library since I was hoping to finish up other side projects today :)

Yup, very helpful. Fork it whenever you want. I know what it feels like to have a bunch of side projects. If I ever publish this on the package site, this would be my 12th package there (knowing that there are 2 that I haven't even had time to upgrade to 0.15 :( ). So, yeah, I hear you. Take your time. But I like your ideas. 


PS: If you want to dive into the deep end of animation, I recommend spending a few hours on this article.

Cool article, I'll make sure to read it right away. 

Max Goldstein

unread,
Jun 20, 2015, 1:16:58 PM6/20/15
to elm-d...@googlegroups.com
I said I wasn't going to, but let me flesh out time-based animations a bit more.

The animation library that I have the most familiarity with is D3. I like that it lets you set durations, delays, and easing functions, and that it provides defaults for these rather than requiring them all up front.

I think we can represent an animation as such:

import Time exposing (Time)

type alias Animation a = { duration : Time
                         , elapsed : Time -- default: 0
                         , delay : Time -- default: 0
                         , start : Float -- default: 0
                         , end : Float -- default: 1
                         , ease : Float -> Float -- default: identity
                         , render : Float -> a
                         }

The first step of the render pipeline takes an animation and the time delta since the last frame. A slightly naive interpolation of delaying by t is to set elapsed to -t but this this falls apart if you want to delay an animation in progress. Instead, we start by subtracting the time delta from delay and saving the new delay (remember the output is an a and a new animation). Anyway, once we have the time that we move for, we add that to elapsed and divide by the duration, clamping to be between 0 and 1.

Then we pass that fraction to eased linear interpolation, elerp frac = start + (end-start) * ease(frac). Notice that this is linear interpolation of a possibly non-linear easing function! Also notice that when start=0 and end=1, this is just applying ease to frac. This extra layer means we can animate, say, an x-coordinate moving from 100px to 200px. Start and end therefore know a little bit about the physics, and we keep that separate from (say) rendering a ball onscreen. The article I linked to talks about changing the destination of an animation midway through (remember that we're applying these in a foldp "loop"; handing a new action is quite reasonable) so I think this representation can support that, although I haven't worked out the details. If we passed the result of ease directly to render, I don't think that would be possible.

Finally we pass the result of elerp to render. To map over an animation with an (a -> b) is to compose that function with the original render, resulting in a (Float -> b).

It may be possible to break this apart into the time component (handles delays, timesteps, makes a new version each iteration) from the ease-elerp-render component, which is essentially a pure function until you change parameters in the closure. That way you could animate, say, the x, y, r, and color of a ball independently but only tracking one time-changing value. We might be able to use Dan's library for the second part, though I haven't looked at it recently.

Hassan Hayat

unread,
Jun 20, 2015, 1:18:20 PM6/20/15
to elm-d...@googlegroups.com
Responding to Aaron: 

I like where this is going in that more people are thinking about the complexities here.

Thanks. Yeah, complexity is exactly what I'm out to fight. A lot of animation libraries are hard to use, especially in statically-typed languages. Languages like Javascript can get away with looping through all the fields of an object, getting the field names by name, and getting all the values and determining their types and then animating based on that. So, that's why I tried making the animations "mappable". My logic is that in animation programs you have this long keyframe interactive diagram you can play with. And I was like, that's just a collection of values, I'm sure you can just map them to be something else. 

 I just got back from vacation, so I haven't had a chance yet to follow up on the previous thread, and I'm sure I will have more thoughts on this one as well, but for now I have just a few to contribute:

I hope you had a great vacation.  

I noticed you implemented all the different interpolation (animation curve) types.

I didn't. I only implemented one "linear". Quadratic and cubic are just mapping a quadratic or cubic function over linear. I didn't seek to implement bezier curves and all that. And, yeah, I know about Easing and I like it but it feels super complicated at times. Like look at the ease function for example:

ease : Easing -> Interpolation a -> a -> a -> Time -> Time -> a

where easing and interpolation are defined as 

type alias Interpolation a = a -> a -> Float -> a

type alias Easing = Float -> Float

I mean, OMG, look at all these arrows. I'm completely lost. It feels like magic when I use it and I don't fully undestand how it works. This was also the big incentive for me to think about a solution that is much simpler, thus, "linear".

linear : Int -> Animation Float

One arrow, it takes an Int. Done. You have an animation. 

More general thoughts:
 - I'm skeptical about precomputing the frames, but I haven't played around with that approach yet.  I do understand the desire to make the model not contain functions, but it just feels incorrect.  Maybe my concern about this is irrelevant in practice.
 - I think you'll have trouble duplicating this effect [Move,Move.elm].  Note that if you click while the square is still moving, it smoothly redirects to the new target location.  In your FollowMouse example, the redirections keep the position continuous, but has discontinuities in the velocity.  The Move demo itself may seem frivolous, but this hints at a whole class of "interruptible" interactions that are partially animated and partially controlled real-time by user input.

I'm skeptical too ;) but I've never heard of the idea before, so, who knows? This could be good or it could be bad, but at least we'd all be part of the scientific process by finding out. 

I think that the idea in duplicating the effect is in how you consider your state. To achieve this properly with simple-animations, the trick would be in saving the position and velocity as well as some values related to angular momentum. You'd like to limit by how much you can actually rotate. I mean, this can totally spawn off an actual physics engine, which would be a good thing. After all, physics based motion is more natural and tends to feel better from a UX point of view. 

For example, you can totally imagine turning everything into an inverse kinematics problem where you're like: I'd like to go from point a to point b but I can't turn more that 5° per frame or move forward faster than 20px per frame. And then this iksolver function could just generate an Animation from the two points and the constraints.  

I've also been trying to reconsider a completely different approach for animations (and one that Evan originally proposed) where we carefully identify a set of things that can be handled in native-space instead of in elm-space if we can find a simple but still flexible way to do that.

I wonder how you'd do that. For one-off animations you can use css transitions. But you can't use keyframe animations with inline styles... So, what I do right now is use css transitions where I can until I need more in which case I give up cuz I don't want to write CSS in a seperate files and manage classes and ids.... yuck. 
 
 I also suspect you'll have trouble implementing more sophisticated animations like these [physics], which I intend to attempt to duplicate next.  We'll need to make things like that easy as well if we want to compete with UIKit (iOS).

Basically I'm trying to come up with something low-level. Something you can write a whole bunch of libraries on top. You want spring physics, you want gravity, you want forces and impulses, etc... Ideally, you should be able to use the same physics library you'd use in a game on a UI component.  

In any case, those are a few thoughts since I only have a brief amount of free time this weekend.  Thanks for sharing what you've started.  I'm curious to see where your path leads.

 
Thanks. I also am curious to see where elm-animations goes. I think that the ability to turn on and off animations is really cool. About that point, the way I imagine simple-animations will deal with that problem is by applying the "One Last Pattern" from the elm architecture tutorial

The idea is to have a request type that will say:

type Request
 
= Animate
 
| StopAnimation
 
| ...
 

And use that to feed the Signal Bool that AnimationFrame.frameWhen from elm-animation-frame needs. 

Thanks a lot for your insight. I'll make sure to think about how to improve things.

Max Goldstein

unread,
Jun 20, 2015, 1:48:42 PM6/20/15
to elm-d...@googlegroups.com
Hassan, I think what you're reacting to in

ease : Easing -> Interpolation a -> a -> a -> Time -> Time -> a

is that all the concerns are muddied together. We see easing, interpolation, timekeeping, and rendering. No offense to the author of the library, but that's not the way to go. Besides being a conceptual burden, it makes it hard to extend with delays, or new end states, or mapping.

To summarize my previous post, the pipeline is timekeeping => easing => elerp => render (fat arrows used to avoid the impression of types). One thing I like about my proposal is that the polymorphism is confined to the render function. This makes mapping easier, composition more natural, and encourages reuse. For example, you don't have to worry about having an Interpolation Color between red and blue. That logic can be confined to the render function and the elerp step works fine by default.

One benefit of splitting up the timekeeping from the calculation is that some parameters (e.g. position) benefit from elerp with start and end, while others like color can put their logic in render, and maybe radius can elerp the area and then use sqrt as render.

Max Goldstein

unread,
Jun 20, 2015, 3:36:03 PM6/20/15
to elm-d...@googlegroups.com
*Sigh* I promised I wouldn't do this... Here is a sprout of how my idea could work.

I feels very idiomatic. None of the type signatures are particularly scary. Only Clock is frame-by-frame stateful; only Animation is polymorphic. And the example shows that it fits the Elm Architecture quite well.

You'll notice that all animations have the same duration, so the speed is slow for short distances and fast for longer ones. I think constant-time is fine for many applications, but we may want to offer constant speed and acceleration as well. We'd also write an easing function combinator library:

cubic x = x*x*x                                                                                                          

out f x = 1 - f (1-x)                                                                                                    

inout f g x = 0.5 * if x < 0.5 then f (2*x) else 2 - g (2 - 2*x)


I think this can work, but I'll wait for feedback and see about getting my other side projects done.

Max Goldstein

unread,
Jun 21, 2015, 12:16:29 AM6/21/15
to elm-d...@googlegroups.com
Alright, I got Turtle out, and I thought about this some more, and I think it's a matter of stepping back and asking: what is the problem we want to solve? Are we trying to make jQuery? Or D3? Or a physics engine?

If you were to make something D3-like, in that it operated on data, you'd want that data to be polymorphic. Therefore you'd either need a concrete view type, or two type variables. D3, while it can be used with canvas (handwave), is primarily for manipulating XML, essentially a concrete view type.

The other thing about D3 is that you can have transitions with per-element durations and delays, based on the data and index, and you can even have multiple independent animations on the same DOM element. Assuming we have just one... I think it could still be reasonably performant with a global clock, you'd need to keep your start time, and your render function would be fairly complex. It would be supplied the data, the index, and the eased fraction (you'd have a lerp function close-at-hand). You need all 3 to make a bar chart where the bars expand to their heights; the index determines the spacing.

render d i f = rect 20 (lerp barWidth (barHeight d) f) |> filled (barColor d) |> moveX (barWidth*i + barMargin* (i-1))

D3 instead gives you only d and i (data and index), but it lets you set values declaratively as both the start and end state of a transition. Because of HTML/SVG attributes, it can do these individually. There doesn't seem to be an Elm-ish way to specify graphics with random access; you have to write it all out as above.

Anyway, you want a lot of control of that sort when you're doing a data visualization. If you're making a sliding panel, it's a completely different set of needs. And if you're doing a physics simulation, it's really completely different.

Sorry for the rambling rant. Reminder: what is the problem we are trying to solve?

Srikumar Subramanian

unread,
Jun 21, 2015, 1:48:00 AM6/21/15
to elm-d...@googlegroups.com
This is an interesting idea - treating animations as a precomputed set of frames. I've needed to coordinate audio and visuals tightly in a browser and from that perspective, I think there are a few problems with this as a general approach (glad to stand corrected) -

1. Animations are not "additive". When a parameter is in flight between two values x1 and x2, and an event occurs that requires it to reach x3 instead of x2, we need a smooth and automatic way to "cross fade" so that the value naturally reaches x3 without disruption. 

2. Animations are frame based rather than time based. If requestAnimationFrame calls occasionally take longer than one frame to finish, then the "time" of the animations starts drifting relative to absolute time. This is probably not a huge issue if only visual animations are in question, but if these are to remain in sync with audio being rendered using the Web Audio API (which has its own clock), they need to be time based and not frame based. 

3. The memory impact of precomputed animations seems significantly larger than function-based animations. I wonder what its impact on mobile browsers would be.

From the perspective of a "core" animation library for Elm, there are a few possible approaches -

1. Freeze a few types of animations general enough to be usable in UIs, but implement them in a time-based and additive way. Linear, Quadratic and Cubic Bezier are more than enough. 

2. Go pure-physics. This could make it hard to go backwards in time, but I think that feature can be built on top of a physics engine if necessary, just like Elm's time travel debugger.

3. Interface with a library like GreenSock (http://greensock.com/gsap) or consider the W3C "web animations" proposal (https://w3c.github.io/web-animations/).

-Srikumar

Hassan Hayat

unread,
Jun 21, 2015, 6:14:46 AM6/21/15
to elm-d...@googlegroups.com
what is the problem we want to solve?

Making this website possible in Elm. http://material.cmiscm.com/

I think we want 

1) Mappable animations (we want the ability for the user to extend animations to their favorite types)
2) Physics-based animations. We want springs, we want forces.
3) We want interruptible animations. We want the ability to interrupt animations (maybe by a "merge : Animation a -> Animation a -> Animation a" function) and we'd even like to tweak these things.

In essence, we need it to be pretty low level. Physics could even be a seperate library that extends this one. But the library should enable it. 

I like your idea of having a render function. That allows use to generate values at any frame, even a fractional one. 

From the article you shared, especially the exponential decay example, we may want animations that depend on their own past states or that of other animations. That could be via finding a good way to represent difference equations

Max Goldstein

unread,
Jun 21, 2015, 12:52:06 PM6/21/15
to elm-d...@googlegroups.com


On Sunday, June 21, 2015 at 6:14:46 AM UTC-4, Hassan Hayat wrote:
what is the problem we want to solve?

Making this website possible in Elm. http://material.cmiscm.com/

Excellent, that makes things a lot more concrete. I'll circle back to it in a moment.
 
1) Mappable animations (we want the ability for the user to extend animations to their favorite types)
Sure.
 
2) Physics-based animations. We want springs, we want forces.
I think there's a difference between analytic functions, where you can predict things mathematically, and physics, where you simulate as you go (and ultimately decouple the physics clock from the render clock). Analytic equations break down into those seen on the Medium example (excluding the ones that are physics), and data-driven like D3. The big difference is whether or not you have a fixed number of animations (like a button or overlay) or a dynamic one (like a variable number of bars in a chart). Having thought about doing D3 in Elm, it's really unappetizing - you want to be able to do a lot of things independently, and D3 is already a lot better (more declarative) than your average JS. So I'm quite happy to focus on physics and a fixed number of analytic animations.

3) We want interruptible animations. We want the ability to interrupt animations (maybe by a "merge : Animation a -> Animation a -> Animation a" function) and we'd even like to tweak these things.

Srikumar mentioned this as well in his point 1. As I mentioned it's in the glory article, specifically the second slideshow starting on slide 34. After going over it a few times, it makes a lot of sense conceptually but I have no idea how to implement it in code. But yes, we want this.
 
From the article you shared, especially the exponential decay example, we may want animations that depend on their own past states or that of other animations. That could be via finding a good way to represent difference equations
Incidentally, my nonempty list library could be useful for this sort of thing. I noticed it lent itself nicely to piecewise integration.

Okay, back to material.

The intro (refresh the page) - This is just a few time-varying proportions piped to a render function, and I think you could implement it with what I proposed above. Notice how useful delays are!

The black border on hover - notice that if you hover out, the animation starts undoing itself without completing, and you can even mouse back and forth and the animation will follow. Once we figure out interruptible animations - to assign a new x3 as Srikumar says - this should be fairly easy. Also notice that interruption means the duration of the animation becomes unpredictable, even indefinite, which should have implications for how we implement clocks. When interrupted, the animation does not slow down, covering a shorter distance in the same time.

Filling the screen on click - Should be fairly easy with the right easing function (notice the squash and stretch) and four elerps, tracking width, height, x, and y. It would be useful for animations to not just support map but andMap, so you can package up those four numbers into something that rendered your entire rectangle.

The close button - you track rotation angle and radius, with a bouncy easing function. Notice that the hover area changes to match the circle area. Should just be a matter of setting up the right elerps and eases, again with interruptibility.

Okay, now the actual demos. (Aside: everything being interpolated is a number, no colors or anything crazy yet.)

Orange balls on pink background - this is straight up physics. You track the radius, x, dx, y, dy, theta, and dtheta for each. You can use a separate physics clock, which applies velocities, collisions, and friction (it seems all acceleration is instantaneous?) and interaction (clicks which make more balls). I think the hardest part might be when you click a small enough ball and it expands to fill the background. Other than that, there's no easing or interpolating going on; you can't predict 5 frames out without explicitly calculating each step. I think this is doable in Elm without any animation framework.

White square, purple background and pink lines, blue background: Other than intro and exit, both of these seem stateless to me. You can pass the collision status out a port and then to window.setCursor to get the drag handle. Calculating the angles of the gradient shape and force lines seems a bit tricky but is a pure function.

Orange ball, white square: another physics simulation. You have to track maybe a dozen variables and handle some interaction but I think it can be done.

Three rhombuses: this involves tracking the mouse exactly while sliding, and then easing up and down. This is the only actual demo that uses analytic animations, and so it could be a little tricky.

The devil is in the details, and the whole is greater than the sum of the parts: I've ignored some of the intro and exit animations but they're what gives the site a cohesive look to it. It's also responsive, rearranging with the window dimensions and aspect ratio. I think each individual piece can be replicated without too much trouble but getting a cohesive whole could be quite difficult.

One way to go about this might be for multiple people to implement different parts, perhaps multiple implementations of the same part, and then we can see what worked and what didn't.


Aaron VonderHaar

unread,
Jun 21, 2015, 1:43:38 PM6/21/15
to elm-d...@googlegroups.com
My examples here [avh4/elm-animations] implement additive and
interruptible animations in the way that you all are describing.
After reading the [glory] article, I think there are some ways to
optimize the memory use and performance of my first attempt, but I
also think the type of API I was experimenting with could have
multiple implementations that merge animations in different ways if
that were useful.

[avh4/elm-animations]: https://github.com/avh4/elm-animations
[glory]: http://acko.net/blog/animate-your-way-to-glory/

Regarding the goals, we should recognize that the [cmiscm] example
already *can* be implemented in Elm. What we want in an animation
module is to make certain types of things easier to write. There is a
line that we will need to draw because at a certain point, an
animation system becomes so flexible that it is then simply an
unnecessary wrapper around the functionality that Elm already
provides. The challenge is in choosing the right point at which
someone would switch from using a simple animation module to
implementing their own animations.

[cmiscm]: http://material.cmiscm.com/

Currently, my thoughts about where that line should be for an
animation module are:

1. it should let people using or familiar with elm-html add
transitions between states of their view
2. it should be opinionated and have good opinions. Specifically,
it should not make it easy to add animations in general or to add bad
animations; it should make it easy to add state-of-the-art animations
(currently, I think that means: additive, interruptible, good default
choice of easings, possibly physics-based)
3. it should not limit the ability to make nice abstractions as your
app grows in complexity
4. it needs to support choreographing a sequence of related
animations (example, a dialog drops in while elements inside it fade
in from left to right)
5. it would be ideal (but not required) if it were independent of
elm-html and could be used with any rendering module
(Graphics.Elements, elm-svg, elm-webgl, etc)
6. It should follow familiar patterns so that if someone wants to
switch from using it to writing their own custom physics, the refactor
is straightforward
7a. it would *not* be used for games like Mario / Pong / Asteroids
-- other game/physics/simulation modules could be written to make
those types of apps easier
7b. it could be used for the UI portion (not the in-game portion) of
games like those in (3)
7c. it could be used for games that are mostly static layouts, like
Scrabble / Battleship / etc

I think an animation module does *not* need to implement the demos
from the [cmiscm] example. It only needs to implement the
transitions, and to provide a way for the demos (which would manage
their own physics) to integrate with the rest of the app.

I would say ultimately that the problem is this: How do we model
animated state-to-state transitions in a functional language in a way
that best encourages beautiful app design and in a way that we can
implement efficiently in Elm?

Max Goldstein

unread,
Jun 21, 2015, 4:54:16 PM6/21/15
to elm-d...@googlegroups.com
Aaron, I think those a good principles. Now, to implement them....

After looking at Aaron's library and not having as much luck with my own as I had hoped, I will probably write a new draft in the next few days.

I tried to implement map2 for my library, with the render functions, and found it extremely difficult. Each render function wants a fraction based on its own start, end, and easing value, but produces a type you want to hide behind the mapped result. I don't see a way to do that.

The other thing I noticed is that Aaron's library, like Dan's easing library, wants to be able to animate/interpolate a polymorphic value and provides multiple helpers for that. I think this leads to a lot of complexity. So, at least temporarily, I am of the opinion that an animation library should be monomorphic. Animation provides time-varying Floats, end of story.

One thing I (respectfully) don't like about Aaron's library is the large number of high-arity functions. For example, the start value is supplied as the second argument to animationState while the end value is supplied as the fifth (!) argument of startAnimation. I think a better approach is a record type with sensible defaults that can be overriden, like Graphics.Collage. If possible, I would like to only define one new type ("what's the difference between an Animator and an AnimationState again?").

I do like the idea of a single running clock : Time, accumulated in a foldp and probably part of a larger state record. If possible, I'd like to avoid something like animationSignal, which seems to dictate a lot about the architecture of the program.

So, very tentatively, something like this:

-- one opaque monomorphic type
type alias Animation = { startTime : Time
                       , delay : Time
                       , duration : Time
                       , ease : Float -> Float
                       , start : Float
                       , end : Float
                       }

animation : Time -> Animation
animation now = Animation startTime 750 aGoodEasingFunction 0 1

delay, duration : Time -> Animation -> Animation
easing : (Float -> Float) -> Animation -> Animation
start, end : Float -> Animation -> Animation

animate : Time -> Animation -> Float
isScheduled, isRunning, isOver : Time -> Animation -> Bool
rerun : Time -> Animation -> Animation

The type will need revision; for example rerun will require an explicitly stored delay instead of adding it to startTime. Compared to my last proposal, there is no render function (and therefore no polymorphism), and we store startTime pegged to a running clock rather than elapsed. That way you can have multiple independent animations without having to tick n clocks. The basic idea is to package up timekeeping (duration, delays), easing (could have a whole library here), and linear interpolation (start, end). I haven't thought much about being additive or interruptible so that may bring this crashing down.




Hassan Hayat

unread,
Jun 21, 2015, 7:53:26 PM6/21/15
to elm-d...@googlegroups.com
Max, what if you tried this?

type alias Animation a = 
  { render : Time -> a
  , time : Time 
  } 

Just keeping it super simple and then sneak in all the easing in the render function via function composition.

One way you can define the map functions for this is as follows:

map : (a -> b) -> Animation a -> Animation b 
map f animation = 
  { animation | render <- animation.render >> f }


map2 : (a -> b -> c) -> Animation a -> Animation b -> Animation c 
map2 f a b = 
  { render = \time -> f (a.render time) (b.render time)
  , time   = max a.time b.time 
  }
  
andMap : Animation (a -> b) -> Animation a -> Animation b 
andMap = 
  map2 (<|)
  

In map2 you just say that the new time is the max of the two times. Just cuz that seems like a good idea.

Then you can define all sorts of functions like this:

linear : Animation Float
linear = 
  initialize identity
  
initialize : (Time -> a) -> Animation a 
initialize render = 
  Animation render 0 
  
  
run : Animation a -> a 
run animation = 
  animation.render animation.time
  
  
nextFrame : Time -> Animation a -> Animation a 
nextFrame delta animation = 
  { animation | time <- animation.time + delta }
  
  
reset : Animation a -> Animation a 
reset animation = 
  { animation | time <- 0 }
  

animate : Animation a -> Signal Time -> Signal a 
animate animation deltas = 
  Signal.map (\delta -> nextFrame delta animation |> run) deltas

I'll investigate more this idea and try to implement a small example to see how it goes. From this it seems like delays would be weird to implement. 

Max Goldstein

unread,
Jun 21, 2015, 8:58:05 PM6/21/15
to elm-d...@googlegroups.com
Well, that is totally different from what I've been up to the last few hours. And for that reason alone I encourage you to keep going with it; I could very easily be wrong in my approach.

Said approach is to take in the current time from a running clock as the first argument to every function. In ~40 lines of library code and ~60 lines of client code, I was able to implement the close button from Material, except for the easing function. What I want to do now is look into interruptibility by studying Aaron's library, and maybe also chaining animations to play sequentially.

Funnily enough I implemented andMap this morning.

Aaron VonderHaar

unread,
Jun 21, 2015, 9:20:44 PM6/21/15
to elm-d...@googlegroups.com
For the additive animations, my lib currently works by keeping a
target value and a list of delta animations, which all end at zero.
When you start a new animation, it sets the target value to the new
target, and adds a new delta animation which starts at (previousTarget
- newTarget) and ends at zero. The current value of the animation is
the target value plus all the delta animations. Any delta animations
that have finished are removed from the list.

I think most of that complexity could be removed in favor of the
approach shown starting at slide 34 in the second slideshow widget in
the [glory] article.

[glory]: http://acko.net/blog/animate-your-way-to-glory/

Max Goldstein

unread,
Jun 21, 2015, 11:32:47 PM6/21/15
to elm-d...@googlegroups.com
Aaron, I'm looking forward to seeing what you came up with, and I'm glad the glory article has been helpful.

Hassan, let me walk through this with you, and hopefully we'll both learn something.

An Animation a defines the value of an animation at a particular moment in time. The smoking gun is that run takes no other parameters (e.g. a clock). The name nextFrame is misleading; it implies that you add a small delta to a large summed up time. In fact, the way animate works is that it adds the delta to 0 (or whatever the fixed value of animation.time is), creating a new record every frame. Notice that animate has no foldp and if deltas really is a signal of deltas instead of the current (summed) time, you're going to keep animating the same value on each step (to the extent that each delta is almost the same).

Your implementation of map seems fine, but in map2 you sweep the notion of combining time under the rug. What happens when you add delay - whose delay do you keep? What if each value really needs to have a different delay? And what about duration - you put that burden on the render function, and linear will happily keep interpolating past 1. Keeping all of this extra information separate when combining is tricky.

So, what if we made a tree?

type Animation a = One (AnimationRecord a) | Pair (Animation a) (Animation a) (a->a->a)

This homogenous version would probably work, but it would be really limiting if the point of map2/andMap is to combine floats and a color to get an Animation Form. So heterogenously,

type Animation a = One (AnimationRecord a) | Pair (Animation b) (Animation c) (b->c->a)

The problem is that b and c are not qualified left of the equals sign. Furthermore, b and c could easily be types from more pairs, recursively. We really want to hide those types, and we should be able to given that we have a function that accepts them. There's probably a Haskell extension that solves this problem. 

But, we're not beaten. My library currently requires a time for just about anything to happen. What if we made animations a function type, so they truly are a value parameterized over time?

type alias AnimFunc a = Time -> a

Then -- get a load of this -- map is just function composition!

map : (a -> b) -> AnimFunc a -> AnimFunc b                                                                               
map = (<<)

And we can finally write map2 in a way that lets each component have full say over rendering details. andMap can be defined as you have it above.

map2 : (a -> b -> c) -> AnimFunc a -> AnimFunc b -> AnimFunc c

map2 f a b = \t -> f (a t) (b t)


Essentially, we can build up a tree using closures rather than storing things in a union type. Polymorphic heterogenous arbitrary-arity mapping - huzzah!


Unfortunately, storing things in the closure has its downside. You can't build things up with small make-a-copy modifiers, like animation |> start 100 |> end 300. And you can't read the values, which is critical if you want to undo or reverse or interrupt an animation.


I'm not sure there's a compromise; this sounds like shallow and deep embedding. (Deep embedding: explicit representation of values as data, a tangible AST. Shallow embedding: everything is a function in the host language, with composition governed by types.) To wit, one of the hallmarks of a shallow embedding is that there's only one meaning of a term, and that is to apply out the function all the way and see what you get. But, just to be sure I'm not missing something, let's try


type alias AnimRecord a = {animated : a, delay : Time} -- with a few more values                                         

type alias AnimFunc a = Time -> AnimRecord a


The result of applying the function with a time is now a record, with the polymorphic value (computed with the values in the closure) and other fields preserving the values from the closure, independent of time (except perhaps the time itself). This means that functions that need to inspect the values can apply an AnimFunc at time 0 to get the closure back, and animation just needs to fish out .animated.


map looks good:


map : (a -> b) -> AnimFunc a -> AnimFunc b

map f a = \t -> let record = a t

                in {record| animated <- f record.animated}


But once again we're left with the problem of combining things like duration and delay from two records into one. Duration is possible (how long was at least one constituent animation playing?) but delay is tricky. Actually, no: delay is just the minimum (then at least one thing starts moving) but it's possible that the two animations do not overlap at all, which means duration is somewhat meaningless. ("The duration is 20 seconds but things only move in the first and last second.") And remember, we need to combine these values in a way that makes it possible to reverse, loop, and interrupt the composite animation later.


So to conclude, I surmise that:

  • Regardless of deep or shallow embedding, polymorphic or not, it is not possible to write andMap in a way that sensibly combines metadata for use by interruptions, etc. (Deep embedding has problems combining two into one; shallow embedding loses access to it entirely.)
  • Without andMap, just map is of little use, especially if animations allow the start and end of linear interpolation to be set.
  • Without andMap and map, polymorphism is of little use.

So -- I wish it wasn't this way -- I seem to be back at my old assumptions. A monomorphic, deeply embedded term, a record type. To do anything with the record, you need the current timestamp, but an animation is not a function type, allowing you to actually access the record values.

Whew. G'night folks.

Srikumar Subramanian

unread,
Jun 21, 2015, 11:54:47 PM6/21/15
to elm-d...@googlegroups.com
Hassan - your  type alias Animatable a is in essence modeling an animation as 
a function Time -> a. This works for scripted animations, but is not so sweet for
interactive animations which demand additivity. Aaron's work is nice in that respect
and the "run deltas that move towards a potentially moving target" approach 
effectively gets you additive animations. This can also be achieved using physics.

Here is a thought thread - 

We have two roles for animations -
  1. What we may call "in scene" animations where something in the scene is
    either continuously animating or responding to some continuous input.
  2. "Transitions" when the structure of the page changes.
For "in scene" animations, there is probably not much more needed beyond Elm's 
model-update-view approach. A library like Aaron's or a physics library would 
work reasonably well, modulo improvements.

For transitions, maybe we can do better. We have a sequence like this -

stable-frame-1 ==> transition(t, stable-frame-1, stable-frame-2) ==> stable-frame-2

Currently, the common model-update-view approach using virtual-dom 
deals easily "stable-frame-1 -> stable-frame-2". To get transitions,
the view function can produce a Build In Out Html rather than just
a Html. "In" and "Out" are "build in" and "build out" animations. To control 
each frame, a step output can be represented using Step Html, and 
these can be parts of a sum type.

Think of how Keynote animates elements between two slides with relatively
little specification. Something like that would be a great starter, particularly
for folks new to Elm .. and could go a long way too. Does that appeal?

Max Goldstein

unread,
Jun 22, 2015, 9:05:53 AM6/22/15
to elm-d...@googlegroups.com
Yes, I would be interested in seeing a transitions library. At the moment, my best idea for handling that (to make, for example, the Material page) would be adding many, many animations for everything and trying to structure it somewhat modularly.

type MyAnimation = Scene1 <record of animations> | Scene2 <record of animations> ...
type CurrentAnimation = Scene MyAnimation | Transition MyAnimation MyAnimation

If we knew that one scene in every transition was a "home" screen, that could make things simpler. But, I would love to see a completely novel approach. Also, will this be render engine independent, or specific to elm-html?

Hassan Hayat

unread,
Jun 22, 2015, 10:03:33 AM6/22/15
to elm-d...@googlegroups.com
Think of how Keynote animates elements between two slides with relatively
little specification. Something like that would be a great starter, particularly
for folks new to Elm .. and could go a long way too. Does that appeal?

I think it does appeal. I think we're maybe trying to find the animation library to rule all animation libraries, but perhaps it may be more interesting to have several animation libraries (one physics based, one easing based, etc...). It does sound like we are looking for an animation library that my be doing too much. For example, something that is physics-based does not need to be mappable. The prerogative there is to track values such as position, velocity, acceleration, mass, etc... On the other hand, an easing-based one may be interested in being mappable. Cuz there, the prerogative is to have something that is easy to use and automagically works with any of the user's types.  

Pete Vilter

unread,
Jun 23, 2015, 9:50:15 PM6/23/15
to elm-d...@googlegroups.com
Just wanted to bring up Izaak Meckler's Piece library (used to be called stage) — seems relevant to this discussion because it has a nice way of composing behaviors over time. Haven't read through this entire thread (quite long) so I'm not sure how it could fit into the animation library to end all animation libraries.

Pete Vilter

unread,
Jun 23, 2015, 9:52:23 PM6/23/15
to elm-d...@googlegroups.com
Forgot to link to this blog post which explains the library's approach 

Max Goldstein

unread,
Jun 23, 2015, 11:08:07 PM6/23/15
to elm-d...@googlegroups.com
Thanks for the link, Pete. A few things of note:

In the blog post, if you double click buttons, the animation skips. We've talked a lot about interruptible animations in the thread, although in this case it would suffice to know whether an animation is running, and drop inputs if one is. But the piece library can't offer that. Why? Because ultimately it's represented as a (possibly infinite duration), and a (Time -> a) function. In my very long post above -- I forgive you for not reading it -- I go into how this "shallow embedding" doesn't allow access of the parameters needed to create an interruptible or inspectable animation. (Short answer: the values are stored only in the closure.)

The library also invokes Debug.crash in a few places where, contrary to the error message, something quite possible has happened. The type system ought to prevent this sort of thing, but instead it bends over backwards to accommodate the forever / for a time distinction. The only way to run an animation is with the run function but that seems very difficult to incorporate into an arbitrary architecture. I prefer how the blog post code incorporates the animation code into the main logic, but mostly behind a function definition.

Finally, the library doesn't seem to actually be much help, since the task of generating values is pushed onto the (Time -> a) render function. There's no easing and no interpolation. The library also falls short of providing mapN functionality, even though it's possible with this setup. I do like that the functions all have a reasonable number of arguments, and that it gives you good control cycling an animation back and forth.

Here is my (partially subjective) list of goals for an animation library. Note that these are type and syntax level, and should be largely independent of the semantic and architectural goals Aaron outlined waaay above us.

  • The library should export a single opaque monomorphic type. Animations are between two Floats. (I wish we could make it work some other way. You can see above that I tried.)
  • Functions should take no more than 2 arguments. Customizing an animation is done by applying many of these return-a-copy "modifiers", not by a long list of arguments.
  • Animations do not need to be updated every frame. Therefore, an animation's start time must be relative to a global running clock, which the client is expected to maintain.
  • The entire library should be pure. No Signals or Tasks allowed. We do not need to scaffold the Elm Architecture for the user; we need to fit inside it.
  • The primary value proposition of the library is to track timekeeping (other than the running clock -- delays and durations), easing, and linear interpolation (between two floats). The library should accept settings data but not ask for complex, custom functions.
  • An animation has a finite running time. Out-of-bounds timestamps are coerced to the nearer end of range (e.g. animating after the end produces the final value). Static animations are possible.
  • Animations should be inspectable (isRunning), interruptible (abort or change target), and reversible (undo).

Max Goldstein

unread,
Jun 26, 2015, 3:42:14 PM6/26/15
to elm-d...@googlegroups.com
Okay, I've put the first draft of a library on GitHub. It's not on the package site yet, and there's a bit of clerical work do. But I think it has a lot of promise. For one thing, it handles interruptions using the method described in the glory article. Here, the blue ball leaves a trail of dots falling down behind it:


Simon

unread,
Oct 27, 2015, 9:01:08 AM10/27/15
to Elm Discuss
Hi
is there anything un-elm-like in using Tick model from the TEA with a pattern that introduces (an element with) an initial class, and use the Tick to apply the destination class, and less CSS transitions handle everything in between.
It certainly works for the simple animations th

Simon

unread,
Oct 27, 2015, 9:02:36 AM10/27/15
to Elm Discuss
Hi
is there anything un-elm-like in using Tick model from the TEA with a pattern that introduces (an element with) an initial class, and use the Tick to apply the destination class, and let CSS transitions handle everything in between.
It certainly works for the simple animations that I am up to!

Simon

Max Goldstein

unread,
Oct 27, 2015, 11:08:02 AM10/27/15
to Elm Discuss
Hi Simon,

You really should start a new thread, but anyway: I'm not sure how much I trust CSS animations to handle transitions between classes. I've tried to do something similar in SVG with D3 and the styling changed abruptly with the class.

Is it "un-elm-like"? Perhaps -- usually any changing style is done in the view, explicitly, as a function of the model (which might contain an Animation!).

CSS animations won't give you control over retargeting, or even easing (I think). If you want to pause animations, sequence them with delays, anything like that, use my animation library in the model and render from that.

That said, if it's working you you, then I can't stop you. And I can certainly understand the tedium of specifying four attributes that all vary in lockstep; CSS classes can group them together.


Simon

unread,
Oct 27, 2015, 1:02:17 PM10/27/15
to Elm Discuss
Thanks Max,
that's a very pragmatic answer. I have so little experience with animations that one advantage of a CSS route is being able to use code fomr the web.
Simon
Reply all
Reply to author
Forward
0 new messages