gampleman/evg/1.0.0

NOTE: the latest version is 1.0.1

Evg.Animate

Declarative animations that don't require any messages or subscriptions.

You describe the animation as part of your view and the browser plays it on its own. There is no Elm update loop involved: no subscriptions, no Time-based messages, no re-rendering every frame.

The vocabulary used throughout this module:

  • durations and keyframe times are in seconds
  • keyframes are snapshots of a value at points in time; the browser smoothly blends between them
  • easing is the speed profile of the motion between two keyframes, such as starting slow and finishing fast

Here is a small example:

import Evg
import Evg.Animate as Animate


-- Animate a circle's radius with easing:
Evg.circle
    [ Evg.fillStr "tomato" ]
    { r = 20, center = ( 50, 50 ) }
    |> Animate.animate [ Animate.repeatIndefinite ]
        "r"
        [ ( 0.5, Animate.easeInOut, "40" )
        , ( 1.5, Animate.easeLinear, "20" )
        ]

This shows a red circle that endlessly swells and shrinks back.

Options

Options that control animation timing, repetition, triggering and how an animation reports back into your program.

The animation repeats forever.

The animation repeats a fixed number of times. Fractional counts are allowed, so 2.5 runs the animation two and a half times.

repeatFor : Float -> AnimationOption msg

The animation keeps repeating until the given number of seconds has elapsed, then stops, even if that means cutting a cycle short.

duration : Float -> AnimationOption msg

The total duration of one animation cycle in seconds.

delay : Float -> AnimationOption msg

Delay before the animation starts, in seconds.

If you also give a trigger with beginOn, the trigger wins and this delay is ignored.

When the animation ends, the element stays at its final state rather than snapping back to the start.

Sets the easing (the speed profile of the motion) used between all keyframes.

Animate.keyframes
    [ Animate.repeatIndefinite
    , Animate.easing Animate.easeInOut
    ]
    [ ( 0
      , Evg.circle []
            { r = 10, center = ( 50, 50 ) }
      )
    , ( 1
      , Evg.circle []
            { r = 40, center = ( 50, 50 ) }
      )
    ]

This shows a circle growing and shrinking, gently accelerating out of each extreme and slowing into the next.

Jump instantly from one keyframe value to the next with no smooth in-between motion, like a slideshow of frames. Per-keyframe easings are ignored.

Move at a constant speed across the whole animation, spacing the keyframes so that equal distances take equal time. Any keyframe times and easings you gave are ignored in favor of this even pacing.

Add this animation on top of any other transforms already on the element, rather than replacing them. Transform animations (rotate, scale, and friends) always do this automatically.

Each repeat builds on the end of the previous one instead of starting over. For example a translate that moves 10 to the right will end up 20 to the right after two repeats, 30 after three, and so on.

Triggers and restarting

Start the animation when the given event happens on the element, instead of right away. Give it more than once to start on any of several events.

Animate.animate [ Animate.beginOn Animate.click ]
    "r"
    [ ( 0.4, Animate.easeOut, "40" ) ]

Nothing moves until the shape is clicked; then it quickly grows.

End the animation early when the given event happens on the element.

type Trigger

Something that can start or stop an animation, such as a click or a mouse hover on the animated element itself.

The element is clicked.

The pointer moves onto the element.

The pointer moves off the element.

The animation never starts on its own. It waits to be started by another animation in a sequence.

If the animation is triggered again while it is already running, ignore the new trigger. A fresh trigger only takes effect once the current run has finished.

The animation can only run once; later triggers are ignored.

Reacting to animations

Send a message the moment the animation starts.

A browser support caveat: browsers only started delivering these animation milestone events reliably in versions released after late 2025. Older Safari (including iOS) may never send them, even though the animation itself plays fine. Don't gate critical state changes on receiving one; if you must support older browsers, a timer matching the animation's timing (e.g. Process.sleep) is the dependable alternative. This applies equally to onEnd and onRepeat.

onEnd : msg -> AnimationOption msg

Send a message the moment the animation ends. Never sent by animations that repeat forever.

See onBegin for a browser support caveat: on older browsers (notably Safari before late 2025) this message may never arrive, so avoid relying on it for critical state changes. For example, prefer a timer over onEnd for removing an element after its exit animation.

