Mid-Flight Interruptions¶
When an animation is running on an element and you trigger another animation on the same element, the result depends on the engine and the properties involved.
Engine Summary¶
| Engine | Same Property | Multiple Properties |
|---|---|---|
| Transition | ✅ Redirects from current position | ✅ Run side by side |
| Keyframe | ❌ Jumps to new animation start | ❌ Replaces all properties |
| Sub | ✅ Redirects from current position | ✅ Run side by side |
| WAAPI | ✅ Redirects from current position | ✅ Run side by side |
Single Property¶
Scenario: A property is animating on an element, and you trigger another animation for the same property on the same element.
The following example uses a CustomColor property (BackgroundColor) to demonstrate the behaviour for each Engine. Click a button to change the background color of the box; the color change will take 3 seconds, click another color button before the change is complete to redirect to a new color.
View Examples
✅ Behaviour: Smooth redirect from current mid-flight value to new end target value
❌ Behaviour: The new @keyframes rules for the animation replace the existing rules.
📖 See: Keyframe Engine — Interrupting Animations for details.
✅ Behaviour: Smooth redirect from current mid-flight value to new end target value
✅ Behaviour: Smooth redirect from current mid-flight value to new end target value
View Source Code
module Animation.Keyframe.InterruptingAnimations.SingleProperty.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForKeyframe)
import Anim.Engine.Keyframe as Keyframe
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = always Sub.none
}
-- MODEL
animGroupName : String
animGroupName =
"movingBox"
type alias Model =
{ animState : Keyframe.AnimState
}
init : ( Model, Cmd Msg )
init =
( { animState =
Keyframe.init
[ CustomColor.init animGroupName CustomColor.BackgroundColor <|
Color.rgb 118 118 118
]
}
, Cmd.none
)
-- ANIMATIONS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
toColor1 : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
toColor1 =
colorBox (CustomColor.to color1)
toColor2 : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
toColor2 =
colorBox (CustomColor.to color2)
toColor3 : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
toColor3 =
colorBox (CustomColor.to color3)
toColor4 : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
toColor4 =
colorBox (CustomColor.to color4)
colorBox : (CustomColor.Builder ForKeyframe -> CustomColor.Builder ForKeyframe) -> (Keyframe.EngineBuilder -> Keyframe.EngineBuilder)
colorBox moveFunc =
CustomColor.begin CustomColor.BackgroundColor
>> moveFunc
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate Keyframe.AnimMsg
| Color1
| Color2
| Color3
| Color4
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Keyframe.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
Color1 ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> toColor1
}
, Cmd.none
)
Color2 ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> toColor2
}
, Cmd.none
)
Color3 ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> toColor3
}
, Cmd.none
)
Color4 ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> toColor4
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" <|
Color.toHex bgColor
]
[ text label ]
color1Button =
button color1 "Color 1" Color1
color2Button =
button color2 "Color 2" Color2
color3Button =
button color3 "Color 3" Color3
color4Button =
button color4 "Color 4" Color4
in
div
[ class "example-stage"
, style "text-align" "center"
]
[ Keyframe.styleNode model.animState
, div [ class "example-controls" ]
[ color1Button
, color2Button
, color3Button
, color4Button
]
, div
(Keyframe.attributes animGroupName model.animState
++ [ class "example-canvas" ]
)
[]
]
module Animation.Transition.InterruptingAnimations.SingleProperty.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForTransition)
import Anim.Engine.Transition as Transition
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = always Sub.none
}
-- MODEL
animGroupName : String
animGroupName =
"movingBox"
type alias Model =
{ animState : Transition.AnimState }
init : ( Model, Cmd Msg )
init =
( { animState =
Transition.init
[ CustomColor.init animGroupName CustomColor.BackgroundColor <|
Color.rgb 118 118 118
]
}
, Cmd.none
)
-- ANIMATIONS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
toColor1 : Transition.EngineBuilder -> Transition.EngineBuilder
toColor1 =
colorBox (CustomColor.to color1)
toColor2 : Transition.EngineBuilder -> Transition.EngineBuilder
toColor2 =
colorBox (CustomColor.to color2)
toColor3 : Transition.EngineBuilder -> Transition.EngineBuilder
toColor3 =
colorBox (CustomColor.to color3)
toColor4 : Transition.EngineBuilder -> Transition.EngineBuilder
toColor4 =
colorBox (CustomColor.to color4)
colorBox : (CustomColor.Builder ForTransition -> CustomColor.Builder ForTransition) -> Transition.EngineBuilder -> Transition.EngineBuilder
colorBox moveFunc =
CustomColor.begin CustomColor.BackgroundColor
>> moveFunc
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate Transition.AnimMsg
| Color1
| Color2
| Color3
| Color4
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Transition.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
Color1 ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> toColor1
}
, Cmd.none
)
Color2 ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> toColor2
}
, Cmd.none
)
Color3 ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> toColor3
}
, Cmd.none
)
Color4 ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> toColor4
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" <|
Color.toHex bgColor
]
[ text label ]
color1Button =
button color1 "Color 1" Color1
color2Button =
button color2 "Color 2" Color2
color3Button =
button color3 "Color 3" Color3
color4Button =
button color4 "Color 4" Color4
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ color1Button
, color2Button
, color3Button
, color4Button
]
, div
(Transition.attributes animGroupName model.animState
++ [ class "example-canvas" ]
)
[]
]
module Animation.Sub.InterruptingAnimations.SingleProperty.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForSub)
import Anim.Engine.Sub as Sub
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- MODEL
animGroupName : String
animGroupName =
"movingBox"
type alias Model =
{ animState : Sub.AnimState
}
init : ( Model, Cmd Msg )
init =
( { animState =
Sub.init
[ CustomColor.init animGroupName CustomColor.BackgroundColor <|
Color.rgb 118 118 118
]
}
, Cmd.none
)
-- ANIMATIONS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
toColor1 : Sub.EngineBuilder -> Sub.EngineBuilder
toColor1 =
colorBox (CustomColor.to color1)
toColor2 : Sub.EngineBuilder -> Sub.EngineBuilder
toColor2 =
colorBox (CustomColor.to color2)
toColor3 : Sub.EngineBuilder -> Sub.EngineBuilder
toColor3 =
colorBox (CustomColor.to color3)
toColor4 : Sub.EngineBuilder -> Sub.EngineBuilder
toColor4 =
colorBox (CustomColor.to color4)
colorBox : (CustomColor.Builder ForSub -> CustomColor.Builder ForSub) -> Sub.EngineBuilder -> Sub.EngineBuilder
colorBox moveFunc =
CustomColor.begin CustomColor.BackgroundColor
>> moveFunc
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate Sub.AnimMsg
| Color1
| Color2
| Color3
| Color4
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Sub.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
Color1 ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> toColor1
}
, Cmd.none
)
Color2 ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> toColor2
}
, Cmd.none
)
Color3 ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> toColor3
}
, Cmd.none
)
Color4 ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> toColor4
}
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub.Sub Msg
subscriptions model =
Sub.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" <|
Color.toHex bgColor
]
[ text label ]
color1Button =
button color1 "Color 1" Color1
color2Button =
button color2 "Color 2" Color2
color3Button =
button color3 "Color 3" Color3
color4Button =
button color4 "Color 4" Color4
in
div
[ class "example-stage"
, style "text-align" "center"
]
[ div [ class "example-controls" ]
[ color1Button
, color2Button
, color3Button
, color4Button
]
, div
(Sub.attributes animGroupName model.animState
++ [ class "example-canvas" ]
)
[]
]
port module Animation.WAAPI.InterruptingAnimations.SingleProperty.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForWAAPI)
import Anim.Engine.WAAPI as WAAPI
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Json.Encode as Encode
import Motion.Easing as Easing exposing (Easing(..))
-- PORTS
port motionCmd : Encode.Value -> Cmd msg
port motionMsg : (Encode.Value -> msg) -> Sub msg
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- MODEL
animGroupName : String
animGroupName =
"colorBox"
type alias Model =
{ animState : WAAPI.AnimState Msg
}
init : ( Model, Cmd Msg )
init =
( { animState =
WAAPI.init motionCmd motionMsg <|
[ CustomColor.init animGroupName CustomColor.BackgroundColor <|
Color.rgb 118 118 118
]
}
, Cmd.none
)
-- ANIMATIONS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
toColor1 : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
toColor1 =
colorBox (CustomColor.to color1)
toColor2 : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
toColor2 =
colorBox (CustomColor.to color2)
toColor3 : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
toColor3 =
colorBox (CustomColor.to color3)
toColor4 : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
toColor4 =
colorBox (CustomColor.to color4)
colorBox : (CustomColor.Builder ForWAAPI -> CustomColor.Builder ForWAAPI) -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
colorBox moveFunc =
CustomColor.begin CustomColor.BackgroundColor
>> moveFunc
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate WAAPI.AnimMsg
| Color1
| Color2
| Color3
| Color4
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
WAAPI.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
Color1 ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroupName
>> toColor1
in
( { model | animState = newAnimState }
, cmd
)
Color2 ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroupName
>> toColor2
in
( { model | animState = newAnimState }
, cmd
)
Color3 ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroupName
>> toColor3
in
( { model | animState = newAnimState }
, cmd
)
Color4 ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroupName
>> toColor4
in
( { model | animState = newAnimState }
, cmd
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
WAAPI.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" <|
Color.toHex bgColor
]
[ text label ]
color1Button =
button color1 "Color 1" Color1
color2Button =
button color2 "Color 2" Color2
color3Button =
button color3 "Color 3" Color3
color4Button =
button color4 "Color 4" Color4
in
div
[ class "example-stage"
, style "text-align" "center"
]
[ div [ class "example-controls" ]
[ color1Button
, color2Button
, color3Button
, color4Button
]
, div
(WAAPI.attributes animGroupName model.animState
++ [ class "example-canvas" ]
)
[]
]
Multiple Properties¶
Adding Properties Mid-Flight¶
Scenario: One property is animating, and another is added mid-flight.
The following example uses Translate and a CustomColor property (BackgroundColor) to demonstrate the behaviour. Click either move button, then a color button before the move is complete to see the behaviour of the Engine.
View Examples
✅ Behaviour: Translate and CustomColor (BackgroundColor) run independently side by side.
❌ Behaviour: The new @keyframes rules for the animation replace the existing rules.
📖 See: Keyframe Engine — Interrupting Animations for details.
✅ Behaviour: Translate and CustomColor (BackgroundColor) run independently side by side.
✅ Behaviour: Translate and CustomColor (BackgroundColor) run independently side by side.
View Source Code
module Animation.Keyframe.InterruptingAnimations.MultipleProperties.Main exposing (main)
import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Keyframe as Keyframe
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
-- MODEL
type alias Model =
{ animState : Keyframe.AnimState }
animGroupName : String
animGroupName =
"movingBox"
{-| Box size expressed as a percentage of the canvas width. The box uses
`boxPct cqw` for both width and height so it always stays square, and
`Translate.toX` targets are
in `cqw` units, so the left, center and right anchors all scale with the
canvas. The box is vertically centered via CSS (`top: calc(50% - half box)`)
so no Y animation is needed - one less moving part and zero Elm-side
resize plumbing.
-}
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
boxHalfPct : Float
boxHalfPct =
boxPct / 2
init : ( Model, Cmd Msg )
init =
( { animState =
Keyframe.init
[ Translate.initX animGroupName centerXCqw >> Translate.cssUnit Cqw
, CustomColor.init animGroupName CustomColor.BackgroundColor <| Color.rgb 118 118 118
]
}
, Cmd.none
)
-- POSITION HELPERS
type XPos
= XLeft
| XCenter
| XRight
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
centerXCqw
XRight ->
100 - boxPct
-- COLORS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
-- ANIMATIONS
moveBoxX : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
moveBoxX x =
Translate.begin
>> Translate.toX x
>> Translate.speed 25
>> Translate.easing BounceOut
>> Translate.end
changeColor : Color -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
changeColor color =
CustomColor.begin CustomColor.BackgroundColor
>> CustomColor.to color
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate Keyframe.AnimMsg
| MoveLeft
| MoveRight
| ChangeColor Color
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Keyframe.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> moveBoxX (targetX XLeft)
}
, Cmd.none
)
MoveRight ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> moveBoxX (targetX XRight)
}
, Cmd.none
)
ChangeColor color ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> changeColor color
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
let
posButton bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
colorButton color label =
Html.button
[ onClick (ChangeColor color)
, class "ui-action-button"
, style "background-color" (Color.toHex color)
]
[ text label ]
in
div [ class "example-stage" ]
[ Keyframe.styleNode model.animState
, div [ class "example-controls" ]
[ posButton "#333" "Move Left" MoveLeft
, posButton "#333" "Move Right" MoveRight
]
, div [ class "example-controls" ]
[ colorButton color1 "Color 1"
, colorButton color2 "Color 2"
, colorButton color3 "Color 3"
, colorButton color4 "Color 4"
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ div
(Keyframe.attributes animGroupName model.animState
++ Keyframe.events GotAnimationUpdate
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqw")
, style "position" "absolute"
, style "top" ("calc(50% - " ++ String.fromFloat boxHalfPct ++ "cqw)")
, style "left" "0"
, style "border-radius" "8px"
]
)
[]
]
]
module Animation.Transition.InterruptingAnimations.MultipleProperties.Main exposing (main)
import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Transition as Transition
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
-- MODEL
type alias Model =
{ animState : Transition.AnimState }
animGroupName : String
animGroupName =
"movingBox"
{-| Box size expressed as a percentage of the canvas width. The box uses
`boxPct cqw` for both width and height so it always stays square, and
`Translate.toX` targets are
in `cqw` units, so the left, center and right anchors all scale with the
canvas. The box is vertically centered via CSS (`top: calc(50% - half box)`)
so no Y animation is needed - one less moving part and zero Elm-side
resize plumbing.
-}
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
boxHalfPct : Float
boxHalfPct =
boxPct / 2
init : ( Model, Cmd Msg )
init =
( { animState =
Transition.init
[ Translate.initX animGroupName centerXCqw >> Translate.cssUnitX Cqw
, CustomColor.init animGroupName CustomColor.BackgroundColor <| Color.rgb 118 118 118
]
}
, Cmd.none
)
-- POSITION HELPERS
type XPos
= XLeft
| XCenter
| XRight
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
centerXCqw
XRight ->
100 - boxPct
-- COLORS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
-- ANIMATIONS
moveBoxX : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
moveBoxX x =
Translate.begin
>> Translate.toX x
>> Translate.speed 25
>> Translate.easing BounceOut
>> Translate.end
changeColor : Color -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
changeColor color =
CustomColor.begin CustomColor.BackgroundColor
>> CustomColor.to color
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate Transition.AnimMsg
| MoveLeft
| MoveRight
| ChangeColor Color
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Transition.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> moveBoxX (targetX XLeft)
}
, Cmd.none
)
MoveRight ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> moveBoxX (targetX XRight)
}
, Cmd.none
)
ChangeColor color ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> changeColor color
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
let
posButton bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
colorButton color label =
Html.button
[ onClick (ChangeColor color)
, class "ui-action-button"
, style "background-color" (Color.toHex color)
]
[ text label ]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ posButton "#333" "Move Left" MoveLeft
, posButton "#333" "Move Right" MoveRight
]
, div [ class "example-controls" ]
[ colorButton color1 "Color 1"
, colorButton color2 "Color 2"
, colorButton color3 "Color 3"
, colorButton color4 "Color 4"
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ div
(Transition.attributes animGroupName model.animState
++ Transition.events GotAnimationUpdate
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqw")
, style "position" "absolute"
, style "top" ("calc(50% - " ++ String.fromFloat boxHalfPct ++ "cqw)")
, style "left" "0"
, style "border-radius" "8px"
]
)
[]
]
]
module Animation.Sub.InterruptingAnimations.MultipleProperties.Main exposing (..)
import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Sub as Sub
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ animState : Sub.AnimState }
type XPos
= XLeft
| XCenter
| XRight
animGroupName : String
animGroupName =
"movingBox"
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
boxHalfPct : Float
boxHalfPct =
boxPct / 2
init : ( Model, Cmd Msg )
init =
( { animState =
Sub.init
[ Translate.initX animGroupName centerXCqw
>> Translate.cssUnitX Cqw
, CustomColor.init animGroupName CustomColor.BackgroundColor <| Color.rgb 118 118 118
]
}
, Cmd.none
)
-- POSITION HELPERS
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
centerXCqw
XRight ->
100 - boxPct
-- COLORS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
-- ANIMATIONS
moveBoxX : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
moveBoxX x =
Translate.begin
>> Translate.toX x
>> Translate.speed 25
>> Translate.easing BounceOut
>> Translate.end
changeColor : Color -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
changeColor color =
CustomColor.begin CustomColor.BackgroundColor
>> CustomColor.to color
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate Sub.AnimMsg
| MoveLeft
| MoveRight
| ChangeColor Color
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Sub.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> moveBoxX (targetX XLeft)
}
, Cmd.none
)
MoveRight ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> moveBoxX (targetX XRight)
}
, Cmd.none
)
ChangeColor color ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> changeColor color
}
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub.Sub Msg
subscriptions model =
Sub.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
posButton bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
colorButton color label =
Html.button
[ onClick (ChangeColor color)
, class "ui-action-button"
, style "background-color" (Color.toHex color)
]
[ text label ]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ posButton "#333" "Move Left" MoveLeft
, posButton "#333" "Move Right" MoveRight
]
, div [ class "example-controls" ]
[ colorButton color1 "Color 1"
, colorButton color2 "Color 2"
, colorButton color3 "Color 3"
, colorButton color4 "Color 4"
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ div
(Sub.attributes animGroupName model.animState
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqw")
, style "position" "absolute"
, style "top" ("calc(50% - " ++ String.fromFloat boxHalfPct ++ "cqw)")
, style "left" "0"
, style "border-radius" "8px"
]
)
[]
]
]
port module Animation.WAAPI.InterruptingAnimations.MultipleProperties.Main exposing (main)
import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.WAAPI as WAAPI
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Json.Encode as Encode
import Motion.Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- PORTS
port motionCmd : Encode.Value -> Cmd msg
port motionMsg : (Encode.Value -> msg) -> Sub msg
-- MODEL
type alias Model =
{ animState : WAAPI.AnimState Msg }
animGroupName : String
animGroupName =
"movingBox"
{-| Box size expressed as a percentage of the canvas width. The box uses
`boxPct cqw` for both width and height so it always stays square, and
`Translate.toX` targets are
in `cqw` units, so the left, center and right anchors all scale with the
canvas. The box is vertically centered via CSS (`top: calc(50% - half box)`)
so no Y animation is needed - one less moving part and zero Elm-side
resize plumbing.
-}
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
boxHalfPct : Float
boxHalfPct =
boxPct / 2
init : ( Model, Cmd Msg )
init =
( { animState =
WAAPI.init motionCmd motionMsg <|
[ Translate.initX animGroupName centerXCqw >> Translate.cssUnitX Cqw
, CustomColor.init animGroupName CustomColor.BackgroundColor <| Color.rgb 118 118 118
]
}
, Cmd.none
)
-- POSITION HELPERS
type XPos
= XLeft
| XCenter
| XRight
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
centerXCqw
XRight ->
100 - boxPct
-- COLORS
color1 : Color
color1 =
Color.rgb 255 87 51
color2 : Color
color2 =
Color.rgb 40 167 69
color3 : Color
color3 =
Color.rgb 111 66 193
color4 : Color
color4 =
Color.rgb 255 193 7
-- ANIMATIONS
moveBoxX : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
moveBoxX x =
Translate.begin
>> Translate.toX x
>> Translate.speed 25
>> Translate.easing BounceOut
>> Translate.end
changeColor : Color -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
changeColor color =
CustomColor.begin CustomColor.BackgroundColor
>> CustomColor.to color
>> CustomColor.duration 3000
>> CustomColor.easing Linear
>> CustomColor.end
-- UPDATE
type Msg
= GotAnimationUpdate WAAPI.AnimMsg
| MoveLeft
| MoveRight
| ChangeColor Color
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
WAAPI.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroupName
>> moveBoxX (targetX XLeft)
in
( { model | animState = newAnimState }, cmd )
MoveRight ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroupName
>> moveBoxX (targetX XRight)
in
( { model | animState = newAnimState }, cmd )
ChangeColor color ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroupName
>> changeColor color
in
( { model | animState = newAnimState }, cmd )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub.Sub Msg
subscriptions model =
WAAPI.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
posButton bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
colorButton color label =
Html.button
[ onClick (ChangeColor color)
, class "ui-action-button"
, style "background-color" (Color.toHex color)
]
[ text label ]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ posButton "#333" "Move Left" MoveLeft
, posButton "#333" "Move Right" MoveRight
]
, div [ class "example-controls" ]
[ colorButton color1 "Color 1"
, colorButton color2 "Color 2"
, colorButton color3 "Color 3"
, colorButton color4 "Color 4"
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ div
(WAAPI.attributes animGroupName model.animState
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqw")
, style "position" "absolute"
, style "top" ("calc(50% - " ++ String.fromFloat boxHalfPct ++ "cqw)")
, style "left" "0"
, style "border-radius" "8px"
]
)
[]
]
]
Properties with Multiple Axes¶
Scenario: Animating one axis, then animating another axis before it completes.
The following example uses the Translate property to demonstrate the behaviour. Click 'Move Right' followed by 'Move Up' before the animation completes to see the behaviour of the Engine.
View Examples
✅ Behaviour: Smooth redirect from current position
❌ Behaviour: The new @keyframes rules for the animation replace the existing rules.
📖 See: Keyframe Engine — Interrupting Animations for details.
✅ Behaviour: Seamless interruption to new target
✅ Behaviour: Seamless interruption to new target
View Source Code
module Animation.Keyframe.InterruptingAnimations.MultipleAxes.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForKeyframe)
import Anim.Engine.Keyframe as Keyframe
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
-- MODEL
animGroupName : String
animGroupName =
"movingBox"
type alias Model =
{ animState : Keyframe.AnimState }
{-| Box size expressed as a percentage of the canvas on each axis.
The box width is `boxPct cqw`, height is `boxPct cqh`, and translate
targets are expressed in the matching axis-unit. Everything scales
with the canvas - no Elm-side resize handling required.
-}
boxPct : Float
boxPct =
12
init : ( Model, Cmd Msg )
init =
( { animState =
Keyframe.init
[ Translate.initXY animGroupName (targetX XCenter) (targetY YCenter) >> Translate.cssUnitX Cqw >> Translate.cssUnitY Cqh
]
}
, Cmd.none
)
-- POSITION HELPERS
type XPos
= XLeft
| XCenter
| XRight
type YPos
= YTop
| YCenter
| YBottom
{-| X target in `cqw` (0..100 - boxPct).
-}
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
(100 - boxPct) / 2
XRight ->
100 - boxPct
{-| Y target in `cqh` (0..100 - boxPct).
-}
targetY : YPos -> Float
targetY pos =
case pos of
YTop ->
0
YCenter ->
(100 - boxPct) / 2
YBottom ->
100 - boxPct
-- ANIMATIONS
moveBoxX : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveBoxX x =
moveBox (Translate.toX x)
moveBoxY : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveBoxY y =
moveBox (Translate.toY y)
moveBox : (Translate.Builder ForKeyframe -> Translate.Builder ForKeyframe) -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveBox moveFunc =
Translate.begin
>> moveFunc
>> Translate.speed 25
>> Translate.easing QuintOut
>> Translate.end
-- UPDATE
type Msg
= GotAnimationUpdate Keyframe.AnimMsg
| MoveLeft
| MoveRight
| MoveUp
| MoveDown
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Keyframe.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> moveBoxX (targetX XLeft)
}
, Cmd.none
)
MoveRight ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> moveBoxX (targetX XRight)
}
, Cmd.none
)
MoveUp ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> moveBoxY (targetY YTop)
}
, Cmd.none
)
MoveDown ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroupName
>> moveBoxY (targetY YBottom)
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
moveLeftButton =
button "#007BFF" "Move Left" MoveLeft
moveRightButton =
button "#28A745" "Move Right" MoveRight
moveUpButton =
button "#6F42C1" "Move Up" MoveUp
moveDownButton =
button "#FFC107" "Move Down" MoveDown
box =
div
(Keyframe.attributes animGroupName model.animState
++ Keyframe.events GotAnimationUpdate
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqh")
, style "background-color" "#FF5733"
, style "border-radius" "8px"
, style "position" "absolute"
, style "top" "0"
, style "left" "0"
]
)
[]
in
div [ class "example-stage" ]
[ Keyframe.styleNode model.animState
, div [ class "example-controls" ]
[ moveLeftButton
, moveRightButton
, moveUpButton
, moveDownButton
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ box ]
]
module Animation.Transition.InterruptingAnimations.MultipleAxes.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForTransition)
import Anim.Engine.Transition as Transition
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
-- MODEL
animGroupName : String
animGroupName =
"movingBox"
type alias Model =
{ animState : Transition.AnimState }
{-| Box size expressed as a percentage of the canvas on each axis.
The box width is `boxPct cqw`, height is `boxPct cqh`, and translate
targets are expressed in the matching axis-unit. Everything scales
with the canvas - no Elm-side resize handling required.
-}
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
centerYCqh : Float
centerYCqh =
(100 - boxPct) / 2
init : ( Model, Cmd Msg )
init =
( { animState =
Transition.init
[ Translate.initXY animGroupName centerXCqw centerYCqh >> Translate.cssUnitX Cqw >> Translate.cssUnitY Cqh
]
}
, Cmd.none
)
-- POSITION HELPERS
type XPos
= XLeft
| XCenter
| XRight
type YPos
= YTop
| YCenter
| YBottom
{-| X target in `cqw` (0..100 - boxPct).
-}
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
(100 - boxPct) / 2
XRight ->
100 - boxPct
{-| Y target in `cqh` (0..100 - boxPct).
-}
targetY : YPos -> Float
targetY pos =
case pos of
YTop ->
0
YCenter ->
(100 - boxPct) / 2
YBottom ->
100 - boxPct
-- ANIMATIONS
moveBoxX : Float -> Transition.EngineBuilder -> Transition.EngineBuilder
moveBoxX x =
moveBox (Translate.toX x)
moveBoxY : Float -> Transition.EngineBuilder -> Transition.EngineBuilder
moveBoxY y =
moveBox (Translate.toY y)
moveBox : (Translate.Builder ForTransition -> Translate.Builder ForTransition) -> Transition.EngineBuilder -> Transition.EngineBuilder
moveBox moveFunc =
Translate.begin
>> moveFunc
>> Translate.speed 25
>> Translate.easing QuintOut
>> Translate.end
-- UPDATE
type Msg
= GotAnimationUpdate Transition.AnimMsg
| MoveLeft
| MoveRight
| MoveUp
| MoveDown
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Transition.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> moveBoxX (targetX XLeft)
}
, Cmd.none
)
MoveRight ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> moveBoxX (targetX XRight)
}
, Cmd.none
)
MoveUp ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> moveBoxY (targetY YTop)
}
, Cmd.none
)
MoveDown ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroupName
>> moveBoxY (targetY YBottom)
}
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
moveLeftButton =
button "#007BFF" "Move Left" MoveLeft
moveRightButton =
button "#28A745" "Move Right" MoveRight
moveUpButton =
button "#6F42C1" "Move Up" MoveUp
moveDownButton =
button "#FFC107" "Move Down" MoveDown
box =
div
(Transition.attributes animGroupName model.animState
++ Transition.events GotAnimationUpdate
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqh")
, style "background-color" "#FF5733"
, style "border-radius" "8px"
, style "position" "absolute"
, style "top" "0"
, style "left" "0"
]
)
[]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ moveLeftButton
, moveRightButton
, moveUpButton
, moveDownButton
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ box ]
]
module Animation.Sub.InterruptingAnimations.MultipleAxes.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForSub)
import Anim.Engine.Sub as Sub
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- MODEL
animGroupName : String
animGroupName =
"movingBox"
type alias Model =
{ animState : Sub.AnimState }
type XPos
= XLeft
| XCenter
| XRight
type YPos
= YTop
| YCenter
| YBottom
boxPct : Float
boxPct =
12
targetSpeed : Float
targetSpeed =
25
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
centerYCqh : Float
centerYCqh =
(100 - boxPct) / 2
init : ( Model, Cmd Msg )
init =
( { animState =
Sub.init
[ Translate.initXY animGroupName centerXCqw centerYCqh
>> Translate.cssUnitX Cqw
>> Translate.cssUnitY Cqh
]
}
, Cmd.none
)
-- POSITION HELPERS
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
(100 - boxPct) / 2
XRight ->
100 - boxPct
targetY : YPos -> Float
targetY pos =
case pos of
YTop ->
0
YCenter ->
(100 - boxPct) / 2
YBottom ->
100 - boxPct
-- ANIMATIONS
moveBoxX : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBoxX x =
moveBox <|
Translate.toX x
moveBoxY : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBoxY y =
moveBox <|
Translate.toY y
moveBox : (Translate.Builder ForSub -> Translate.Builder ForSub) -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBox moveFunc =
Translate.begin
>> moveFunc
>> Translate.speed targetSpeed
>> Translate.easing QuintOut
>> Translate.end
-- UPDATE
type Msg
= GotAnimationUpdate Sub.AnimMsg
| MoveLeft
| MoveRight
| MoveUp
| MoveDown
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Sub.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> moveBoxX (targetX XLeft)
}
, Cmd.none
)
MoveRight ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> moveBoxX (targetX XRight)
}
, Cmd.none
)
MoveUp ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> moveBoxY (targetY YTop)
}
, Cmd.none
)
MoveDown ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroupName
>> moveBoxY (targetY YBottom)
}
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub.Sub Msg
subscriptions model =
Sub.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
moveLeftButton =
button "#007BFF" "Move Left" MoveLeft
moveRightButton =
button "#28A745" "Move Right" MoveRight
moveUpButton =
button "#6F42C1" "Move Up" MoveUp
moveDownButton =
button "#FFC107" "Move Down" MoveDown
box =
div
(Sub.attributes animGroupName model.animState
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqh")
, style "background-color" "#FF5733"
, style "border-radius" "8px"
, style "position" "absolute"
, style "top" "0"
, style "left" "0"
]
)
[]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ moveLeftButton
, moveRightButton
, moveUpButton
, moveDownButton
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ box ]
]
port module Animation.WAAPI.InterruptingAnimations.MultipleAxes.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForWAAPI)
import Anim.Engine.WAAPI as WAAPI
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Json.Encode as Encode
import Motion.Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- PORTS
port motionCmd : Encode.Value -> Cmd msg
port motionMsg : (Encode.Value -> msg) -> Sub msg
-- MODEL
animGroup : String
animGroup =
"movingBox"
type alias Model =
{ animState : WAAPI.AnimState Msg }
{-| Box size expressed as a percentage of the canvas on each axis.
The box width is `boxPct cqw`, height is `boxPct cqh`, and translate
targets are expressed in the matching axis-unit. Everything scales
with the canvas - no Elm-side resize handling required.
-}
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
centerYCqh : Float
centerYCqh =
(100 - boxPct) / 2
-- INIT
init : ( Model, Cmd Msg )
init =
( { animState =
WAAPI.init motionCmd motionMsg <|
[ Translate.initXY animGroup centerXCqw centerYCqh >> Translate.cssUnitX Cqw >> Translate.cssUnitY Cqh
]
}
, Cmd.none
)
-- POSITION HELPERS
type XPos
= XLeft
| XCenter
| XRight
type YPos
= YTop
| YCenter
| YBottom
{-| X target in `cqw` (0..100 - boxPct).
-}
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
(100 - boxPct) / 2
XRight ->
100 - boxPct
{-| Y target in `cqh` (0..100 - boxPct).
-}
targetY : YPos -> Float
targetY pos =
case pos of
YTop ->
0
YCenter ->
(100 - boxPct) / 2
YBottom ->
100 - boxPct
-- ANIMATIONS
moveBoxX : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBoxX x =
moveBox <|
Translate.toX x
moveBoxY : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBoxY y =
moveBox <|
Translate.toY y
moveBox : (Translate.Builder ForWAAPI -> Translate.Builder ForWAAPI) -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBox moveFunc =
WAAPI.for animGroup
>> Translate.begin
>> moveFunc
>> Translate.speed 25
>> Translate.easing QuintOut
>> Translate.end
-- UPDATE
type Msg
= GotAnimationUpdate WAAPI.AnimMsg
| MoveLeft
| MoveRight
| MoveUp
| MoveDown
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
WAAPI.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
moveBoxX (targetX XLeft)
in
( { model | animState = newAnimState }
, cmd
)
MoveRight ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
moveBoxX (targetX XRight)
in
( { model | animState = newAnimState }
, cmd
)
MoveUp ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
moveBoxY (targetY YTop)
in
( { model | animState = newAnimState }
, cmd
)
MoveDown ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
moveBoxY (targetY YBottom)
in
( { model | animState = newAnimState }
, cmd
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub.Sub Msg
subscriptions model =
WAAPI.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
moveLeftButton =
button "#007BFF" "Move Left" MoveLeft
moveRightButton =
button "#28A745" "Move Right" MoveRight
moveUpButton =
button "#6F42C1" "Move Up" MoveUp
moveDownButton =
button "#FFC107" "Move Down" MoveDown
box =
div
(WAAPI.attributes animGroup model.animState
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqh")
, style "background-color" "#FF5733"
, style "border-radius" "8px"
, style "position" "absolute"
, style "top" "0"
, style "left" "0"
]
)
[]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ moveLeftButton
, moveRightButton
, moveUpButton
, moveDownButton
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ box ]
]
Freezing Axes with freeze*¶
The example above demonstrates the default behaviour, but this can be changed with the family of freeze* functions.
The freeze* functions let you opt in to freezing specific axes at their current mid-flight values. When a frozen axis is encountered during animation, it holds its current position while only the specified axes animate to new targets.
In the examples below, try the same sequence — click "Move Right" then "Move Up". The box now travels straight up from wherever it is, because freezeX holds the X axis at its current position.
View Examples
✅ Behaviour: Frozen axis holds its current position while the other axis animates to the new target
✅ Behaviour: Frozen axis holds its current position while the other axis animates to the new target
View Source Code
module Animation.Sub.InterruptingAnimations.FreezeAxis.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForSub)
import Anim.Engine.Sub as Sub
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- MODEL
animGroupName : String
animGroupName =
"movingBox"
type alias Model =
{ animState : Sub.AnimState }
type XPos
= XLeft
| XCenter
| XRight
type YPos
= YTop
| YCenter
| YBottom
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
centerYCqh : Float
centerYCqh =
(100 - boxPct) / 2
init : ( Model, Cmd Msg )
init =
( { animState =
Sub.init
[ Translate.initXY animGroupName centerXCqw centerYCqh >> Translate.cssUnitX Cqw >> Translate.cssUnitY Cqh
]
}
, Cmd.none
)
-- POSITION HELPERS
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
(100 - boxPct) / 2
XRight ->
100 - boxPct
targetY : YPos -> Float
targetY pos =
case pos of
YTop ->
0
YCenter ->
(100 - boxPct) / 2
YBottom ->
100 - boxPct
-- ANIMATIONS
moveBoxX : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBoxX x =
moveBox <|
Translate.toX x
moveBoxY : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBoxY y =
moveBox <|
Translate.toY y
moveBox : (Translate.Builder ForSub -> Translate.Builder ForSub) -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBox moveFunc =
Translate.begin
>> moveFunc
>> Translate.speed 25
>> Translate.easing BounceOut
>> Translate.end
-- UPDATE
type Msg
= GotAnimationUpdate Sub.AnimMsg
| MoveLeft
| MoveRight
| MoveUp
| MoveDown
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
Sub.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
( { model
| animState =
Sub.animate model.animState <|
Sub.freezeY [ Sub.translate ]
>> Sub.for animGroupName
>> moveBoxX (targetX XLeft)
}
, Cmd.none
)
MoveRight ->
( { model
| animState =
Sub.animate model.animState <|
Sub.freezeY [ Sub.translate ]
>> Sub.for animGroupName
>> moveBoxX (targetX XRight)
}
, Cmd.none
)
MoveUp ->
( { model
| animState =
Sub.animate model.animState <|
Sub.freezeX [ Sub.translate ]
>> Sub.for animGroupName
>> moveBoxY (targetY YTop)
}
, Cmd.none
)
MoveDown ->
( { model
| animState =
Sub.animate model.animState <|
Sub.freezeX [ Sub.translate ]
>> Sub.for animGroupName
>> moveBoxY (targetY YBottom)
}
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub.Sub Msg
subscriptions model =
Sub.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
moveLeftButton =
button "#007BFF" "Move Left" MoveLeft
moveRightButton =
button "#28A745" "Move Right" MoveRight
moveUpButton =
button "#6F42C1" "Move Up" MoveUp
moveDownButton =
button "#FFC107" "Move Down" MoveDown
box =
div
(Sub.attributes animGroupName model.animState
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqh")
, style "background-color" "#FF5733"
, style "border-radius" "8px"
, style "position" "absolute"
, style "top" "0"
, style "left" "0"
]
)
[]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ moveLeftButton
, moveRightButton
, moveUpButton
, moveDownButton
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ box ]
]
port module Animation.WAAPI.InterruptingAnimations.FreezeAxis.Main exposing (main)
import Anim.Builder exposing (AnimBuilder, ForWAAPI)
import Anim.Engine.WAAPI as WAAPI
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Json.Encode as Encode
import Motion.Easing exposing (Easing(..))
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, update = update
, view = view
, subscriptions = subscriptions
}
-- PORTS
port motionCmd : Encode.Value -> Cmd msg
port motionMsg : (Encode.Value -> msg) -> Sub msg
-- MODEL
animGroup : String
animGroup =
"movingBox"
type alias Model =
{ animState : WAAPI.AnimState Msg }
{-| Box size expressed as a percentage of the canvas on each axis.
The box width is `boxPct cqw`, height is `boxPct cqh`, and translate
targets are expressed in the matching axis-unit. Everything scales
with the canvas - no Elm-side resize handling required.
-}
boxPct : Float
boxPct =
12
centerXCqw : Float
centerXCqw =
(100 - boxPct) / 2
centerYCqh : Float
centerYCqh =
(100 - boxPct) / 2
-- INIT
init : ( Model, Cmd Msg )
init =
( { animState =
WAAPI.init motionCmd motionMsg <|
[ Translate.initXY animGroup centerXCqw centerYCqh >> Translate.cssUnitX Cqw >> Translate.cssUnitY Cqh
]
}
, Cmd.none
)
-- POSITION HELPERS
type XPos
= XLeft
| XCenter
| XRight
type YPos
= YTop
| YCenter
| YBottom
{-| X target in `cqw` (0..100 - boxPct).
-}
targetX : XPos -> Float
targetX pos =
case pos of
XLeft ->
0
XCenter ->
(100 - boxPct) / 2
XRight ->
100 - boxPct
{-| Y target in `cqh` (0..100 - boxPct).
-}
targetY : YPos -> Float
targetY pos =
case pos of
YTop ->
0
YCenter ->
(100 - boxPct) / 2
YBottom ->
100 - boxPct
-- ANIMATIONS
moveBoxX : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBoxX x =
moveBox <|
Translate.toX x
moveBoxY : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBoxY y =
moveBox <|
Translate.toY y
moveBox : (Translate.Builder ForWAAPI -> Translate.Builder ForWAAPI) -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBox moveFunc =
WAAPI.for animGroup
>> Translate.begin
>> moveFunc
>> Translate.speed 25
>> Translate.easing BounceOut
>> Translate.end
-- UPDATE
type Msg
= GotAnimationUpdate WAAPI.AnimMsg
| MoveLeft
| MoveRight
| MoveUp
| MoveDown
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimationUpdate animationMsg ->
let
( newAnimState, _ ) =
WAAPI.update animationMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
MoveLeft ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.freezeY [ WAAPI.translate ]
>> moveBoxX (targetX XLeft)
in
( { model | animState = newAnimState }, cmd )
MoveRight ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.freezeY [ WAAPI.translate ]
>> moveBoxX (targetX XRight)
in
( { model | animState = newAnimState }, cmd )
MoveUp ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.freezeX [ WAAPI.translate ]
>> moveBoxY (targetY YTop)
in
( { model | animState = newAnimState }, cmd )
MoveDown ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.freezeX [ WAAPI.translate ]
>> moveBoxY (targetY YBottom)
in
( { model | animState = newAnimState }, cmd )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub.Sub Msg
subscriptions model =
WAAPI.subscriptions GotAnimationUpdate model.animState
-- VIEW
view : Model -> Html Msg
view model =
let
button bgColor label onClickMsg =
Html.button
[ onClick onClickMsg
, class "ui-action-button"
, style "background-color" bgColor
]
[ text label ]
moveLeftButton =
button "#007BFF" "Move Left" MoveLeft
moveRightButton =
button "#28A745" "Move Right" MoveRight
moveUpButton =
button "#6F42C1" "Move Up" MoveUp
moveDownButton =
button "#FFC107" "Move Down" MoveDown
box =
div
(WAAPI.attributes animGroup model.animState
++ [ style "width" (String.fromFloat boxPct ++ "cqw")
, style "height" (String.fromFloat boxPct ++ "cqh")
, style "background-color" "#FF5733"
, style "border-radius" "8px"
, style "position" "absolute"
, style "top" "0"
, style "left" "0"
]
)
[]
in
div [ class "example-stage" ]
[ div [ class "example-controls" ]
[ moveLeftButton
, moveRightButton
, moveUpButton
, moveDownButton
]
, div
[ class "example-canvas--fluid"
, style "container-type" "size"
]
[ box ]
]
freeze* is available on the Sub and WAAPI engines.
Next Steps¶
Now that you understand how animations handle interruptions, learn how to control them.