Documentation
¶
Overview ¶
Package motion provides enter/exit/transition primitives for animating widgets into, out of, and between visual states.
Each primitive composes github.com/vibrantgio/pulse/tween (deterministic, frame-indexed opacity) and github.com/vibrantgio/pulse/spring (physics-driven scale): opacity provides predictable timing, spring provides physical character. The two run on independent timescales — opacity finishes at frame Frames, scale settles when the spring's restoring force balances out.
Composition with widgets ¶
Apply is the bridge from a State to a Gio layout.Widget. It records the widget once (to obtain its layout footprint), then replays the recorded ops inside an op.Affine scale-around-centre transform and a paint.PushOpacity layer. The widget is laid out exactly once; the visual transformation is purely a paint-time effect.
enter := motion.NewEnter(motion.Options{})
for !enter.Settled(0.005) {
enter.Tick(60) // 60 Hz frame loop
motion.Apply(gtx, enter.State(), buttonWidget)
}
Variant pattern ¶
Pulse never decorates prism globally. A motion-aware component is an explicit variant, exported alongside its prism counterpart and chosen by name at the call site, so that reading the call tells you the component animates and the dependency runs pulse → prism and never back. Apply is the mechanism: wrap a prism render function with it.
bw := button.Render(shaper, label, colors, sp, rad, labelStyle, density, btnState) motion.Apply(gtx, primitive.State(), bw)
This package ships only the primitives. The one variant that exists today is github.com/vibrantgio/pulse/springbutton, and it is built on spring directly rather than on Apply.
The defaults do not line up ¶
DefaultFrames and DefaultSpring were meant to finish together and do not. Measured with invDt=60: NewEnter on a zero Options has an opacity tween complete at frame 30, and its scale spring does not reach Settled(0.005) until frame 52 — scale is still 0.991 when the fade ends. A caller looping until Enter.Settled therefore runs 22 frames past the visible end of the animation, and a caller that stops at Frames leaves the widget fractionally undersized.
Options.Spring falls back to DefaultSpring only when the whole struct is zero, but since FX.2 that distinction has no teeth: github.com/vibrantgio/pulse/spring's own defaults are the same SpringDefault values (Stiffness 80, Mass 1), and a zero Damping derives critical damping 2·√(k·m) from the resolved fields. A partial override — Spring: spring.Options{Stiffness: 200} — is therefore a critically damped k=200 spring, not the ζ ≈ 0.02 ringer it used to be.
Index ¶
Constants ¶
const ( // DefaultFromScale is the scale a widget starts at on Enter (and // returns to on Exit). DefaultFromScale = 0.85 )
Defaults applied when a corresponding Options field is zero-valued. Since E3.1 the timing defaults resolve from the spectrum motion tokens (tokens.Motion, the value every theme.Theme.Motion emits by default) rather than from local constants.
Variables ¶
var DefaultFrames = FramesAt(tokens.Motion.DurXSlow, defaultFPS)
DefaultFrames is the opacity-tween duration in frames: the motion scale's slowest stop (DurXSlow, MD3 long2 = 500 ms) at the 60 Hz reference rate — 30 frames, the same count as the constant it replaced, because E3.1 pinned DurXSlow to the 500 ms this package already animated over.
var DefaultSpring = SpringOptions(tokens.Motion.SpringDefault)
DefaultSpring is the spring.Options used when Options.Spring is zero-valued — and only when it is entirely zero-valued. It is the motion scale's SpringDefault preset: critically damped (c = 2·√(k·m)) at k=80, m=1 — a brisk, no-overshoot curve that reaches Settled(0.005) at frame 52 with invDt=60, which is 22 frames past DefaultFrames rather than level with it.
var Hidden = State{Opacity: 0, Scale: DefaultFromScale}
Hidden is the canonical end-state of an Exit and the starting state of an Enter: fully transparent, scaled down to DefaultFromScale.
var Visible = State{Opacity: 1, Scale: 1}
Visible is the canonical end-state of an Enter and the starting state of an Exit: fully opaque, identity scale.
Functions ¶
func Apply ¶
Apply renders w with the visual transformation s applied: an op.Affine scale around the widget's centre and a paint.PushOpacity layer. Returns w's natural layout dimensions (the visual scale does not change the widget's footprint).
w is laid out exactly once: Apply records w into a macro to obtain its dimensions, then replays the macro inside the transform/opacity stack. Dimensions therefore stay stable across an animation, so surrounding layout does not jitter.
Apply does not short-circuit on Opacity == 0 — the widget is still laid out and its ops are still recorded so the returned dimensions remain the true widget footprint at every frame.
func FramesAt ¶ added in v0.0.11
FramesAt converts a tokens.MotionScale duration stop into a whole frame count at the given frame rate, rounding to nearest. It is how a theme-driven caller derives Options.Frames from its Theme.Motion snapshot:
opts := motion.Options{Frames: motion.FramesAt(m.DurNormal, 60)}
func SpringOptions ¶ added in v0.0.11
SpringOptions converts a tokens.Spring preset from the theme's motion scale into spring.Options for this package and its siblings.
Types ¶
type Enter ¶
type Enter struct {
// contains filtered or unexported fields
}
Enter animates a widget from Hidden to Visible. Construct with NewEnter, advance with Enter.Tick, query with Enter.State or Enter.Settled.
func (*Enter) Frame ¶
Frame returns the current frame index (0 before any Enter.Tick).
func (*Enter) Settled ¶
Settled reports whether the animation has reached Visible within tolerance: the opacity tween has finished AND the scale spring is at rest at 1.0.
func (*Enter) Tick ¶
Tick advances the animation by one simulation step. invDt mirrors spring.Spring.Tick: the DESIGN-recommended frame loop convention is max(1, fps/30).
type Exit ¶
type Exit struct {
// contains filtered or unexported fields
}
Exit animates a widget from Visible to Hidden. Mirrors Enter.
type Options ¶
type Options struct {
// Frames is the opacity-tween duration in frames. Zero defaults to
// [DefaultFrames]. The scale spring runs alongside on its own
// physical timescale.
Frames int
// Spring is the [spring.Options] for the scale animation. Only the
// wholly zero value is replaced with [DefaultSpring]; a partly
// filled value goes to [spring.New] as-is, whose own defaults have
// matched [DefaultSpring] since FX.2 (zero Stiffness/Mass fill with
// 80/1, zero Damping derives critical damping for the resolved
// values), so a partial override stays critically damped at the
// stiffness it names.
Spring spring.Options
// FromScale is the scale at the Hidden end of the animation. Zero
// defaults to [DefaultFromScale].
FromScale float64
}
Options configures a single motion primitive. Zero-valued fields are replaced with package defaults at construction time, so the zero Options value produces a working primitive with the canonical feel.
type State ¶
type State struct {
// Opacity in [0,1]: 0 = fully transparent, 1 = fully opaque. Drives
// a [paint.PushOpacity] layer in [Apply].
Opacity float64
// Scale factor: 1.0 = identity. Drives an [op.Affine] scale around
// the widget's centre in [Apply], so 0.85 shrinks the widget toward
// its midpoint without changing its layout footprint.
Scale float64
}
State captures the visual transformation applied to a widget at one instant of an animation. Both fields are normalised: Opacity in [0,1], Scale as a multiplier (1.0 = identity).
type Transition ¶
type Transition struct {
// contains filtered or unexported fields
}
Transition cross-fades between two widgets — an outgoing widget that runs an Exit and an incoming widget that runs an Enter in parallel, both ticked together.
func NewTransition ¶
func NewTransition(opts Options) *Transition
NewTransition constructs a Transition with opts. Both Exit and Enter halves use the same opts.
func (*Transition) Frame ¶
func (t *Transition) Frame() int
Frame returns the current frame index (incremented once per Transition.Tick).
func (*Transition) In ¶
func (t *Transition) In() State
In returns the incoming widget's current state.
func (*Transition) Out ¶
func (t *Transition) Out() State
Out returns the outgoing widget's current state.
func (*Transition) Settled ¶
func (t *Transition) Settled(tol float64) bool
Settled reports whether both halves have reached their respective resting states within tolerance.
func (*Transition) Tick ¶
func (t *Transition) Tick(invDt float64)
Tick advances both halves by one simulation step.