Send a message each time the animation completes a repeat.

See onBegin for a browser support caveat that also applies here.

Easing Curves

type Easing

The speed profile of the motion between two keyframes: whether it moves at a constant rate, starts slow and speeds up, or the reverse. The values covered are the same either way; easing only changes how fast the animation travels through them at each moment.

Constant speed from start to finish.

Starts slow, speeds up, then slows down at the end.

Starts slow, then speeds up.

Starts fast, then slows down.

cubicBezier : Float -> Float -> Float -> Float -> Easing

A custom speed profile defined by a cubic bezier curve, the same format used by CSS cubic-bezier(...) timing functions. The four numbers are the two control points (x1, y1) and (x2, y2).

Animate.cubicBezier 0.68 -0.55 0.265 1.55

This one makes the motion pull back slightly, race forward, overshoot the target and settle, like a spring.

Animation Builders

List (AnimationOption msg)
-> String
-> List ( Float, Easing, String )
-> Evg msg
-> Evg msg

Animates a single attribute of a shape between keyframe values with easing.

Each keyframe is a tuple of ( time, easing, value ) where:

  • time is seconds from the start of the animation when this value is reached
  • easing is the speed profile of the motion leading TO this value
  • value is the attribute value at this keyframe (as a string)

The initial value comes from the element's own attributes.

-- Pulse a rectangle's corner radius:
Evg.rect [ Evg.cornerRadius 30, Evg.fillStr "navy" ]
    { x = 10, y = 10, width = 100, height = 100 }
    |> Animate.animate [ Animate.repeatIndefinite ]
        "rx"
        [ ( 0.8, Animate.easeLinear, "50" )
        , ( 1.8, Animate.easeInOut, "20" )
        , ( 3.0, Animate.easeLinear, "30" )
        ]

This shows a navy square whose corners continuously round off and sharpen again.

set : List (AnimationOption msg) -> String -> String -> Evg msg -> Evg msg

Instantly sets an attribute to a value at a chosen moment, with no smooth transition. Handy for toggling something on at a delay or on a click.

-- Turn a circle red the moment it is clicked:
Evg.circle
    [ Evg.fillStr "gray" ]
    { r = 20, center = ( 50, 50 ) }
    |> Animate.set
        [ Animate.beginOn Animate.click ]
        "fill"
        "red"

The circle stays gray until clicked, then instantly turns red.

Since nothing is blended, the easing option has no effect here. The duration option means something different: it is how long the set value lasts, after which the value snaps back (unless you also use fillFreeze).

keyframes : List (AnimationOption msg) -> List ( Float, Evg msg ) -> Evg msg

Animates between complete shapes at specified times. Each entry is a snapshot of the whole shape at a moment in seconds; the library works out which attributes change across snapshots and generates synchronized animations for each one, and the browser blends between them.

This is the highest-level animation API: you describe what the shape looks like at each moment, and the library handles the rest.

import Evg
import Evg.Animate as Animate


pulsingCircle =
    Animate.keyframes
        [ Animate.repeatIndefinite
        , Animate.easing Animate.easeInOut
        ]
        [ ( 0.0
          , Evg.circle
                [ Evg.fillStr "rgb(20,60,20)" ]
                { r = 10, center = ( 50, 50 ) }
          )
        , ( 0.6
          , Evg.circle
                [ Evg.fillStr "rgb(20,180,20)" ]
                { r = 40, center = ( 50, 50 ) }
          )
        , ( 1.0
          , Evg.circle
                [ Evg.fillStr "rgb(20,60,20)" ]
                { r = 10, center = ( 50, 50 ) }
          )
        ]

This shows a circle repeatedly growing while brightening to a vivid green, then shrinking and darkening back.

If frames have different element types, the shapes are swapped out at the right moments instead of smoothly blended.

List (AnimationOption msg)
-> { samples : Int
, duration : Float
}
-> (Float -> Evg msg)
-> Evg msg

Creates an animation by sampling a function at regular intervals.

