DEV Community

Cover image for 🍣 Kaiten Sushi 🍣 Approaches to Web Animations
lucamug
lucamug

Posted on

🍣 Kaiten Sushi 🍣 Approaches to Web Animations

In this tutorial we will analyze several techniques to implement a simple animation of a sushi plate moving on a conveyor.

Alt Text

We will start from CSS, go through Javascript and arrive at Elm.

Part I (this one)

  1. CSS Transition
  2. CSS Animation
  3. Vanilla Javascript Animation
  4. Vanilla Elm Animation
  5. elm-animator Inline Animation

Part II (under development)

  1. elm-animator Inline Animation II
  2. elm-animator Hybrid Animation
  3. elm-playground SVG Animation
  4. elm-playground-3d SVG Animation
  5. 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:

Alt Text

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

To control the animation we use a similar system as before, toggling a class in the DOM:

Alt Text

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:

Alt Text

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
  • Subscription to requestAnimationFrame
  • Main loop
  • DOM modifications
    • Elm: through the Virtual DOM (function view + changeStyle)
    • Javascript: directly with the style property (function changeStyle)
  • 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:

Alt Text

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
    }
Enter fullscreen mode Exit fullscreen mode

After

type alias Model =
    { currentState : Animator.Timeline State }

init =
    { currentState = Animator.init onTheKaiten }
Enter fullscreen mode Exit fullscreen mode

Messages

elm-animator use absolute time instead of delta time:

Before

type Msg
    = AnimationFrame Float
    | ClickOnPage
Enter fullscreen mode Exit fullscreen mode

After

type Msg
    = AnimationFrame Time.Posix
    | ClickOnPage
Enter fullscreen mode Exit fullscreen mode

Subscription

Subscription is also handled by the library

Before

subscriptions model =
    case model.progress of
        Just _ ->
            Browser.Events.onAnimationFrameDelta AnimationFrame

        Nothing ->
            Sub.none
Enter fullscreen mode Exit fullscreen mode

After

subscriptions model =
    animator
        |> Animator.toSubscription AnimationFrame model
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After

animationFrame model time =
    Animator.update time animator model
Enter fullscreen mode Exit fullscreen mode

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
        }
Enter fullscreen mode Exit fullscreen mode

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
        }
Enter fullscreen mode Exit fullscreen mode

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")
    ]
Enter fullscreen mode Exit fullscreen mode

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
        )
    ]
Enter fullscreen mode Exit fullscreen mode

We cleaned-up a lot of code!

The model is storing some internal data needed by the library to handle the animation.

Alt Text

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)

Collapse
 
robole profile image
Rob OLeary

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 use translateX instead of left

Collapse
 
lucamug profile image
lucamug

You are right, translate would be better. I think I mentioned somewhere that opacity and transform are usually better... and then I used left ;-)

If you decide to try Elm, I suggest you to join Elm Slack, it is very beginner-friendly

Collapse
 
emma profile image
Emma Goto 🍙

That's so cute! I could really go for some kaitenzushi right about now. 🍣