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        drop(lt);
48        *a.get()
49    }
50}
51
52/// Animate f32 to the given target; starts at the target on first mount (legacy behavior).
53pub fn animate_f32(key: impl Into<String>, target: f32, spec: AnimationSpec) -> f32 {
54    animate_f32_from(key, target, target, spec)
55}
56
57macro_rules! animate_from_impl {
58    ($name:ident, $type:ty, $prefix:expr) => {
59        pub fn $name(
60            key: impl Into<String>,
61            initial: $type,
62            target: $type,
63            spec: AnimationSpec,
64        ) -> $type {
65            let key = key.into();
66            let anim_key = format!("anim:{}:{}", $prefix, key);
67            let anim = remember_state_with_key(&anim_key, || AnimatedValue::new(initial, spec));
68            let last =
69                remember_state_with_key(format!("anim:{}_last:{}", $prefix, key), || None::<$type>);
70
71            repose_core::animation_driver::touch(&anim_key);
72
73            let mut a = anim.borrow_mut();
74            let mut lt = last.borrow_mut();
75            if lt.is_none() || lt.as_ref().unwrap() != &target {
76                a.set_spec(spec);
77                a.set_target(target);
78                *lt = Some(target);
79                drop(lt);
80
81                // Register with AnimationDriver
82                let reg_key = anim_key.clone();
83                let reg_anim = anim.clone();
84                repose_core::animation_driver::register(
85                    reg_key,
86                    std::rc::Rc::new(std::cell::RefCell::new(move || {
87                        reg_anim.borrow_mut().update()
88                    })),
89                );
90                repose_core::request_frame();
91
92                *a.get()
93            } else {
94                drop(lt);
95                *a.get()
96            }
97        }
98    };
99}
100
101animate_from_impl!(animate_color_from, Color, "color");
102animate_from_impl!(animate_vec2_from, Vec2, "vec2");
103animate_from_impl!(animate_size_from, Size, "size");
104animate_from_impl!(animate_rect_from, Rect, "rect");
105
106/// Animate Color to the given target; starts at the target on first mount (legacy behavior).
107pub fn animate_color(key: impl Into<String>, target: Color, spec: AnimationSpec) -> Color {
108    animate_color_from(key, target, target, spec)
109}
110
111/// Animate Vec2 to the given target; starts at the target on first mount.
112pub fn animate_vec2(key: impl Into<String>, target: Vec2, spec: AnimationSpec) -> Vec2 {
113    animate_vec2_from(key, target, target, spec)
114}
115
116/// Animate Size to the given target; starts at the target on first mount.
117pub fn animate_size(key: impl Into<String>, target: Size, spec: AnimationSpec) -> Size {
118    animate_size_from(key, target, target, spec)
119}
120
121/// Animate Rect to the given target; starts at the target on first mount.
122pub fn animate_rect(key: impl Into<String>, target: Rect, spec: AnimationSpec) -> Rect {
123    animate_rect_from(key, target, target, spec)
124}
125
126/// Animate f32 through a sequence of keyframes.
127///
128/// The keyframe timestamps range from 0.0 to 1.0, mapped across `spec`'s duration.
129/// The animation loops if `repeat` is set on the spec.
130///
131/// Example:
132/// ```ignore
133/// let val = animate_keyframes("bounce", KeyframesSpec::new(vec![
134///     (0.0, 0.0),
135///     (0.3, 100.0),
136///     (0.6, 80.0),
137///     (1.0, 100.0),
138/// ]), AnimationSpec::tween(Duration::from_millis(600), Easing::EaseOut));
139/// ```
140pub fn animate_keyframes(
141    key: impl Into<String>,
142    keyframes: KeyframesSpec<f32>,
143    spec: AnimationSpec,
144) -> f32 {
145    let key = key.into();
146    let anim = remember_state_with_key(format!("anim:kf:{key}"), || AnimatedValue::new(0.0, spec));
147    let mut a = anim.borrow_mut();
148    if !a.has_keyframes() {
149        a.set_keyframes(keyframes);
150    }
151    a.update();
152    *a.get()
153}
154
155/// Animate f32 through a sequence of keyframes using a smooth cubic Hermite spline.
156///
157/// Produces C1-continuous animation (smooth derivatives at keyframe boundaries),
158/// unlike `animate_keyframes` which uses C0 linear interpolation.
159///
160/// Example:
161/// ```ignore
162/// let val = animate_spline_keyframes("bounce", SplineKeyframes::new(vec![
163///     (0.0, 0.0),
164///     (0.3, 100.0),
165///     (0.6, 80.0),
166///     (1.0, 100.0),
167/// ]), AnimationSpec::tween(Duration::from_millis(600), Easing::EaseInOut));
168/// ```
169pub fn animate_spline_keyframes(
170    key: impl Into<String>,
171    keyframes: SplineKeyframes,
172    spec: AnimationSpec,
173) -> f32 {
174    let key = key.into();
175    // Animate progress 0..1 using standard AnimatedValue (handles easing, repeat, frame clock)
176    let anim = remember_state_with_key(format!("anim:spkf_progress:{key}"), || {
177        AnimatedValue::new(0.0, spec)
178    });
179    let spline = remember_state_with_key(format!("anim:spkf:{key}"), || keyframes);
180
181    let mut a = anim.borrow_mut();
182    let s = spline.borrow();
183
184    a.set_target(1.0);
185    a.update();
186    let progress = *a.get();
187    s.evaluate(progress)
188}
189
190fn with_infinite_repeat(spec: AnimationSpec) -> AnimationSpec {
191    if spec.repeat.is_none() {
192        spec.repeated(RepeatableSpec::infinite().reverse())
193    } else {
194        spec
195    }
196}
197
198/// A scoped API for continuous/repeating animations.
199///
200/// All child animations created via this transition loop indefinitely
201/// between the given initial and target values (ping-pong with `reverse`).
202///
203/// If you need a custom repeat configuration, pass an `AnimationSpec` that
204/// already has `.repeated(...)` set - the transition will respect it instead
205/// of applying the default infinite reverse.
206pub struct InfiniteTransition {
207    _private: (),
208}
209
210/// Creates an `InfiniteTransition` scoped to the current composition slot.
211///
212/// Use its `animate_float`, `animate_color`, `animate_vec2`, `animate_size`,
213/// and `animate_rect` methods for continuous looping animations.
214///
215/// # Example
216///
217/// ```ignore
218/// let t = remember_infinite_transition();
219/// let pulse = t.animate_float("pulse", 0.0, 1.0,
220///     AnimationSpec::tween(Duration::from_millis(600), Easing::EaseInOut));
221/// ```
222pub fn remember_infinite_transition() -> InfiniteTransition {
223    InfiniteTransition { _private: () }
224}
225
226macro_rules! inf_method {
227    ($method:ident, $from_fn:ident, $type:ty) => {
228        pub fn $method(
229            &self,
230            key: impl Into<String>,
231            initial: $type,
232            target: $type,
233            spec: AnimationSpec,
234        ) -> $type {
235            let key = key.into();
236            $from_fn(
237                format!("inf:{}", key),
238                initial,
239                target,
240                with_infinite_repeat(spec),
241            )
242        }
243    };
244}
245
246impl InfiniteTransition {
247    inf_method!(animate_float, animate_f32_from, f32);
248    inf_method!(animate_color, animate_color_from, Color);
249    inf_method!(animate_vec2, animate_vec2_from, Vec2);
250    inf_method!(animate_size, animate_size_from, Size);
251    inf_method!(animate_rect, animate_rect_from, Rect);
252}
253
254/// A scope for multi-target animations driven by a changing state.
255///
256/// When the target state changes (detected via `PartialEq`), all child
257/// animations registered via `animate_float`, `animate_color`, etc.
258/// automatically animate from their current value toward the new mapped
259/// target value.
260///
261/// # Example
262///
263/// ```ignore
264/// let t = update_transition("panel", is_expanded, AnimationSpec::spring_gentle());
265/// let h  = t.animate_float("height", |e| if *e { 200.0 } else { 48.0 });
266/// let bg = t.animate_color("bg", |e| if *e { theme().primary } else { theme().surface });
267/// ```
268pub struct TransitionScope<T> {
269    key: String,
270    spec: AnimationSpec,
271    state: Rc<RefCell<T>>,
272}
273
274/// Creates a `TransitionScope` keyed to the given state.
275///
276/// Whenever `target_state` differs from the previous call (via `PartialEq`),
277/// all child animation targets are updated and the transition animates toward
278/// the new values.
279pub fn update_transition<T>(
280    key: impl Into<String>,
281    target_state: T,
282    spec: AnimationSpec,
283) -> TransitionScope<T>
284where
285    T: PartialEq + Clone + 'static,
286{
287    let key = key.into();
288    let state: Rc<RefCell<T>> =
289        remember_state_with_key(format!("tr_state:{key}"), || target_state.clone());
290    if *state.borrow() != target_state {
291        *state.borrow_mut() = target_state;
292    }
293    TransitionScope { key, spec, state }
294}
295
296macro_rules! tr_method {
297    ($method:ident, $fn:ident, $type:ty) => {
298        pub fn $method<F>(&self, child_key: impl Into<String>, map: F) -> $type
299        where
300            F: Fn(&T) -> $type,
301        {
302            let target = map(&*self.state.borrow());
303            $fn(
304                format!("tr:{}:{}", self.key, child_key.into()),
305                target,
306                self.spec,
307            )
308        }
309    };
310}
311
312impl<T> TransitionScope<T> {
313    tr_method!(animate_float, animate_f32, f32);
314    tr_method!(animate_color, animate_color, Color);
315    tr_method!(animate_vec2, animate_vec2, Vec2);
316    tr_method!(animate_size, animate_size, Size);
317    tr_method!(animate_rect, animate_rect, Rect);
318}