The function receives a value from 0.0 to 1.0 representing progress through the animation, and should return the shape at that point in time. The library generates keyframes by evaluating the function at evenly-spaced sample points.

-- A progress bar that fills from left to right:
Animate.sampled [ Animate.repeatIndefinite ]
    { samples = 10, duration = 3 }
    (\progress ->
        Evg.rect [ Evg.fillStr "green" ]
            { x = 0
            , y = 40
            , width = progress * 200
            , height = 20
            }
    )

This shows a green bar repeatedly stretching from empty to full width over three seconds.

More samples means smoother animation but larger DOM output. For most animations, 6 to 12 samples is sufficient.

Animating transforms

List (AnimationOption msg)
-> List ( Float, Easing, Float )
-> Evg msg
-> Evg msg

Spins a shape by a number of degrees. Each keyframe is ( time, easing, angle ), where angle is in degrees. Positive angles turn clockwise.

The rotation is added on top of any transforms already on the element, and it turns around the coordinate system's origin (the top-left corner). To spin a shape in place, use rotateAround.

-- A shape spinning a full turn every 2 seconds:
Evg.rect
    [ Evg.fillStr "teal" ]
    { x = 40, y = 40, width = 20, height = 20 }
    |> Animate.rotate [ Animate.repeatIndefinite ]
        [ ( 2, Animate.easeLinear, 360 ) ]

This shows a square sweeping around the top-left corner of the drawing in a wide circle.

List (AnimationOption msg)
-> ( Float, Float )
-> List ( Float, Easing, Float )
-> Evg msg
-> Evg msg

Like rotate, but spins around a chosen center point ( cx, cy ). This is the usual way to rotate a shape "in place".

-- Spin a square around its center at (50, 50):
Evg.rect
    [ Evg.fillStr "teal" ]
    { x = 40, y = 40, width = 20, height = 20 }
    |> Animate.rotateAround
        [ Animate.repeatIndefinite ]
        ( 50, 50 )
        [ ( 2, Animate.easeLinear, 360 ) ]

This shows a square smoothly turning in place, completing a revolution every two seconds.

List (AnimationOption msg)
-> List ( Float, Easing, Float )
-> Evg msg
-> Evg msg

Grows or shrinks a shape uniformly. A factor of 1 is the original size, 2 is double, 0.5 is half. Each keyframe is ( time, easing, factor ).

-- A pulsing "heartbeat" scale:
Evg.circle
    [ Evg.fillStr "crimson" ]
    { r = 20, center = ( 50, 50 ) }
    |> Animate.scale [ Animate.repeatIndefinite ]
        [ ( 0.3, Animate.easeOut, 1.3 )
        , ( 0.6, Animate.easeInOut, 1 )
        ]

This shows a circle throbbing larger and back like a heartbeat.

Note that scaling happens relative to the coordinate system origin, so shapes away from the top-left corner also drift as they grow.

List (AnimationOption msg)
-> List ( Float, Easing, ( Float, Float ) )
-> Evg msg
-> Evg msg

Like scale, but with separate horizontal and vertical factors. Each keyframe value is a pair ( sx, sy ).

Evg.rect
    [ Evg.fillStr "teal" ]
    { x = 40, y = 40, width = 20, height = 20 }
    |> Animate.scaleXY [ Animate.repeatIndefinite ]
        [ ( 1, Animate.easeInOut, ( 2, 0.5 ) ) ]

This shows a square repeatedly squashing into a wide, flat rectangle.

List (AnimationOption msg)
-> List ( Float, Easing, ( Float, Float ) )
-> Evg msg
-> Evg msg

Slides a shape by a horizontal and vertical offset. Each keyframe value is a pair ( dx, dy ), added on top of the element's own position.

-- Slide 100 to the right and back:
Evg.circle
    [ Evg.fillStr "orange" ]
    { r = 10, center = ( 20, 50 ) }
    |> Animate.translate
        [ Animate.repeatIndefinite ]
        [ ( 1, Animate.easeInOut, ( 100, 0 ) )
        , ( 2, Animate.easeInOut, ( 0, 0 ) )
        ]

This shows a circle gliding to the right and back again, over and over.

List (AnimationOption msg)
-> List ( Float, Easing, Float )
-> Evg msg
-> Evg msg

