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    on_complete: Option<Box<dyn FnMut()>>,
126}
127
128impl Tween {
129    /// Create a new tween from `from` to `to` over `duration_ticks` ticks.
130    ///
131    /// Uses linear easing by default. Call [`Tween::easing`] to change it.
132    /// The tween starts paused; call [`Tween::reset`] with the current tick
133    /// before reading values.
134    pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
135        Self {
136            from,
137            to,
138            duration_ticks,
139            start_tick: 0,
140            easing: ease_linear,
141            done: false,
142            on_complete: None,
143        }
144    }
145
146    /// Set the easing function used to interpolate the value.
147    ///
148    /// Any function with signature `fn(f64) -> f64` that maps `[0, 1]` to
149    /// `[0, 1]` works. The nine built-in options are in this module.
150    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
151        self.easing = f;
152        self
153    }
154
155    /// Register a callback that runs once when the tween completes.
156    pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
157        self.on_complete = Some(Box::new(f));
158        self
159    }
160
161    /// Return the interpolated value at the given `tick`.
162    ///
163    /// Returns `to` immediately if the tween has finished or `duration_ticks`
164    /// is zero. Marks the tween as done once `tick >= start_tick + duration_ticks`.
165    pub fn value(&mut self, tick: u64) -> f64 {
166        let was_done = self.done;
167        if self.done {
168            return self.to;
169        }
170
171        if self.duration_ticks == 0 {
172            self.done = true;
173            if !was_done && self.done {
174                if let Some(cb) = &mut self.on_complete {
175                    cb();
176                }
177            }
178            return self.to;
179        }
180
181        let elapsed = tick.wrapping_sub(self.start_tick);
182        if elapsed >= self.duration_ticks {
183            self.done = true;
184            if !was_done && self.done {
185                if let Some(cb) = &mut self.on_complete {
186                    cb();
187                }
188            }
189            return self.to;
190        }
191
192        let progress = elapsed as f64 / self.duration_ticks as f64;
193        let eased = (self.easing)(clamp01(progress));
194        lerp(self.from, self.to, eased)
195    }
196
197    /// Returns `true` if the tween has reached its end value.
198    pub fn is_done(&self) -> bool {
199        self.done
200    }
201
202    /// Restart the tween, treating `tick` as the new start time.
203    pub fn reset(&mut self, tick: u64) {
204        self.start_tick = tick;
205        self.done = false;
206    }
207}
208
209/// Defines how an animation behaves after reaching its end.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum LoopMode {
212    /// Play once, then stay at the final value.
213    Once,
214    /// Restart from the beginning each cycle.
215    Repeat,
216    /// Alternate forward and backward each cycle.
217    PingPong,
218}
219
220#[derive(Clone, Copy)]
221struct KeyframeStop {
222    position: f64,
223    value: f64,
224}
225
226/// Multi-stop keyframe animation over a fixed tick duration.
227///
228/// `Keyframes` is similar to CSS `@keyframes`: define multiple stops in the
229/// normalized `[0.0, 1.0]` timeline, then sample the value with
230/// [`Keyframes::value`] using the current render tick.
231///
232/// Stops are sorted by position when sampled. Each segment between adjacent
233/// stops can use its own easing function.
234///
235/// # Example
236///
237/// ```
238/// use slt::anim::{ease_in_cubic, ease_out_quad, Keyframes, LoopMode};
239///
240/// let mut keyframes = Keyframes::new(60)
241///     .stop(0.0, 0.0)
242///     .stop(0.5, 100.0)
243///     .stop(1.0, 40.0)
244///     .segment_easing(0, ease_out_quad)
245///     .segment_easing(1, ease_in_cubic)
246///     .loop_mode(LoopMode::PingPong);
247///
248/// keyframes.reset(10);
249/// let _ = keyframes.value(40);
250/// ```
251pub struct Keyframes {
252    duration_ticks: u64,
253    start_tick: u64,
254    stops: Vec<KeyframeStop>,
255    default_easing: fn(f64) -> f64,
256    segment_easing: Vec<fn(f64) -> f64>,
257    loop_mode: LoopMode,
258    done: bool,
259    on_complete: Option<Box<dyn FnMut()>>,
260}
261
262impl Keyframes {
263    /// Create a new keyframe animation with total `duration_ticks`.
264    ///
265    /// Uses linear easing by default and [`LoopMode::Once`]. Add stops with
266    /// [`Keyframes::stop`], optionally configure easing, then call
267    /// [`Keyframes::reset`] before sampling.
268    pub fn new(duration_ticks: u64) -> Self {
269        Self {
270            duration_ticks,
271            start_tick: 0,
272            stops: Vec::new(),
273            default_easing: ease_linear,
274            segment_easing: Vec::new(),
275            loop_mode: LoopMode::Once,
276            done: false,
277            on_complete: None,
278        }
279    }
280
281    /// Add a keyframe stop at normalized `position` with `value`.
282    ///
283    /// `position` is clamped to `[0.0, 1.0]`.
284    pub fn stop(mut self, position: f64, value: f64) -> Self {
285        self.stops.push(KeyframeStop {
286            position: clamp01(position),
287            value,
288        });
289        if self.stops.len() >= 2 {
290            self.segment_easing.push(self.default_easing);
291        }
292        self
293    }
294
295    /// Set the default easing used for segments without explicit overrides.
296    ///
297    /// Existing segments are updated to this easing, unless you later override
298    /// them with [`Keyframes::segment_easing`].
299    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
300        self.default_easing = f;
301        self.segment_easing.fill(f);
302        self
303    }
304
305    /// Override easing for a specific segment index.
306    ///
307    /// Segment `0` is between the first and second stop, segment `1` between
308    /// the second and third, and so on. Out-of-range indices are ignored.
309    pub fn segment_easing(mut self, segment_index: usize, f: fn(f64) -> f64) -> Self {
310        if let Some(slot) = self.segment_easing.get_mut(segment_index) {
311            *slot = f;
312        }
313        self
314    }
315
316    /// Set loop behavior used after the first full pass.
317    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
318        self.loop_mode = mode;
319        self
320    }
321
322    /// Register a callback that runs once when the animation completes.
323    pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
324        self.on_complete = Some(Box::new(f));
325        self
326    }
327
328    /// Return the interpolated keyframe value at `tick`.
329    pub fn value(&mut self, tick: u64) -> f64 {
330        let was_done = self.done;
331        if self.stops.is_empty() {
332            self.done = true;
333            if !was_done && self.done {
334                if let Some(cb) = &mut self.on_complete {
335                    cb();
336                }
337            }
338            return 0.0;
339        }
340        if self.stops.len() == 1 {
341            self.done = true;
342            if !was_done && self.done {
343                if let Some(cb) = &mut self.on_complete {
344                    cb();
345                }
346            }
347            return self.stops[0].value;
348        }
349
350        let mut stops = self.stops.clone();
351        stops.sort_by(|a, b| a.position.total_cmp(&b.position));
352
353        let end_value = stops.last().map_or(0.0, |s| s.value);
354        let loop_tick = match map_loop_tick(
355            tick,
356            self.start_tick,
357            self.duration_ticks,
358            self.loop_mode,
359            &mut self.done,
360        ) {
361            Some(v) => v,
362            None => {
363                if !was_done && self.done {
364                    if let Some(cb) = &mut self.on_complete {
365                        cb();
366                    }
367                }
368                return end_value;
369            }
370        };
371
372        let progress = loop_tick as f64 / self.duration_ticks as f64;
373
374        if progress <= stops[0].position {
375            return stops[0].value;
376        }
377        if progress >= 1.0 {
378            return end_value;
379        }
380
381        for i in 0..(stops.len() - 1) {
382            let a = stops[i];
383            let b = stops[i + 1];
384            if progress <= b.position {
385                let span = b.position - a.position;
386                if span <= f64::EPSILON {
387                    return b.value;
388                }
389                let local = clamp01((progress - a.position) / span);
390                let easing = self
391                    .segment_easing
392                    .get(i)
393                    .copied()
394                    .unwrap_or(self.default_easing);
395                let eased = easing(local);
396                return lerp(a.value, b.value, eased);
397            }
398        }
399
400        end_value
401    }
402
403    /// Returns `true` if the animation finished in [`LoopMode::Once`].
404    pub fn is_done(&self) -> bool {
405        self.done
406    }
407
408    /// Restart the keyframe animation from `tick`.
409    pub fn reset(&mut self, tick: u64) {
410        self.start_tick = tick;
411        self.done = false;
412    }
413}
414
415#[derive(Clone, Copy)]
416struct SequenceSegment {
417    from: f64,
418    to: f64,
419    duration_ticks: u64,
420    easing: fn(f64) -> f64,
421}
422
423/// Sequential timeline that chains multiple animation segments.
424///
425/// Use [`Sequence::then`] to append segments. Sampling automatically advances
426/// through each segment as ticks increase.
427///
428/// # Example
429///
430/// ```
431/// use slt::anim::{ease_in_cubic, ease_out_quad, LoopMode, Sequence};
432///
433/// let mut seq = Sequence::new()
434///     .then(0.0, 100.0, 30, ease_out_quad)
435///     .then(100.0, 50.0, 20, ease_in_cubic)
436///     .loop_mode(LoopMode::Repeat);
437///
438/// seq.reset(0);
439/// let _ = seq.value(25);
440/// ```
441pub struct Sequence {
442    segments: Vec<SequenceSegment>,
443    loop_mode: LoopMode,
444    start_tick: u64,
445    done: bool,
446    on_complete: Option<Box<dyn FnMut()>>,
447}
448
449impl Default for Sequence {
450    fn default() -> Self {
451        Self::new()
452    }
453}
454
455impl Sequence {
456    /// Create an empty sequence.
457    ///
458    /// Defaults to [`LoopMode::Once`]. Add segments with [`Sequence::then`]
459    /// and call [`Sequence::reset`] before sampling.
460    pub fn new() -> Self {
461        Self {
462            segments: Vec::new(),
463            loop_mode: LoopMode::Once,
464            start_tick: 0,
465            done: false,
466            on_complete: None,
467        }
468    }
469
470    /// Append a segment from `from` to `to` over `duration_ticks` ticks.
471    pub fn then(mut self, from: f64, to: f64, duration_ticks: u64, easing: fn(f64) -> f64) -> Self {
472        self.segments.push(SequenceSegment {
473            from,
474            to,
475            duration_ticks,
476            easing,
477        });
478        self
479    }
480
481    /// Set loop behavior used after the first full pass.
482    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
483        self.loop_mode = mode;
484        self
485    }
486
487    /// Register a callback that runs once when the sequence completes.
488    pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
489        self.on_complete = Some(Box::new(f));
490        self
491    }
492
493    /// Return the sequence value at `tick`.
494    pub fn value(&mut self, tick: u64) -> f64 {
495        let was_done = self.done;
496        if self.segments.is_empty() {
497            self.done = true;
498            if !was_done && self.done {
499                if let Some(cb) = &mut self.on_complete {
500                    cb();
501                }
502            }
503            return 0.0;
504        }
505
506        let total_duration = self
507            .segments
508            .iter()
509            .fold(0_u64, |acc, s| acc.saturating_add(s.duration_ticks));
510        let end_value = self.segments.last().map_or(0.0, |s| s.to);
511
512        let loop_tick = match map_loop_tick(
513            tick,
514            self.start_tick,
515            total_duration,
516            self.loop_mode,
517            &mut self.done,
518        ) {
519            Some(v) => v,
520            None => {
521                if !was_done && self.done {
522                    if let Some(cb) = &mut self.on_complete {
523                        cb();
524                    }
525                }
526                return end_value;
527            }
528        };
529
530        let mut remaining = loop_tick;
531        for segment in &self.segments {
532            if segment.duration_ticks == 0 {
533                continue;
534            }
535            if remaining < segment.duration_ticks {
536                let progress = remaining as f64 / segment.duration_ticks as f64;
537                let eased = (segment.easing)(clamp01(progress));
538                return lerp(segment.from, segment.to, eased);
539            }
540            remaining -= segment.duration_ticks;
541        }
542
543        end_value
544    }
545
546    /// Returns `true` if the sequence finished in [`LoopMode::Once`].
547    pub fn is_done(&self) -> bool {
548        self.done
549    }
550
551    /// Restart the sequence, treating `tick` as the new start time.
552    pub fn reset(&mut self, tick: u64) {
553        self.start_tick = tick;
554        self.done = false;
555    }
556}
557
558/// Parallel staggered animation where each item starts after a fixed delay.
559///
560/// `Stagger` applies one tween configuration to many items. The start tick for
561/// each item is `start_tick + delay_ticks * item_index`.
562///
563/// By default the animation plays once ([`LoopMode::Once`]). Use
564/// [`Stagger::loop_mode`] to repeat or ping-pong. The total cycle length
565/// includes the delay of every item, so all items finish before the next
566/// cycle begins.
567///
568/// # Example
569///
570/// ```
571/// use slt::anim::{ease_out_quad, Stagger, LoopMode};
572///
573/// let mut stagger = Stagger::new(0.0, 100.0, 30)
574///     .easing(ease_out_quad)
575///     .delay(5)
576///     .loop_mode(LoopMode::Repeat);
577///
578/// stagger.reset(100);
579/// let _ = stagger.value(120, 3);
580/// ```
581pub struct Stagger {
582    from: f64,
583    to: f64,
584    duration_ticks: u64,
585    start_tick: u64,
586    delay_ticks: u64,
587    easing: fn(f64) -> f64,
588    loop_mode: LoopMode,
589    item_count: usize,
590    done: bool,
591    on_complete: Option<Box<dyn FnMut()>>,
592}
593
594impl Stagger {
595    /// Create a new stagger animation template.
596    ///
597    /// Uses linear easing, zero delay, and [`LoopMode::Once`] by default.
598    pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
599        Self {
600            from,
601            to,
602            duration_ticks,
603            start_tick: 0,
604            delay_ticks: 0,
605            easing: ease_linear,
606            loop_mode: LoopMode::Once,
607            item_count: 0,
608            done: false,
609            on_complete: None,
610        }
611    }
612
613    /// Set easing for each item's tween.
614    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
615        self.easing = f;
616        self
617    }
618
619    /// Set delay in ticks between consecutive item starts.
620    pub fn delay(mut self, ticks: u64) -> Self {
621        self.delay_ticks = ticks;
622        self
623    }
624
625    /// Set loop behavior. [`LoopMode::Repeat`] restarts after all items
626    /// finish; [`LoopMode::PingPong`] reverses direction each cycle.
627    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
628        self.loop_mode = mode;
629        self
630    }
631
632    /// Register a callback that runs once when the sampled item completes.
633    pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
634        self.on_complete = Some(Box::new(f));
635        self
636    }
637
638    /// Set the number of items for cycle length calculation.
639    ///
640    /// When using [`LoopMode::Repeat`] or [`LoopMode::PingPong`], the total
641    /// cycle length is `duration_ticks + delay_ticks * (item_count - 1)`.
642    /// If not set, it is inferred from the highest `item_index` seen.
643    pub fn items(mut self, count: usize) -> Self {
644        self.item_count = count;
645        self
646    }
647
648    /// Return the value for `item_index` at `tick`.
649    pub fn value(&mut self, tick: u64, item_index: usize) -> f64 {
650        let was_done = self.done;
651        if item_index >= self.item_count {
652            self.item_count = item_index + 1;
653        }
654
655        let total_cycle = self.total_cycle_ticks();
656
657        let effective_tick = if self.loop_mode == LoopMode::Once {
658            tick
659        } else {
660            let elapsed = tick.wrapping_sub(self.start_tick);
661            let mapped = match self.loop_mode {
662                LoopMode::Repeat => {
663                    if total_cycle == 0 {
664                        0
665                    } else {
666                        elapsed % total_cycle
667                    }
668                }
669                LoopMode::PingPong => {
670                    if total_cycle == 0 {
671                        0
672                    } else {
673                        let full = total_cycle.saturating_mul(2);
674                        let phase = elapsed % full;
675                        if phase < total_cycle {
676                            phase
677                        } else {
678                            full - phase
679                        }
680                    }
681                }
682                LoopMode::Once => unreachable!(),
683            };
684            self.start_tick.wrapping_add(mapped)
685        };
686
687        let delay = self.delay_ticks.wrapping_mul(item_index as u64);
688        let item_start = self.start_tick.wrapping_add(delay);
689
690        if effective_tick < item_start {
691            self.done = false;
692            return self.from;
693        }
694
695        if self.duration_ticks == 0 {
696            self.done = true;
697            if !was_done && self.done {
698                if let Some(cb) = &mut self.on_complete {
699                    cb();
700                }
701            }
702            return self.to;
703        }
704
705        let elapsed = effective_tick - item_start;
706        if elapsed >= self.duration_ticks {
707            self.done = true;
708            if !was_done && self.done {
709                if let Some(cb) = &mut self.on_complete {
710                    cb();
711                }
712            }
713            return self.to;
714        }
715
716        self.done = false;
717        let progress = elapsed as f64 / self.duration_ticks as f64;
718        let eased = (self.easing)(clamp01(progress));
719        lerp(self.from, self.to, eased)
720    }
721
722    fn total_cycle_ticks(&self) -> u64 {
723        let max_delay = self
724            .delay_ticks
725            .wrapping_mul(self.item_count.saturating_sub(1) as u64);
726        self.duration_ticks.saturating_add(max_delay)
727    }
728
729    /// Returns `true` if the most recently sampled item reached its end value.
730    pub fn is_done(&self) -> bool {
731        self.done
732    }
733
734    /// Restart stagger timing, treating `tick` as the base start time.
735    pub fn reset(&mut self, tick: u64) {
736        self.start_tick = tick;
737        self.done = false;
738    }
739}
740
741fn map_loop_tick(
742    tick: u64,
743    start_tick: u64,
744    duration_ticks: u64,
745    loop_mode: LoopMode,
746    done: &mut bool,
747) -> Option<u64> {
748    if duration_ticks == 0 {
749        *done = true;
750        return None;
751    }
752
753    let elapsed = tick.wrapping_sub(start_tick);
754    match loop_mode {
755        LoopMode::Once => {
756            if elapsed >= duration_ticks {
757                *done = true;
758                None
759            } else {
760                *done = false;
761                Some(elapsed)
762            }
763        }
764        LoopMode::Repeat => {
765            *done = false;
766            Some(elapsed % duration_ticks)
767        }
768        LoopMode::PingPong => {
769            *done = false;
770            let cycle = duration_ticks.saturating_mul(2);
771            if cycle == 0 {
772                return Some(0);
773            }
774            let phase = elapsed % cycle;
775            if phase < duration_ticks {
776                Some(phase)
777            } else {
778                Some(cycle - phase)
779            }
780        }
781    }
782}
783
784/// Spring physics animation that settles toward a target value.
785///
786/// Models a damped harmonic oscillator. Call [`Spring::set_target`] to change
787/// the goal, then call [`Spring::tick`] once per frame to advance the
788/// simulation. Read the current position with [`Spring::value`].
789///
790/// Tune behavior with `stiffness` (how fast it accelerates toward the target)
791/// and `damping` (how quickly oscillations decay). A damping value close to
792/// 1.0 is overdamped (no oscillation); lower values produce more bounce.
793///
794/// # Example
795///
796/// ```
797/// use slt::Spring;
798///
799/// let mut spring = Spring::new(0.0, 0.2, 0.85);
800/// spring.set_target(100.0);
801///
802/// for _ in 0..200 {
803///     spring.tick();
804///     if spring.is_settled() { break; }
805/// }
806///
807/// assert!((spring.value() - 100.0).abs() < 0.01);
808/// ```
809pub struct Spring {
810    value: f64,
811    target: f64,
812    velocity: f64,
813    stiffness: f64,
814    damping: f64,
815    settled: bool,
816    on_settle: Option<Box<dyn FnMut()>>,
817}
818
819impl Spring {
820    /// Create a new spring at `initial` position with the given physics parameters.
821    ///
822    /// - `stiffness`: acceleration per unit of displacement (try `0.1`..`0.5`)
823    /// - `damping`: velocity multiplier per tick, `< 1.0` (try `0.8`..`0.95`)
824    pub fn new(initial: f64, stiffness: f64, damping: f64) -> Self {
825        Self {
826            value: initial,
827            target: initial,
828            velocity: 0.0,
829            stiffness,
830            damping,
831            settled: true,
832            on_settle: None,
833        }
834    }
835
836    /// Register a callback that runs once when the spring settles.
837    pub fn on_settle(mut self, f: impl FnMut() + 'static) -> Self {
838        self.on_settle = Some(Box::new(f));
839        self
840    }
841
842    /// Set the target value the spring will move toward.
843    pub fn set_target(&mut self, target: f64) {
844        self.target = target;
845        self.settled = self.is_settled();
846    }
847
848    /// Advance the spring simulation by one tick.
849    ///
850    /// Call this once per frame before reading [`Spring::value`].
851    pub fn tick(&mut self) {
852        let displacement = self.target - self.value;
853        let spring_force = displacement * self.stiffness;
854        self.velocity = (self.velocity + spring_force) * self.damping;
855        self.value += self.velocity;
856
857        let is_settled = self.is_settled();
858        if !self.settled && is_settled {
859            self.settled = true;
860            if let Some(cb) = &mut self.on_settle {
861                cb();
862            }
863        }
864    }
865
866    /// Return the current spring position.
867    pub fn value(&self) -> f64 {
868        self.value
869    }
870
871    /// Returns `true` if the spring has effectively settled at its target.
872    ///
873    /// Settled means both the distance to target and the velocity are below
874    /// `0.01`.
875    pub fn is_settled(&self) -> bool {
876        (self.target - self.value).abs() < 0.01 && self.velocity.abs() < 0.01
877    }
878}
879
880fn clamp01(t: f64) -> f64 {
881    t.clamp(0.0, 1.0)
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use std::cell::Cell;
888    use std::rc::Rc;
889
890    fn assert_endpoints(f: fn(f64) -> f64) {
891        assert_eq!(f(0.0), 0.0);
892        assert_eq!(f(1.0), 1.0);
893    }
894
895    #[test]
896    fn easing_functions_have_expected_endpoints() {
897        let easing_functions: [fn(f64) -> f64; 9] = [
898            ease_linear,
899            ease_in_quad,
900            ease_out_quad,
901            ease_in_out_quad,
902            ease_in_cubic,
903            ease_out_cubic,
904            ease_in_out_cubic,
905            ease_out_elastic,
906            ease_out_bounce,
907        ];
908
909        for easing in easing_functions {
910            assert_endpoints(easing);
911        }
912    }
913
914    #[test]
915    fn tween_returns_start_middle_end_values() {
916        let mut tween = Tween::new(0.0, 10.0, 10);
917        tween.reset(100);
918
919        assert_eq!(tween.value(100), 0.0);
920        assert_eq!(tween.value(105), 5.0);
921        assert_eq!(tween.value(110), 10.0);
922        assert!(tween.is_done());
923    }
924
925    #[test]
926    fn tween_reset_restarts_animation() {
927        let mut tween = Tween::new(0.0, 1.0, 10);
928        tween.reset(0);
929        let _ = tween.value(10);
930        assert!(tween.is_done());
931
932        tween.reset(20);
933        assert!(!tween.is_done());
934        assert_eq!(tween.value(20), 0.0);
935        assert_eq!(tween.value(30), 1.0);
936        assert!(tween.is_done());
937    }
938
939    #[test]
940    fn tween_on_complete_fires_once() {
941        let count = Rc::new(Cell::new(0));
942        let callback_count = Rc::clone(&count);
943        let mut tween = Tween::new(0.0, 10.0, 10).on_complete(move || {
944            callback_count.set(callback_count.get() + 1);
945        });
946
947        tween.reset(0);
948        assert_eq!(count.get(), 0);
949
950        assert_eq!(tween.value(5), 5.0);
951        assert_eq!(count.get(), 0);
952
953        assert_eq!(tween.value(10), 10.0);
954        assert_eq!(count.get(), 1);
955
956        assert_eq!(tween.value(11), 10.0);
957        assert_eq!(count.get(), 1);
958    }
959
960    #[test]
961    fn spring_settles_to_target() {
962        let mut spring = Spring::new(0.0, 0.2, 0.85);
963        spring.set_target(10.0);
964
965        for _ in 0..300 {
966            spring.tick();
967            if spring.is_settled() {
968                break;
969            }
970        }
971
972        assert!(spring.is_settled());
973        assert!((spring.value() - 10.0).abs() < 0.01);
974    }
975
976    #[test]
977    fn spring_on_settle_fires_once() {
978        let count = Rc::new(Cell::new(0));
979        let callback_count = Rc::clone(&count);
980        let mut spring = Spring::new(0.0, 0.2, 0.85).on_settle(move || {
981            callback_count.set(callback_count.get() + 1);
982        });
983        spring.set_target(10.0);
984
985        for _ in 0..500 {
986            spring.tick();
987            if spring.is_settled() {
988                break;
989            }
990        }
991
992        assert!(spring.is_settled());
993        assert_eq!(count.get(), 1);
994
995        for _ in 0..50 {
996            spring.tick();
997        }
998
999        assert_eq!(count.get(), 1);
1000    }
1001
1002    #[test]
1003    fn lerp_interpolates_values() {
1004        assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
1005        assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
1006        assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
1007    }
1008
1009    #[test]
1010    fn keyframes_interpolates_across_multiple_stops() {
1011        let mut keyframes = Keyframes::new(100)
1012            .stop(0.0, 0.0)
1013            .stop(0.3, 100.0)
1014            .stop(0.7, 50.0)
1015            .stop(1.0, 80.0)
1016            .easing(ease_linear);
1017
1018        keyframes.reset(0);
1019        assert_eq!(keyframes.value(0), 0.0);
1020        assert_eq!(keyframes.value(15), 50.0);
1021        assert_eq!(keyframes.value(30), 100.0);
1022        assert_eq!(keyframes.value(50), 75.0);
1023        assert_eq!(keyframes.value(70), 50.0);
1024        assert_eq!(keyframes.value(85), 65.0);
1025        assert_eq!(keyframes.value(100), 80.0);
1026        assert!(keyframes.is_done());
1027    }
1028
1029    #[test]
1030    fn keyframes_repeat_loop_restarts() {
1031        let mut keyframes = Keyframes::new(10)
1032            .stop(0.0, 0.0)
1033            .stop(1.0, 10.0)
1034            .loop_mode(LoopMode::Repeat);
1035
1036        keyframes.reset(0);
1037        assert_eq!(keyframes.value(5), 5.0);
1038        assert_eq!(keyframes.value(10), 0.0);
1039        assert_eq!(keyframes.value(12), 2.0);
1040        assert!(!keyframes.is_done());
1041    }
1042
1043    #[test]
1044    fn keyframes_pingpong_reverses_direction() {
1045        let mut keyframes = Keyframes::new(10)
1046            .stop(0.0, 0.0)
1047            .stop(1.0, 10.0)
1048            .loop_mode(LoopMode::PingPong);
1049
1050        keyframes.reset(0);
1051        assert_eq!(keyframes.value(8), 8.0);
1052        assert_eq!(keyframes.value(10), 10.0);
1053        assert_eq!(keyframes.value(12), 8.0);
1054        assert_eq!(keyframes.value(15), 5.0);
1055        assert!(!keyframes.is_done());
1056    }
1057
1058    #[test]
1059    fn sequence_chains_segments_in_order() {
1060        let mut sequence = Sequence::new()
1061            .then(0.0, 100.0, 30, ease_linear)
1062            .then(100.0, 50.0, 20, ease_linear)
1063            .then(50.0, 200.0, 40, ease_linear);
1064
1065        sequence.reset(0);
1066        assert_eq!(sequence.value(15), 50.0);
1067        assert_eq!(sequence.value(30), 100.0);
1068        assert_eq!(sequence.value(40), 75.0);
1069        assert_eq!(sequence.value(50), 50.0);
1070        assert_eq!(sequence.value(70), 125.0);
1071        assert_eq!(sequence.value(90), 200.0);
1072        assert!(sequence.is_done());
1073    }
1074
1075    #[test]
1076    fn sequence_loop_modes_repeat_and_pingpong_work() {
1077        let mut repeat = Sequence::new()
1078            .then(0.0, 10.0, 10, ease_linear)
1079            .loop_mode(LoopMode::Repeat);
1080        repeat.reset(0);
1081        assert_eq!(repeat.value(12), 2.0);
1082        assert!(!repeat.is_done());
1083
1084        let mut pingpong = Sequence::new()
1085            .then(0.0, 10.0, 10, ease_linear)
1086            .loop_mode(LoopMode::PingPong);
1087        pingpong.reset(0);
1088        assert_eq!(pingpong.value(12), 8.0);
1089        assert!(!pingpong.is_done());
1090    }
1091
1092    #[test]
1093    fn stagger_applies_per_item_delay() {
1094        let mut stagger = Stagger::new(0.0, 100.0, 20).easing(ease_linear).delay(5);
1095
1096        stagger.reset(0);
1097        assert_eq!(stagger.value(4, 3), 0.0);
1098        assert_eq!(stagger.value(15, 3), 0.0);
1099        assert_eq!(stagger.value(20, 3), 25.0);
1100        assert_eq!(stagger.value(35, 3), 100.0);
1101        assert!(stagger.is_done());
1102    }
1103}