Skip to main content

slt/
anim.rs

1//! Animation primitives: tweens, springs, keyframes, sequences, and staggers.
2//!
3//! All animations are tick-based — call the `value()` method each frame with
4//! the current [`Context::tick`](crate::Context::tick) to advance. No timers
5//! or threads involved.
6
7use std::f64::consts::PI;
8
9/// Linear interpolation between `a` and `b` at position `t` (0.0..=1.0).
10///
11/// Values of `t` outside `[0, 1]` are not clamped; use an easing function
12/// first if you need clamping.
13pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
14    a + (b - a) * t
15}
16
17/// Linear easing: constant rate from 0.0 to 1.0.
18pub fn ease_linear(t: f64) -> f64 {
19    clamp01(t)
20}
21
22/// Quadratic ease-in: slow start, fast end.
23pub fn ease_in_quad(t: f64) -> f64 {
24    let t = clamp01(t);
25    t * t
26}
27
28/// Quadratic ease-out: fast start, slow end.
29pub fn ease_out_quad(t: f64) -> f64 {
30    let t = clamp01(t);
31    1.0 - (1.0 - t) * (1.0 - t)
32}
33
34/// Quadratic ease-in-out: slow start, fast middle, slow end.
35pub fn ease_in_out_quad(t: f64) -> f64 {
36    let t = clamp01(t);
37    if t < 0.5 {
38        2.0 * t * t
39    } else {
40        1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
41    }
42}
43
44/// Cubic ease-in: slow start, fast end (stronger than quadratic).
45pub fn ease_in_cubic(t: f64) -> f64 {
46    let t = clamp01(t);
47    t * t * t
48}
49
50/// Cubic ease-out: fast start, slow end (stronger than quadratic).
51pub fn ease_out_cubic(t: f64) -> f64 {
52    let t = clamp01(t);
53    1.0 - (1.0 - t).powi(3)
54}
55
56/// Cubic ease-in-out: slow start, fast middle, slow end (stronger than quadratic).
57pub fn ease_in_out_cubic(t: f64) -> f64 {
58    let t = clamp01(t);
59    if t < 0.5 {
60        4.0 * t * t * t
61    } else {
62        1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
63    }
64}
65
66/// Elastic ease-out: overshoots the target and oscillates before settling.
67pub fn ease_out_elastic(t: f64) -> f64 {
68    let t = clamp01(t);
69    if t == 0.0 {
70        0.0
71    } else if t == 1.0 {
72        1.0
73    } else {
74        let c4 = (2.0 * PI) / 3.0;
75        2f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
76    }
77}
78
79/// Bounce ease-out: simulates a ball bouncing before coming to rest.
80pub fn ease_out_bounce(t: f64) -> f64 {
81    let t = clamp01(t);
82    let n1 = 7.5625;
83    let d1 = 2.75;
84
85    if t < 1.0 / d1 {
86        n1 * t * t
87    } else if t < 2.0 / d1 {
88        let t = t - 1.5 / d1;
89        n1 * t * t + 0.75
90    } else if t < 2.5 / d1 {
91        let t = t - 2.25 / d1;
92        n1 * t * t + 0.9375
93    } else {
94        let t = t - 2.625 / d1;
95        n1 * t * t + 0.984_375
96    }
97}
98
99/// Linear interpolation between two values over a duration, with optional easing.
100///
101/// A `Tween` advances from `from` to `to` over `duration_ticks` render ticks.
102/// Call [`Tween::value`] each frame with the current tick to get the
103/// interpolated value. The tween is inactive until [`Tween::reset`] is called
104/// with a start tick.
105///
106/// # Example
107///
108/// ```
109/// use slt::Tween;
110/// use slt::anim::ease_out_quad;
111///
112/// let mut tween = Tween::new(0.0, 100.0, 20).easing(ease_out_quad);
113/// tween.reset(0);
114///
115/// let v = tween.value(10); // roughly halfway, eased
116/// assert!(v > 50.0);       // ease-out is faster at the start
117/// ```
118pub struct Tween {
119    from: f64,
120    to: f64,
121    duration_ticks: u64,
122    start_tick: u64,
123    easing: fn(f64) -> f64,
124    done: bool,
125}
126
127impl Tween {
128    /// Create a new tween from `from` to `to` over `duration_ticks` ticks.
129    ///
130    /// Uses linear easing by default. Call [`Tween::easing`] to change it.
131    /// The tween starts paused; call [`Tween::reset`] with the current tick
132    /// before reading values.
133    pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
134        Self {
135            from,
136            to,
137            duration_ticks,
138            start_tick: 0,
139            easing: ease_linear,
140            done: false,
141        }
142    }
143
144    /// Set the easing function used to interpolate the value.
145    ///
146    /// Any function with signature `fn(f64) -> f64` that maps `[0, 1]` to
147    /// `[0, 1]` works. The nine built-in options are in this module.
148    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
149        self.easing = f;
150        self
151    }
152
153    /// Return the interpolated value at the given `tick`.
154    ///
155    /// Returns `to` immediately if the tween has finished or `duration_ticks`
156    /// is zero. Marks the tween as done once `tick >= start_tick + duration_ticks`.
157    pub fn value(&mut self, tick: u64) -> f64 {
158        if self.done {
159            return self.to;
160        }
161
162        if self.duration_ticks == 0 {
163            self.done = true;
164            return self.to;
165        }
166
167        let elapsed = tick.wrapping_sub(self.start_tick);
168        if elapsed >= self.duration_ticks {
169            self.done = true;
170            return self.to;
171        }
172
173        let progress = elapsed as f64 / self.duration_ticks as f64;
174        let eased = (self.easing)(clamp01(progress));
175        lerp(self.from, self.to, eased)
176    }
177
178    /// Returns `true` if the tween has reached its end value.
179    pub fn is_done(&self) -> bool {
180        self.done
181    }
182
183    /// Restart the tween, treating `tick` as the new start time.
184    pub fn reset(&mut self, tick: u64) {
185        self.start_tick = tick;
186        self.done = false;
187    }
188}
189
190/// Defines how an animation behaves after reaching its end.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum LoopMode {
193    /// Play once, then stay at the final value.
194    Once,
195    /// Restart from the beginning each cycle.
196    Repeat,
197    /// Alternate forward and backward each cycle.
198    PingPong,
199}
200
201#[derive(Clone, Copy)]
202struct KeyframeStop {
203    position: f64,
204    value: f64,
205}
206
207/// Multi-stop keyframe animation over a fixed tick duration.
208///
209/// `Keyframes` is similar to CSS `@keyframes`: define multiple stops in the
210/// normalized `[0.0, 1.0]` timeline, then sample the value with
211/// [`Keyframes::value`] using the current render tick.
212///
213/// Stops are sorted by position when sampled. Each segment between adjacent
214/// stops can use its own easing function.
215///
216/// # Example
217///
218/// ```
219/// use slt::anim::{ease_in_cubic, ease_out_quad, Keyframes, LoopMode};
220///
221/// let mut keyframes = Keyframes::new(60)
222///     .stop(0.0, 0.0)
223///     .stop(0.5, 100.0)
224///     .stop(1.0, 40.0)
225///     .segment_easing(0, ease_out_quad)
226///     .segment_easing(1, ease_in_cubic)
227///     .loop_mode(LoopMode::PingPong);
228///
229/// keyframes.reset(10);
230/// let _ = keyframes.value(40);
231/// ```
232pub struct Keyframes {
233    duration_ticks: u64,
234    start_tick: u64,
235    stops: Vec<KeyframeStop>,
236    default_easing: fn(f64) -> f64,
237    segment_easing: Vec<fn(f64) -> f64>,
238    loop_mode: LoopMode,
239    done: bool,
240}
241
242impl Keyframes {
243    /// Create a new keyframe animation with total `duration_ticks`.
244    ///
245    /// Uses linear easing by default and [`LoopMode::Once`]. Add stops with
246    /// [`Keyframes::stop`], optionally configure easing, then call
247    /// [`Keyframes::reset`] before sampling.
248    pub fn new(duration_ticks: u64) -> Self {
249        Self {
250            duration_ticks,
251            start_tick: 0,
252            stops: Vec::new(),
253            default_easing: ease_linear,
254            segment_easing: Vec::new(),
255            loop_mode: LoopMode::Once,
256            done: false,
257        }
258    }
259
260    /// Add a keyframe stop at normalized `position` with `value`.
261    ///
262    /// `position` is clamped to `[0.0, 1.0]`.
263    pub fn stop(mut self, position: f64, value: f64) -> Self {
264        self.stops.push(KeyframeStop {
265            position: clamp01(position),
266            value,
267        });
268        if self.stops.len() >= 2 {
269            self.segment_easing.push(self.default_easing);
270        }
271        self
272    }
273
274    /// Set the default easing used for segments without explicit overrides.
275    ///
276    /// Existing segments are updated to this easing, unless you later override
277    /// them with [`Keyframes::segment_easing`].
278    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
279        self.default_easing = f;
280        self.segment_easing.fill(f);
281        self
282    }
283
284    /// Override easing for a specific segment index.
285    ///
286    /// Segment `0` is between the first and second stop, segment `1` between
287    /// the second and third, and so on. Out-of-range indices are ignored.
288    pub fn segment_easing(mut self, segment_index: usize, f: fn(f64) -> f64) -> Self {
289        if let Some(slot) = self.segment_easing.get_mut(segment_index) {
290            *slot = f;
291        }
292        self
293    }
294
295    /// Set loop behavior used after the first full pass.
296    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
297        self.loop_mode = mode;
298        self
299    }
300
301    /// Return the interpolated keyframe value at `tick`.
302    pub fn value(&mut self, tick: u64) -> f64 {
303        if self.stops.is_empty() {
304            self.done = true;
305            return 0.0;
306        }
307        if self.stops.len() == 1 {
308            self.done = true;
309            return self.stops[0].value;
310        }
311
312        let mut stops = self.stops.clone();
313        stops.sort_by(|a, b| a.position.total_cmp(&b.position));
314
315        let end_value = stops.last().map_or(0.0, |s| s.value);
316        let loop_tick = match map_loop_tick(
317            tick,
318            self.start_tick,
319            self.duration_ticks,
320            self.loop_mode,
321            &mut self.done,
322        ) {
323            Some(v) => v,
324            None => return end_value,
325        };
326
327        let progress = loop_tick as f64 / self.duration_ticks as f64;
328
329        if progress <= stops[0].position {
330            return stops[0].value;
331        }
332        if progress >= 1.0 {
333            return end_value;
334        }
335
336        for i in 0..(stops.len() - 1) {
337            let a = stops[i];
338            let b = stops[i + 1];
339            if progress <= b.position {
340                let span = b.position - a.position;
341                if span <= f64::EPSILON {
342                    return b.value;
343                }
344                let local = clamp01((progress - a.position) / span);
345                let easing = self
346                    .segment_easing
347                    .get(i)
348                    .copied()
349                    .unwrap_or(self.default_easing);
350                let eased = easing(local);
351                return lerp(a.value, b.value, eased);
352            }
353        }
354
355        end_value
356    }
357
358    /// Returns `true` if the animation finished in [`LoopMode::Once`].
359    pub fn is_done(&self) -> bool {
360        self.done
361    }
362
363    /// Restart the keyframe animation from `tick`.
364    pub fn reset(&mut self, tick: u64) {
365        self.start_tick = tick;
366        self.done = false;
367    }
368}
369
370#[derive(Clone, Copy)]
371struct SequenceSegment {
372    from: f64,
373    to: f64,
374    duration_ticks: u64,
375    easing: fn(f64) -> f64,
376}
377
378/// Sequential timeline that chains multiple animation segments.
379///
380/// Use [`Sequence::then`] to append segments. Sampling automatically advances
381/// through each segment as ticks increase.
382///
383/// # Example
384///
385/// ```
386/// use slt::anim::{ease_in_cubic, ease_out_quad, LoopMode, Sequence};
387///
388/// let mut seq = Sequence::new()
389///     .then(0.0, 100.0, 30, ease_out_quad)
390///     .then(100.0, 50.0, 20, ease_in_cubic)
391///     .loop_mode(LoopMode::Repeat);
392///
393/// seq.reset(0);
394/// let _ = seq.value(25);
395/// ```
396pub struct Sequence {
397    segments: Vec<SequenceSegment>,
398    loop_mode: LoopMode,
399    start_tick: u64,
400    done: bool,
401}
402
403impl Default for Sequence {
404    fn default() -> Self {
405        Self::new()
406    }
407}
408
409impl Sequence {
410    /// Create an empty sequence.
411    ///
412    /// Defaults to [`LoopMode::Once`]. Add segments with [`Sequence::then`]
413    /// and call [`Sequence::reset`] before sampling.
414    pub fn new() -> Self {
415        Self {
416            segments: Vec::new(),
417            loop_mode: LoopMode::Once,
418            start_tick: 0,
419            done: false,
420        }
421    }
422
423    /// Append a segment from `from` to `to` over `duration_ticks` ticks.
424    pub fn then(mut self, from: f64, to: f64, duration_ticks: u64, easing: fn(f64) -> f64) -> Self {
425        self.segments.push(SequenceSegment {
426            from,
427            to,
428            duration_ticks,
429            easing,
430        });
431        self
432    }
433
434    /// Set loop behavior used after the first full pass.
435    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
436        self.loop_mode = mode;
437        self
438    }
439
440    /// Return the sequence value at `tick`.
441    pub fn value(&mut self, tick: u64) -> f64 {
442        if self.segments.is_empty() {
443            self.done = true;
444            return 0.0;
445        }
446
447        let total_duration = self
448            .segments
449            .iter()
450            .fold(0_u64, |acc, s| acc.saturating_add(s.duration_ticks));
451        let end_value = self.segments.last().map_or(0.0, |s| s.to);
452
453        let loop_tick = match map_loop_tick(
454            tick,
455            self.start_tick,
456            total_duration,
457            self.loop_mode,
458            &mut self.done,
459        ) {
460            Some(v) => v,
461            None => return end_value,
462        };
463
464        let mut remaining = loop_tick;
465        for segment in &self.segments {
466            if segment.duration_ticks == 0 {
467                continue;
468            }
469            if remaining < segment.duration_ticks {
470                let progress = remaining as f64 / segment.duration_ticks as f64;
471                let eased = (segment.easing)(clamp01(progress));
472                return lerp(segment.from, segment.to, eased);
473            }
474            remaining -= segment.duration_ticks;
475        }
476
477        end_value
478    }
479
480    /// Returns `true` if the sequence finished in [`LoopMode::Once`].
481    pub fn is_done(&self) -> bool {
482        self.done
483    }
484
485    /// Restart the sequence, treating `tick` as the new start time.
486    pub fn reset(&mut self, tick: u64) {
487        self.start_tick = tick;
488        self.done = false;
489    }
490}
491
492/// Parallel staggered animation where each item starts after a fixed delay.
493///
494/// `Stagger` applies one tween configuration to many items. The start tick for
495/// each item is `start_tick + delay_ticks * item_index`.
496///
497/// By default the animation plays once ([`LoopMode::Once`]). Use
498/// [`Stagger::loop_mode`] to repeat or ping-pong. The total cycle length
499/// includes the delay of every item, so all items finish before the next
500/// cycle begins.
501///
502/// # Example
503///
504/// ```
505/// use slt::anim::{ease_out_quad, Stagger, LoopMode};
506///
507/// let mut stagger = Stagger::new(0.0, 100.0, 30)
508///     .easing(ease_out_quad)
509///     .delay(5)
510///     .loop_mode(LoopMode::Repeat);
511///
512/// stagger.reset(100);
513/// let _ = stagger.value(120, 3);
514/// ```
515pub struct Stagger {
516    from: f64,
517    to: f64,
518    duration_ticks: u64,
519    start_tick: u64,
520    delay_ticks: u64,
521    easing: fn(f64) -> f64,
522    loop_mode: LoopMode,
523    item_count: usize,
524    done: bool,
525}
526
527impl Stagger {
528    /// Create a new stagger animation template.
529    ///
530    /// Uses linear easing, zero delay, and [`LoopMode::Once`] by default.
531    pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
532        Self {
533            from,
534            to,
535            duration_ticks,
536            start_tick: 0,
537            delay_ticks: 0,
538            easing: ease_linear,
539            loop_mode: LoopMode::Once,
540            item_count: 0,
541            done: false,
542        }
543    }
544
545    /// Set easing for each item's tween.
546    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
547        self.easing = f;
548        self
549    }
550
551    /// Set delay in ticks between consecutive item starts.
552    pub fn delay(mut self, ticks: u64) -> Self {
553        self.delay_ticks = ticks;
554        self
555    }
556
557    /// Set loop behavior. [`LoopMode::Repeat`] restarts after all items
558    /// finish; [`LoopMode::PingPong`] reverses direction each cycle.
559    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
560        self.loop_mode = mode;
561        self
562    }
563
564    /// Set the number of items for cycle length calculation.
565    ///
566    /// When using [`LoopMode::Repeat`] or [`LoopMode::PingPong`], the total
567    /// cycle length is `duration_ticks + delay_ticks * (item_count - 1)`.
568    /// If not set, it is inferred from the highest `item_index` seen.
569    pub fn items(mut self, count: usize) -> Self {
570        self.item_count = count;
571        self
572    }
573
574    /// Return the value for `item_index` at `tick`.
575    pub fn value(&mut self, tick: u64, item_index: usize) -> f64 {
576        if item_index >= self.item_count {
577            self.item_count = item_index + 1;
578        }
579
580        let total_cycle = self.total_cycle_ticks();
581
582        let effective_tick = if self.loop_mode == LoopMode::Once {
583            tick
584        } else {
585            let elapsed = tick.wrapping_sub(self.start_tick);
586            let mapped = match self.loop_mode {
587                LoopMode::Repeat => {
588                    if total_cycle == 0 {
589                        0
590                    } else {
591                        elapsed % total_cycle
592                    }
593                }
594                LoopMode::PingPong => {
595                    if total_cycle == 0 {
596                        0
597                    } else {
598                        let full = total_cycle.saturating_mul(2);
599                        let phase = elapsed % full;
600                        if phase < total_cycle {
601                            phase
602                        } else {
603                            full - phase
604                        }
605                    }
606                }
607                LoopMode::Once => unreachable!(),
608            };
609            self.start_tick.wrapping_add(mapped)
610        };
611
612        let delay = self.delay_ticks.wrapping_mul(item_index as u64);
613        let item_start = self.start_tick.wrapping_add(delay);
614
615        if effective_tick < item_start {
616            self.done = false;
617            return self.from;
618        }
619
620        if self.duration_ticks == 0 {
621            self.done = true;
622            return self.to;
623        }
624
625        let elapsed = effective_tick - item_start;
626        if elapsed >= self.duration_ticks {
627            self.done = true;
628            return self.to;
629        }
630
631        self.done = false;
632        let progress = elapsed as f64 / self.duration_ticks as f64;
633        let eased = (self.easing)(clamp01(progress));
634        lerp(self.from, self.to, eased)
635    }
636
637    fn total_cycle_ticks(&self) -> u64 {
638        let max_delay = self
639            .delay_ticks
640            .wrapping_mul(self.item_count.saturating_sub(1) as u64);
641        self.duration_ticks.saturating_add(max_delay)
642    }
643
644    /// Returns `true` if the most recently sampled item reached its end value.
645    pub fn is_done(&self) -> bool {
646        self.done
647    }
648
649    /// Restart stagger timing, treating `tick` as the base start time.
650    pub fn reset(&mut self, tick: u64) {
651        self.start_tick = tick;
652        self.done = false;
653    }
654}
655
656fn map_loop_tick(
657    tick: u64,
658    start_tick: u64,
659    duration_ticks: u64,
660    loop_mode: LoopMode,
661    done: &mut bool,
662) -> Option<u64> {
663    if duration_ticks == 0 {
664        *done = true;
665        return None;
666    }
667
668    let elapsed = tick.wrapping_sub(start_tick);
669    match loop_mode {
670        LoopMode::Once => {
671            if elapsed >= duration_ticks {
672                *done = true;
673                None
674            } else {
675                *done = false;
676                Some(elapsed)
677            }
678        }
679        LoopMode::Repeat => {
680            *done = false;
681            Some(elapsed % duration_ticks)
682        }
683        LoopMode::PingPong => {
684            *done = false;
685            let cycle = duration_ticks.saturating_mul(2);
686            if cycle == 0 {
687                return Some(0);
688            }
689            let phase = elapsed % cycle;
690            if phase < duration_ticks {
691                Some(phase)
692            } else {
693                Some(cycle - phase)
694            }
695        }
696    }
697}
698
699/// Spring physics animation that settles toward a target value.
700///
701/// Models a damped harmonic oscillator. Call [`Spring::set_target`] to change
702/// the goal, then call [`Spring::tick`] once per frame to advance the
703/// simulation. Read the current position with [`Spring::value`].
704///
705/// Tune behavior with `stiffness` (how fast it accelerates toward the target)
706/// and `damping` (how quickly oscillations decay). A damping value close to
707/// 1.0 is overdamped (no oscillation); lower values produce more bounce.
708///
709/// # Example
710///
711/// ```
712/// use slt::Spring;
713///
714/// let mut spring = Spring::new(0.0, 0.2, 0.85);
715/// spring.set_target(100.0);
716///
717/// for _ in 0..200 {
718///     spring.tick();
719///     if spring.is_settled() { break; }
720/// }
721///
722/// assert!((spring.value() - 100.0).abs() < 0.01);
723/// ```
724pub struct Spring {
725    value: f64,
726    target: f64,
727    velocity: f64,
728    stiffness: f64,
729    damping: f64,
730}
731
732impl Spring {
733    /// Create a new spring at `initial` position with the given physics parameters.
734    ///
735    /// - `stiffness`: acceleration per unit of displacement (try `0.1`..`0.5`)
736    /// - `damping`: velocity multiplier per tick, `< 1.0` (try `0.8`..`0.95`)
737    pub fn new(initial: f64, stiffness: f64, damping: f64) -> Self {
738        Self {
739            value: initial,
740            target: initial,
741            velocity: 0.0,
742            stiffness,
743            damping,
744        }
745    }
746
747    /// Set the target value the spring will move toward.
748    pub fn set_target(&mut self, target: f64) {
749        self.target = target;
750    }
751
752    /// Advance the spring simulation by one tick.
753    ///
754    /// Call this once per frame before reading [`Spring::value`].
755    pub fn tick(&mut self) {
756        let displacement = self.target - self.value;
757        let spring_force = displacement * self.stiffness;
758        self.velocity = (self.velocity + spring_force) * self.damping;
759        self.value += self.velocity;
760    }
761
762    /// Return the current spring position.
763    pub fn value(&self) -> f64 {
764        self.value
765    }
766
767    /// Returns `true` if the spring has effectively settled at its target.
768    ///
769    /// Settled means both the distance to target and the velocity are below
770    /// `0.01`.
771    pub fn is_settled(&self) -> bool {
772        (self.target - self.value).abs() < 0.01 && self.velocity.abs() < 0.01
773    }
774}
775
776fn clamp01(t: f64) -> f64 {
777    t.clamp(0.0, 1.0)
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783
784    fn assert_endpoints(f: fn(f64) -> f64) {
785        assert_eq!(f(0.0), 0.0);
786        assert_eq!(f(1.0), 1.0);
787    }
788
789    #[test]
790    fn easing_functions_have_expected_endpoints() {
791        let easing_functions: [fn(f64) -> f64; 9] = [
792            ease_linear,
793            ease_in_quad,
794            ease_out_quad,
795            ease_in_out_quad,
796            ease_in_cubic,
797            ease_out_cubic,
798            ease_in_out_cubic,
799            ease_out_elastic,
800            ease_out_bounce,
801        ];
802
803        for easing in easing_functions {
804            assert_endpoints(easing);
805        }
806    }
807
808    #[test]
809    fn tween_returns_start_middle_end_values() {
810        let mut tween = Tween::new(0.0, 10.0, 10);
811        tween.reset(100);
812
813        assert_eq!(tween.value(100), 0.0);
814        assert_eq!(tween.value(105), 5.0);
815        assert_eq!(tween.value(110), 10.0);
816        assert!(tween.is_done());
817    }
818
819    #[test]
820    fn tween_reset_restarts_animation() {
821        let mut tween = Tween::new(0.0, 1.0, 10);
822        tween.reset(0);
823        let _ = tween.value(10);
824        assert!(tween.is_done());
825
826        tween.reset(20);
827        assert!(!tween.is_done());
828        assert_eq!(tween.value(20), 0.0);
829        assert_eq!(tween.value(30), 1.0);
830        assert!(tween.is_done());
831    }
832
833    #[test]
834    fn spring_settles_to_target() {
835        let mut spring = Spring::new(0.0, 0.2, 0.85);
836        spring.set_target(10.0);
837
838        for _ in 0..300 {
839            spring.tick();
840            if spring.is_settled() {
841                break;
842            }
843        }
844
845        assert!(spring.is_settled());
846        assert!((spring.value() - 10.0).abs() < 0.01);
847    }
848
849    #[test]
850    fn lerp_interpolates_values() {
851        assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
852        assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
853        assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
854    }
855
856    #[test]
857    fn keyframes_interpolates_across_multiple_stops() {
858        let mut keyframes = Keyframes::new(100)
859            .stop(0.0, 0.0)
860            .stop(0.3, 100.0)
861            .stop(0.7, 50.0)
862            .stop(1.0, 80.0)
863            .easing(ease_linear);
864
865        keyframes.reset(0);
866        assert_eq!(keyframes.value(0), 0.0);
867        assert_eq!(keyframes.value(15), 50.0);
868        assert_eq!(keyframes.value(30), 100.0);
869        assert_eq!(keyframes.value(50), 75.0);
870        assert_eq!(keyframes.value(70), 50.0);
871        assert_eq!(keyframes.value(85), 65.0);
872        assert_eq!(keyframes.value(100), 80.0);
873        assert!(keyframes.is_done());
874    }
875
876    #[test]
877    fn keyframes_repeat_loop_restarts() {
878        let mut keyframes = Keyframes::new(10)
879            .stop(0.0, 0.0)
880            .stop(1.0, 10.0)
881            .loop_mode(LoopMode::Repeat);
882
883        keyframes.reset(0);
884        assert_eq!(keyframes.value(5), 5.0);
885        assert_eq!(keyframes.value(10), 0.0);
886        assert_eq!(keyframes.value(12), 2.0);
887        assert!(!keyframes.is_done());
888    }
889
890    #[test]
891    fn keyframes_pingpong_reverses_direction() {
892        let mut keyframes = Keyframes::new(10)
893            .stop(0.0, 0.0)
894            .stop(1.0, 10.0)
895            .loop_mode(LoopMode::PingPong);
896
897        keyframes.reset(0);
898        assert_eq!(keyframes.value(8), 8.0);
899        assert_eq!(keyframes.value(10), 10.0);
900        assert_eq!(keyframes.value(12), 8.0);
901        assert_eq!(keyframes.value(15), 5.0);
902        assert!(!keyframes.is_done());
903    }
904
905    #[test]
906    fn sequence_chains_segments_in_order() {
907        let mut sequence = Sequence::new()
908            .then(0.0, 100.0, 30, ease_linear)
909            .then(100.0, 50.0, 20, ease_linear)
910            .then(50.0, 200.0, 40, ease_linear);
911
912        sequence.reset(0);
913        assert_eq!(sequence.value(15), 50.0);
914        assert_eq!(sequence.value(30), 100.0);
915        assert_eq!(sequence.value(40), 75.0);
916        assert_eq!(sequence.value(50), 50.0);
917        assert_eq!(sequence.value(70), 125.0);
918        assert_eq!(sequence.value(90), 200.0);
919        assert!(sequence.is_done());
920    }
921
922    #[test]
923    fn sequence_loop_modes_repeat_and_pingpong_work() {
924        let mut repeat = Sequence::new()
925            .then(0.0, 10.0, 10, ease_linear)
926            .loop_mode(LoopMode::Repeat);
927        repeat.reset(0);
928        assert_eq!(repeat.value(12), 2.0);
929        assert!(!repeat.is_done());
930
931        let mut pingpong = Sequence::new()
932            .then(0.0, 10.0, 10, ease_linear)
933            .loop_mode(LoopMode::PingPong);
934        pingpong.reset(0);
935        assert_eq!(pingpong.value(12), 8.0);
936        assert!(!pingpong.is_done());
937    }
938
939    #[test]
940    fn stagger_applies_per_item_delay() {
941        let mut stagger = Stagger::new(0.0, 100.0, 20).easing(ease_linear).delay(5);
942
943        stagger.reset(0);
944        assert_eq!(stagger.value(4, 3), 0.0);
945        assert_eq!(stagger.value(15, 3), 0.0);
946        assert_eq!(stagger.value(20, 3), 25.0);
947        assert_eq!(stagger.value(35, 3), 100.0);
948        assert!(stagger.is_done());
949    }
950}