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