In this tutorial we will analyze several techniques to implement a simple animation of a sushi plate moving on a conveyor.
We will start from CSS, go through Javascript and arrive at Elm.
Part I (this one)
- CSS Transition
- CSS Animation
- Vanilla Javascript Animation
- Vanilla Elm Animation
- elm-animator Inline Animation
Part II (under development)
- elm-animator Inline Animation II
- elm-animator Hybrid Animation
- elm-playground SVG Animation
- elm-playground-3d SVG Animation
- Elm WebGL Animation
Why Elm?
There are several reasons that make Elm an interesting selection, specifically to animation:
- There are few libraries available with interesting concepts. We will look at elm-animator, elm-playground and WebGL for Elm in this tutorial.
- The Virtual DOM optimization that Elm makes, thanks to the fact that it is a pure language, make Elm one of the fastest framework out there.
- The embedded Elm Debugger, (a.k.a Time Travelling Debugger), makes it easy to play the animation back and forward. Very useful for debugging and fine-tuning complex animations.
From a broader point of view, the appeal of Elm is that it is a pure functional language with a productive compiler.
We don't get runtime errors and functions are easy to read because they don't depend on any state but just on their declared inputs.
There are also advantages related to user experience, such as the finer grained dead code elimination that means small assets and shorter loading time.
As Evan Czaplicki - the author of Elm - puts it: "One of the best things about Elm is that there are entire categories of problems you just do not have to worry about. There are no surprise exceptions to catch, and functions cannot mutate data in surprising ways."
These are couple of talks that, even if not not related directly to Elm, describe well this concept: Functional architecture - The pits of success by Mark Seemann and Refactoring to Immutability by Kevlin Henney, that contain several nice quotes such as "OOP makes code understandable by encapsulating moving parts. FP makes code understandable by minimizing moving parts".
Ok, back to animations...
1. CSS Transition
Probably the simplest animation that we can write is using CSS transitions. CSS transitions let you move from one CSS style to another CSS style gradually. Almost all CSS properties can be transitioned. Some of them, like opacity
and transform
, perform very well, as the browser doesn't need to redraw the page (they occupy the same space in the page during the transition). The others, such as width and height, may take more resources.
CSS transitions are extremely simple, we just need to create two different styles and add transition: 1s
into our CSS.
Clicking on the page is simply adding or removing a class:
Clicking while the animation is happening, it reverses it from whatever position is at that moment. This is not always trivial to do, as we will see later.
Pausing the animation is more complicated. It would require, for example, reading the current style using getComputedStyle / getPropertyValue and write them back into the element.
2. CSS Animation
What if, instead having a transition between two states, we want to have a more complex transition across multiple states? We can add keyframes using CSS animations.
CSS Animations are different from CSS transitions.
- Transitions means that when a property changes it should do gradually over over a period of time
- Animations instead just run, so when the animation finish, the element go back to its original state unless we use
animation-fill-mode
These are the keyframes that we can use to give a "cartoonish" aspect to the animation:
@keyframes toTheKaiten {
from {
left: 600px;
transform: scale(0)
}
40% {
left: 20px;
transform: scale(1.2, 0.8)
}
to {
left: 50px;
transform: scale(1)
}
}
@keyframes toTheKitchen {
from {
left: 50px;
transform: scale(1)
}
20% {
left: 100px;
transform: scale(0.8, 1.2)
}
40% {
left: 20px;
transform: scale(1.2, 0.8)
}
to {
left: 600px;
transform: scale(0)
}
}
To control the animation we use a similar system as before, toggling a class in the DOM:
To reverse an animation mid-way or to pause requires more advanced strategy, similarly to CSS transitions.
CSS animations are usually good to create animation that don't require much interaction or that are in an infinite loop.
There is a close friend of CSS animation that is the Web Animations API, but we will not cover it in this tutorial.
3. Vanilla Javascript Animation
Let's regain some power moving to a Vanilla Javascript methodology.
So now instead of letting the browser calculate all the intermediate steps for us, we do ourselves in Javascript.
As you can see, the style in the DOM is updated at each frame, while in the two previous cases we were only changing classes:
But with great power comes great responsibility.
Our code is now more complicated. While certain things are easier now (e.g. pausing the animation), others are more difficult (e.g. "easing").
The animation would also be less smooth in most situations so, why bother? For simple animations like this one, doesn't make sense but for more complicated stuff it could be beneficial to have more control.
If you want to go down this road, using a library is probably better. There are plenty out there. In this tutorial I will examine two of them: elm-animator and elm-playground.
4. Vanilla Elm Animation
Before moving to the elm libraries, let's refresh our knowledge of Elm rewriting the previous Vanilla Javascript example in Elm.
If you are new to Elm you can refer to a short Elm overview that I wrote in my previous post or, even better, the official documentation.
These two implementations are quite similar. Actually I tried to write both of them in the same order so that it is possible to compare the code side by side. I will put the comparison in a separate post.
These are the main differences:
- HTML
- Elm: generated by the
view
function - Javascript: not a Javascript concern
- Elm: generated by the
- Subscription to
requestAnimationFrame
- Elm: handled by the Elm runtime using onAnimationFrameDelta
- Javascript: handled directly with requestAnimationFrame
- Main loop
- Elm: managed by The Elm Architecture
- Javascript: managed directly
- DOM modifications
- Elm: through the Virtual DOM (function
view
+changeStyle
) - Javascript: directly with the
style
property (functionchangeStyle
)
- Elm: through the Virtual DOM (function
- Calculation of the time passed since the previous frame (delta)
- Elm: calculated by the Elm runtime
- Javascript: calculated directly (function
calculateDelta
)
Now, using the Elm debugger, we can move back and forward in the animation timeline. The Model
, among other things, store exactly the new attributes of the element that is animated:
- Editable Demo (without Elm Debugger)
- Demo
- Code
5. elm-animator Inline Animation
elm-animator
requires some boilerplate but will handle most of the things that we were handling manually before.
Model and Init
Before we needed to handle many things ourselves. Now is the library that will take care of most of them:
Before
type alias Model =
{ currentState : State
, target : State
, animationLength : Float
, progress : Maybe Float
, animationStart : State
}
init =
{ currentState = onTheKaiten
, animationStart = onTheKaiten
, target = onTheKaiten
, animationLength = 0
, progress = Nothing
}
After
type alias Model =
{ currentState : Animator.Timeline State }
init =
{ currentState = Animator.init onTheKaiten }
Messages
elm-animator use absolute time instead of delta time:
Before
type Msg
= AnimationFrame Float
| ClickOnPage
After
type Msg
= AnimationFrame Time.Posix
| ClickOnPage
Subscription
Subscription is also handled by the library
Before
subscriptions model =
case model.progress of
Just _ ->
Browser.Events.onAnimationFrameDelta AnimationFrame
Nothing ->
Sub.none
After
subscriptions model =
animator
|> Animator.toSubscription AnimationFrame model
Function animationFrame
All this part is basically gone! This is what the library is doing for us
Before
animationFrame model delta =
case model.progress of
Just progress ->
if progress < model.animationLength then
let
animationRatio =
Basics.min 1 (progress / model.animationLength)
newX =
model.animationStart.x
+ (model.target.x - model.animationStart.x)
* animationRatio
newScale =
model.animationStart.scale
+ (model.target.scale - model.animationStart.scale)
* animationRatio
in
{ model
| progress = Just <| progress + delta
, currentState = { x = newX, scale = newScale }
}
else
{ model
| progress = Nothing
, currentState = model.target
}
Nothing ->
model
After
animationFrame model time =
Animator.update time animator model
Function clickOnPage
This became simpler because the Model
is simpler
Before
clickOnPage model =
if model.target == onTheKaiten then
{ model
| target = inTheKitchen
, animationStart = model.currentState
, animationLength = 1000
, progress = Just 0
}
else
{ model
| target = onTheKaiten
, animationStart = model.currentState
, animationLength = 1000
, progress = Just 0
}
After
clickOnPage model =
if Animator.current model.currentState == onTheKaiten then
{ model
| currentState =
Animator.go
(Animator.seconds 1)
inTheKitchen
model.currentState
}
else
{ model
| currentState =
Animator.go
(Animator.seconds 1)
onTheKaiten
model.currentState
}
Function changeStyle
Here we need to handle things a bit differently
Before
changeStyle { scale, x } =
[ style "transform" ("scale(" ++ String.fromFloat scale ++ ")")
, style "left" (String.fromFloat x ++ "px")
]
After
changeStyle state =
[ Animator.Inline.style
state
"left"
(\float -> String.fromFloat float ++ "px")
(\state_ ->
if state_ == inTheKitchen then
Animator.at inTheKitchen.x
else
Animator.at onTheKaiten.x
)
, Animator.Inline.scale
state
(\state_ ->
if state_ == inTheKitchen then
Animator.at inTheKitchen.scale
else
Animator.at onTheKaiten.scale
)
]
- Editable Demo (without Elm Debugger)
- Demo
- Code
We cleaned-up a lot of code!
The model is storing some internal data needed by the library to handle the animation.
We got the "easing" back by default and now we are ready to leverage the full potentiality of this library. Stay tuned for the part II of this tutorial.
Thanks to the Elm community in Slack for the support, specifically to @mgriffith for reviewing some examples and for putting so much effort in writing elm-animator
and to @dmy for fixing some bug.
Photograph: "Inside a Conveyor Belt Sushi Shop" by Alberto Carrasco Casado - CC BY 2.0.
Top comments (3)
I like the demo, and am interested in trying Elm out. One recommendation, performance is far better for transforms than using properties such as
left
. You could usetranslateX
instead ofleft
You are right,
translate
would be better. I think I mentioned somewhere thatopacity
andtransform
are usually better... and then I usedleft
;-)If you decide to try Elm, I suggest you to join Elm Slack, it is very beginner-friendly
That's so cute! I could really go for some kaitenzushi right about now. π£