Skip to main content

animation

Package animation provides animation primitives for building smooth, physics-based animations in Drift applications.

Core Components

The animation system consists of several key components:

  • AnimationController: Drives animations over time, managing value progression from 0.0 to 1.0 with configurable duration and easing curves.

  • Tween: Interpolates between begin and end values of any type using the controller's current value. Generic tweens support float64, Color, Offset, etc.

  • Curves: Easing functions that transform linear progress into natural-feeling motion. Includes standard curves like EaseIn, EaseOut, EaseInOut.

  • SpringSimulation: Physics-based spring animation for natural bounce effects, commonly used for scroll overscroll and gesture-driven animations.

Basic Usage

Create a controller, configure a tween, and use AddListener to rebuild on changes:

// In InitState
s.controller = animation.NewAnimationController(300 * time.Millisecond)
s.controller.Curve = animation.EaseInOut
s.opacityTween = animation.TweenFloat64(0, 1)
s.controller.AddListener(func() {
s.SetState(func() {})
})
s.controller.Forward()

// In Build
opacity := s.opacityTween.Transform(s.controller)
return widgets.Opacity{Opacity: opacity, Child: child}

// In Dispose
s.controller.Dispose()

Implicit Animations

For simpler cases, use implicit animation widgets like [widgets.AnimatedContainer] or [widgets.AnimatedOpacity] which manage controllers internally.

Variables

Ease is a standard cubic bezier curve for general-purpose easing. Equivalent to CSS ease.

var Ease = CubicBezier(0.25, 0.1, 0.25, 1.0)

EaseIn starts slowly and accelerates. Use for elements exiting the screen. Equivalent to CSS ease-in.

var EaseIn = CubicBezier(0.4, 0.0, 1.0, 1.0)

EaseInOut starts and ends slowly with acceleration in the middle. Use for elements that stay on screen but change state. Equivalent to CSS ease-in-out.

var EaseInOut = CubicBezier(0.4, 0.0, 0.2, 1.0)

EaseOut starts quickly and decelerates. Use for elements entering the screen. Equivalent to CSS ease-out.

var EaseOut = CubicBezier(0.0, 0.0, 0.2, 1.0)

IOSNavigationCurve approximates iOS navigation transition easing.

var IOSNavigationCurve = CubicBezier(0.22, 1.0, 0.36, 1.0)

func CubicBezier

func CubicBezier(x1, y1, x2, y2 float64) func(float64) float64

CubicBezier returns a cubic-bezier easing function matching CSS cubic-bezier(). The parameters define the two control points (x1,y1) and (x2,y2) of the curve. The curve starts at (0,0) and ends at (1,1).

Example:

This example shows how to create a custom easing curve.

package main

import (
"fmt"

"github.com/go-drift/drift/pkg/animation"
)

func main() {
// Create a custom curve matching CSS cubic-bezier(0.4, 0.0, 0.2, 1.0)
customEase := animation.CubicBezier(0.4, 0.0, 0.2, 1.0)

// The curve transforms linear progress to eased progress
fmt.Printf("Progress 0.0 -> %.2f\n", customEase(0.0))
fmt.Printf("Progress 0.5 -> %.2f\n", customEase(0.5))
fmt.Printf("Progress 1.0 -> %.2f\n", customEase(1.0))

}

Output

Progress 0.0 -> 0.00
Progress 0.5 -> 0.78
Progress 1.0 -> 1.00

func HasActiveTickers

func HasActiveTickers() bool

HasActiveTickers returns true if any tickers are active.

func LerpAlignment

func LerpAlignment(a, b layout.Alignment, t float64) layout.Alignment

LerpAlignment linearly interpolates between two Alignment values.

func LerpColor

func LerpColor(a, b graphics.Color, t float64) graphics.Color

LerpColor linearly interpolates between two Color values.

func LerpEdgeInsets

func LerpEdgeInsets(a, b layout.EdgeInsets, t float64) layout.EdgeInsets

LerpEdgeInsets linearly interpolates between two EdgeInsets values.

func LerpFloat64

func LerpFloat64(a, b float64, t float64) float64

LerpFloat64 linearly interpolates between two float64 values.

func LerpOffset

func LerpOffset(a, b graphics.Offset, t float64) graphics.Offset

LerpOffset linearly interpolates between two Offset values.

func LerpRadius

func LerpRadius(a, b graphics.Radius, t float64) graphics.Radius

LerpRadius linearly interpolates between two Radius values.

func LinearCurve

func LinearCurve(t float64) float64

