Skip to content

Animations

mike-ward edited this page May 17, 2026 · 1 revision

Animations

Go-gui's animation system is value-based and frame-driven. Each animation is a small struct that implements the Animation interface; the framework calls Update every frame until the animation reports it is stopped. Animations never block the view function — they run inside the render loop and push value updates into state.


The Animation interface

type Animation interface {
    ID() string
    RefreshKind() AnimationRefreshKind
    IsStopped() bool
    SetStart(t time.Time)
    Update(w *Window, dt float32, ac *AnimationCommands) bool
}

Update returns false when the animation has finished; the loop retires it. The AnimationCommands batch (ac) collects OnValue and OnDone callbacks that fire after Update returns, preventing re-entrancy into the animation loop.

RefreshKind controls how expensive the next frame is:

Constant Effect
AnimationRefreshNone No refresh
AnimationRefreshRenderOnly Repaint without rebuilding layout
AnimationRefreshLayout Full layout rebuild (default for most types)

Registering animations

Call w.AnimationAdd(a) from any event callback:

a := gui.NewTweenAnimation("my-anim", 0, 300, func(v float32, w *gui.Window) {
    gui.State[App](w).PanelWidth = v
})
w.AnimationAdd(a)

If an animation with the same ID is already running, AnimationAdd replaces it.


Tween

TweenAnimation interpolates a float32 from From to To over a fixed duration using an easing function.

a := gui.NewTweenAnimation(
    "slide-in",          // ID
    app.PanelX,          // from
    float32(300),        // to
    func(v float32, w *gui.Window) {
        gui.State[App](w).PanelX = v
    },
)
// Optional overrides:
a.Duration = 400 * time.Millisecond
a.Easing   = gui.EaseOutBack
a.OnDone   = func(w *gui.Window) { /* cleanup */ }
w.AnimationAdd(a)

Defaults: 300 ms, EaseOutCubic.


Spring

SpringAnimation uses physics-based spring simulation. Unlike Tween, it carries momentum — if you retarget mid-flight, the element continues from its current velocity.

a := gui.NewSpringAnimation("spring-x", func(v float32, w *gui.Window) {
    gui.State[App](w).PanelX = v
})
a.SpringTo(app.PanelX, float32(300))
w.AnimationAdd(a)

Retarget during motion (e.g., user dragged while animating):

a.Retarget(newTarget)

Spring presets (assign to a.Config):

Preset Character
SpringDefault Balanced (stiffness 100, damping 10)
SpringGentle Slow and soft
SpringBouncy High stiffness, low damping
SpringStiff Fast, minimal overshoot

Keyframe

KeyframeAnimation interpolates through multiple waypoints, each with its own easing.

a := gui.NewKeyframeAnimation(
    "bounce-x",
    []gui.Keyframe{
        {At: 0,    Value: 0,   Easing: gui.EaseLinear},
        {At: 0.25, Value: 300, Easing: gui.EaseOutCubic},
        {At: 0.5,  Value: 100, Easing: gui.EaseInOutQuad},
        {At: 0.75, Value: 250, Easing: gui.EaseOutBounce},
        {At: 1.0,  Value: 0,   Easing: gui.EaseOutCubic},
    },
    func(v float32, w *gui.Window) {
        gui.State[App](w).BallX = v
    },
)
a.Duration = 800 * time.Millisecond
a.Repeat   = true   // loop indefinitely
w.AnimationAdd(a)

At is a normalised time position (0.0–1.0). Each keyframe's Easing controls the curve into that waypoint from the previous one.

Default duration: 500 ms.


AnimateLayout (FLIP transition)

w.AnimateLayout(cfg) records a snapshot of current element positions and then, after the next layout rebuild, interpolates everything from old positions to new positions. Call it before the state change that triggers the layout change:

OnClick: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
    w.AnimateLayout(gui.LayoutTransitionCfg{
        Duration: 250 * time.Millisecond,
        Easing:   gui.EaseOutCubic,
    })
    // Now change state — the layout rebuild will be animated
    gui.State[App](w).ItemOrder = reorder(app.ItemOrder)
    e.IsHandled = true
},

This is the technique known as FLIP (First, Last, Invert, Play). It handles list reorders, accordion expansions, sidebar slides, and any other layout change — without writing per-element animation logic.


HeroTransition

HeroTransition morphs a shared element between two different views. Mark the element in both views with the same HeroID string; when you trigger the transition, the framework animates the element from its position in the outgoing view to its position in the incoming view.

// Trigger before switching views:
h := gui.NewHeroTransition(gui.HeroTransitionCfg{
    Duration: 300 * time.Millisecond,
    Easing:   gui.EaseOutCubic,
})
w.AnimationAdd(h)
gui.State[App](w).ActiveView = "detail"

Only one HeroTransition can be active at a time; a second call replaces the first.


Easing functions

EasingFn is func(float32) float32. All built-ins:

Function Curve
EaseLinear Constant speed
EaseInQuad / EaseOutQuad / EaseInOutQuad Quadratic
EaseInCubic / EaseOutCubic / EaseInOutCubic Cubic
EaseInBack / EaseOutBack Overshoot
EaseOutElastic Elastic spring-back
EaseOutBounce Bouncing ball
EaseCSS / EaseInCSS / EaseOutCSS / EaseInOutCSS CSS standard curves
CubicBezier(x1, y1, x2, y2) Custom cubic Bézier

Custom easing is any func(float32) float32:

a.Easing = func(t float32) float32 {
    return t * t * (3 - 2*t) // smoothstep
}

Clone this wiki locally