Skip to main content

repose_ui/
anim.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use repose_core::{
5    Color, Rect, Size, Vec2,
6    animation::{AnimatedValue, AnimationSpec, KeyframesSpec, RepeatableSpec, SplineKeyframes},
7    animation_driver, remember_state_with_key, request_frame,
8};
9
10/// Animate f32 from an explicit initial value to a target.
11/// - On first creation for this key, starts at `initial` then animates toward `target`.
12/// - On later calls, animates from the current value to the new target.
13pub fn animate_f32_from(
14    key: impl Into<String>,
15    initial: f32,
16    target: f32,
17    spec: AnimationSpec,
18) -> f32 {
19    let key = key.into();
20    let anim_key = format!("anim:f32:{key}");
21    let anim = remember_state_with_key(&anim_key, || AnimatedValue::new(initial, spec));
22    let last = remember_state_with_key(format!("anim:f32_last:{key}"), || f32::NAN);
23
24    // Doesn't remove entries from pages that are still being composed with this.
25    animation_driver::touch(&anim_key);
26
27    let mut a = anim.borrow_mut();
28    let mut lt = last.borrow_mut();
29    let should_set_target = lt.is_nan() || (*lt - target).abs() > 1e-6;
30    if should_set_target {
31        a.set_spec(spec);
32        a.set_target(target);
33        *lt = target;
34        drop(lt);
35
36        // Register with AnimationDriver for pre-composition advancement.
37        let reg_key = anim_key;
38        let reg_anim = anim.clone();
39        animation_driver::register(
40            reg_key,
41            Rc::new(RefCell::new(move || reg_anim.borrow_mut().update())),
42        );
43        request_frame();
44
45        *a.get()
46    } else {
47        let needs_reregister = !animation_driver::is_registered(&anim_key) && a.is_animating();
48        let cur = *a.get();
49        drop(lt);
50        drop(a);
51        if needs_reregister {
52            let reg_anim = anim.clone();
53            animation_driver::register(
54                anim_key,
55                Rc::new(RefCell::new(move || reg_anim.borrow_mut().update())),
56            );
57            request_frame();
58        }
59        cur
60    }
61}
62
63/// Animate f32 to target. Starts at target on first mount.
64pub fn animate_f32(key: impl Into<String>, target: f32, spec: AnimationSpec) -> f32 {
65    animate_f32_from(key, target, target, spec)
66}
67
68macro_rules! animate_from_impl {
69    ($name:ident, $type:ty, $prefix:expr) => {
70        pub fn $name(
71            key: impl Into<String>,
72            initial: $type,
73            target: $type,
74            spec: AnimationSpec,
75        ) -> $type {
76            let key = key.into();
77            let anim_key = format!("anim:{}:{}", $prefix, key);
78            let anim = remember_state_with_key(&anim_key, || AnimatedValue::new(initial, spec));
79            let last =
80                remember_state_with_key(format!("anim:{}_last:{}", $prefix, key), || None::<$type>);
81
82            repose_core::animation_driver::touch(&anim_key);
83
84            let mut a = anim.borrow_mut();
85            let mut lt = last.borrow_mut();
86            if lt.is_none() || lt.as_ref().unwrap() != &target {
87                a.set_spec(spec);
88                a.set_target(target);
89                *lt = Some(target);
90                drop(lt);
91
92                // Register with AnimationDriver
93                let reg_key = anim_key.clone();
94                let reg_anim = anim.clone();
95                repose_core::animation_driver::register(
96                    reg_key,
97                    std::rc::Rc::new(std::cell::RefCell::new(move || {
98                        reg_anim.borrow_mut().update()
99                    })),
100                );
101                repose_core::request_frame();
102
103                *a.get()
104            } else {
105                let needs_reregister =
106                    !repose_core::animation_driver::is_registered(&anim_key) && a.is_animating();
107                let cur = *a.get();
108                drop(lt);
109                drop(a);
110                if needs_reregister {
111                    let reg_anim = anim.clone();
112                    repose_core::animation_driver::register(
113                        anim_key,
114                        std::rc::Rc::new(std::cell::RefCell::new(move || {
115                            reg_anim.borrow_mut().update()
116                        })),
117                    );
118                    repose_core::request_frame();
119                }
120                cur
121            }
122        }
123    };
124}
125
126animate_from_impl!(animate_color_from, Color, "color");
127animate_from_impl!(animate_vec2_from, Vec2, "vec2");
128animate_from_impl!(animate_size_from, Size, "size");
129animate_from_impl!(animate_rect_from, Rect, "rect");
130
131/// Animate Color to target. Starts at target on first mount.
132pub fn animate_color(key: impl Into<String>, target: Color, spec: AnimationSpec) -> Color {
133    animate_color_from(key, target, target, spec)
134}
135
136/// Animate Vec2 to target. Starts at target on first mount.
137pub fn animate_vec2(key: impl Into<String>, target: Vec2, spec: AnimationSpec) -> Vec2 {
138    animate_vec2_from(key, target, target, spec)
139}
140
141/// Animate Size to target. Starts at target on first mount.
142pub fn animate_size(key: impl Into<String>, target: Size, spec: AnimationSpec) -> Size {
143    animate_size_from(key, target, target, spec)
144}
145
146/// Animate Rect to target. Starts at target on first mount.
147pub fn animate_rect(key: impl Into<String>, target: Rect, spec: AnimationSpec) -> Rect {
148    animate_rect_from(key, target, target, spec)
149}
150
151/// Animate f32 through a sequence of keyframes.
152///
153/// The keyframe timestamps range from 0.0 to 1.0, mapped across `spec`'s duration.
154/// The animation loops if `repeat` is set on the spec.
155///
156/// Example:
157/// ```ignore
158/// let val = animate_keyframes("bounce", KeyframesSpec::new(vec![
159///     (0.0, 0.0),
160///     (0.3, 100.0),
161///     (0.6, 80.0),
162///     (1.0, 100.0),
163/// ]), AnimationSpec::tween(Duration::from_millis(600), Easing::EaseOut));
164/// ```
165pub fn animate_keyframes(
166    key: impl Into<String>,
167    keyframes: KeyframesSpec<f32>,
168    spec: AnimationSpec,
169) -> f32 {
170    let key = key.into();
171    let anim_key = format!("anim:kf:{key}");
172    let anim = remember_state_with_key(&anim_key, || AnimatedValue::new(0.0, spec));
173    let last = remember_state_with_key(format!("anim:kf_last:{key}"), || None::<String>);
174    animation_driver::touch(&anim_key);
175
176    let fingerprint = format!("{:?}|{:?}", keyframes.keyframes, spec);
177    let should_restart = {
178        let lt = last.borrow();
179        lt.as_ref() != Some(&fingerprint)
180    };
181    let mut a = anim.borrow_mut();
182    if should_restart {
183        a.set_spec(spec);
184        a.set_keyframes(keyframes);
185        *last.borrow_mut() = Some(fingerprint);
186        drop(a);
187        let reg_anim = anim.clone();
188        animation_driver::register(
189            anim_key.clone(),
190            Rc::new(RefCell::new(move || reg_anim.borrow_mut().update())),
191        );
192        request_frame();
193        *anim.borrow().get()
194    } else {
195        let needs_reregister = !animation_driver::is_registered(&anim_key) && a.is_animating();
196        let cur = *a.get();
197        drop(a);
198        if needs_reregister {
199            let reg_anim = anim.clone();
200            animation_driver::register(
201                anim_key,
202                Rc::new(RefCell::new(move || reg_anim.borrow_mut().update())),
203            );
204            request_frame();
205        }
206        cur
207    }
208}
209
210/// Animate f32 through a sequence of keyframes using a smooth cubic Hermite spline.
211///
212/// Produces C1-continuous animation (smooth derivatives at keyframe boundaries),
213/// unlike `animate_keyframes` which uses C0 linear interpolation.
214///
215/// Example:
216/// ```ignore
217/// let val = animate_spline_keyframes("bounce", SplineKeyframes::new(vec![
218///     (0.0, 0.0),
219///     (0.3, 100.0),
220///     (0.6, 80.0),
221///     (1.0, 100.0),
222/// ]), AnimationSpec::tween(Duration::from_millis(600), Easing::EaseInOut));
223/// ```
224pub fn animate_spline_keyframes(
225    key: impl Into<String>,
226    keyframes: SplineKeyframes,
227    spec: AnimationSpec,
228) -> f32 {
229    let key = key.into();
230    // Animate progress 0..1 using standard AnimatedValue (handles easing, repeat, frame clock)
231    let anim = remember_state_with_key(format!("anim:spkf_progress:{key}"), || {
232        AnimatedValue::new(0.0, spec)
233    });
234    let spline = remember_state_with_key(format!("anim:spkf:{key}"), || keyframes.clone());
235
236    let anim_key = format!("anim:spkf_progress:{key}");
237    let last = remember_state_with_key(format!("anim:spkf_last:{key}"), || None::<String>);
238    animation_driver::touch(&anim_key);
239    let fingerprint = format!("{}|{spec:?}", keyframes.fingerprint());
240    let should_restart = {
241        let lt = last.borrow();
242        lt.as_ref() != Some(&fingerprint)
243    };
244
245    let progress = {
246        let mut a = anim.borrow_mut();
247        if should_restart {
248            a.set_spec(spec);
249            a.set_target(1.0);
250            *last.borrow_mut() = Some(fingerprint);
251            *spline.borrow_mut() = keyframes;
252            drop(a);
253            let reg_anim = anim.clone();
254            animation_driver::register(
255                anim_key.clone(),
256                Rc::new(RefCell::new(move || reg_anim.borrow_mut().update())),
257            );
258            request_frame();
259            *anim.borrow().get()
260        } else {
261            let needs_reregister = !animation_driver::is_registered(&anim_key) && a.is_animating();
262            let cur = *a.get();
263            drop(a);
264            if needs_reregister {
265                let reg_anim = anim.clone();
266                animation_driver::register(
267                    anim_key,
268                    Rc::new(RefCell::new(move || reg_anim.borrow_mut().update())),
269                );
270                request_frame();
271            }
272            cur
273        }
274    };
275    spline.borrow().evaluate(progress)
276}
277
278fn with_infinite_repeat(spec: AnimationSpec) -> AnimationSpec {
279    if spec.repeat.is_none() {
280        spec.repeated(RepeatableSpec::infinite().reverse())
281    } else {
282        spec
283    }
284}
285
286/// A scoped API for continuous/repeating animations.
287///
288/// All child animations created via this transition loop indefinitely
289/// between the given initial and target values (ping-pong with `reverse`).
290///
291/// If you need a custom repeat configuration, pass an `AnimationSpec` that
292/// already has `.repeated(...)` set - the transition will respect it instead
293/// of applying the default infinite reverse.
294pub struct InfiniteTransition {
295    _private: (),
296}
297
298/// `InfiniteTransition` scoped to the current composition slot.
299///
300/// Use its `animate_float`, `animate_color`, `animate_vec2`, `animate_size`,
301/// and `animate_rect` methods for continuous looping animations.
302///
303/// # Example
304///
305/// ```ignore
306/// let t = remember_infinite_transition();
307/// let pulse = t.animate_float("pulse", 0.0, 1.0,
308///     AnimationSpec::tween(Duration::from_millis(600), Easing::EaseInOut));
309/// ```
310pub fn remember_infinite_transition() -> InfiniteTransition {
311    InfiniteTransition { _private: () }
312}
313
314macro_rules! inf_method {
315    ($method:ident, $from_fn:ident, $type:ty) => {
316        pub fn $method(
317            &self,
318            key: impl Into<String>,
319            initial: $type,
320            target: $type,
321            spec: AnimationSpec,
322        ) -> $type {
323            let key = key.into();
324            $from_fn(
325                format!("inf:{}", key),
326                initial,
327                target,
328                with_infinite_repeat(spec),
329            )
330        }
331    };
332}
333
334impl InfiniteTransition {
335    inf_method!(animate_float, animate_f32_from, f32);
336    inf_method!(animate_color, animate_color_from, Color);
337    inf_method!(animate_vec2, animate_vec2_from, Vec2);
338    inf_method!(animate_size, animate_size_from, Size);
339    inf_method!(animate_rect, animate_rect_from, Rect);
340}
341
342/// A scope for multi-target animations driven by a changing state.
343///
344/// When the target state changes (detected via `PartialEq`), all child
345/// animations registered via `animate_float`, `animate_color`, etc.
346/// automatically animate from their current value toward the new mapped
347/// target value.
348///
349/// # Example
350///
351/// ```ignore
352/// let t = update_transition("panel", is_expanded, AnimationSpec::spring_gentle());
353/// let h  = t.animate_float("height", |e| if *e { 200.0 } else { 48.0 });
354/// let bg = t.animate_color("bg", |e| if *e { theme().primary } else { theme().surface });
355/// ```
356pub struct TransitionScope<T> {
357    key: String,
358    spec: AnimationSpec,
359    state: Rc<RefCell<T>>,
360}
361
362/// `TransitionScope` keyed to the given state.
363///
364/// Whenever `target_state` differs from the previous call (via `PartialEq`),
365/// all child animation targets are updated and the transition animates toward
366/// the new values.
367pub fn update_transition<T>(
368    key: impl Into<String>,
369    target_state: T,
370    spec: AnimationSpec,
371) -> TransitionScope<T>
372where
373    T: PartialEq + Clone + 'static,
374{
375    let key = key.into();
376    let state: Rc<RefCell<T>> =
377        remember_state_with_key(format!("tr_state:{key}"), || target_state.clone());
378    if *state.borrow() != target_state {
379        *state.borrow_mut() = target_state;
380    }
381    TransitionScope { key, spec, state }
382}
383
384macro_rules! tr_method {
385    ($method:ident, $fn:ident, $type:ty) => {
386        pub fn $method<F>(&self, child_key: impl Into<String>, map: F) -> $type
387        where
388            F: Fn(&T) -> $type,
389        {
390            let target = map(&*self.state.borrow());
391            $fn(
392                format!("tr:{}:{}", self.key, child_key.into()),
393                target,
394                self.spec,
395            )
396        }
397    };
398}
399
400impl<T> TransitionScope<T> {
401    tr_method!(animate_float, animate_f32, f32);
402    tr_method!(animate_color, animate_color, Color);
403    tr_method!(animate_vec2, animate_vec2, Vec2);
404    tr_method!(animate_size, animate_size, Size);
405    tr_method!(animate_rect, animate_rect, Rect);
406}