Skip to main content

pax_runtime_api/
animation.rs

1//! Animation primitives: interpolation, easing curves, transition queues, and timelines. See also: properties.ease_to and properties.ease_to_later.
2
3use super::*;
4
5/// A duration used by animation and transition systems.
6///
7/// `Frames` preserves Pax's historical frame-count semantics, while
8/// `Milliseconds` and `Seconds` advance from the chassis-provided monotonic
9/// wall clock.
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
11#[serde(crate = "crate::serde")]
12pub enum Duration {
13    /// Duration measured in runtime frames.
14    Frames(Numeric),
15    /// Duration measured in milliseconds.
16    Milliseconds(Numeric),
17    /// Duration measured in seconds.
18    Seconds(Numeric),
19}
20
21impl Default for Duration {
22    fn default() -> Self {
23        Self::Frames(Numeric::I64(0))
24    }
25}
26
27impl Display for Duration {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Duration::Frames(value) => write!(f, "{}f", value),
31            Duration::Milliseconds(value) => write!(f, "{}ms", value),
32            Duration::Seconds(value) => write!(f, "{}s", value),
33        }
34    }
35}
36
37impl Interpolatable for Duration {
38    fn interpolate(&self, other: &Self, t: f64) -> Self {
39        match (self, other) {
40            (Duration::Frames(start), Duration::Frames(end)) => {
41                Duration::Frames(start.interpolate(end, t))
42            }
43            (Duration::Milliseconds(start), Duration::Milliseconds(end)) => {
44                Duration::Milliseconds(start.interpolate(end, t))
45            }
46            (Duration::Seconds(start), Duration::Seconds(end)) => {
47                Duration::Seconds(start.interpolate(end, t))
48            }
49            _ => {
50                let start_ms = self.as_milliseconds_f64();
51                let end_ms = other.as_milliseconds_f64();
52                Duration::Milliseconds(Numeric::F64(start_ms + (end_ms - start_ms) * t))
53            }
54        }
55    }
56}
57
58impl Duration {
59    pub const NOMINAL_FRAME_MILLIS: f64 = 1000.0 / 60.0;
60
61    /// Raw value in this duration's own unit.
62    pub fn raw_value(&self) -> f64 {
63        match self {
64            Duration::Frames(value) | Duration::Milliseconds(value) | Duration::Seconds(value) => {
65                value.to_float()
66            }
67        }
68    }
69
70    /// Returns true when the duration is expressed in frame units.
71    pub fn is_frame_based(&self) -> bool {
72        matches!(self, Duration::Frames(_))
73    }
74
75    /// Converts the duration to milliseconds, using 60fps as the nominal
76    /// frame duration when converting frame units for mixed-unit authoring.
77    pub fn as_milliseconds_f64(&self) -> f64 {
78        match self {
79            Duration::Frames(value) => value.to_float() * Self::NOMINAL_FRAME_MILLIS,
80            Duration::Milliseconds(value) => value.to_float(),
81            Duration::Seconds(value) => value.to_float() * 1000.0,
82        }
83    }
84
85    /// Converts the duration to frames, using 60fps as the nominal conversion
86    /// rate when converting wall-clock units for mixed-unit authoring.
87    pub fn as_frames_f64(&self) -> f64 {
88        match self {
89            Duration::Frames(value) => value.to_float(),
90            Duration::Milliseconds(value) => value.to_float() / Self::NOMINAL_FRAME_MILLIS,
91            Duration::Seconds(value) => value.to_float() * 1000.0 / Self::NOMINAL_FRAME_MILLIS,
92        }
93    }
94}
95
96macro_rules! impl_duration_from_number {
97    ($($type:ty),* $(,)?) => {
98        $(
99            impl From<$type> for Duration {
100                fn from(value: $type) -> Self {
101                    Duration::Frames(Numeric::from(value))
102                }
103            }
104        )*
105    };
106}
107
108impl_duration_from_number!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64);
109
110// Represents an atomic segment of an imperative transition timeline.
111//
112// This segment is defined with length `duration`, is interpolated according to `curve`, and settles at
113// `ending_value` at the end of its interval.
114pub struct TransitionQueueEntry<T> {
115    // Length of this transition segment.
116    pub duration: Duration,
117    // Easing curve applied over the segment.
118    pub curve: EasingCurve,
119    // Value reached when the segment completes.
120    pub ending_value: T,
121}
122
123#[cfg(debug_assertions)]
124impl<T> std::fmt::Debug for TransitionQueueEntry<T> {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        f.debug_struct("TransitionQueueEntry")
127            .field("duration", &self.duration)
128            // .field("ending_value", &self.ending_value)
129            .finish()
130    }
131}
132
133impl<T: Interpolatable> Interpolatable for Option<T> {
134    fn interpolate(&self, other: &Self, t: f64) -> Self {
135        match &self {
136            Self::Some(s) => match other {
137                Self::Some(o) => Some(s.interpolate(o, t)),
138                _ => None,
139            },
140            Self::None => None,
141        }
142    }
143}
144
145// Manages transition queues for property interpolation, e.g. for `ease_to_later`.
146pub struct TransitionManager<T> {
147    queue: VecDeque<TransitionQueueEntry<T>>,
148    /// The value we are currently transitioning from
149    transition_checkpoint_value: T,
150    /// The frame clock value when the current transition started.
151    origin_frames_elapsed: u64,
152    /// The millisecond clock value when the current transition started.
153    origin_millis_elapsed: u64,
154}
155
156#[cfg(debug_assertions)]
157impl<T> std::fmt::Debug for TransitionManager<T> {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("TransitionManager")
160            .field("queue", &self.queue)
161            // .field("value", &self.transition_checkpoint_value)
162            .finish()
163    }
164}
165
166impl<T: Interpolatable> TransitionManager<T> {
167    // Creates an empty transition queue starting from `value`.
168    pub fn new(value: T, current_frames: u64, current_millis: u64) -> Self {
169        Self {
170            queue: VecDeque::new(),
171            transition_checkpoint_value: value,
172            origin_frames_elapsed: current_frames,
173            origin_millis_elapsed: current_millis,
174        }
175    }
176
177    // Appends one transition segment to the queue.
178    pub fn push_transition(&mut self, transition: TransitionQueueEntry<T>) {
179        self.queue.push_back(transition);
180    }
181
182    // Clears queued transitions after first checkpointing the current eased value.
183    pub fn reset_transitions(&mut self, current_frames: u64, current_millis: u64) {
184        let eased_value = self.compute_eased_value(current_frames, current_millis);
185        self.transition_checkpoint_value =
186            eased_value.unwrap_or(self.transition_checkpoint_value.clone());
187        self.queue.clear();
188        self.origin_frames_elapsed = current_frames;
189        self.origin_millis_elapsed = current_millis;
190    }
191
192    // Computes the current eased value and advances completed transition segments.
193    pub fn compute_eased_value(&mut self, frames_elapsed: u64, millis_elapsed: u64) -> Option<T> {
194        let global_frames = frames_elapsed;
195        let global_millis = millis_elapsed;
196        let mut completed_transition = false;
197
198        // Fast-forward transitions that have already completed, including
199        // zero-duration transitions that should take effect immediately.
200        while let Some(current_transition) = self.queue.front() {
201            let elapsed = transition_elapsed(
202                current_transition.duration,
203                global_frames,
204                global_millis,
205                self.origin_frames_elapsed,
206                self.origin_millis_elapsed,
207            );
208            let duration = transition_duration_units(current_transition.duration);
209            if elapsed < duration {
210                break;
211            }
212            let curr = self.queue.pop_front()?;
213            advance_transition_origin(
214                curr.duration,
215                &mut self.origin_frames_elapsed,
216                &mut self.origin_millis_elapsed,
217            );
218            self.transition_checkpoint_value = curr.ending_value;
219            completed_transition = true;
220        }
221
222        let current_transition = match self.queue.front() {
223            Some(current_transition) => current_transition,
224            None => {
225                return if completed_transition {
226                    Some(self.transition_checkpoint_value.clone())
227                } else {
228                    None
229                };
230            }
231        };
232
233        let elapsed = transition_elapsed(
234            current_transition.duration,
235            global_frames,
236            global_millis,
237            self.origin_frames_elapsed,
238            self.origin_millis_elapsed,
239        );
240        let duration = transition_duration_units(current_transition.duration);
241        let progress = if duration <= f64::EPSILON {
242            1.0
243        } else {
244            (elapsed / duration).clamp(0.0, 1.0)
245        };
246        let interpolated_val = current_transition.curve.interpolate(
247            &self.transition_checkpoint_value,
248            &current_transition.ending_value,
249            progress,
250        );
251        Some(interpolated_val)
252    }
253}
254
255fn transition_duration_units(duration: Duration) -> f64 {
256    match duration {
257        Duration::Frames(value) | Duration::Milliseconds(value) => value.to_float().max(0.0),
258        Duration::Seconds(value) => (value.to_float() * 1000.0).max(0.0),
259    }
260}
261
262fn transition_elapsed(
263    duration: Duration,
264    global_frames: u64,
265    global_millis: u64,
266    origin_frames: u64,
267    origin_millis: u64,
268) -> f64 {
269    match duration {
270        Duration::Frames(_) => global_frames.saturating_sub(origin_frames) as f64,
271        Duration::Milliseconds(_) | Duration::Seconds(_) => {
272            global_millis.saturating_sub(origin_millis) as f64
273        }
274    }
275}
276
277fn advance_transition_origin(duration: Duration, origin_frames: &mut u64, origin_millis: &mut u64) {
278    match duration {
279        Duration::Frames(value) => {
280            *origin_frames = origin_frames.saturating_add(value.to_float().max(0.0).round() as u64)
281        }
282        Duration::Milliseconds(value) => {
283            *origin_millis = origin_millis.saturating_add(value.to_float().max(0.0).round() as u64)
284        }
285        Duration::Seconds(value) => {
286            *origin_millis =
287                origin_millis.saturating_add((value.to_float().max(0.0) * 1000.0).round() as u64)
288        }
289    }
290}
291
292/// Pre-built easing curves for use in transitions, as well as a `Custom` variant that can be used to
293/// specify an arbitrary easing curve via a function `f: f64 -> f64` mapping a time on the unit interval to a multiplier on the unit interval.
294pub enum EasingCurve {
295    /// A linear easing curve, where the interpolated value changes at a constant rate over time.
296    Linear,
297    /// A hold easing curve, where the interpolated value remains constant until the end of the transition, at which point it jumps to the final value.
298    Hold,
299    /// A quadratic easing curve where the interpolated value starts slow and accelerates towards the end of the transition.
300    InQuad,
301    /// A quadratic easing curve where the interpolated value starts fast and decelerates towards the end of the transition.
302    OutQuad,
303    /// A quadratic easing curve where the interpolated value starts slow, accelerates towards the middle of the transition, and then decelerates towards the end of the transition.
304    InOutQuad,
305    /// A back easing curve where the interpolated value starts by briefly moving in the opposite direction before accelerating towards the final value.
306    InBack,
307    /// A back easing curve where the interpolated value overshoots the final value before settling back to it.
308    OutBack,
309    /// A back easing curve where the interpolated value starts by briefly moving in the opposite direction, then accelerates towards the final value, overshooting it before settling back to it.
310    InOutBack,
311    /// A custom easing curve defined by a user-provided function that maps a time on the unit interval to a multiplier on the unit interval.
312    Custom(Box<dyn Fn(f64) -> f64>),
313}
314
315struct EasingEvaluators {}
316impl EasingEvaluators {
317    fn linear(t: f64) -> f64 {
318        t
319    }
320    #[allow(dead_code)]
321    fn none(t: f64) -> f64 {
322        if t == 1.0 {
323            1.0
324        } else {
325            0.0
326        }
327    }
328    fn in_quad(t: f64) -> f64 {
329        t * t
330    }
331    fn out_quad(t: f64) -> f64 {
332        1.0 - (1.0 - t) * (1.0 - t)
333    }
334    fn in_out_quad(t: f64) -> f64 {
335        if t < 0.5 {
336            2.0 * t * t
337        } else {
338            1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
339        }
340    }
341    fn in_back(t: f64) -> f64 {
342        const C1: f64 = 1.70158;
343        const C3: f64 = C1 + 1.00;
344        C3 * t * t * t - C1 * t * t
345    }
346    fn out_back(t: f64) -> f64 {
347        const C1: f64 = 1.70158;
348        const C3: f64 = C1 + 1.00;
349        1.0 + C3 * (t - 1.0).powi(3) + C1 * (t - 1.0).powi(2)
350    }
351
352    fn in_out_back(t: f64) -> f64 {
353        const C1: f64 = 1.70158;
354        const C2: f64 = C1 * 1.525;
355        if t < 0.5 {
356            ((2.0 * t).powi(2) * ((C2 + 1.0) * 2.0 * t - C2)) / 2.0
357        } else {
358            ((2.0 * t - 2.0).powi(2) * ((C2 + 1.0) * (t * 2.0 - 2.0) + C2) + 2.0) / 2.0
359        }
360    }
361}
362
363impl EasingCurve {
364    /// Interpolates between `v0` and `v1` using `t` as time on the unit interval.
365    pub fn interpolate<T: Interpolatable>(&self, v0: &T, v1: &T, t: f64) -> T /*vt*/ {
366        let multiplier = match self {
367            EasingCurve::Linear => EasingEvaluators::linear(t),
368            EasingCurve::Hold => EasingEvaluators::none(t),
369            EasingCurve::InQuad => EasingEvaluators::in_quad(t),
370            EasingCurve::OutQuad => EasingEvaluators::out_quad(t),
371            EasingCurve::InOutQuad => EasingEvaluators::in_out_quad(t),
372            EasingCurve::InBack => EasingEvaluators::in_back(t),
373            EasingCurve::OutBack => EasingEvaluators::out_back(t),
374            EasingCurve::InOutBack => EasingEvaluators::in_out_back(t),
375            EasingCurve::Custom(evaluator) => (*evaluator)(t),
376        };
377
378        v0.interpolate(v1, multiplier)
379    }
380}
381
382impl<I: Clone + 'static> ImplToFromPaxAny for std::ops::Range<I> {}
383impl<T: 'static> ImplToFromPaxAny for Rc<T> {}
384impl<T: Clone + 'static> ImplToFromPaxAny for Weak<T> {}
385impl<T: Clone + 'static> ImplToFromPaxAny for Option<T> {}
386
387impl<T1: Clone + 'static, T2: Clone + 'static> ImplToFromPaxAny for (T1, T2) {}
388
389/// Marks a value as able to participate in Pax property transitions.
390pub trait Interpolatable
391where
392    Self: Sized + Clone,
393{
394    /// Interpolates this value toward `other` at time `t` on the unit interval.
395    ///
396    /// The default implementation acts like a `None` ease: the first value is
397    /// simply retained until the transition completes.
398    fn interpolate(&self, _other: &Self, _t: f64) -> Self {
399        self.clone()
400    }
401}
402
403impl<I: Interpolatable> Interpolatable for std::ops::Range<I> {
404    fn interpolate(&self, _other: &Self, _t: f64) -> Self {
405        self.start.interpolate(&_other.start, _t)..self.end.interpolate(&_other.end, _t)
406    }
407}
408impl Interpolatable for () {}
409
410impl<T: ?Sized + Clone> Interpolatable for HashSet<T> {}
411impl<T: ?Sized + Clone> Interpolatable for VecDeque<T> {}
412impl<T: ?Sized> Interpolatable for Rc<T> {}
413impl<T: Interpolatable> Interpolatable for Weak<T> {}
414impl<T1: Interpolatable, T2: Interpolatable> Interpolatable for (T1, T2) {}
415impl<I: Interpolatable> Interpolatable for Vec<I> {
416    fn interpolate(&self, other: &Self, t: f64) -> Self {
417        //FUTURE: could revisit the following assertion/constraint, perhaps with a "don't-care" approach to disjoint vec elements
418        assert_eq!(
419            self.len(),
420            other.len(),
421            "cannot interpolate between vecs of different lengths"
422        );
423
424        self.iter()
425            .enumerate()
426            .map(|(i, elem)| elem.interpolate(other.get(i).unwrap(), t))
427            .collect()
428    }
429}
430
431impl Interpolatable for kurbo::BezPath {}
432
433impl Interpolatable for Instant {}
434
435impl Interpolatable for char {}
436
437impl Interpolatable for f64 {
438    fn interpolate(&self, other: &f64, t: f64) -> f64 {
439        self + (*other - self) * t
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::{
446        Duration, EasingCurve, Interpolatable, Numeric, Rotation, TransitionManager,
447        TransitionQueueEntry,
448    };
449
450    #[test]
451    fn zero_duration_transition_applies_immediately() {
452        let mut tm = TransitionManager::new(0.0, 0, 0);
453        tm.push_transition(TransitionQueueEntry {
454            duration: Duration::Frames(0.into()),
455            curve: EasingCurve::Linear,
456            ending_value: 10.0,
457        });
458
459        assert_eq!(tm.compute_eased_value(0, 0), Some(10.0));
460        assert_eq!(tm.compute_eased_value(1, 16), None);
461    }
462
463    #[test]
464    fn completed_transition_returns_final_value_when_polled_late() {
465        let mut tm = TransitionManager::new(0.0, 0, 0);
466        tm.push_transition(TransitionQueueEntry {
467            duration: Duration::Frames(10.into()),
468            curve: EasingCurve::Linear,
469            ending_value: 10.0,
470        });
471
472        assert_eq!(tm.compute_eased_value(11, 183), Some(10.0));
473        assert_eq!(tm.compute_eased_value(12, 200), None);
474    }
475
476    #[test]
477    fn zero_duration_transition_chains_into_next_transition() {
478        let mut tm = TransitionManager::new(0.0, 0, 0);
479        tm.push_transition(TransitionQueueEntry {
480            duration: Duration::Frames(0.into()),
481            curve: EasingCurve::Linear,
482            ending_value: 10.0,
483        });
484        tm.push_transition(TransitionQueueEntry {
485            duration: Duration::Frames(10.into()),
486            curve: EasingCurve::Linear,
487            ending_value: 20.0,
488        });
489
490        assert_eq!(tm.compute_eased_value(0, 0), Some(10.0));
491        assert_eq!(tm.compute_eased_value(5, 83), Some(15.0));
492        assert_eq!(tm.compute_eased_value(10, 167), Some(20.0));
493        assert_eq!(tm.compute_eased_value(11, 183), None);
494    }
495
496    #[test]
497    fn millisecond_transition_uses_wall_clock() {
498        let mut tm = TransitionManager::new(0.0, 0, 0);
499        tm.push_transition(TransitionQueueEntry {
500            duration: Duration::Milliseconds(100.into()),
501            curve: EasingCurve::Linear,
502            ending_value: 10.0,
503        });
504
505        assert_eq!(tm.compute_eased_value(1, 50), Some(5.0));
506        assert_eq!(tm.compute_eased_value(2, 100), Some(10.0));
507    }
508
509    #[test]
510    fn rotation_interpolates_linearly_in_degrees() {
511        let start = Rotation::Degrees(Numeric::F64(-4.0));
512        let end = Rotation::Degrees(Numeric::F64(8.0));
513
514        let midpoint = start.interpolate(&end, 0.5);
515        assert!((midpoint.get_as_degrees() - 2.0).abs() < 0.0001);
516    }
517
518    #[test]
519    fn rotation_percent_normalizes_to_unit_interval() {
520        let rotation = Rotation::Percent(Numeric::F64(100.0));
521        assert!((rotation.to_float_0_1() - 1.0).abs() < f64::EPSILON);
522    }
523
524    #[test]
525    fn in_out_quad_eases_symmetrically() {
526        let start = 0.0f64;
527        let end = 10.0f64;
528
529        let first_quarter = EasingCurve::InOutQuad.interpolate(&start, &end, 0.25);
530        let midpoint = EasingCurve::InOutQuad.interpolate(&start, &end, 0.5);
531        let third_quarter = EasingCurve::InOutQuad.interpolate(&start, &end, 0.75);
532
533        assert!((first_quarter - 1.25).abs() < 0.0001);
534        assert!((midpoint - 5.0).abs() < 0.0001);
535        assert!((third_quarter - 8.75).abs() < 0.0001);
536    }
537
538    #[test]
539    fn option_interpolates_inner_value_when_both_sides_are_present() {
540        let start = Some(0.0f64);
541        let end = Some(10.0f64);
542
543        assert_eq!(start.interpolate(&end, 0.0), Some(0.0));
544        assert_eq!(start.interpolate(&end, 0.5), Some(5.0));
545        assert_eq!(start.interpolate(&end, 1.0), Some(10.0));
546    }
547}
548
549impl Interpolatable for bool {
550    fn interpolate(&self, _other: &bool, _t: f64) -> bool {
551        *self
552    }
553}
554
555impl Interpolatable for usize {
556    fn interpolate(&self, other: &usize, t: f64) -> usize {
557        (*self as f64 + (*other - self) as f64 * t) as usize
558    }
559}
560
561impl Interpolatable for isize {
562    fn interpolate(&self, other: &isize, t: f64) -> isize {
563        (*self as f64 + (*other - self) as f64 * t) as isize
564    }
565}
566
567impl Interpolatable for i64 {
568    fn interpolate(&self, other: &i64, t: f64) -> i64 {
569        (*self as f64 + (*other - self) as f64 * t) as i64
570    }
571}
572
573impl Interpolatable for i128 {
574    fn interpolate(&self, other: &i128, t: f64) -> i128 {
575        (*self as f64 + (*other - self) as f64 * t) as i128
576    }
577}
578
579impl Interpolatable for u128 {
580    fn interpolate(&self, other: &u128, t: f64) -> u128 {
581        (*self as f64 + (*other - self) as f64 * t) as u128
582    }
583}
584
585impl Interpolatable for u64 {
586    fn interpolate(&self, other: &u64, t: f64) -> u64 {
587        (*self as f64 + (*other - self) as f64 * t) as u64
588    }
589}
590
591impl Interpolatable for u8 {
592    fn interpolate(&self, other: &u8, t: f64) -> u8 {
593        (*self as f64 + (*other - *self) as f64 * t) as u8
594    }
595}
596
597impl Interpolatable for u16 {
598    fn interpolate(&self, other: &u16, t: f64) -> u16 {
599        (*self as f64 + (*other - *self) as f64 * t) as u16
600    }
601}
602
603impl Interpolatable for u32 {
604    fn interpolate(&self, other: &u32, t: f64) -> u32 {
605        (*self as f64 + (*other - *self) as f64 * t) as u32
606    }
607}
608
609impl Interpolatable for i8 {
610    fn interpolate(&self, other: &i8, t: f64) -> i8 {
611        (*self as f64 + (*other - *self) as f64 * t) as i8
612    }
613}
614
615impl Interpolatable for i16 {
616    fn interpolate(&self, other: &i16, t: f64) -> i16 {
617        (*self as f64 + (*other - *self) as f64 * t) as i16
618    }
619}
620
621impl Interpolatable for i32 {
622    fn interpolate(&self, other: &i32, t: f64) -> i32 {
623        (*self as f64 + (*other - *self) as f64 * t) as i32
624    }
625}
626
627impl Interpolatable for String {}
628
629/// Minimal timeline state used by animation-oriented controls.
630pub struct Timeline {
631    /// Current playhead frame.
632    pub playhead_position: usize,
633    /// Total number of frames.
634    pub frame_count: usize,
635    /// Whether the timeline is currently advancing.
636    pub is_playing: bool,
637}