# Springs
Physics-based animation creates motion that feels organic, natural, and inherently interactive. Unlike a [Tween](tween.md), a `Spring` has **no fixed duration**. Instead, it uses a damped harmonic oscillator simulation — the same mathematics that governs a ball on a rubber band.
You set a `target`, and the spring pulls the value toward that target based on its tension and friction (stiffness and damping). The result is motion that **overshoots**, **bounces**, and **settles** — just like real physics.
---
## Creating a Spring
The easiest way to create a spring is using one of the 4 built-in presets:
```rust
use spanda::spring::{Spring, SpringConfig};
let mut spring = Spring::new(SpringConfig::wobbly());
```
### Presets
| `gentle()` | 60 | 14 | Slow, smooth — great for background elements |
| `wobbly()` | 180 | 12 | Bouncy, playful — great for interactive UI |
| `stiff()` | 210 | 20 | Fast, minimal bounce — great for snappy responses |
| `slow()` | 37 | 14 | Very relaxed, lazy — great for ambient motion |
### Starting Position
By default, a spring starts at position `0.0`. Use `with_position()` to start elsewhere:
```rust
let mut spring = Spring::new(SpringConfig::gentle())
.with_position(50.0); // Start at 50
```
---
## Moving the Spring
To animate the spring, change its target. It immediately begins accelerating toward the new destination, **preserving its current velocity**. This is what makes springs perfect for interactive UI elements — you can retarget mid-flight without jarring transitions:
```rust
use spanda::traits::Update;
spring.set_target(100.0);
// In your render loop:
spring.update(dt);
let current_pos = spring.position();
// render(current_pos);
// User clicks elsewhere? Retarget instantly:
spring.set_target(250.0); // velocity carries over smoothly
```
---
## Settle Detection
Because springs never *truly* stop mathematically (they approach the target asymptotically), spanda uses an `epsilon` threshold. Once the spring's **velocity** and **distance** from the target both fall below `epsilon`, the spring is:
1. **Clamped** to the exact target value (no sub-pixel jitter)
2. **Velocity zeroed** out
3. Marked as **settled**
```rust
if spring.is_settled() {
println!("Spring has stopped moving.");
}
```
The default `epsilon` is `0.001`. For pixel-based animations, you might want a slightly larger value (e.g., `0.5`) to settle faster.
---
## Custom Spring Configs
If the presets don't quite fit, define your own exact physics parameters:
```rust
let custom_config = SpringConfig {
stiffness: 150.0, // "Tension" — higher = faster pull toward target
damping: 10.0, // "Friction" — higher = less bounce, settles faster
mass: 1.0, // "Weight" — higher = slower acceleration
epsilon: 0.001, // Rest threshold — lower = more precise but slower to settle
};
let spring = Spring::new(custom_config);
```
### Parameter Guide
| **Stiffness** | Slow, lazy pull | Snappy, fast pull |
| **Damping** | More bounce, oscillation | Less bounce, direct path |
| **Mass** | Quick to accelerate | Sluggish, heavy feel |
| **Epsilon** | Very precise settling | Faster settling (less precise) |
### Understanding the Physics
The spring uses the **damped harmonic oscillator** equation:
```
acceleration = (-stiffness × displacement - damping × velocity) / mass
```
- **Stiffness** is the "pull force" — how strongly the spring pulls toward the target
- **Damping** is the "friction" — how quickly oscillation dies down
- **Mass** is the "inertia" — how resistant the spring is to acceleration
---
## Sub-Stepping (Stability)
Large `dt` values (e.g., when a browser tab is inactive or a game hitches) can cause springs to "explode" — velocity grows without bound. Spanda prevents this with automatic **sub-stepping**:
- The maximum internal step size is `1/120` seconds (120 Hz)
- If `dt` is larger, it's broken into multiple smaller steps
- This guarantees **numerical stability** even with `dt` spikes of 1+ seconds
You don't need to do anything to enable this — it's always active.
---
## NaN Safety
Springs guard against degenerate configurations:
| `stiffness = 0.0` | Spring snaps directly to target, no oscillation |
| Negative `dt` | Treated as `0.0` — no backward time |
| Position is `NaN` | *Should not occur* due to sub-stepping. If it does, `debug_assert!` will catch it in debug builds |
---
## Springs vs. Easing-Based Tweens
| **Duration** | Fixed (you specify it) | Dynamic (settles naturally) |
| **Retargeting** | Must reset and create a new tween | Call `.set_target()` mid-flight |
| **Overshoot** | Only with certain easings (Back, Elastic) | Natural and physically correct |
| **Interactivity** | Awkward — "cancel and restart" | Seamless — velocity preserves momentum |
| **Predictability** | Exact timing, exact progress | Approximate (settled within epsilon) |
**Use tweens when**: you need exact timing (e.g., a 0.3s fade-in, a loading bar).
**Use springs when**: you need responsiveness (e.g., a slider thumb, a tooltip following the cursor, drag interactions).
---
## Key Methods
| `Spring::new(config)` | Create a new spring at position `0.0` |
| `.with_position(pos)` | Builder — set starting position |
| `.set_target(target)` | Set a new target (spring begins moving immediately) |
| `.position()` | Current position |
| `.velocity()` | Current velocity |
| `.target()` | Current target value |
| `.is_settled()` | Whether the spring has settled within `epsilon` |
| `.reset()` | Reset position and velocity to `0.0` |
| `.update(dt)` | Advance physics by `dt` seconds (returns `false` when settled) |
---
## Multi-Dimensional Springs: `SpringN<T>`
`Spring` animates a single `f32`. For 2D, 3D, or 4D (RGBA) values, use `SpringN`:
```rust
use spanda::spring::{SpringN, SpringConfig};
use spanda::traits::Update;
// 2D position spring
let mut spring = SpringN::new(SpringConfig::wobbly(), [0.0_f32, 0.0]);
spring.set_target([100.0, 200.0]);
for _ in 0..1000 {
spring.update(1.0 / 60.0);
}
let pos = spring.position(); // [f32; 2]
```
### Supported Types
| `f32` | Single axis (opacity, rotation, size) |
| `[f32; 2]` | 2D position, UV coordinates |
| `[f32; 3]` | 3D position, RGB colour |
| `[f32; 4]` | RGBA colour, quaternion-like |
### Custom Types
Implement `SpringAnimatable` on your own types:
```rust
use spanda::spring::SpringAnimatable;
#[derive(Clone)]
struct Point { x: f32, y: f32 }
impl SpringAnimatable for Point {
fn to_components(&self) -> Vec<f32> {
vec![self.x, self.y]
}
fn from_components(c: &[f32]) -> Self {
Point { x: c[0], y: c[1] }
}
}
```
### SpringN Key Methods
| `SpringN::new(config, initial)` | Create with initial value |
| `.set_target(value)` | Set a new target (velocity preserved) |
| `.position()` | Current value as `T` |
| `.position_components()` | Raw per-component positions |
| `.velocity_components()` | Raw per-component velocities |
| `.is_settled()` | Whether all components are within epsilon |
| `.reset()` | Reset all positions/velocities to zero |
| `.update(dt)` | Advance physics (returns `false` when settled) |
---
## Bevy Integration
With the `bevy` feature, `Spring` is a Bevy `Component`. The `SpandaPlugin` automatically ticks all Spring components using Bevy's `Time` resource and fires `SpringSettled` events when they come to rest:
```rust
use spanda::integrations::bevy::{SpandaPlugin, SpringSettled};
commands.spawn((
SpriteBundle { /* ... */ },
Spring::new(SpringConfig::wobbly()),
));
// Listen for settled events
fn on_rest(mut events: EventReader<SpringSettled>) {
for ev in events.read() {
println!("Spring on {:?} settled", ev.entity);
}
}
```