Slants a shape sideways by an angle in degrees (its top edge leans left or right). Each keyframe is ( time, easing, angle ).

List (AnimationOption msg)
-> List ( Float, Easing, Float )
-> Evg msg
-> Evg msg

Slants a shape vertically by an angle in degrees (its side edges lean up or down). Each keyframe is ( time, easing, angle ).

Motion along a path

motion : List (AnimationOption msg) -> List Path -> Evg msg -> Evg msg

Moves an element along a path over the animation's duration. By default the element also turns to face the direction it is travelling.

import Evg.Path exposing (Path(..), PathSegment(..))


-- A circle repeatedly gliding along a curve:
Evg.circle
    [ Evg.fillStr "red" ]
    { r = 5, center = ( 0, 0 ) }
    |> Animate.motion
        [ Animate.repeatIndefinite
        , Animate.duration 3
        ]
        [ M ( 10, 80 )
            [ C ( 40, 10 ) ( 65, 10 ) ( 95, 80 ) ]
        ]

This shows a small red dot swooping up and over an arc, then starting again.

Use upright, orientReverse or orientAngle to change how the element is turned as it moves.

List (AnimationOption msg)
-> List Path
-> List ( Float, Easing, Float )
-> Evg msg
-> Evg msg

Moves an element along a path like motion, but lets you control how far along the path the element is at each moment. Each keyframe is ( time, easing, progress ), where progress is a fraction from 0 (start of the path) to 1 (end).

This lets you speed up, slow down, pause or even reverse along a fixed path.

import Evg.Path exposing (Path(..), PathSegment(..))


Evg.circle
    [ Evg.fillStr "red" ]
    { r = 5, center = ( 0, 0 ) }
    |> Animate.motionKeyframes
        [ Animate.repeatIndefinite
        , Animate.duration 4
        ]
        [ M ( 10, 80 )
            [ C ( 40, 10 ) ( 65, 10 ) ( 95, 80 ) ]
        ]
        [ ( 1, Animate.easeInOut, 0.5 )
        , ( 4, Animate.easeInOut, 1 )
        ]

The dot rushes to the middle of the curve in the first second, then drifts slowly through the rest.

For motion and motionKeyframes: keep the element upright as it travels along the path, instead of turning to face its direction of movement.

For motion and motionKeyframes: turn the element to face backwards along its direction of movement.

For motion and motionKeyframes: hold the element at a fixed angle (in degrees) as it travels, regardless of the path's direction.

Sequencing

sequence : List (AnimationOption msg) -> List (Step msg) -> Evg msg -> Evg msg

Runs a list of animation steps one after another on the same element. Each step waits for the previous one to finish before it starts.

Evg.rect
    [ Evg.fillStr "teal" ]
    { x = 40, y = 40, width = 20, height = 20 }
    |> Animate.sequence [ Animate.fillFreeze ]
        [ Animate.stepTranslate
            [ Animate.duration 1 ]
            [ ( 1, Animate.easeOut, ( 40, 0 ) ) ]
        , Animate.stepRotateAround
            [ Animate.duration 1 ]
            ( 50, 50 )
            [ ( 1, Animate.easeInOut, 180 ) ]
        ]

This shows a square sliding to the right, then rotating a half turn once the slide has finished.

If you start the sequence with a trigger (beginOn Animate.click), the whole chain waits for that event and then runs step by step. Repeating an event-triggered sequence is not supported.

type Step msg

A single stage of a sequence. Build one with step, stepRotateAround, stepTranslate or stepScale.

List (AnimationOption msg)
-> String
-> List ( Float, Easing, String )
-> Step msg

A sequence step that animates a single attribute, exactly like animate.

List (AnimationOption msg)
-> ( Float, Float )
-> List ( Float, Easing, Float )
-> Step msg

A sequence step that rotates around a center point, like rotateAround.

List (AnimationOption msg)
-> List ( Float, Easing, ( Float, Float ) )
-> Step msg

A sequence step that slides the element, like translate.

List (AnimationOption msg)
-> List ( Float, Easing, Float )
-> Step msg

A sequence step that grows or shrinks the element, like scale.