Basic Framework for Elm HTML Animations

I’ve been dabbling a bit with the mdgriffith/elm-style-animation package for Elm 0.18 and so far it looks like quite a capable library. I did encounter some headwinds with layering multiple effects on the same HTML element at different times, so I scratched out a basic framework for extending animations. I’ll walk you through the basic extensions I’ve layered on top of the standard Elm Architecture.

What’s the goal of all this? Being able to define easily reusable animations, just like CSS classes! And being able to manage the complexity that comes along with creating many different animations.

Read on for details, or just grab the code from this Github repo and run with it.

First, some bookkeeping

From a blank directory, you can elm-make to generate the basic project structure. Then, you’ll need to elm-package install the following packages:

I use Visual Studio Code to manage all this stuff, as I’ve written previously.

Also, our imports:

import Animation
import Animation.Messenger
import Dict exposing (..)
import Ease exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Task exposing (..)

The standard Elm Architecture

Okay! Let’s walk through the setup from the standard Elm Architecture. Not much interesting going on here, but I’ll highlight a couple of things. This section is all meant to be cookie-cutter, that you can just copy and paste into your own project as a starting point.

The app entry point

Our app entry point isn’t anything special. I’ll circle back around to initializeApp later.

-- ENTRY POINT

main : Program Never Model Msg
main =
  Html.program
    { init = initializeApp
    , subscriptions = subscriptions
    , update = update
    , view = view
  }

The Model

Up next comes our Model:

-- MODEL

type alias AnimationId =
  String

type alias Model =
  { animations        : Dict AnimationId (Animation.Messenger.State Msg)
  , animationMappings : AnimationMappings
  }

type alias AnimationMappings =
  Dict AnimationId (Maybe (Animation.Messenger.State Msg) -> Animation.Messenger.State Msg)

AnimationId is a simple type alias to access an animation by name.

The basis of our Model consists of two things: The actual, current animations we have (animations), and a set of mappings for retrieving an animation by name (animationMappings). This is primarily a helper to manage complexity. It doesn’t strictly have to be in our Model, but I didn’t want to rebuild the dictionary every time I accessed it.

The type on our animations looks a little funny, but it’s just a dictionary to a set of animations in progress, hashed by their identifier.

AnimationMappings looks even funnier. Here’s what it is: A dictionary that, given an animation identifier, yields a function that takes a Maybe animation as input and yields a new animation as output. I’ll show you how we’re using this in a bit. Why this funny type? It just fell out naturally as I cleaned up and reduced code footprint. I promise, its usage is actually pretty clean, but I would never have thought to build it this way from the beginning! (This is an example of starting from a specific implementation, then building a cleaner abstraction from it.)

Then, we have a couple of helper functions:

initializeModel : Model
initializeModel =
  Model empty animationMappings

-- Return the animation, or if the animation hasn't been defined yet, use the provided default
animationOrDefault : Maybe (Animation.Messenger.State Msg) -> Animation.Messenger.State Msg -> Animation.Messenger.State Msg
animationOrDefault manim def =
  case manim of
    Nothing -> def
    Just a  -> a

Our initializeModel function isn’t anything special. It references a custom animationMappings definition that we’ll come back to in a bit.

The animationOrDefault function is just a simple helper I found myself using in each of my animation definitions. It simply takes a Maybe animation, returns the Just definition, or else a supplied default so that we always have a valid animation.

Subscriptions and View

-- VIEW

view : Model -> Html Msg
view =
  appView

-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg
subscriptions model =
  Animation.subscription UpdateAnimation <| values model.animations

Our view just redirects to a custom function we’ll supply later. I only wrote it this way to keep the basic view definition within the “cookie-cutter” part of the code.

Our subscriptions are responsible for communicating updates to our animations. Here, we simply take the values from our animations dictionary and feed them into the animation package’s Animation.subscription messenger for relaying updates. UpdateAnimation is just a regular Msg definition that we’re about to see.

Update

So far, so good! The only real complexity comes with our update part of the standard Elm Architecture. I’ll walk through it slowly.

First, our Msg type:

-- UPDATE

type Msg
  = Initialize
  -- Start a new animation
  | ExecuteAnimation AnimationId
  -- Update an "in progress" animation
  | UpdateAnimation Animation.Msg

