mdgriffith / elm-animator / Animator

Getting started

elm-animator is about taking pieces of your model, turning them into Timelines of values, and animate between their states


type alias Timeline state =
Internal.Timeline.Timeline state

A timeline of state values.

Behind the scenes this is roughly a list of states and the times that they should occur!

init : state -> Timeline state

Create a timeline with an initial state.

So, if you previously had a Bool in your model:

type alias Model = { checked : Bool }

-- created via
{ checked = False }

You could replace that with an Animator.Timeline Bool

type alias Model = { checked : Animator.Timeline Bool }

-- created via
{ checked = Animator.init False }

Reading the timeline

You might be wondering, 'How do we get our value "out" of a Timeline?'

Well, we can ask the Timeline all sorts of questions.

current : Timeline state -> state

Get the current state of the timeline.

This value will switch to a new value when a transition begins.

If you had a timeline that went from A to B to C, here's what current would be at various points on the timeline.

          A---------B---------C
               ^    ^    ^    ^
current:       B    B    C    C

Note — If you want to detect the moment when you arrive at a new state, try using arrivedAt

previous : Timeline state -> state

Get the previous state on this timeline.

As you'll see in the Loading example, it means we can use previous to refer to data that we've already "deleted" or set to Nothing.

How cool!

          A---------B---------C
               ^    ^    ^
previous:      A    A    B

upcoming : state -> Timeline state -> Basics.Bool

Check to see if a state is upcoming on a timeline.

Note — This can be used to ensure a set of states can only be queued if they aren't already running.

Note 2 — This only checks if an event is in the future, but does not check the value you're currently at. You might need to use arrived as well if you also care about the current state.

upcomingWith : (state -> Basics.Bool) -> Timeline state -> Basics.Bool

For complicated values it can be computationally expensive to use ==.

upcomingWith allows you to specify your own equality function, so you can be smarter in checking how two value are equal.

arrived : Timeline state -> state

Subtley different than current, this will provide the new state as soon as the transition has finished.

          A---------B---------C
               ^    ^    ^    ^
arrived:       A    B    B    C

arrivedAt : state -> Time.Posix -> Timeline state -> Basics.Bool

Sometimes we want to know when we've arrived at a state so we can trigger some other work.

You can use arrivedAt in the Tick branch of your update to see if you will arrive at an event on this tick.

Tick time ->
    if Animator.arrivedAt MyState time model.timeline then
        --...do something special

arrivedAtWith : (state -> Basics.Bool) -> Time.Posix -> Timeline state -> Basics.Bool

Again, sometimes you'll want to supply your own equality function!

Wiring up the animation

Once we have a Timeline, we need a way to update it.

That's the job of the Animator!


type alias Animator model =
Internal.Timeline.Animator model

An Animator knows how to read and write all the Timelines within your Model.

Here's an animator from the Checkbox.elm example,

animator : Animator.Animator Model
animator =
    Animator.animator
        |> Animator.watching
            -- we tell the animator how
            -- to get the checked timeline using .checked
            .checked
            -- and we tell the animator how
            -- to update that timeline as well
            (\newChecked model ->
                { model | checked = newChecked }
            )

Notice you could add any number of timelines to this animator.

Note — You likely only need one animator for a given project.

Note 2 — Once we have an Animator Model, we have two more steps in order to set things up:

animator : Animator model

watching : (model -> Timeline state) -> (Timeline state -> model -> model) -> Animator model -> Animator model

watching will ensure that AnimationFrame is running when the animator is transformed into a subscription.

Note — It will actually make the animation frame subscription run all the time! At some point you'll probably want to optimize when the subscription runs, which means either using watchingWith or Animator.Css.watching.

watchingWith : (model -> Timeline state) -> (Timeline state -> model -> model) -> (state -> Basics.Bool) -> Animator model -> Animator model

watchingWith will allow you to have more control over when AnimationFrame runs.

The main thing you need to do here is capture which states are animated when they're resting.

Let's say we have a checkbox that, for whatever reason, we want to say is spinning forever when the value is False.

animator : Animator.Animator Model
animator =
    Animator.animator
        |> Animator.watchingWith .checked
            (\newChecked model ->
                { model | checked = newChecked }
            )
            -- here is where we tell the animator that we still need
            -- AnimationFrame when the timeline has a current value of `False`
            (\checked ->
                checked == False
            )

Note — if you're using Animator.Css to generate keyframes along with Animator.Css.watching, you don't need to worry about this.

toSubscription : (Time.Posix -> msg) -> model -> Animator model -> Platform.Sub.Sub msg