LinearCurve returns linear progress (no easing).

func Now

func Now() time.Time

Now returns the current time from the active clock.

func StepTickers

func StepTickers()

StepTickers advances all active tickers. This should be called once per frame from the engine.

type AnimationController

AnimationController drives an animation by producing values over time.

The controller manages a Value that progresses from LowerBound (default 0.0) to UpperBound (default 1.0) over the specified Duration. The Curve function transforms linear progress into eased motion.

Use Tween to map the 0-1 value to other ranges or types like colors or sizes.

Always call Dispose when done to stop the animation and release resources. See ExampleAnimationController for usage patterns.

type AnimationController struct {
// Value is the current animation value, ranging from 0.0 to 1.0.
Value float64

// Duration is the length of the animation.
Duration time.Duration

// Curve transforms linear progress (optional).
Curve func(float64) float64

// LowerBound is the minimum value (default 0.0).
LowerBound float64

// UpperBound is the maximum value (default 1.0).
UpperBound float64
// contains filtered or unexported fields
}

Example:

This example shows how to create and control an animation.

package main

import (
"fmt"
"time"

"github.com/go-drift/drift/pkg/animation"
)

func main() {
controller := animation.NewAnimationController(300 * time.Millisecond)
controller.Curve = animation.EaseOut

// Listen for value changes
controller.AddListener(func() {
fmt.Printf("Value: %.2f\n", controller.Value)
})

// Animate forward (0 -> 1)
controller.Forward()

// Later, animate in reverse (1 -> 0)
controller.Reverse()

// Clean up when done
controller.Dispose()
}

Example (Status Listener):

This example shows how to listen for animation status changes.

package main

import (
"fmt"
"time"

"github.com/go-drift/drift/pkg/animation"
)

func main() {
controller := animation.NewAnimationController(300 * time.Millisecond)

controller.AddStatusListener(func(status animation.AnimationStatus) {
switch status {
case animation.AnimationDismissed:
fmt.Println("Animation at start (0)")
case animation.AnimationForward:
fmt.Println("Animating forward")
case animation.AnimationReverse:
fmt.Println("Animating in reverse")
case animation.AnimationCompleted:
fmt.Println("Animation completed (1)")
}
})

controller.Forward()
controller.Dispose()
}

Example (With Tween):

This example shows how to use tweens with an animation controller.

package main

import (
"time"

"github.com/go-drift/drift/pkg/animation"
"github.com/go-drift/drift/pkg/graphics"
)

func main() {
controller := animation.NewAnimationController(500 * time.Millisecond)

// Create tweens to map 0-1 range to other values
sizeTween := animation.TweenFloat64(100, 200)
colorTween := animation.TweenColor(
graphics.RGB(255, 0, 0), // red
graphics.RGB(0, 0, 255), // blue
)

controller.AddListener(func() {
size := sizeTween.Transform(controller)
color := colorTween.Transform(controller)
_ = size
_ = color
})

controller.Forward()
controller.Dispose()
}

func NewAnimationController

func NewAnimationController(duration time.Duration) *AnimationController

NewAnimationController creates an animation controller with the given duration.

func (*AnimationController) AddListener

func (c *AnimationController) AddListener(fn func()) func()

AddListener adds a callback that fires whenever the value changes. Returns an unsubscribe function.

func (*AnimationController) AddStatusListener

func (c *AnimationController) AddStatusListener(fn func(AnimationStatus)) func()

AddStatusListener adds a callback that fires whenever the status changes. Returns an unsubscribe function.

func (*AnimationController) AnimateTo

func (c *AnimationController) AnimateTo(target float64)

AnimateTo animates to a specific target value.

func (*AnimationController) Dispose

func (c *AnimationController) Dispose()

Dispose cleans up resources used by the controller.

func (*AnimationController) Forward

func (c *AnimationController) Forward()

Forward animates from the current value to the upper bound (1.0).

func (*AnimationController) IsAnimating

func (c *AnimationController) IsAnimating() bool

IsAnimating returns true if the animation is currently running.

func (*AnimationController) IsCompleted

func (c *AnimationController) IsCompleted() bool

IsCompleted returns true if the animation finished at the upper bound.

func (*AnimationController) IsDismissed

func (c *AnimationController) IsDismissed() bool

IsDismissed returns true if the animation is at the lower bound.

func (*AnimationController) Reset

func (c *AnimationController) Reset()

Reset immediately sets the value to the lower bound.

func (*AnimationController) Reverse

func (c *AnimationController) Reverse()

