Skip to content

Latest commit

 

History

History
358 lines (276 loc) · 12 KB

File metadata and controls

358 lines (276 loc) · 12 KB

Animation

SLT animations are standalone structs, not Context methods. Each animation computes an f64 value from tick counts. You pass the computed value to style or layout methods yourself.

let mut fade = Tween::new(0.0, 1.0, 30).easing(ease_out_quad);
fade.reset(ui.tick());

// Each frame: compute the value, use it however you want
let opacity = fade.value(ui.tick());
ui.text("Hello").fg(Color::Rgb(255, 255, (255.0 * opacity) as u8));

Tick-based model

Animations run on frame ticks, not wall-clock time.

  • ui.tick() returns a u64 frame counter that increments every render frame.
  • RunConfig::default().tick_rate(Duration::from_millis(16)) controls the polling interval (default 16ms / ~60fps).
  • All animation types use reset(tick) to set a start time and value(tick) to sample.
  • Lower tick_rate = smoother animations but more CPU usage.

Animation types

Tween

Linear interpolation from A to B over a fixed number of ticks.

use slt::{Tween, Context, Color};
use slt::anim::ease_out_quad;

let mut tween = Tween::new(0.0, 100.0, 60)
    .easing(ease_out_quad);

slt::run(|ui: &mut Context| {
    if ui.tick() == 0 {
        tween.reset(ui.tick());
    }
    let x = tween.value(ui.tick()) as u16;
    ui.text("sliding").ml(x);
});

Key methods:

  • Tween::new(from, to, duration_ticks) — constructor, linear easing by default
  • .easing(fn) — set easing function (builder)
  • .on_complete(fn) — callback when done (builder)
  • .reset(tick) — start/restart the tween
  • .value(tick) -> f64 — sample current value
  • .is_done() -> bool — completion check

Spring

Physics-based damped harmonic oscillator. Unlike Tween, Spring has no fixed duration -- it settles naturally based on stiffness and damping.

use slt::{Spring, Context};

let mut spring = Spring::new(0.0, 0.2, 0.85);

slt::run(|ui: &mut Context| {
    let hovered = ui.button("Hover me").hovered;
    spring.set_target(if hovered { 10.0 } else { 0.0 });
    spring.tick();

    let offset = spring.value() as u16;
    ui.text("bouncy").ml(offset);
});

Key methods:

  • Spring::new(initial, stiffness, damping) — constructor
    • stiffness: acceleration per unit displacement (0.1..0.5)
    • damping: per-tick velocity multiplier, must satisfy 0.0 < damping < 1.0 (0.8..0.95). Both bounds are enforced via debug_assert! in Spring::new (v0.19.1); release builds do not panic but values outside this range conserve or amplify energy and never settle.
  • .on_settle(fn) — callback when settled (builder)
  • .set_target(value) — change the goal position (interactive use)
  • .tick() — advance simulation by one frame (call once per frame)
  • .value() -> f64 — current position
  • .is_settled() -> bool — true when velocity and distance are both < 0.01

Spring does not use reset(tick). Call .tick() every frame and .set_target() to change direction.

Damping note: This damping is not the standard ODE damping ratio ζ — it is a velocity multiplier applied each tick (velocity *= damping after the spring force). A value of 1.0 would conserve energy (eternal oscillation); > 1.0 would amplify it. The recommended 0.80..=0.95 range covers fast-settle to slow-bouncy UI feel.

Keyframes

Multi-stop timeline animation, like CSS @keyframes. Each segment between stops can use its own easing.

use slt::anim::{Keyframes, LoopMode, ease_out_quad, ease_in_cubic};

let mut kf = Keyframes::new(90)
    .stop(0.0, 0.0)       // start at 0
    .stop(0.3, 100.0)     // ramp up to 100 at 30%
    .stop(0.7, 100.0)     // hold at 100 until 70%
    .stop(1.0, 40.0)      // ease down to 40
    .segment_easing(0, ease_out_quad)
    .segment_easing(2, ease_in_cubic)
    .loop_mode(LoopMode::PingPong);

kf.reset(ui.tick());
let brightness = kf.value(ui.tick());

