z5h / timeline / Timeline

Animating

As seen, given a timeline of your model, you can get the current value via:

Timeline.value timeline

You can also ask something like:

 Timeline.transition 500 timeline

this says: suppose transitions between different states take 500ms. Where am I at? The answer is of type Status which is either:

  1. At t int: there is no current transition, we have been at this value for int milliseconds
  2. Transitioning t t float: we are transitioning from an old t to a new one, and float goes from 1.0 -> 0.0 as we approach

Now that you know if you are statically at a state, or transitioning to that state from a previous one, you can use the transition factor to do animations.

Important types


type alias Timeline t =
{ current : Event t
, history : List (Event t)
, limit : Basics.Int
, now : Time.Posix
, start : Time.Posix 
}

A timeline of your model's history. History is only held long enough to ensure smooth transitions, based on the maximum transition duration value supplied to init. You never need to create these directly.


type Msg m

A timeline Msg.


type Status t
    = At t Basics.Int
    | Transitioning t t Basics.Float

A transition status. Either At a value for a specific number of milliseconds, or Transitioning from one value to another, with a "remaining" float value.

Note that during a transition, the float value goes from 1.0 -> 0.0. The "remaining" float value returned is not linear, but a dampened-spring eased value. Use it directly (with a multiplier) to fade, scale, translate with a natural and pleasant effect.

Integration helpers

init : Basics.Int -> Time.Posix -> ( model, Platform.Cmd.Cmd msg ) -> ( Timeline model, Platform.Cmd.Cmd (Msg msg) )

Converts the output of your existing init to a Timeline compatible init.

limit =
    500

now =
    Time.millisToPosix 0

timelineInit =
    \flags -> myInit flags |> Timeline.init limit now

The limit parameter is the duration your longest animation (transition) will take. This helps Timeline know when to throw away history it no longer needs. When you do timeline |> Timeline.transition 500 you are creating a 500ms transition. If that's the longest transition in your app, then limit in init should be 500.

The now parameter will become the timestamp for the initial model state. It's perfectly safe to set this to any time in history (e.g. Time.millisToPosix 0). The only caveat is if you later inquire how long a state has been at rest, you won't get a correct answer for your initial state. This probably doesn't matter in most use cases. But seeing as you can animate a static state based on it's age, this is important to know.

update : (msg -> model -> ( model, Platform.Cmd.Cmd msg )) -> Msg msg -> Timeline model -> ( Timeline model, Platform.Cmd.Cmd (Msg msg) )

Converts your existing update to a Timeline compatible update.

See Basic Setup.

subscriptions : (model -> Platform.Sub.Sub msg) -> Timeline model -> Platform.Sub.Sub (Msg msg)

Converts your existing subscriptions to a Timeline compatible subscriptions.

See Basic Setup.

view : (Timeline model -> Html msg) -> Timeline model -> Html (Msg msg)

Convenience function.

Converts your view : Timeline model -> Html msg to a Timeline model -> Html (Timeline.Msg msg) for use in your main.

viewDocument : (Timeline model -> Browser.Document msg) -> Timeline model -> Browser.Document (Msg msg)

Convenience function.

Converts your view : Timeline model -> Browser.Document msg to a Timeline model -> Browser.Document (Timeline.Msg msg) for use in your main.

msg : m -> Msg m

Maps a msg to a Timeline.Msg msg.

Useful in your main when defining things such as:

onUrlChange =
    Timeline.msg << OnUrlChange

Rendering and animating during view

value : Timeline t -> t

Extract the current (most recent) value from the timeline.

transition : Basics.Int -> Timeline t -> Status t

Given a duration for how long a transition on a timeline takes, what's the status of our timeline? Are we transitioning between values, or statically at a value?

let
hamburgerMenuStatus =
    modelTimeline
    |> Timeline.map .hamburgerMenuState
    |> transition 300
in
    case hamburgerMenuState of
        At state t ->
            -- our menu is fully open/closed, and has been for `t` ms

        Transitioning from to remaining ->
            -- our menu is transitioning.
            -- `remaining` goes `1.0 -> 0.0` as `from -> to`

The remaining float value returned is not linear, but a dampened-spring eased value. Use it directly (with a multiplier) to fade, scale, translate with a natural and pleasant effect.

map : (t -> s) -> Timeline t -> Timeline s

Maps a timeline. Very importantly the new timeline can have a different change history.

If

type alias Model =
    { a : Int, b : Int }

then

modelTimeline |> Timeline.map .a

returns a timeline of the history of a irrespective of b.

modelTimeline |> Timeline.map .b

returns a timeline of the history of b irrespective of a.

One can thus animate transitions of a and b (or any other model properties) independently.

withDefault : t -> Timeline (Maybe t) -> Timeline t

This is for treating non-continuous Timelines as continuous. Usually occurs when mapping your model (and hence Timeline) can result in unwanted maybes.

e.g.

type alias PageAModel =
    { a : Bool }

type alias PageBModel =
    { b : Bool }

type Model
    = PageA PageAModel
    | PageB PageBModel

Here, the state of the page models are not continuous. They don't always exist. Elm's type system will remind us that we cannot animate a thing that might not exist.

To animate PageA and PageB views, the following is required:

  1. have functions to extract Maybe Page?Model values from Model
  2. create a timeline of Maybe values and use withDefault

e.g.

pageAModel : Model -> Maybe PageAModel
pageAModel model =
    case model of
        PageA pageAModel_ ->
            Just pageAModel_

        _ ->
            Nothing


pageBModel : Model -> Maybe PageBModel
pageBModel model =
    ...

view : Timeline Model -> Html Msg
view timeline =
    let
        model =
            Timeline.value timeline
    in
    case model of
        PageA currentPageAModel ->
            let
                continuousPageATimeline : Timeline PageAModel
                continuousPageATimeline =
                    timeline
                        |> Timeline.map pageAModel
                        |> Timeline.withDefault currentPageAModel
            in
                renderPageA continuousPageATimeline

        PageB currentPageBModel ->
            ...

sequence : (a -> id) -> Timeline (List a) -> List (Timeline (Maybe a))

Turns a Timeline (List a) to a List (Timeline (Maybe a)).

When we look at a changing list, typically that list is changing because values are being inserted or removed or modified. So, if we are rendering a list into a list-like view, we want to know what the Timeline is for each slot. That's what this is for.

Because a slot (in this visualisation) might start as empty and have something inserted, or vice versa, we return a Timeline of Maybe a.

The initial parameter is an id function which allows us to track an entry's index in the list as it's value changes over time.

Utility

currentTime : Timeline t -> Time.Posix

Get the current time from a timeline. Updated internally via Browser.Events.onAnimationFrame, thus inheriting its resolution.

Having this prevents the need from tracking a high resolution time value in your model, because doing such a thing would prohibit use of Timeline due to GC issues.

Other

push : Event t -> Timeline t -> Timeline t

Exposed for testing. You don't need this.