Reverse animates from the current value to the lower bound (0.0).

func (*AnimationController) Status

func (c *AnimationController) Status() AnimationStatus

Status returns the current animation status.

func (*AnimationController) Stop

func (c *AnimationController) Stop()

Stop stops the animation at the current value.

type AnimationStatus

AnimationStatus represents the current state of an animation.

The status follows this state machine:

Forward()
Dismissed ──────────────────► Completed
▲ │
Reverse()
└──────────────────────────────┘

While animating, status is AnimationForward or AnimationReverse. When stopped, status is AnimationDismissed (at 0) or AnimationCompleted (at 1).

type AnimationStatus int
const (
// AnimationDismissed means the animation is stopped at the lower bound (0.0).
AnimationDismissed AnimationStatus = iota
// AnimationForward means the animation is playing toward the upper bound (1.0).
AnimationForward
// AnimationReverse means the animation is playing toward the lower bound (0.0).
AnimationReverse
// AnimationCompleted means the animation is stopped at the upper bound (1.0).
AnimationCompleted
)

func (AnimationStatus) String

func (s AnimationStatus) String() string

String returns a human-readable representation of the animation status.

type Clock

Clock provides time for animations. The default implementation uses system time. Tests can inject a fake clock via SetClock to control animation timing deterministically.

type Clock interface {
Now() time.Time
}

func SetClock

func SetClock(c Clock) Clock

SetClock replaces the animation clock. Returns the previous clock so callers can restore it during cleanup.

type SpringDescription

SpringDescription describes the physical properties of a spring for physics-based animations.

Springs create natural-feeling motion that responds to velocity and can overshoot the target before settling. They're ideal for gesture-driven animations, scroll overscroll effects, and transitions that should feel physically plausible.

The DampingRatio controls oscillation: <1.0 bounces, =1.0 is critically damped, >1.0 is overdamped. Use IOSSpring or BouncySpring for common configurations.

See ExampleSpringSimulation for usage patterns.

type SpringDescription struct {
// Stiffness controls how quickly the spring returns to rest (higher = faster).
Stiffness float64
// DampingRatio controls energy dissipation.
// 1.0 = critically damped (no overshoot)
// < 1.0 = underdamped (bouncy)
// > 1.0 = overdamped (sluggish)
DampingRatio float64
}

func BouncySpring

func BouncySpring() SpringDescription

BouncySpring returns slightly underdamped spring for a subtle bounce effect.

func IOSSpring

func IOSSpring() SpringDescription

IOSSpring returns spring parameters that approximate iOS scroll bounce.

type SpringSimulation

SpringSimulation simulates a damped spring moving toward a target position.

Unlike AnimationController which uses duration-based timing, SpringSimulation uses physics to determine motion. The simulation accounts for initial velocity, making it ideal for gesture-driven animations where the user "throws" an element.

The simulation automatically handles numerical stability by sub-stepping large time deltas. See ExampleSpringSimulation for usage patterns.

type SpringSimulation struct {
// contains filtered or unexported fields
}

Example:

This example shows how to use spring physics for natural motion.

package main

import (
"fmt"

"github.com/go-drift/drift/pkg/animation"
)

func main() {
// Create a bouncy spring simulation
spring := animation.BouncySpring()
sim := animation.NewSpringSimulation(
spring,
0, // current position
500, // initial velocity (e.g., from a fling gesture)
300, // target position
)

// Step the simulation (typically done each frame)
dt := 0.016 // ~60fps
for !sim.IsDone() {
done := sim.Step(dt)
_ = sim.Position()
_ = sim.Velocity()
if done {
break
}
}

fmt.Printf("Final position: %.0f\n", sim.Position())

}

Output

Final position: 300

func NewSpringSimulation

func NewSpringSimulation(spring SpringDescription, position, velocity, target float64) *SpringSimulation

NewSpringSimulation creates a spring simulation from current position/velocity to target.

func (*SpringSimulation) IsDone

func (s *SpringSimulation) IsDone() bool

IsDone returns true if the simulation has settled at the target.

func (*SpringSimulation) Position

func (s *SpringSimulation) Position() float64

Position returns the current position.

func (*SpringSimulation) Step

func (s *SpringSimulation) Step(dt float64) bool

Step advances the simulation by dt seconds. Returns true if the simulation has settled (is done).

func (*SpringSimulation) Target

func (s *SpringSimulation) Target() float64

Target returns the target position.

func (*SpringSimulation) Velocity

func (s *SpringSimulation) Velocity() float64

Velocity returns the current velocity.