Key methods:

  • Keyframes::new(duration_ticks) — constructor
  • .stop(position, value) — add a stop at normalized position [0.0, 1.0]. Stops are kept sorted by position after every call, so the order in which you append them does not matter — a .stop(0.7, 100.0) after .stop(1.0, 40.0) lands in the right slot. Code that relied on insertion order to identify segments will see the sorted-by-time order instead.
  • .easing(fn) — default easing for all segments
  • .segment_easing(index, fn) — override easing for segment index (0 = first-to-second stop). Out-of-range indices are silently ignored in release builds (preserving the panic-free guarantee for runtime code) and trigger a debug_assert! panic in debug builds (v0.19.1) so builder-order mistakes — calling segment_easing(2, ...) before three stops have been added — surface during development.
  • .loop_mode(mode) — set loop behavior
  • .on_complete(fn) — callback when done
  • .reset(tick) — start/restart
  • .value(tick) -> f64 — sample current value
  • .is_done() -> bool — completion check (always false for looping modes)

Sequence

Chain multiple tween segments end-to-end into a single timeline.

use slt::anim::{Sequence, LoopMode, ease_linear, ease_out_quad, ease_in_cubic};

let mut seq = Sequence::new()
    .then(0.0, 100.0, 30, ease_out_quad)   // slide right
    .then(100.0, 100.0, 10, ease_linear)   // pause
    .then(100.0, 0.0, 20, ease_in_cubic)   // slide back
    .loop_mode(LoopMode::Repeat);

seq.reset(ui.tick());
let x = seq.value(ui.tick());

Key methods:

  • Sequence::new() — constructor
  • .then(from, to, duration_ticks, easing) — append a segment
  • .loop_mode(mode) — set loop behavior
  • .on_complete(fn) — callback when done
  • .reset(tick) — start/restart
  • .value(tick) -> f64 — sample current value
  • .is_done() -> bool — completion check

Stagger

Apply the same tween to N items with a fixed delay between each start.

use slt::anim::{Stagger, LoopMode, ease_out_quad};

let items = vec!["Alpha", "Beta", "Gamma", "Delta"];
let mut stagger = Stagger::new(0.0, 1.0, 20)
    .easing(ease_out_quad)
    .delay(5)
    .items(items.len())
    .loop_mode(LoopMode::Once);

stagger.reset(ui.tick());

for (i, label) in items.iter().enumerate() {
    let opacity = stagger.value(ui.tick(), i);
    let gray = (255.0 * opacity) as u8;
    ui.text(*label).fg(Color::Rgb(gray, gray, gray));
}

Key methods:

  • Stagger::new(from, to, duration_ticks) — constructor
  • .easing(fn) — easing for each item's tween
  • .delay(ticks) — ticks between consecutive item starts
  • .items(count) — set item count (inferred from usage if not set)
  • .loop_mode(mode) — set loop behavior
  • .on_complete(fn) — callback when done
  • .reset(tick) — start/restart
  • .value(tick, item_index) -> f64 — sample value for a specific item
  • .is_done() -> bool — true if the most recently sampled item finished
  • .is_all_done(tick, item_count) -> bool — true once every item has finished, computed from pure tick arithmetic (v0.19.1). Use this when you need a single completion signal that does not depend on which item happened to be sampled last. With LoopMode::Repeat / PingPong it only reports true for the first cycle (loops re-enter after completion).

Easing functions

All easing functions have signature fn(f64) -> f64, mapping [0, 1] to [0, 1].

Function Curve Use case
ease_linear Constant rate Default, mechanical motion
ease_in_quad Slow start Accelerating elements
ease_out_quad Slow end Decelerating, natural stops
ease_in_out_quad Slow both ends Smooth transitions
ease_in_cubic Slower start Stronger acceleration
ease_out_cubic Slower end Stronger deceleration
ease_in_out_cubic Slower both ends Emphasis on middle speed
ease_out_elastic Overshoot + oscillate Attention-grabbing, playful
ease_out_bounce Bouncing ball Landing, drop effects

Helper: lerp(a, b, t) — linear interpolation, not clamped. Apply easing to t before calling.

use slt::anim::{lerp, ease_out_quad};

