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        if self.done {
167            return self.to;
168        }
169
170        if self.duration_ticks == 0 {
171            self.done = true;
172            if let Some(cb) = &mut self.on_complete {
173                cb();
174            }
175            return self.to;
176        }
177
178        let elapsed = tick.wrapping_sub(self.start_tick);
179        if elapsed >= self.duration_ticks {
180            self.done = true;
181            if let Some(cb) = &mut self.on_complete {
182                cb();
183            }
184            return self.to;
185        }
186
187        let progress = elapsed as f64 / self.duration_ticks as f64;
188        let eased = (self.easing)(clamp01(progress));
189        lerp(self.from, self.to, eased)
190    }
191
192    /// Returns `true` if the tween has reached its end value.
193    pub fn is_done(&self) -> bool {
194        self.done
195    }
196
197    /// Restart the tween, treating `tick` as the new start time.
198    pub fn reset(&mut self, tick: u64) {
199        self.start_tick = tick;
200        self.done = false;
201    }
202}
203
204/// Default animation duration in ticks used by
205/// [`Context::animate_bool`](crate::Context::animate_bool) and
206/// [`Context::animate_value`](crate::Context::animate_value) when no explicit
207/// duration is supplied.
208///
209/// 12 ticks at the default 60 Hz tick rate is roughly 200 ms — short enough
210/// to feel snappy, long enough to read as motion.
211pub const DEFAULT_ANIMATE_TICKS: u64 = 12;
212
213/// Internal state used by [`Context::animate_value`] /
214/// [`Context::animate_bool`] to drive an implicit
215/// `Tween` keyed in `Context::named_states`.
216///
217/// Stores the most recently seen `target` so the tween can smoothly retarget
218/// when the caller changes the goal mid-animation. Not part of the public
219/// API; users keying their own animation state should construct a [`Tween`]
220/// directly.
221pub(crate) struct AnimState {
222    pub(crate) tween: Tween,
223    pub(crate) last_target: f64,
224}
225
226impl AnimState {
227    /// Initialize with the tween already at its target so the first sample
228    /// has no visible animation pop.
229    pub(crate) fn new(target: f64, tick: u64) -> Self {
230        let mut tween = Tween::new(target, target, 0);
231        tween.reset(tick);
232        Self {
233            tween,
234            last_target: target,
235        }
236    }
237
238    /// Sample the current value, retargeting if the goal changed.
239    ///
240    /// On retarget the new tween starts from the current interpolated value,
241    /// avoiding a visible jump when the target flips mid-flight. A
242    /// `duration_ticks` of 0 snaps to the new target immediately.
243    pub(crate) fn sample(&mut self, target: f64, duration_ticks: u64, tick: u64) -> f64 {
244        // Compare bit patterns so two NaNs are treated as equal — avoids
245        // re-resetting forever if a caller threads NaN through.
246        if self.last_target.to_bits() != target.to_bits() {
247            let current = self.tween.value(tick);
248            self.tween = Tween::new(current, target, duration_ticks);
249            self.tween.reset(tick);
250            self.last_target = target;
251        }
252        self.tween.value(tick)
253    }
254}
255
256/// Defines how an animation behaves after reaching its end.
257#[non_exhaustive]
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum LoopMode {
260    /// Play once, then stay at the final value.
261    Once,
262    /// Restart from the beginning each cycle.
263    Repeat,
264    /// Alternate forward and backward each cycle.
265    PingPong,
266}
267
268#[derive(Clone, Copy)]
269struct KeyframeStop {
270    position: f64,
271    value: f64,
272}
273
274/// Multi-stop keyframe animation over a fixed tick duration.
275///
276/// `Keyframes` is similar to CSS `@keyframes`: define multiple stops in the
277/// normalized `[0.0, 1.0]` timeline, then sample the value with
278/// [`Keyframes::value`] using the current render tick.
279///
280/// Stops are sorted by position when sampled. Each segment between adjacent
281/// stops can use its own easing function.
282///
283/// # Example
284///
285/// ```
286/// use slt::anim::{ease_in_cubic, ease_out_quad, Keyframes, LoopMode};
287///
288/// let mut keyframes = Keyframes::new(60)
289///     .stop(0.0, 0.0)
290///     .stop(0.5, 100.0)
291///     .stop(1.0, 40.0)
292///     .segment_easing(0, ease_out_quad)
293///     .segment_easing(1, ease_in_cubic)
294///     .loop_mode(LoopMode::PingPong);
295///
296/// keyframes.reset(10);
297/// let _ = keyframes.value(40);
298/// ```
299pub struct Keyframes {
300    duration_ticks: u64,
301    start_tick: u64,
302    stops: Vec<KeyframeStop>,
303    default_easing: fn(f64) -> f64,
304    segment_easing: Vec<fn(f64) -> f64>,
305    loop_mode: LoopMode,
306    done: bool,
307    completion_fired: bool,
308    on_complete: Option<Box<dyn FnMut()>>,
309}
310
311impl Keyframes {
312    /// Create a new keyframe animation with total `duration_ticks`.
313    ///
314    /// Uses linear easing by default and [`LoopMode::Once`]. Add stops with
315    /// [`Keyframes::stop`], optionally configure easing, then call
316    /// [`Keyframes::reset`] before sampling.
317    pub fn new(duration_ticks: u64) -> Self {
318        Self {
319            duration_ticks,
320            start_tick: 0,
321            stops: Vec::new(),
322            default_easing: ease_linear,
323            segment_easing: Vec::new(),
324            loop_mode: LoopMode::Once,
325            done: false,
326            completion_fired: false,
327            on_complete: None,
328        }
329    }
330
331    /// Add a keyframe stop at normalized `position` with `value`.
332    ///
333    /// `position` is clamped to `[0.0, 1.0]`.
334    pub fn stop(mut self, position: f64, value: f64) -> Self {
335        self.stops.push(KeyframeStop {
336            position: clamp01(position),
337            value,
338        });
339        if self.stops.len() >= 2 {
340            self.segment_easing.push(self.default_easing);
341        }
342        self.stops.sort_by(|a, b| a.position.total_cmp(&b.position));
343        self
344    }
345
346    /// Set the default easing used for segments without explicit overrides.
347    ///
348    /// Existing segments are updated to this easing, unless you later override
349    /// them with [`Keyframes::segment_easing`].
350    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
351        self.default_easing = f;
352        self.segment_easing.fill(f);
353        self
354    }
355
356    /// Override easing for a specific segment index.
357    ///
358    /// Segment `0` is between the first and second stop, segment `1` between
359    /// the second and third, and so on. Out-of-range indices are ignored in
360    /// release builds; debug builds panic via `debug_assert!` to catch
361    /// builder-order mistakes (call `stop()` to add stops before assigning
362    /// per-segment easing).
363    pub fn segment_easing(mut self, segment_index: usize, f: fn(f64) -> f64) -> Self {
364        debug_assert!(
365            segment_index < self.segment_easing.len(),
366            "Keyframes::segment_easing: index {} is out of range \
367             (only {} segments defined; call stop() first to add more stops)",
368            segment_index,
369            self.segment_easing.len(),
370        );
371        if let Some(slot) = self.segment_easing.get_mut(segment_index) {
372            *slot = f;
373        }
374        self
375    }
376
377    /// Set loop behavior used after the first full pass.
378    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
379        self.loop_mode = mode;
380        self
381    }
382
383    /// Register a callback that runs once when the animation completes.
384    pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
385        self.on_complete = Some(Box::new(f));
386        self
387    }
388
389    /// Return the interpolated keyframe value at `tick`.
390    pub fn value(&mut self, tick: u64) -> f64 {
391        if self.stops.is_empty() {
392            self.complete_once();
393            return 0.0;
394        }
395        if self.stops.len() == 1 {
396            self.complete_once();
397            return self.stops[0].value;
398        }
399
400        let stops = &self.stops;
401
402        let end_value = stops.last().map_or(0.0, |s| s.value);
403        let loop_tick = match map_loop_tick(
404            tick,
405            self.start_tick,
406            self.duration_ticks,
407            self.loop_mode,
408            &mut self.done,
409        ) {
410            Some(v) => v,
411            None => {
412                self.complete_once();
413                return end_value;
414            }
415        };
416
417        if self.duration_ticks == 0 {
418            return stops.last().map_or(0.0, |s| s.value);
419        }
420        let progress = loop_tick as f64 / self.duration_ticks as f64;
421
422        if progress <= stops[0].position {
423            return stops[0].value;
424        }
425        if progress >= 1.0 {
426            return end_value;
427        }
428
429        for i in 0..(stops.len() - 1) {
430            let a = stops[i];
431            let b = stops[i + 1];
432            if progress <= b.position {
433                let span = b.position - a.position;
434                if span <= f64::EPSILON {
435                    return b.value;
436                }
437                let local = clamp01((progress - a.position) / span);
438                let easing = self
439                    .segment_easing
440                    .get(i)
441                    .copied()
442                    .unwrap_or(self.default_easing);
443                let eased = easing(local);
444                return lerp(a.value, b.value, eased);
445            }
446        }
447
448        end_value
449    }
450
451    /// Returns `true` if the animation finished in [`LoopMode::Once`].
452    pub fn is_done(&self) -> bool {
453        self.done
454    }
455
456    /// Restart the keyframe animation from `tick`.
457    pub fn reset(&mut self, tick: u64) {
458        self.start_tick = tick;
459        self.done = false;
460        self.completion_fired = false;
461    }
462
463    fn complete_once(&mut self) {
464        self.done = true;
465        if self.loop_mode == LoopMode::Once && !self.completion_fired {
466            self.completion_fired = true;
467            if let Some(cb) = &mut self.on_complete {
468                cb();
469            }
470        }
471    }
472}
473
474#[derive(Clone, Copy)]
475struct SequenceSegment {
476    from: f64,
477    to: f64,
478    duration_ticks: u64,
479    easing: fn(f64) -> f64,
480}
481
482/// Sequential timeline that chains multiple animation segments.
483///
484/// Use [`Sequence::then`] to append segments. Sampling automatically advances
485/// through each segment as ticks increase.
486///
487/// # Example
488///
489/// ```
490/// use slt::anim::{ease_in_cubic, ease_out_quad, LoopMode, Sequence};
491///
492/// let mut seq = Sequence::new()
493///     .then(0.0, 100.0, 30, ease_out_quad)
494///     .then(100.0, 50.0, 20, ease_in_cubic)
495///     .loop_mode(LoopMode::Repeat);
496///
497/// seq.reset(0);
498/// let _ = seq.value(25);
499/// ```
500pub struct Sequence {
501    segments: Vec<SequenceSegment>,
502    loop_mode: LoopMode,
503    start_tick: u64,
504    done: bool,
505    completion_fired: bool,
506    on_complete: Option<Box<dyn FnMut()>>,
507}
508
509impl Default for Sequence {
510    fn default() -> Self {
511        Self::new()
512    }
513}
514
515impl Sequence {
516    /// Create an empty sequence.
517    ///
518    /// Defaults to [`LoopMode::Once`]. Add segments with [`Sequence::then`]
519    /// and call [`Sequence::reset`] before sampling.
520    pub fn new() -> Self {
521        Self {
522            segments: Vec::new(),
523            loop_mode: LoopMode::Once,
524            start_tick: 0,
525            done: false,
526            completion_fired: false,
527            on_complete: None,
528        }
529    }
530
531    /// Append a segment from `from` to `to` over `duration_ticks` ticks.
532    pub fn then(mut self, from: f64, to: f64, duration_ticks: u64, easing: fn(f64) -> f64) -> Self {
533        self.segments.push(SequenceSegment {
534            from,
535            to,
536            duration_ticks,
537            easing,
538        });
539        self
540    }
541
542    /// Set loop behavior used after the first full pass.
543    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
544        self.loop_mode = mode;
545        self
546    }
547
548    /// Register a callback that runs once when the sequence completes.
549    pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
550        self.on_complete = Some(Box::new(f));
551        self
552    }
553
554    /// Return the sequence value at `tick`.
555    pub fn value(&mut self, tick: u64) -> f64 {
556        if self.segments.is_empty() {
557            self.complete_once();
558            return 0.0;
559        }
560
561        let total_duration = self
562            .segments
563            .iter()
564            .fold(0_u64, |acc, s| acc.saturating_add(s.duration_ticks));
565        let end_value = self.segments.last().map_or(0.0, |s| s.to);
566
567        let loop_tick = match map_loop_tick(
568            tick,
569            self.start_tick,
570            total_duration,
571            self.loop_mode,
572            &mut self.done,
573        ) {
574            Some(v) => v,
575            None => {
576                self.complete_once();
577                return end_value;
578            }
579        };
580
581        let mut remaining = loop_tick;
582        for segment in &self.segments {
583            if segment.duration_ticks == 0 {
584                continue;
585            }
586            if remaining < segment.duration_ticks {
587                let progress = remaining as f64 / segment.duration_ticks as f64;
588                let eased = (segment.easing)(clamp01(progress));
589                return lerp(segment.from, segment.to, eased);
590            }
591            remaining -= segment.duration_ticks;
592        }
593
594        end_value
595    }
596
597    /// Returns `true` if the sequence finished in [`LoopMode::Once`].
598    pub fn is_done(&self) -> bool {
599        self.done
600    }
601
602    /// Restart the sequence, treating `tick` as the new start time.
603    pub fn reset(&mut self, tick: u64) {
604        self.start_tick = tick;
605        self.done = false;
606        self.completion_fired = false;
607    }
608
609    fn complete_once(&mut self) {
610        self.done = true;
611        if self.loop_mode == LoopMode::Once && !self.completion_fired {
612            self.completion_fired = true;
613            if let Some(cb) = &mut self.on_complete {
614                cb();
615            }
616        }
617    }
618}
619
620/// Parallel staggered animation where each item starts after a fixed delay.
621///
622/// `Stagger` applies one tween configuration to many items. The start tick for
623/// each item is `start_tick + delay_ticks * item_index`.
624///
625/// By default the animation plays once ([`LoopMode::Once`]). Use
626/// [`Stagger::loop_mode`] to repeat or ping-pong. The total cycle length
627/// includes the delay of every item, so all items finish before the next
628/// cycle begins.
629///
630/// # Example
631///
632/// ```
633/// use slt::anim::{ease_out_quad, Stagger, LoopMode};
634///
635/// let mut stagger = Stagger::new(0.0, 100.0, 30)
636///     .easing(ease_out_quad)
637///     .delay(5)
638///     .loop_mode(LoopMode::Repeat);
639///
640/// stagger.reset(100);
641/// let _ = stagger.value(120, 3);
642/// ```
643pub struct Stagger {
644    from: f64,
645    to: f64,
646    duration_ticks: u64,
647    start_tick: u64,
648    delay_ticks: u64,
649    easing: fn(f64) -> f64,
650    loop_mode: LoopMode,
651    item_count: usize,
652    done: bool,
653    completion_fired: bool,
654    on_complete: Option<Box<dyn FnMut()>>,
655}
656
657impl Stagger {
658    /// Create a new stagger animation template.
659    ///
660    /// Uses linear easing, zero delay, and [`LoopMode::Once`] by default.
661    pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
662        Self {
663            from,
664            to,
665            duration_ticks,
666            start_tick: 0,
667            delay_ticks: 0,
668            easing: ease_linear,
669            loop_mode: LoopMode::Once,
670            item_count: 0,
671            done: false,
672            completion_fired: false,
673            on_complete: None,
674        }
675    }
676
677    /// Set easing for each item's tween.
678    pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
679        self.easing = f;
680        self
681    }
682
683    /// Set delay in ticks between consecutive item starts.
684    pub fn delay(mut self, ticks: u64) -> Self {
685        self.delay_ticks = ticks;
686        self
687    }
688
689    /// Set loop behavior. [`LoopMode::Repeat`] restarts after all items
690    /// finish; [`LoopMode::PingPong`] reverses direction each cycle.
691    pub fn loop_mode(mut self, mode: LoopMode) -> Self {
692        self.loop_mode = mode;
693        self
694    }
695
696    /// Register a callback that runs once when the sampled item completes.
697    pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
698        self.on_complete = Some(Box::new(f));
699        self
700    }
701
702    /// Set the number of items for cycle length calculation.
703    ///
704    /// When using [`LoopMode::Repeat`] or [`LoopMode::PingPong`], the total
705    /// cycle length is `duration_ticks + delay_ticks * (item_count - 1)`.
706    /// If not set, it is inferred from the highest `item_index` seen.
707    pub fn items(mut self, count: usize) -> Self {
708        self.item_count = count;
709        self
710    }
711
712    /// Return the value for `item_index` at `tick`.
713    pub fn value(&mut self, tick: u64, item_index: usize) -> f64 {
714        if item_index >= self.item_count {
715            self.item_count = item_index + 1;
716        }
717
718        let total_cycle = self.total_cycle_ticks();
719
720        let effective_tick = if self.loop_mode == LoopMode::Once {
721            tick
722        } else {
723            let elapsed = tick.wrapping_sub(self.start_tick);
724            let mapped = match self.loop_mode {
725                LoopMode::Repeat => {
726                    if total_cycle == 0 {
727                        0
728                    } else {
729                        elapsed % total_cycle
730                    }
731                }
732                LoopMode::PingPong => {
733                    if total_cycle == 0 {
734                        0
735                    } else {
736                        let full = total_cycle.saturating_mul(2);
737                        let phase = elapsed % full;
738                        if phase < total_cycle {
739                            phase
740                        } else {
741                            full - phase
742                        }
743                    }
744                }
745                LoopMode::Once => unreachable!(),
746            };
747            self.start_tick.wrapping_add(mapped)
748        };
749
750        let delay = self.delay_ticks.wrapping_mul(item_index as u64);
751        let item_start = self.start_tick.wrapping_add(delay);
752
753        if effective_tick < item_start {
754            self.done = false;
755            return self.from;
756        }
757
758        if self.duration_ticks == 0 {
759            self.done = true;
760            self.complete_once();
761            return self.to;
762        }
763
764        let elapsed = effective_tick - item_start;
765        if elapsed >= self.duration_ticks {
766            self.done = true;
767            self.complete_once();
768            return self.to;
769        }
770
771        self.done = false;
772        let progress = elapsed as f64 / self.duration_ticks as f64;
773        let eased = (self.easing)(clamp01(progress));
774        lerp(self.from, self.to, eased)
775    }
776
777    fn total_cycle_ticks(&self) -> u64 {
778        let max_delay = self
779            .delay_ticks
780            .wrapping_mul(self.item_count.saturating_sub(1) as u64);
781        self.duration_ticks.saturating_add(max_delay)
782    }
783
784    /// Returns `true` if the **last-sampled** item reached its end value.
785    ///
786    /// `done` is updated on every [`Stagger::value`] call; sampling
787    /// `value(tick, i)` for a non-final `i` after a later item completed can
788    /// reset this flag to `false`. To check whether the entire stagger has
789    /// finished — independent of which item was sampled last — use
790    /// [`Stagger::is_all_done`].
791    pub fn is_done(&self) -> bool {
792        self.done
793    }
794
795    /// Returns `true` if all `item_count` items have passed their end tick.
796    ///
797    /// Independent of which item was sampled last: uses pure tick arithmetic
798    /// against the configured `delay_ticks`, `duration_ticks`, and
799    /// `start_tick`. With [`LoopMode::Repeat`] / [`LoopMode::PingPong`] this
800    /// only reports `true` for the first cycle (loops are re-entered after
801    /// completion).
802    ///
803    /// # Example
804    /// ```
805    /// use slt::Stagger;
806    /// let stagger = Stagger::new(0.0, 100.0, 10).delay(5);
807    /// // 10 items: last item starts at tick 45, ends at tick 55.
808    /// assert!(stagger.is_all_done(60, 10));
809    /// assert!(!stagger.is_all_done(40, 10));
810    /// ```
811    pub fn is_all_done(&self, tick: u64, item_count: usize) -> bool {
812        if item_count == 0 {
813            return true;
814        }
815        let last_start = self.start_tick.saturating_add(
816            self.delay_ticks
817                .saturating_mul(item_count.saturating_sub(1) as u64),
818        );
819        tick >= last_start.saturating_add(self.duration_ticks)
820    }
821
822    /// Restart stagger timing, treating `tick` as the base start time.
823    pub fn reset(&mut self, tick: u64) {
824        self.start_tick = tick;
825        self.done = false;
826        self.completion_fired = false;
827    }
828
829    fn complete_once(&mut self) {
830        if self.loop_mode == LoopMode::Once && !self.completion_fired {
831            self.completion_fired = true;
832            if let Some(cb) = &mut self.on_complete {
833                cb();
834            }
835        }
836    }
837}
838
839fn map_loop_tick(
840    tick: u64,
841    start_tick: u64,
842    duration_ticks: u64,
843    loop_mode: LoopMode,
844    done: &mut bool,
845) -> Option<u64> {
846    if duration_ticks == 0 {
847        *done = true;
848        return None;
849    }
850
851    let elapsed = tick.wrapping_sub(start_tick);
852    match loop_mode {
853        LoopMode::Once => {
854            if elapsed >= duration_ticks {
855                *done = true;
856                None
857            } else {
858                *done = false;
859                Some(elapsed)
860            }
861        }
862        LoopMode::Repeat => {
863            *done = false;
864            Some(elapsed % duration_ticks)
865        }
866        LoopMode::PingPong => {
867            *done = false;
868            let cycle = duration_ticks.saturating_mul(2);
869            if cycle == 0 {
870                return Some(0);
871            }
872            let phase = elapsed % cycle;
873            if phase < duration_ticks {
874                Some(phase)
875            } else {
876                Some(cycle - phase)
877            }
878        }
879    }
880}
881
882/// Spring physics animation that settles toward a target value.
883///
884/// Models a damped harmonic oscillator. Call [`Spring::set_target`] to change
885/// the goal, then call [`Spring::tick`] once per frame to advance the
886/// simulation. Read the current position with [`Spring::value`].
887///
888/// Tune behavior with `stiffness` (how fast it accelerates toward the target)
889/// and `damping` (velocity multiplier per tick). `damping` must be strictly in
890/// `(0.0, 1.0)` — this is **not** the ODE damping ratio ζ. Values `>= 1.0`
891/// conserve or amplify energy, causing perpetual oscillation or divergence.
892///
893/// Recommended range: `0.80..=0.95`.
894/// - `0.95`: slow settle, noticeable oscillation
895/// - `0.85`: balanced (typical UI spring)
896/// - `0.80`: fast settle, minimal oscillation
897///
898/// # Example
899///
900/// ```
901/// use slt::Spring;
902///
903/// let mut spring = Spring::new(0.0, 0.2, 0.85);
904/// spring.set_target(100.0);
905///
906/// for _ in 0..200 {
907///     spring.tick();
908///     if spring.is_settled() { break; }
909/// }
910///
911/// assert!((spring.value() - 100.0).abs() < 0.01);
912/// ```
913pub struct Spring {
914    value: f64,
915    target: f64,
916    velocity: f64,
917    stiffness: f64,
918    damping: f64,
919    settled: bool,
920    on_settle: Option<Box<dyn FnMut()>>,
921}
922
923impl Spring {
924    /// Create a new spring at `initial` position with the given physics parameters.
925    ///
926    /// - `stiffness`: acceleration per unit of displacement (try `0.1`..`0.5`)
927    /// - `damping`: velocity multiplier per tick, `< 1.0` (try `0.8`..`0.95`)
928    pub fn new(initial: f64, stiffness: f64, damping: f64) -> Self {
929        debug_assert!(
930            damping > 0.0 && damping < 1.0,
931            "Spring::new: damping must be in (0, 1), got {damping}. \
932             Values >= 1.0 conserve or amplify energy and never settle."
933        );
934        Self {
935            value: initial,
936            target: initial,
937            velocity: 0.0,
938            stiffness,
939            damping,
940            settled: true,
941            on_settle: None,
942        }
943    }
944
945    /// Register a callback that runs once when the spring settles.
946    pub fn on_settle(mut self, f: impl FnMut() + 'static) -> Self {
947        self.on_settle = Some(Box::new(f));
948        self
949    }
950
951    /// Set the target value the spring will move toward.
952    pub fn set_target(&mut self, target: f64) {
953        self.target = target;
954        self.settled = self.is_settled();
955    }
956
957    /// Advance the spring simulation by one tick.
958    ///
959    /// Call this once per frame before reading [`Spring::value`].
960    pub fn tick(&mut self) {
961        let displacement = self.target - self.value;
962        let spring_force = displacement * self.stiffness;
963        self.velocity = (self.velocity + spring_force) * self.damping;
964        self.value += self.velocity;
965
966        let is_settled = self.is_settled();
967        if !self.settled && is_settled {
968            self.settled = true;
969            if let Some(cb) = &mut self.on_settle {
970                cb();
971            }
972        }
973    }
974
975    /// Return the current spring position.
976    pub fn value(&self) -> f64 {
977        self.value
978    }
979
980    /// Returns `true` if the spring has effectively settled at its target.
981    ///
982    /// Settled means both the distance to target and the velocity are below
983    /// `0.01`.
984    pub fn is_settled(&self) -> bool {
985        (self.target - self.value).abs() < 0.01 && self.velocity.abs() < 0.01
986    }
987}
988
989fn clamp01(t: f64) -> f64 {
990    t.clamp(0.0, 1.0)
991}
992
993#[cfg(test)]
994mod tests {
995    use super::*;
996    use std::cell::Cell;
997    use std::rc::Rc;
998
999    fn assert_endpoints(f: fn(f64) -> f64) {
1000        assert_eq!(f(0.0), 0.0);
1001        assert_eq!(f(1.0), 1.0);
1002    }
1003
1004    #[test]
1005    fn easing_functions_have_expected_endpoints() {
1006        let easing_functions: [fn(f64) -> f64; 9] = [
1007            ease_linear,
1008            ease_in_quad,
1009            ease_out_quad,
1010            ease_in_out_quad,
1011            ease_in_cubic,
1012            ease_out_cubic,
1013            ease_in_out_cubic,
1014            ease_out_elastic,
1015            ease_out_bounce,
1016        ];
1017
1018        for easing in easing_functions {
1019            assert_endpoints(easing);
1020        }
1021    }
1022
1023    #[test]
1024    fn tween_returns_start_middle_end_values() {
1025        let mut tween = Tween::new(0.0, 10.0, 10);
1026        tween.reset(100);
1027
1028        assert_eq!(tween.value(100), 0.0);
1029        assert_eq!(tween.value(105), 5.0);
1030        assert_eq!(tween.value(110), 10.0);
1031        assert!(tween.is_done());
1032    }
1033
1034    #[test]
1035    fn tween_reset_restarts_animation() {
1036        let mut tween = Tween::new(0.0, 1.0, 10);
1037        tween.reset(0);
1038        let _ = tween.value(10);
1039        assert!(tween.is_done());
1040
1041        tween.reset(20);
1042        assert!(!tween.is_done());
1043        assert_eq!(tween.value(20), 0.0);
1044        assert_eq!(tween.value(30), 1.0);
1045        assert!(tween.is_done());
1046    }
1047
1048    #[test]
1049    fn tween_on_complete_fires_once() {
1050        let count = Rc::new(Cell::new(0));
1051        let callback_count = Rc::clone(&count);
1052        let mut tween = Tween::new(0.0, 10.0, 10).on_complete(move || {
1053            callback_count.set(callback_count.get() + 1);
1054        });
1055
1056        tween.reset(0);
1057        assert_eq!(count.get(), 0);
1058
1059        assert_eq!(tween.value(5), 5.0);
1060        assert_eq!(count.get(), 0);
1061
1062        assert_eq!(tween.value(10), 10.0);
1063        assert_eq!(count.get(), 1);
1064
1065        assert_eq!(tween.value(11), 10.0);
1066        assert_eq!(count.get(), 1);
1067    }
1068
1069    #[test]
1070    fn completion_callbacks_fire_once_per_reset() {
1071        let keyframe_count = Rc::new(Cell::new(0));
1072        let mut keyframes = Keyframes::new(10)
1073            .stop(0.0, 0.0)
1074            .stop(1.0, 1.0)
1075            .on_complete({
1076                let count = Rc::clone(&keyframe_count);
1077                move || count.set(count.get() + 1)
1078            });
1079        keyframes.reset(0);
1080        assert_eq!(keyframes.value(10), 1.0);
1081        assert_eq!(keyframes.value(11), 1.0);
1082        assert_eq!(keyframe_count.get(), 1);
1083        keyframes.reset(20);
1084        assert_eq!(keyframes.value(30), 1.0);
1085        assert_eq!(keyframe_count.get(), 2);
1086
1087        let sequence_count = Rc::new(Cell::new(0));
1088        let mut sequence = Sequence::new()
1089            .then(0.0, 1.0, 10, ease_linear)
1090            .on_complete({
1091                let count = Rc::clone(&sequence_count);
1092                move || count.set(count.get() + 1)
1093            });
1094        sequence.reset(0);
1095        assert_eq!(sequence.value(10), 1.0);
1096        assert_eq!(sequence.value(100), 1.0);
1097        assert_eq!(sequence_count.get(), 1);
1098        sequence.reset(200);
1099        assert_eq!(sequence.value(210), 1.0);
1100        assert_eq!(sequence_count.get(), 2);
1101
1102        let stagger_count = Rc::new(Cell::new(0));
1103        let mut stagger = Stagger::new(0.0, 1.0, 10).on_complete({
1104            let count = Rc::clone(&stagger_count);
1105            move || count.set(count.get() + 1)
1106        });
1107        stagger.reset(0);
1108        assert_eq!(stagger.value(10, 0), 1.0);
1109        assert_eq!(stagger.value(11, 0), 1.0);
1110        assert_eq!(stagger_count.get(), 1);
1111        stagger.reset(20);
1112        assert_eq!(stagger.value(30, 0), 1.0);
1113        assert_eq!(stagger_count.get(), 2);
1114    }
1115
1116    #[test]
1117    fn degenerate_animations_complete_once() {
1118        let empty_keyframe_count = Rc::new(Cell::new(0));
1119        let mut empty_keyframes = Keyframes::new(10).on_complete({
1120            let count = Rc::clone(&empty_keyframe_count);
1121            move || count.set(count.get() + 1)
1122        });
1123        assert_eq!(empty_keyframes.value(0), 0.0);
1124        assert_eq!(empty_keyframes.value(1), 0.0);
1125        assert_eq!(empty_keyframe_count.get(), 1);
1126
1127        let single_keyframe_count = Rc::new(Cell::new(0));
1128        let mut single_keyframe = Keyframes::new(10).stop(0.5, 7.0).on_complete({
1129            let count = Rc::clone(&single_keyframe_count);
1130            move || count.set(count.get() + 1)
1131        });
1132        assert_eq!(single_keyframe.value(0), 7.0);
1133        assert_eq!(single_keyframe.value(100), 7.0);
1134        assert_eq!(single_keyframe_count.get(), 1);
1135
1136        let zero_sequence_count = Rc::new(Cell::new(0));
1137        let mut zero_sequence = Sequence::new().then(1.0, 2.0, 0, ease_linear).on_complete({
1138            let count = Rc::clone(&zero_sequence_count);
1139            move || count.set(count.get() + 1)
1140        });
1141        assert_eq!(zero_sequence.value(0), 2.0);
1142        assert_eq!(zero_sequence.value(1), 2.0);
1143        assert_eq!(zero_sequence_count.get(), 1);
1144    }
1145
1146    #[test]
1147    fn looping_animations_do_not_report_terminal_completion() {
1148        let keyframe_count = Rc::new(Cell::new(0));
1149        let mut keyframes = Keyframes::new(10)
1150            .stop(0.0, 0.0)
1151            .stop(1.0, 1.0)
1152            .loop_mode(LoopMode::Repeat)
1153            .on_complete({
1154                let count = Rc::clone(&keyframe_count);
1155                move || count.set(count.get() + 1)
1156            });
1157        for tick in [10, 20, 100] {
1158            let _ = keyframes.value(tick);
1159        }
1160        assert_eq!(keyframe_count.get(), 0);
1161        assert!(!keyframes.is_done());
1162
1163        let sequence_count = Rc::new(Cell::new(0));
1164        let mut sequence = Sequence::new()
1165            .then(0.0, 1.0, 10, ease_linear)
1166            .loop_mode(LoopMode::PingPong)
1167            .on_complete({
1168                let count = Rc::clone(&sequence_count);
1169                move || count.set(count.get() + 1)
1170            });
1171        for tick in [10, 20, 100] {
1172            let _ = sequence.value(tick);
1173        }
1174        assert_eq!(sequence_count.get(), 0);
1175        assert!(!sequence.is_done());
1176
1177        let stagger_count = Rc::new(Cell::new(0));
1178        let mut stagger = Stagger::new(0.0, 1.0, 10)
1179            .loop_mode(LoopMode::Repeat)
1180            .on_complete({
1181                let count = Rc::clone(&stagger_count);
1182                move || count.set(count.get() + 1)
1183            });
1184        for tick in [10, 20, 100] {
1185            let _ = stagger.value(tick, 0);
1186        }
1187        assert_eq!(stagger_count.get(), 0);
1188    }
1189
1190    #[test]
1191    fn spring_settles_to_target() {
1192        let mut spring = Spring::new(0.0, 0.2, 0.85);
1193        spring.set_target(10.0);
1194
1195        for _ in 0..300 {
1196            spring.tick();
1197            if spring.is_settled() {
1198                break;
1199            }
1200        }
1201
1202        assert!(spring.is_settled());
1203        assert!((spring.value() - 10.0).abs() < 0.01);
1204    }
1205
1206    #[test]
1207    fn spring_on_settle_fires_once() {
1208        let count = Rc::new(Cell::new(0));
1209        let callback_count = Rc::clone(&count);
1210        let mut spring = Spring::new(0.0, 0.2, 0.85).on_settle(move || {
1211            callback_count.set(callback_count.get() + 1);
1212        });
1213        spring.set_target(10.0);
1214
1215        for _ in 0..500 {
1216            spring.tick();
1217            if spring.is_settled() {
1218                break;
1219            }
1220        }
1221
1222        assert!(spring.is_settled());
1223        assert_eq!(count.get(), 1);
1224
1225        for _ in 0..50 {
1226            spring.tick();
1227        }
1228
1229        assert_eq!(count.get(), 1);
1230    }
1231
1232    #[cfg(debug_assertions)]
1233    #[test]
1234    #[should_panic(expected = "damping must be in (0, 1)")]
1235    fn spring_damping_one_panics_in_debug() {
1236        let _ = Spring::new(0.0, 0.5, 1.0);
1237    }
1238
1239    #[cfg(debug_assertions)]
1240    #[test]
1241    #[should_panic(expected = "damping must be in (0, 1)")]
1242    fn spring_damping_gt_one_panics_in_debug() {
1243        let _ = Spring::new(0.0, 0.5, 2.0);
1244    }
1245
1246    #[test]
1247    fn spring_valid_damping_settles() {
1248        for &d in &[0.5_f64, 0.7, 0.85, 0.95] {
1249            let mut s = Spring::new(0.0, 0.2, d);
1250            s.set_target(100.0);
1251            for _ in 0..1000 {
1252                s.tick();
1253                if s.is_settled() {
1254                    break;
1255                }
1256            }
1257            assert!(s.is_settled(), "damping={d} should settle");
1258            assert!((s.value() - 100.0).abs() < 0.01, "damping={d} value off");
1259        }
1260    }
1261
1262    #[test]
1263    fn lerp_interpolates_values() {
1264        assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
1265        assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
1266        assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
1267    }
1268
1269    #[test]
1270    fn keyframes_interpolates_across_multiple_stops() {
1271        let mut keyframes = Keyframes::new(100)
1272            .stop(0.0, 0.0)
1273            .stop(0.3, 100.0)
1274            .stop(0.7, 50.0)
1275            .stop(1.0, 80.0)
1276            .easing(ease_linear);
1277
1278        keyframes.reset(0);
1279        assert_eq!(keyframes.value(0), 0.0);
1280        assert_eq!(keyframes.value(15), 50.0);
1281        assert_eq!(keyframes.value(30), 100.0);
1282        assert_eq!(keyframes.value(50), 75.0);
1283        assert_eq!(keyframes.value(70), 50.0);
1284        assert_eq!(keyframes.value(85), 65.0);
1285        assert_eq!(keyframes.value(100), 80.0);
1286        assert!(keyframes.is_done());
1287    }
1288
1289    #[test]
1290    fn keyframes_repeat_loop_restarts() {
1291        let mut keyframes = Keyframes::new(10)
1292            .stop(0.0, 0.0)
1293            .stop(1.0, 10.0)
1294            .loop_mode(LoopMode::Repeat);
1295
1296        keyframes.reset(0);
1297        assert_eq!(keyframes.value(5), 5.0);
1298        assert_eq!(keyframes.value(10), 0.0);
1299        assert_eq!(keyframes.value(12), 2.0);
1300        assert!(!keyframes.is_done());
1301    }
1302
1303    #[test]
1304    fn keyframes_pingpong_reverses_direction() {
1305        let mut keyframes = Keyframes::new(10)
1306            .stop(0.0, 0.0)
1307            .stop(1.0, 10.0)
1308            .loop_mode(LoopMode::PingPong);
1309
1310        keyframes.reset(0);
1311        assert_eq!(keyframes.value(8), 8.0);
1312        assert_eq!(keyframes.value(10), 10.0);
1313        assert_eq!(keyframes.value(12), 8.0);
1314        assert_eq!(keyframes.value(15), 5.0);
1315        assert!(!keyframes.is_done());
1316    }
1317
1318    #[test]
1319    fn sequence_chains_segments_in_order() {
1320        let mut sequence = Sequence::new()
1321            .then(0.0, 100.0, 30, ease_linear)
1322            .then(100.0, 50.0, 20, ease_linear)
1323            .then(50.0, 200.0, 40, ease_linear);
1324
1325        sequence.reset(0);
1326        assert_eq!(sequence.value(15), 50.0);
1327        assert_eq!(sequence.value(30), 100.0);
1328        assert_eq!(sequence.value(40), 75.0);
1329        assert_eq!(sequence.value(50), 50.0);
1330        assert_eq!(sequence.value(70), 125.0);
1331        assert_eq!(sequence.value(90), 200.0);
1332        assert!(sequence.is_done());
1333    }
1334
1335    #[test]
1336    fn sequence_loop_modes_repeat_and_pingpong_work() {
1337        let mut repeat = Sequence::new()
1338            .then(0.0, 10.0, 10, ease_linear)
1339            .loop_mode(LoopMode::Repeat);
1340        repeat.reset(0);
1341        assert_eq!(repeat.value(12), 2.0);
1342        assert!(!repeat.is_done());
1343
1344        let mut pingpong = Sequence::new()
1345            .then(0.0, 10.0, 10, ease_linear)
1346            .loop_mode(LoopMode::PingPong);
1347        pingpong.reset(0);
1348        assert_eq!(pingpong.value(12), 8.0);
1349        assert!(!pingpong.is_done());
1350    }
1351
1352    #[test]
1353    fn stagger_applies_per_item_delay() {
1354        let mut stagger = Stagger::new(0.0, 100.0, 20).easing(ease_linear).delay(5);
1355
1356        stagger.reset(0);
1357        assert_eq!(stagger.value(4, 3), 0.0);
1358        assert_eq!(stagger.value(15, 3), 0.0);
1359        assert_eq!(stagger.value(20, 3), 25.0);
1360        assert_eq!(stagger.value(35, 3), 100.0);
1361        assert!(stagger.is_done());
1362    }
1363
1364    /// Regression test for issue #127:
1365    /// `is_all_done` reports completion across all items, independent of last sample.
1366    #[test]
1367    fn stagger_is_all_done_returns_false_mid_animation() {
1368        let stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1369        // 5 items: item 0 ends at tick 10, item 4 ends at tick 30.
1370        assert!(!stagger.is_all_done(15, 5), "items still in progress");
1371    }
1372
1373    /// Regression test for issue #127: `is_all_done` returns true after last item.
1374    #[test]
1375    fn stagger_is_all_done_returns_true_after_last_item() {
1376        let stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1377        assert!(stagger.is_all_done(31, 5), "all items done by tick 31");
1378    }
1379
1380    /// Regression test for issue #127: `is_done` reflects last sampled item only.
1381    #[test]
1382    fn stagger_is_done_reflects_last_sampled_item_only() {
1383        let mut stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1384        stagger.value(100, 4); // item 4 already past end → done = true
1385        assert!(stagger.is_done());
1386        // Sample item 2 mid-flight → done resets.
1387        stagger.value(15, 2);
1388        assert!(!stagger.is_done(), "is_done reflects last sampled item");
1389    }
1390
1391    /// Regression test for issue #127: empty stagger trivially "all done".
1392    #[test]
1393    fn stagger_is_all_done_zero_items() {
1394        let stagger = Stagger::new(0.0, 100.0, 10);
1395        assert!(stagger.is_all_done(0, 0));
1396    }
1397
1398    /// Regression test for issue #130: out-of-range segment_easing panics in debug builds.
1399    #[cfg(debug_assertions)]
1400    #[test]
1401    #[should_panic(expected = "out of range")]
1402    fn keyframes_segment_easing_oob_panics_in_debug() {
1403        // 2 stops → 1 segment (only index 0 valid).
1404        let _ = Keyframes::new(60)
1405            .stop(0.0, 0.0)
1406            .stop(1.0, 100.0)
1407            .segment_easing(5, ease_linear);
1408    }
1409
1410    /// Regression test for issue #130: valid segment_easing index does not panic.
1411    #[test]
1412    fn keyframes_segment_easing_valid_index() {
1413        let kf = Keyframes::new(60)
1414            .stop(0.0, 0.0)
1415            .stop(0.5, 50.0)
1416            .stop(1.0, 100.0)
1417            .segment_easing(0, ease_in_quad)
1418            .segment_easing(1, ease_out_quad);
1419        let _ = kf;
1420    }
1421}