Skip to main content

slt/
anim.rs

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