let t = ease_out_quad(0.5);
let value = lerp(0.0, 100.0, t); // ~75.0

LoopMode

Controls what happens when an animation reaches its end.

Mode Behavior
LoopMode::Once Play once, hold at final value. is_done() returns true.
LoopMode::Repeat Restart from the beginning each cycle.
LoopMode::PingPong Alternate forward and backward each cycle.

Applies to Keyframes, Sequence, and Stagger. Tween always plays once (use Sequence with LoopMode::Repeat for looping tweens). Spring has no loop mode -- it settles naturally.

Common patterns

Fade-in on mount

let mut fade = Tween::new(0.0, 1.0, 30).easing(ease_out_quad);
let mut started = false;

slt::run(|ui: &mut Context| {
    if !started {
        fade.reset(ui.tick());
        started = true;
    }
    let alpha = fade.value(ui.tick());
    let g = (255.0 * alpha) as u8;
    ui.text("Welcome").fg(Color::Rgb(g, g, g));
});

Button hover spring

let mut spring = Spring::new(0.0, 0.3, 0.8);

slt::run(|ui: &mut Context| {
    let btn = ui.button("Click me");
    spring.set_target(if btn.hovered { 2.0 } else { 0.0 });
    spring.tick();

    let pad = spring.value() as u16;
    ui.text(">>").ml(pad);
});

Staggered list entry

let items = vec!["One", "Two", "Three", "Four", "Five"];
let mut stagger = Stagger::new(0.0, 1.0, 15)
    .easing(ease_out_cubic)
    .delay(3)
    .items(items.len());

let mut started = false;

slt::run(|ui: &mut Context| {
    if !started {
        stagger.reset(ui.tick());
        started = true;
    }
    for (i, item) in items.iter().enumerate() {
        let t = stagger.value(ui.tick(), i);
        let brightness = (255.0 * t) as u8;
        ui.text(*item).fg(Color::Rgb(brightness, brightness, brightness));
    }
});

Loading sequence (multi-phase)

let mut loading = Keyframes::new(120)
    .stop(0.0, 0.0)     // idle
    .stop(0.25, 100.0)  // fill bar
    .stop(0.5, 100.0)   // hold
    .stop(0.75, 0.0)    // reset
    .stop(1.0, 0.0)     // idle
    .easing(ease_in_out_quad)
    .loop_mode(LoopMode::Repeat);

loading.reset(ui.tick());
let pct = loading.value(ui.tick());
ui.text(format!("[{:>3.0}%]", pct));

Chained transitions

let mut chain = Sequence::new()
    .then(0.0, 50.0, 20, ease_out_quad)    // move to center
    .then(50.0, 50.0, 30, ease_linear)     // hold
    .then(50.0, 100.0, 20, ease_in_cubic); // move to end

chain.reset(ui.tick());
let pos = chain.value(ui.tick()) as u16;
ui.text("->").ml(pos);

API quick reference

Tween

Constructor Tween::new(from, to, duration_ticks)
Builder .easing(fn), .on_complete(fn)
Control .reset(tick)
Sample .value(tick) -> f64
Done .is_done() -> bool

Spring

Constructor Spring::new(initial, stiffness, damping)
Builder .on_settle(fn)
Control .set_target(value), .tick()
Sample .value() -> f64
Done .is_settled() -> bool

Keyframes

Constructor Keyframes::new(duration_ticks)
Builder .stop(pos, val), .easing(fn), .segment_easing(idx, fn), .loop_mode(mode), .on_complete(fn)
Control .reset(tick)
Sample .value(tick) -> f64
Done .is_done() -> bool

Sequence

Constructor Sequence::new()
Builder .then(from, to, ticks, easing), .loop_mode(mode), .on_complete(fn)
Control .reset(tick)
Sample .value(tick) -> f64
Done .is_done() -> bool

Stagger

Constructor Stagger::new(from, to, duration_ticks)
Builder .easing(fn), .delay(ticks), .items(count), .loop_mode(mode), .on_complete(fn)
Control .reset(tick)
Sample .value(tick, item_index) -> f64
Done .is_done() -> bool (last-sampled item), .is_all_done(tick, item_count) -> bool (every item)

Related docs