spring

package
v0.0.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package spring provides physics-based motion for animating a scalar value toward a target. It is the Pulse layer's bridge to the traer particle system: every Spring owns a tiny traer.ParticleSystem containing one fixed anchor (at the target) and one free particle (whose 1D position is the animated value), connected by a damped linear spring.

Reach for spring when motion needs to feel physical: button presses, list reveals, focus rings, drag inertia. For non-physical motion — fades, slides, simple colour interpolation — github.com/vibrantgio/pulse/tween is cheaper and more predictable.

The zero Options is not a usable default

Options fills its zero fields from DefaultStiffness, DefaultDamping and DefaultMass, and that combination is far softer than any UI motion wants: measured at invDt=60, a spring.New(0, 1, spring.Options{}) does not reach Settled(0.005) until frame 873 — about fifteen seconds of continuous redraw. Neither consumer in this module goes near it; motion uses k=80 and springbutton k=300, and both set all three fields. Treat Options as required rather than optional, and pick the three values together — Options.Damping is only meaningful relative to Options.Stiffness and Options.Mass, so overriding one field and inheriting the others is how a spring ends up ringing.

Defer-scoped allocation

A Spring is stateful. It must be allocated once per subscription and kept alive across emissions and frames; reconstructing it inside a per-emission map function or a per-frame layout function would reset the simulation every render. The canonical pattern — the same one every prism component uses for its interaction state — is to allocate inside an rx.Defer closure:

rx.Defer(func() rx.Observable[layout.Widget] {
    sp := spring.New(0, 0, spring.Options{Stiffness: 20, Damping: 8.94})
    return rx.Map(targets, func(target float64) layout.Widget {
        sp.SetTarget(target)
        return func(gtx layout.Context) layout.Dimensions {
            sp.Tick(math.Max(1, fps.Value/30))
            // ... render using sp.Value() ...
            if !sp.Settled(0.005) {
                gtx.Execute(op.InvalidateCmd{})
            }
            return layout.Dimensions{}
        }
    })
})

Time step

Spring.Tick takes invDt — the inverse of the simulation step, matching traer.ParticleSystem.Tick. The convention across this module is max(1, fps/30): floor the step at 1 (ensuring a dt no larger than 1 s under starvation) and otherwise pass the instantaneous frame rate divided by 30, the original Traer Physics reference rate. This trades real-time accuracy for Verlet stability when the host stutters, and it is why a spring's duration is measured in frames here rather than in seconds.

Index

Constants

View Source
const (
	DefaultStiffness = 0.4
	DefaultDamping   = 0.7
	DefaultMass      = 1.0
)

Default parameters describe a very soft, lightly underdamped spring: ζ ≈ 0.55, and slow enough at invDt=60 that Spring.Settled at tolerance 0.005 is not reached until frame 873. They are a fallback for a partially filled Options, not a recommendation. Override all three.

Variables

This section is empty.

Functions

This section is empty.

Types

type Options

type Options struct {
	// Stiffness is the spring constant k in the textbook damped-spring
	// model m·ẍ = −k·x − c·ẋ. Higher values produce faster oscillation
	// and a stiffer pull toward the target.
	Stiffness float64

	// Damping is the linear damping coefficient c. Critical damping
	// (no overshoot, fastest settle without ringing) is c = 2·√(k·m).
	// Below critical, the spring oscillates; above, it creeps.
	Damping float64

	// Mass of the free particle (m). Higher mass adds inertia,
	// slowing the response without changing the underlying frequency
	// ratio.
	Mass float64
}

Options configures a Spring at construction. Zero-valued fields are replaced with package defaults; pass explicit values for any field whose default does not match the desired feel.

type Spring

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

Spring animates a scalar value toward a target via a 1-D damped spring. The simulation is fully deterministic: identical construction arguments and identical Spring.Tick calls produce bit-identical trajectories.

func New

func New(start, target float64, opts Options) *Spring

New constructs a Spring whose value starts at start and is heading toward target. Zero-valued option fields are replaced with package defaults.

func (*Spring) SetTarget

func (s *Spring) SetTarget(target float64)

SetTarget changes the target without resetting the free particle's position or velocity, so the in-flight motion blends smoothly into the new trajectory.

func (*Spring) Settled

func (s *Spring) Settled(tolerance float64) bool

Settled reports whether the spring is at rest within tolerance: the value is within tolerance of the target AND the velocity magnitude is within tolerance of zero. Both checks are required because a damped oscillator passes through the target at peak velocity on its way to settling.

func (*Spring) Target

func (s *Spring) Target() float64

Target returns the value the spring is currently pulling toward.

func (*Spring) Tick

func (s *Spring) Tick(invDt float64) float64

Tick advances the simulation by 1/invDt seconds. It returns the activity metric reported by the underlying particle system — √(Σ |v|²) across all particles — which callers can use to decide whether to schedule another frame.

func (*Spring) Value

func (s *Spring) Value() float64

Value returns the current scalar value (the free particle's position along the spring axis).

func (*Spring) Velocity

func (s *Spring) Velocity() float64

Velocity returns the current rate of change of the value.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL