mdgriffith / elm-animator

A timeline-based animation engine for Elm

Home Page:https://package.elm-lang.org/packages/mdgriffith/elm-animator/latest/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

move returns values outside the bounds of current state

jerith666 opened this issue · comments

Sometimes move will return a value that's outside the bounds of what's been passed to go to update the model. This is illustrated in this ellie; code reproduced below for reference.

See #19 for discussion of the general structure of this code. There's a new column now labeled "move". Sometimes the value in the "move" column will be larger than the "real" value, which should be impossible. You have to build with --debug and step through frame by frame to actually be able to see this. When values like this are being used to drive a visual animation, though, it manifests as a visible "bounce".

module Main exposing (..)

import Animator exposing (Animator, Timeline, arrived, at, current, go, move, previous, toSubscription, veryQuickly, watchingWith)
import Browser exposing (Document, element)
import Delay exposing (TimeUnit(..), after)
import Html exposing (Html, div, text)
import Html.Attributes exposing (style)
import Random exposing (Seed, float, initialSeed, step)
import String exposing (fromFloat, fromInt)
import Time exposing (Posix)


type alias AnimatedModel =
    { real : Int
    , timeline : Timeline Int
    , seed : Seed
    }


type AnimationMsg
    = Tick Posix
    | IncrementSomething


main : Program () AnimatedModel AnimationMsg
main =
    element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


delayMillis =
    100


fuzz =
    50


limit =
    8


init : () -> ( AnimatedModel, Cmd AnimationMsg )
init () =
    ( { real = 0
      , timeline = Animator.init 0
      , seed = initialSeed 1
      }
    , after delayMillis Millisecond IncrementSomething
    )


update : AnimationMsg -> AnimatedModel -> ( AnimatedModel, Cmd AnimationMsg )
update msg model =
    case msg of
        Tick posix ->
            ( Animator.update posix animator model, Cmd.none )

        IncrementSomething ->
            let
                newModel =
                    model.real + 1

                ( fuzzedDelay, newSeed ) =
                    step (float (delayMillis - fuzz) (delayMillis + fuzz)) model.seed

                maybeKeepGoing =
                    if continue newModel then
                        after fuzzedDelay Millisecond IncrementSomething

                    else
                        Cmd.none
            in
            ( { model
                | real = newModel
                , timeline = go veryQuickly newModel model.timeline
                , seed = newSeed
              }
            , maybeKeepGoing
            )


continue i =
    i <= limit


view : AnimatedModel -> Html AnimationMsg
view model =
    div
        [ style "display" "flex"
        , style "flex-direction" "row"
        ]
        [ viewImpl "real" <| toFloat model.real
        , viewImpl "previous" <| toFloat <| previous model.timeline
        , viewImpl "current" <| toFloat <| current model.timeline
        , viewImpl "arrived" <| toFloat <| arrived model.timeline
        , viewImpl "move" <| move model.timeline (toFloat >> at)
        ]


viewImpl : String -> Float -> Html msg
viewImpl label i =
    div [ style "margin" "10px" ]
        [ div [] [ text label ]
        , div [] [ text <| fromFloat i ]
        ]


animator : Animator AnimatedModel
animator =
    let
        updateTimeline nt m =
            { m | timeline = nt }
    in
    Animator.animator
        |> watchingWith .timeline
            updateTimeline
            continue


subscriptions : AnimatedModel -> Sub AnimationMsg
subscriptions model =
    toSubscription Tick model animator

Your Ellie is broken: "The Delay module does not expose TimeUnit"

Weird -- elm-delay 3.0.0 does expose it: https://package.elm-lang.org/packages/andrewMacmurray/elm-delay/3.0.0/Delay#TimeUnit. 4.0.0 removed it (andrewMacmurray/elm-delay#15).

The Ellie claims it's still at 3.0.0, but it seems like it's not really.

Looks like it's a known issue with Ellie: ellie-app/ellie#121 (see also ellie-app/ellie#57). I'll see about updating to the latest though.

Here's a new Ellie that reproduces it with elm-delay 4.0.0: https://ellie-app.com/g24Z3wVcPVqa1. It's non-deterministic. Here's the code:

module Main exposing (..)

import Animator exposing (Animator, Timeline, arrived, at, current, go, move, previous, toSubscription, veryQuickly, watchingWith)
import Browser exposing (Document, element)
import Delay exposing (after)
import Html exposing (Html, div, text)
import Html.Attributes exposing (style)
import Random exposing (Seed, initialSeed, int, step)
import String exposing (fromFloat)
import Time exposing (Posix)


