sycamore/
motion.rs

1//! Utilities for smooth transitions and animations.
2
3use std::cell::OnceCell;
4use std::rc::Rc;
5
6use crate::reactive::*;
7
8/// Type returned by [`create_raf`] and [`create_raf_loop`].
9type RafState = (Signal<bool>, Rc<dyn Fn() + 'static>, Rc<dyn Fn() + 'static>);
10
11/// Schedule a callback to be called on each animation frame.
12/// Does nothing if not on `wasm32` target.
13///
14/// Returns a tuple of `(running, start, stop)`. The first item is a boolean signal representing
15/// whether the raf is currently running. The second item is a function to start the raf. The
16/// third item is a function to stop the raf.
17///
18/// The raf is not started by default. Call the `start` function to initiate the raf.
19pub fn create_raf(mut cb: impl FnMut() + 'static) -> RafState {
20    let running = create_signal(false);
21    let start: Rc<dyn Fn()>;
22    let stop: Rc<dyn Fn()>;
23    let _ = &mut cb;
24
25    // Only run on wasm32 architecture.
26    #[cfg(all(target_arch = "wasm32", feature = "web"))]
27    {
28        use std::cell::RefCell;
29
30        use wasm_bindgen::prelude::*;
31
32        use crate::web::window;
33
34        let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
35        let g = Rc::clone(&f);
36
37        *g.borrow_mut() = Some(Closure::new(move || {
38            if running.get() {
39                // Verified that scope is still valid. We can access `extended` in here.
40                cb();
41                // Request the next raf frame.
42                window()
43                    .request_animation_frame(
44                        f.borrow().as_ref().unwrap_throw().as_ref().unchecked_ref(),
45                    )
46                    .unwrap_throw();
47            }
48        }));
49        start = Rc::new(move || {
50            if !running.get() {
51                running.set(true);
52                window()
53                    .request_animation_frame(
54                        g.borrow().as_ref().unwrap_throw().as_ref().unchecked_ref(),
55                    )
56                    .unwrap_throw();
57            }
58        });
59        stop = Rc::new(move || running.set(false));
60    }
61    #[cfg(not(all(target_arch = "wasm32", feature = "web")))]
62    {
63        start = Rc::new(move || running.set(true));
64        stop = Rc::new(move || running.set(false));
65    }
66
67    (running, start, stop)
68}
69
70/// Schedule a callback to be called on each animation frame.
71/// Does nothing if not on `wasm32` target.
72///
73/// Instead of using `start` and `stop` functions, the callback is kept on looping until it
74/// returns `false`. `start` and `stop` are returned regardless to allow controlling the
75/// looping from outside the function.
76///
77/// The raf is not started by default. Call the `start` function to initiate the raf.
78pub fn create_raf_loop(mut f: impl FnMut() -> bool + 'static) -> RafState {
79    let stop_shared = Rc::new(OnceCell::new());
80    let (running, start, stop) = create_raf({
81        let stop_shared = Rc::clone(&stop_shared);
82        move || {
83            if !f() {
84                stop_shared.get();
85            }
86        }
87    });
88    stop_shared.set(Rc::clone(&stop)).ok().unwrap();
89    (running, start, stop)
90}
91
92/// Create a new [`Tweened`] signal.
93pub fn create_tweened_signal<T: Lerp + Clone>(
94    initial: T,
95    transition_duration: std::time::Duration,
96    easing_fn: impl Fn(f32) -> f32 + 'static,
97) -> Tweened<T> {
98    Tweened::new(initial, transition_duration, easing_fn)
99}
100
101/// Describes a trait that can be linearly interpolate between two points.
102pub trait Lerp {
103    /// Get a value between `cx` and `other` at a `scalar`.
104    ///
105    /// `0.0 <= scalar <= 1`
106    fn lerp(&self, other: &Self, scalar: f32) -> Self;
107}
108
109macro_rules! impl_lerp_for_float {
110    ($($f: path),*) => {
111        $(
112            impl Lerp for $f {
113                fn lerp(&self, other: &Self, scalar: f32) -> Self {
114                    self + (other - self) * scalar as $f
115                }
116            }
117        )*
118    };
119}
120
121impl_lerp_for_float!(f32, f64);
122
123macro_rules! impl_lerp_for_int {
124    ($($i: path),*) => {
125        $(
126            impl Lerp for $i {
127                fn lerp(&self, other: &Self, scalar: f32) -> Self {
128                    (*self as f32 + (other - self) as f32 * scalar).round() as $i
129                }
130            }
131        )*
132    };
133}
134
135impl_lerp_for_int!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);
136
137impl<T: Lerp + Clone, const N: usize> Lerp for [T; N] {
138    fn lerp(&self, other: &Self, scalar: f32) -> Self {
139        let mut tmp = (*self).clone();
140
141        for (t, other) in tmp.iter_mut().zip(other) {
142            *t = t.lerp(other, scalar);
143        }
144
145        tmp
146    }
147}
148
149/// A state that is interpolated when it is set.
150pub struct Tweened<T: Lerp + Clone + 'static>(Signal<TweenedInner<T>>);
151impl<T: Lerp + Clone> std::fmt::Debug for Tweened<T> {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.debug_struct("Tweened").finish()
154    }
155}
156
157struct TweenedInner<T: Lerp + Clone + 'static> {
158    value: Signal<T>,
159    is_tweening: Signal<bool>,
160    raf_state: Option<RafState>,
161    transition_duration_ms: f32,
162    easing_fn: Rc<dyn Fn(f32) -> f32>,
163}
164
165impl<T: Lerp + Clone> Tweened<T> {
166    /// Create a new tweened state with the given value.
167    ///
168    /// End users should use [`Scope::create_tweened_signal`] instead.
169    pub(crate) fn new(
170        initial: T,
171        transition_duration: std::time::Duration,
172        easing_fn: impl Fn(f32) -> f32 + 'static,
173    ) -> Self {
174        let value = create_signal(initial);
175        Self(create_signal(TweenedInner {
176            value,
177            is_tweening: create_signal(false),
178            raf_state: None,
179            transition_duration_ms: transition_duration.as_millis() as f32,
180            easing_fn: Rc::new(easing_fn),
181        }))
182    }
183
184    /// Set the target value for the `Tweened`. The existing value will be interpolated to the
185    /// target value with the specified `transition_duration` and `easing_fn`.
186    ///
187    /// If the value is being interpolated already due to a previous call to `set()`, the previous
188    /// task will be canceled.
189    ///
190    /// To immediately set the value without interpolating the value, use `signal().set(...)`
191    /// instead.
192    ///
193    /// If not running on `wasm32-unknown-unknown`, does nothing.
194    pub fn set(&self, _new_value: T) {
195        #[cfg(all(target_arch = "wasm32", feature = "web"))]
196        {
197            use web_sys::js_sys::Date;
198
199            let start = self.signal().get_clone_untracked();
200            let easing_fn = Rc::clone(&self.0.with(|this| this.easing_fn.clone()));
201
202            let start_time = Date::now();
203            let signal = self.0.with(|this| this.value.clone());
204            let is_tweening = self.0.with(|this| this.is_tweening.clone());
205            let transition_duration_ms = self.0.with(|this| this.transition_duration_ms);
206
207            // If previous raf is still running, call stop() to cancel it.
208            if let Some((running, _, stop)) = &self.0.with(|this| this.raf_state.clone()) {
209                if running.get_untracked() {
210                    stop();
211                }
212            }
213
214            let (running, start, stop) = create_raf_loop(move || {
215                let now = Date::now();
216
217                let since_start = now - start_time;
218                let scalar = since_start as f32 / transition_duration_ms;
219
220                if now < start_time + transition_duration_ms as f64 {
221                    signal.set(start.lerp(&_new_value, easing_fn(scalar)));
222                    true
223                } else {
224                    signal.set(_new_value.clone());
225                    is_tweening.set(false);
226                    false
227                }
228            });
229            start();
230            is_tweening.set(true);
231            self.0
232                .update(|this| this.raf_state = Some((running, start, stop)));
233        }
234    }
235
236    /// Alias for `signal().get()`.
237    pub fn get(&self) -> T
238    where
239        T: Copy,
240    {
241        self.signal().get()
242    }
243
244    /// Alias for `signal().get_untracked()`.
245    pub fn get_untracked(&self) -> T
246    where
247        T: Copy,
248    {
249        self.signal().get_untracked()
250    }
251
252    /// Get the inner signal backing the state.
253    pub fn signal(&self) -> Signal<T> {
254        self.0.with(|this| this.value)
255    }
256
257    /// Returns `true` if the value is currently being tweened/interpolated. This value is reactive
258    /// and can be tracked.
259    pub fn is_tweening(&self) -> bool {
260        self.0.with(|this| this.is_tweening.get())
261    }
262}
263
264impl<T: Lerp + Clone + 'static> Clone for Tweened<T> {
265    fn clone(&self) -> Self {
266        *self
267    }
268}
269impl<T: Lerp + Clone + 'static> Copy for Tweened<T> {}
270
271impl<T: Lerp + Clone + 'static> Clone for TweenedInner<T> {
272    fn clone(&self) -> Self {
273        Self {
274            value: self.value,
275            is_tweening: self.is_tweening,
276            raf_state: self.raf_state.clone(),
277            transition_duration_ms: self.transition_duration_ms,
278            easing_fn: Rc::clone(&self.easing_fn),
279        }
280    }
281}