Here, we just define a few types. Initialize, for when our app starts up. ExecuteAnimation starts a new animation, given an identifier for the animation. UpdateAnimation is for responding to animation update subscriptions, as we just saw used.

And now, for our actual update function:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Initialize ->
      ( model, Cmd.none )

    ExecuteAnimation animationId ->
      let
        _ = Debug.log ("ExecuteAnimation " ++ animationId) ()
        executeAnimation dict =
          case get animationId model.animationMappings of
            Just f  -> insert animationId (f <| get animationId dict) dict
            Nothing -> dict
      in
        ( { model | animations = executeAnimation model.animations }
        , Cmd.none
        )

    UpdateAnimation animMsg ->
      let
        f k v         =  Animation.Messenger.update animMsg v
                      |> (\(v, c) -> (c, (k, v)))
        (cmds, anims) =  Dict.toList model.animations
                      |> List.map (\(k, v) -> f k v)
                      |> List.unzip
      in
        ( { model | animations = Dict.fromList anims }
        , Cmd.batch cmds
        )

This is where things get a little more hairy. Thankfully, Initialize is nothing special, so we’ll move on.

ExecuteAnimation, at its root, just finds a known animation with the given identifier and adds it to our set of active animations – that’s what our model update is doing at the bottom. The executeAnimation helper function isn’t actually doing much: It just looks for the specified animation and adds it to the current dictionary, or else just returns the original dictionary. Once we have a valid mapping, this is where our funny AnimationMappings type comes in. We get the current animation with the same identifier and feed it into our selected mapping. Then, we take the result and plug it back into our dictionary of current animations.

Confused? It will make more sense once we see our individual animation definitions, but basically what it’s doing is this:

  • For clarity, we log the ExecuteAnimation message to the console, so that we know what’s been fired. You can remove this if you want.
  • We find the specified animation. If the specifier is unknown, we don’t update anything.
  • We add the new animation to our current animations dictionary, feeding the new animation our current (pre-existing) animation with the same identifier, if it already existed.

The reason why we’re feeding in our existing animations is so that we can interrupt any animations currently active. This is handled by the animation package’s internal details, but we still need to feed in the current state.

UpdateAnimation also looks a little hairy, but it’s really not bad:

  • We take our currently active animations; then
  • Feed each of them through the animation package’s Animations.Messenger.update function to generate updated animations, doing some reshuffling of the tuple result; then
  • Assigning the updated animations back into our model; and
  • Executing any lingering Cmd results that we were given.

The funny tuple shuffling is just to make it easier to unzip the resulting Cmd and animation updates into separate lists. Part of why it looks so funny is that we’re actually dealing with a dictionary of animations, remember? So, we have some k keys and v values floating around in there.

Custom stuff specific to your app

Surprisingly, once you wrap your head around it, there’s not much going on in the cookie-cutter section, but it turns out to be surprisingly powerful! From that reusable basis, you can focus on layering on the effects of your individual animations, without getting distracted by the details of the plumbing. Just what every developer wants!

I’ll run through the simple, custom app I built for the demo. Your code will be entirely different, but it will give you a taste for how I’m structuring things.

First, our custom appView that we referred to earlier:

appView : Model -> Html Msg
appView model =
  div
    (  (values model.animations |> List.map Animation.render |> List.concat)
    ++ [ onClick (ExecuteAnimation "fade_out_fade_in")
       , style
          [ ( "position", "relative" )
          , ( "margin", "100px auto" )
          , ( "padding", "25px" )
          , ( "width", "200px" )
          , ( "height", "200px" )
          , ( "background-color", "#268bd2" )
          , ( "color", "white" )
          ]
        ]
    )
    [ text "Click to Animate!" ]

It’s just a clickable box with some text, shamelessly stolen from the animation package’s original demo. There are only really two interesting things going on here: We’re telling all of our active animations to render on the element; and our onClick event is being told to fire an ExecuteAnimation by the identifier "fade_out_fade_in".

This gives us something to actually look at, other than a blank canvas. The only pieces we’re missing now are our app initialization and our actual animation definitions. First, the app initialization:

-- Initialize and fire off any "at startup" animations
initializeApp : (Model, Cmd Msg)
initializeApp =
  ( initializeModel
  , Task.perform (\identifier -> ExecuteAnimation identifier) (Task.succeed "fade_in")
  )