type alias AnimatedModel =
    { real : Int
    , timeline : Timeline Int
    , arrivedEverMoved : Bool
    , arrivedWasBad : Bool
    , moveWasBad : Bool
    , seed : Seed
    }


type AnimationMsg
    = Tick Posix
    | IncrementSomething


main : Program () AnimatedModel AnimationMsg
main =
    element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


delayMillis =
    100


fuzz =
    50


limit =
    8


init : () -> ( AnimatedModel, Cmd AnimationMsg )
init () =
    ( { real = 0
      , timeline = Animator.init 0
      , arrivedEverMoved = False
      , arrivedWasBad = False
      , moveWasBad = False
      , seed = initialSeed 4
      }
    , after delayMillis IncrementSomething
    )


update : AnimationMsg -> AnimatedModel -> ( AnimatedModel, Cmd AnimationMsg )
update msg model =
    case msg of
        Tick posix ->
            ( updateEverWas <| Animator.update posix animator model, Cmd.none )

        IncrementSomething ->
            let
                newModel =
                    model.real + 1

                ( fuzzedDelay, newSeed ) =
                    step (int (delayMillis - fuzz) (delayMillis + fuzz)) model.seed

                maybeKeepGoing =
                    if continue newModel then
                        after fuzzedDelay IncrementSomething

                    else
                        Cmd.none

                t =
                    go veryQuickly newModel model.timeline

                m =
                    { model
                        | real = newModel
                        , timeline = t
                        , seed = newSeed
                    }
            in
            ( updateEverWas m
            , maybeKeepGoing
            )


updateEverWas : AnimatedModel -> AnimatedModel
updateEverWas model =
    let
        m =
            { model
                | arrivedEverMoved = model.arrivedEverMoved || (arrived model.timeline > 0)
            }

        mWB =
            m.moveWasBad || moveIsBad m

        aWB =
            m.arrivedWasBad || arrivedIsBad m
    in
    { m
        | moveWasBad = mWB
        , arrivedWasBad = aWB
    }


continue i =
    i <= limit


view : AnimatedModel -> Html AnimationMsg
view model =
    div
        [ style "display" "flex"
        , style "flex-direction" "row"
        ]
        [ viewImpl "real" False False <| toFloat model.real
        , viewImpl "previous" False False <| toFloat <| previous model.timeline
        , viewImpl "current" False False <| toFloat <| current model.timeline
        , viewImpl "arrived" (arrivedIsBad model) model.arrivedWasBad <| toFloat <| arrived model.timeline
        , viewImpl "move" (moveIsBad model) model.moveWasBad <| move model.timeline (toFloat >> at)
        ]


arrivedIsBad : AnimatedModel -> Bool
arrivedIsBad model =
    case model.arrivedEverMoved of
        False ->
            False

        True ->
            case model.real of
                0 ->
                    False

                _ ->
                    case arrived model.timeline of
                        0 ->
                            True

                        _ ->
                            False


moveIsBad : AnimatedModel -> Bool
moveIsBad model =
    let
        moveVal =
            move model.timeline (toFloat >> at)
    in
    toFloat model.real < moveVal


viewImpl : String -> Bool -> Bool -> Float -> Html msg
viewImpl label isBad wasBad i =
    let
        c =
            case isBad of
                True ->
                    "red"

                False ->
                    case wasBad of
                        True ->
                            "darksalmon"

                        False ->
                            "black"
    in
    div [ style "margin" "10px" ]
        [ div [ style "color" c ] [ text label ]
        , div [] [ text <| fromFloat i ]
        ]


animator : Animator AnimatedModel
animator =
    let
        updateTimeline nt m =
            { m | timeline = nt }
    in
    Animator.animator
        |> watchingWith .timeline
            updateTimeline
            continue


subscriptions : AnimatedModel -> Sub AnimationMsg
subscriptions model =
    toSubscription Tick model animator

I'm having the same issue, where the ranges should be constrained to 0.0 and 1.0 at the bounds, but it goes above that when calling this:

valueProgress : Float
valueProgress =
    Animator.move timeline <|
        \state ->
            case state of
                Idle ->
                    Animator.at 0

                Active ->
                    Animator.at 1.0

                Inplace ->
                    Animator.at 0.5

It appears to depend on the duration of the animation itself, where if its short enough, it'll stick inside the bounds, but otherwise it'll go out, as high as 1.3.