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.