type Ticker

Ticker calls a callback on each frame while active.

Ticker is the low-level timing primitive used by AnimationController. Most code should use AnimationController directly rather than Ticker.

The callback receives the elapsed time since Start was called. Tickers are driven by the engine's frame loop via StepTickers.

type Ticker struct {
// contains filtered or unexported fields
}

func NewTicker

func NewTicker(callback func(elapsed time.Duration)) *Ticker

NewTicker creates a new ticker with the given callback.

func (*Ticker) Elapsed

func (t *Ticker) Elapsed() time.Duration

Elapsed returns the time since the ticker started.

func (*Ticker) IsActive

func (t *Ticker) IsActive() bool

IsActive returns whether the ticker is currently running.

func (*Ticker) Start

func (t *Ticker) Start()

Start activates the ticker.

func (*Ticker) Stop

func (t *Ticker) Stop()

Stop deactivates the ticker.

type TickerProvider

TickerProvider creates tickers.

type TickerProvider interface {
CreateTicker(callback func(time.Duration)) *Ticker
}

type Tween

Tween interpolates between Begin and End values based on animation progress.

Tween maps the 0-1 range of an AnimationController to any value range or type. Use the helper constructors (TweenFloat64, TweenColor, TweenOffset) for common types, or create custom tweens with a Lerp function.

See ExampleTween and ExampleTween_customType for usage patterns.

type Tween[T any] struct {
// Begin is the starting value (when t = 0).
Begin T
// End is the ending value (when t = 1).
End T
// Lerp linearly interpolates between Begin and End. Receives the begin value,
// end value, and progress t in [0, 1]. Returns the interpolated value.
Lerp func(a, b T, t float64) T
}

Example:

This example shows how to create a tween for basic interpolation.

package main

import (
"fmt"

"github.com/go-drift/drift/pkg/animation"
"github.com/go-drift/drift/pkg/graphics"
)

func main() {
// Create tweens for different value types
opacity := animation.TweenFloat64(0.0, 1.0)
position := animation.TweenOffset(
graphics.Offset{X: 0, Y: 0},
graphics.Offset{X: 100, Y: 50},
)

// Evaluate at different progress values
fmt.Printf("Opacity at 0.5: %.1f\n", opacity.Evaluate(0.5))
fmt.Printf("Position at 1.0: (%.0f, %.0f)\n", position.Evaluate(1.0).X, position.Evaluate(1.0).Y)

}

Output

Opacity at 0.5: 0.5
Position at 1.0: (100, 50)

Example (Custom Type):

This example shows how to create a custom tween with a Lerp function.

package main

import (
"fmt"

"github.com/go-drift/drift/pkg/animation"
)

func main() {
type Point struct {
X, Y float64
}

pointTween := &animation.Tween[Point]{
Begin: Point{0, 0},
End: Point{100, 200},
Lerp: func(a, b Point, t float64) Point {
return Point{
X: a.X + (b.X-a.X)*t,
Y: a.Y + (b.Y-a.Y)*t,
}
},
}

midpoint := pointTween.Evaluate(0.5)
fmt.Printf("Midpoint: (%.0f, %.0f)\n", midpoint.X, midpoint.Y)

}

Output

Midpoint: (50, 100)

func TweenAlignment

func TweenAlignment(begin, end layout.Alignment) *Tween[layout.Alignment]

TweenAlignment creates a tween for Alignment values.

func TweenColor

func TweenColor(begin, end graphics.Color) *Tween[graphics.Color]

TweenColor creates a tween for Color values.

func TweenEdgeInsets

func TweenEdgeInsets(begin, end layout.EdgeInsets) *Tween[layout.EdgeInsets]

TweenEdgeInsets creates a tween for EdgeInsets values.

func TweenFloat64

func TweenFloat64(begin, end float64) *Tween[float64]

TweenFloat64 creates a tween for float64 values.

func TweenOffset

func TweenOffset(begin, end graphics.Offset) *Tween[graphics.Offset]

TweenOffset creates a tween for Offset values.

func TweenRadius

func TweenRadius(begin, end graphics.Radius) *Tween[graphics.Radius]

TweenRadius creates a tween for Radius values.

func (*Tween[T]) Evaluate

func (tw *Tween[T]) Evaluate(t float64) T

Evaluate returns the interpolated value at t (0.0 to 1.0).

func (*Tween[T]) Transform

func (tw *Tween[T]) Transform(controller *AnimationController) T

Transform returns the interpolated value using the controller's current value.

Generated by gomarkdoc