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