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.