elm-animator
is about taking pieces of your model, turning them into Timelines of values, and animate between their states
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 }
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!
Once we have a Timeline
, we need a way to update it.
That's the job of the Animator
!
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
.
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 —
Duration
for how long this transition should take.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.
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
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
]
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.
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.Css
and 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
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
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.
0
means we leave at the normal time.0.2
means we'll leave when the transition is at 20%.1
means we leave at the end of the transition and instantly flip to the new state at that time.arriveEarly : Basics.Float -> Movement -> Movement
We can also arrive early to this state.
0
means we arrive at the normal time.0.2
means we'll arrive early by 20% of the total duration.1
means we arrive at the start of the transition. So basically we instantly transition over.Weird math note — arriveEarly
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.
leaveSmoothly 0
is essentially linear animation.leaveSmoothly 1
means the animation will start slowly and smoothly begin to accelerate.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.
arriveSmoothly 0
means no smoothing, which means more of a linear animation.arriveSmoothly 1
means the animation will "ease out" or "arrive slowly".withWobble : Basics.Float -> Movement -> Movement
This will make the transition use a spring instead of bezier curves!
withWobble 0
- absolutely no wobblewithWobble 1
- all the wobbleUse your wobble responsibly.
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)
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.
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
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).
Internal.Timeline.Resting item
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.