Skip to main content

oxframe_core/
tween.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::rc::Rc;
4use std::task::{Context, Poll};
5
6use crate::ease::presets;
7use crate::runtime;
8use crate::{Ease, Lerp, Signal, TIME_EPSILON};
9
10/// A length of time. `f64` means seconds; other types (a theme pace) convert.
11pub trait Seconds {
12    /// Duration in seconds.
13    fn seconds(self) -> f64;
14}
15
16impl Seconds for f64 {
17    fn seconds(self) -> f64 {
18        self
19    }
20}
21
22/// Animates a [`Signal`] towards a target. Created by [`Signal::to`].
23///
24/// The first poll captures the start value and the task time; every later
25/// poll is one frame. On completion the task time is advanced to exactly
26/// `start + duration`, not to the frame time, so chained tweens do not
27/// accumulate rounding drift.
28#[must_use = "a tween does nothing unless awaited"]
29pub struct Tween<T> {
30    signal: Signal<T>,
31    from: Option<T>,
32    target: T,
33    duration: f64,
34    ease: Rc<dyn Ease>,
35    start: Option<f64>,
36}
37
38impl<T: Lerp + Clone + 'static> Tween<T> {
39    pub(crate) fn new(signal: Signal<T>, target: T, duration: f64) -> Self {
40        Self {
41            signal,
42            from: None,
43            target,
44            duration: duration.max(0.0),
45            ease: Rc::new(presets::DEFAULT),
46            start: None,
47        }
48    }
49
50    /// Easing curve of this tween; default [`presets::DEFAULT`](crate::presets::DEFAULT).
51    pub fn ease(mut self, ease: impl Ease + 'static) -> Self {
52        self.ease = Rc::new(ease);
53        self
54    }
55
56    /// Changes the length, e.g. of a helper like `fade_in()` that defaults to
57    /// the theme's pace: `title.fade_in().duration(0.3)`.
58    pub fn duration(mut self, duration: impl Seconds) -> Self {
59        self.duration = duration.seconds().max(0.0);
60        self
61    }
62
63    /// Starts from `value` instead of the signal's value at the first poll.
64    pub fn from(mut self, value: T) -> Self {
65        self.from = Some(value);
66        self
67    }
68}
69
70/// Progress of a timed operation that started at `start`; `None` once done
71/// (and then the task time is set to the exact end).
72pub(crate) fn progress(start: f64, duration: f64) -> Option<f64> {
73    runtime::with(|rt| {
74        let end = start + duration;
75        let now = rt.now();
76        if now + TIME_EPSILON >= end {
77            rt.task_time.set(end);
78            None
79        } else {
80            Some(((now - start) / duration).clamp(0.0, 1.0))
81        }
82    })
83}
84
85impl<T: Lerp + Clone + Unpin + 'static> Future for Tween<T> {
86    type Output = ();
87
88    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
89        let this = self.get_mut();
90        let start = match this.start {
91            Some(s) => s,
92            None => {
93                this.from.get_or_insert_with(|| this.signal.peek());
94                *this.start.insert(runtime::time())
95            }
96        };
97        match progress(start, this.duration) {
98            None => this.signal.set(this.target.clone()),
99            Some(t) => {
100                let from = this.from.as_ref().expect("captured on first poll");
101                this.signal.set(from.lerp(&this.target, this.ease.ease(t)));
102                return Poll::Pending;
103            }
104        }
105        Poll::Ready(())
106    }
107}