Not much going on here, except that we’re telling our app to fire off an animation as soon as it starts, by the identifier "fade_in". You can easily extend this to perform multiple animations as soon as the app starts.

That just leaves us with our animation definitions! First, we start with our known identifier-to-animation mappings:

-- Define function mappings for our different hashed animations
animationMappings : AnimationMappings
animationMappings =
  Dict.fromList
    [ ( "fade_in",          fadeIn )
    , ( "fade_out_fade_in", fadeOutFadeIn )
    ]

Pretty simple, we’re just building a dictionary of identifier keys that link to specific animation functions.

The "fade_in" animation is an example just about the simplest definition of animation you can find, with some extra layering:

-- Example of basic animation usage
fadeIn : Maybe (Animation.Messenger.State Msg) -> Animation.Messenger.State Msg
fadeIn manim =
  Animation.interrupt
    -- Animation to perform
    [ Animation.to [ Animation.opacity 1 ] ]
    -- Prior animation state, or a default
    (animationOrDefault manim (Animation.style [ Animation.opacity 0.0 ]))

We’re defining this in three parts:

  • We’re saying this is an animation that should interrupt any previously running animation.
  • We’re defining the new animation as a simple transition to full opacity.
  • We’re supplying a default static animation with full transparency to start from.

All of our animation function definitions will follow this 3-part format. The only really interesting part of the default animation is the actual definition; the rest just feeds back into the funny Maybe-typed function definitions we ran across earlier.

For a little more complexity, we’re going to customize the "fade_out_fade_in" animation:

-- Example of animation with custom duration and easing
fadeOutFadeIn : Maybe (Animation.Messenger.State Msg) -> Animation.Messenger.State Msg
fadeOutFadeIn manim =
  Animation.interrupt
    -- Animation to perform
    [ Animation.toWithEach [ (Animation.easing { duration = 500, ease = linear },  Animation.opacity 0) ]
    , Animation.toWithEach [ (Animation.easing { duration = 300, ease = outExpo }, Animation.opacity 1) ]
    ]
    -- Prior animation state, or a default
    (animationOrDefault manim (Animation.style [ Animation.opacity 1.0 ]))

This is just a demonstration of how to build a more complicated animation. Once again, our three parts:

  • We’re saying this is an animation that should interrupt any previously running animation.
  • We’re defining the new animation. This demonstrates using different easing methods at different durations to first fade out to full transparency, then back in to full opacity. Using Animation.toWithEach isn’t really necessary here (we could simply use Animation.toWith instead), but we can easily extend each of these lists with multiple other effects, all to be run at the same time.
  • We’re supplying a default static animation with full opacity to start from.

And that’s it! If you run the code, you’ll see the block fade in, then you can click it to see it fade out and back in again. If you click it rapidly, you can see how the animation interrupt works; the new animation will simply pick up from whatever the current state of the block happens to be.

Parting thoughts

As you build out more animations, all you need to do at this point is extend the animationMappings we saw at the end and define a new 3-part animation function.

Most of the abstraction complexity comes down to solving two goals:

  • Being able to easily find and execute an animation, given a specific identifier (primarily, to avoid having to traverse lists to find the animation).
  • Being able to just define an animation in a “fire and forget” way that doesn’t require interweaving definitions for the animation in different places. That’s the primary intent of the “default” definitions – putting all of our animation information in the same place.

If you try to naively thread these throughout your code, you’d be surprised how effects won’t run correctly because of how different animations might stomp on one another. Getting around this, you might run into trouble with not having a starting place to run an animation from (the animation package will actually log errors in this case).

I ran into this exact scenario when I was playing around with the animations, which led to iterating until I had built this little framework. The issue was that I wanted two different transparency animations on the same element: One to fire on app start and another to fire when I clicked on the box. With my first implementation attempt, the second animation was stomping on the first. A bit of a play later and this natural extension of the Elm Architecture was the result.

I hope you find this useful! And I’m always curious to hear other ways to solve this problem.

Cheers!

2 comments

  1. Pingback: Swapping Elm Animations | voyage in tech
  2. Pingback: Catching Completed Elm Animations | voyage in tech