Convert an Animator to a subscription.

This is where the animator will decide if a running animation needs another frame or not.

subscriptions model =
    Animator.toSubscription Tick model animator

update : Time.Posix -> Animator model -> model -> model

When new messages come in, we then need to update our model. This looks something like this:

type Msg
    = Tick Time.Posix

update msg model =
    case msg of
        Tick newTime ->
            ( Animator.update newTime animator model
            , Cmd.none
            )

And voilà, we can begin animating!

Note — To animate more things, all you need to do is add a new with to your Animator.

updateTimeline : Time.Posix -> Timeline state -> Timeline state

If you're creating something like a game, you might want to update your Timelines manually instead of using an Animator.

This will allow you to do whatever calculations you need while updating each Timeline.

Note — You'll have to take care of subscribing to Browser.Events.onAnimationFrame.

Transitioning to a new state

Now that we have a Timeline set up, we likely want to set a new value.

In order to do that we need to specify both —

go : Duration -> state -> Timeline state -> Timeline state

Go to a new state!

You'll need to specify a Duration as well. Try starting with Animator.quickly and adjust up or down as necessary.


type alias Duration =
Internal.Time.Duration

Choosing a nice duration can depend on:

So, start with a nice default and adjust it as you start to understand your specific needs.

Note — Here's a very good overview on animation durations and speeds.

immediately : Duration

0ms

veryQuickly : Duration

100ms.

quickly : Duration

200ms - Likely a good place to start!

slowly : Duration

400ms.

verySlowly : Duration

500ms.

millis : Basics.Float -> Duration

seconds : Basics.Float -> Duration

Interruptions and Queueing

In some more advanced cases you might want to define a series of states to animate through instead of just going to one directly.

Animator.interrupt
    [ Animator.wait (Animator.millis 300)

    -- after waiting 300 milliseconds,
    -- start transitioning to a new state, Griffyndor
    -- Take 1 whole second to make the transition
    , Animator.event (Animator.seconds 1) Griffyndor

    -- Once we've arrived at Griffyndor,
    -- immediately start transitioning to Slytherin
    -- and take half a second to make the transition
    , Animator.event (Animator.seconds 0.5) Slytherin
    ]


type Step state

wait : Duration -> Step state

event : Duration -> state -> Step state

interrupt : List (Step state) -> Timeline state -> Timeline state

Interrupt what's currently happening with a new list.

queue : List (Step state) -> Timeline state -> Timeline state

Wait until the current timeline is finished and then continue with these new steps.

Animating

Finally, animating!

This part of the package is for animating color and number values directly.

However! You're probably more interested in animating CSS or Inline styles.

Those things live in the Animator.Cssand Animator.Inline modules.

Check them out on the side bar 👉

Though you should also check out the 👇 Transition Personality section as well.

color : Timeline state -> (state -> Color) -> Color


type alias Movement =
Internal.Interpolate.DefaultableMovement

at : Basics.Float -> Movement

move : Timeline state -> (state -> Movement) -> Basics.Float

xy : Timeline state -> (state -> { x : Movement, y : Movement }) -> { x : Basics.Float, y : Basics.Float }

xyz : Timeline state -> (state -> { x : Movement, y : Movement, z : Movement }) -> { x : Basics.Float, y : Basics.Float, z : Basics.Float }

linear : Timeline state -> (state -> Movement) -> Basics.Float

Interpolate linearly between destinations. This is a shortcut to help you out.

You can do this with move by doing

Animator.move timeline <|
    \state ->
        if state then
            Animator.at 0
                |> Animator.leaveSmoothly 0
                |> Animator.arriveSmoothly 0

        else
            Animator.at 1
                |> Animator.leaveSmoothly 0
                |> Animator.arriveSmoothly 0

Which is equivalent to

Animator.linear timeline <|
    \state ->
        if state then
            Animator.at 0

        else
            Animator.at 1

Transition personality

While there are some nice defaults baked in, sometimes you might want to adjust how an animation happens.

These adjustments talk about arriving or leaving. That's referring to the part of the animation that is arriving to or departing from a certain state.

So, for this code example:

 case state of
    True ->
        Animator.at 0
            |> Animator.leaveLate 0.2

    False ->
       Animator.at 50

If we're at a state of True and go to any other state, we're going to leave True a little later than the normal time.

Note — These adjustments all take a Float between 0 and 1. Behind the scenes they will be clamped at those values.

leaveLate : Basics.Float -> Movement -> Movement

Even though the transition officially starts at a certain time on the timeline, we can leave a little late.

arriveEarly : Basics.Float -> Movement -> Movement

We can also arrive early to this state.

Weird math notearriveEarly and leaveLate will collaborate to figure out how the transition happens. If arriveEarly and leaveLate sum up to more 1 for a transition, then their sum will be the new maximum. Likely you don't need to worry about this :D.

The intended use for arriveEarly and leaveLate is for staggering items in a list. In those cases, these values are pretty small ~0.1.

leaveSmoothly : Basics.Float -> Movement -> Movement

Underneath the hood this library uses Bézier curves to model motion.

Because of this you can adjust the "smoothness" of the curve that's ultimately used.

Here's a general diagram of what's going on:

Note — The values in the above diagram are the built in defaults for most movements in elm-animator. They come from Material Design.

Note 2 — An interactive version of the above diagram is also available.

arriveSmoothly : Basics.Float -> Movement -> Movement

We can also smooth out our arrival.

withWobble : Basics.Float -> Movement -> Movement

This will make the transition use a spring instead of bezier curves!

Use your wobble responsibly.

Resting at a state

We've mostly talked about transitioning from one state to another, like moving from True to False.

But what if we want an animation when we're just resting at a state?

An obvious example would be an icon that spins when we're Loading.

Well, in that case you can use an Oscillator.

case state of
    Loaded ->
        Animator.at 0

    Loading ->
        -- animate from 0deg to 360deg and
        -- then wrap back around to 0deg
        -- we're using radians here, so 2 * pi == 360deg
        Animator.wrap 0 (2 * pi)
            -- loop every 700ms
            |> Animator.loop (Animator.millis 700)


type alias Oscillator =
Internal.Timeline.Oscillator

wave : Basics.Float -> Basics.Float -> Oscillator

This is basically a sine wave! It will "wave" between the two numbers you give it.

wrap : Basics.Float -> Basics.Float -> Oscillator

Start at one number and move linearly to another, then immediately start again at the first.

This was originally intended for animating rotation where you'd want 360deg to "wrap" to 0deg.

zigzag : Basics.Float -> Basics.Float -> Oscillator

Start at one number, move linearly to another, and then linearly back.

loop : Duration -> Oscillator -> Movement

once : Duration -> Oscillator -> Movement

repeat : Basics.Int -> Duration -> Oscillator -> Movement

shift : Basics.Float -> Oscillator -> Oscillator

Shift an oscillator over by a certain amount.

It's expecting a number between 0 and 1.

Sprites

Ok! What else could there be?

What about the wonderful world of Sprite animation?

Sprite animation is where we literally have a list of images and flip through them like a flip-book.

Like Mario! In fact we have a Mario example!

Here's an abreviated example of what the code looks like:

Animator.step model.mario <|
    \(Mario action) ->
        case action of
            Walking ->
                -- if we're in a `Walking` state,
                -- then we're cycling through
                -- the following frames at
                -- 15 frames per second:
                --  step1, step2, stand
                Animator.framesWith
                    { transition =
                        sprite.tail.stand
                    , resting =
                        Animator.cycle
                            (Animator.fps 15)
                            [ sprite.tail.step1
                            , sprite.tail.step2
                            , sprite.tail.stand
                            ]
                    }

            Jumping ->
                -- show a single frame
                sprite.tail.jump

            Ducking ->
                sprite.tail.duck

            Standing ->
                sprite.tail.stand

step : Timeline state -> (state -> Frames sprite) -> sprite


type alias Frames item =
Internal.Timeline.Frames item

frame : sprite -> Frames sprite

Show a single sprite.

hold : Basics.Int -> sprite -> Frames sprite

Show this sprite for a number of frames. Only really useful if you're using walk or cycle.

walk : sprite -> List (Frames sprite) -> Frames sprite

Walk through a list of frames as we're transitioning to this state.

framesWith : { transition : Frames item, resting : Resting item } -> Frames item

Here we have the same distinction of transition and resting that the rest of the library has.

With framesWith we can define the frames it takes to transition to this state, as well as what to do when we're in this state (maybe we want to loop through a number of frames when we're in this state).


type alias Resting item =
Internal.Timeline.Resting item


type FramesPerSecond

fps : Basics.Float -> FramesPerSecond

cycle : FramesPerSecond -> List (Frames sprite) -> Resting sprite

While we're at this specific state, cycle through a list of frames at this fps.

cycleN : Basics.Int -> FramesPerSecond -> List (Frames sprite) -> Resting sprite

Same as cycle, but only for n number of times.