Skip to main content

rgpui_component/
animation.rs

1use std::{rc::Rc, time::Duration};
2
3use rgpui::{
4    Animation, AnimationExt, ElementId, IntoElement, Pixels, Point, Styled, point,
5    prelude::FluentBuilder, px,
6};
7use smallvec::SmallVec;
8
9/// A cubic bezier function like CSS `cubic-bezier`.
10///
11/// Builder:
12///
13/// https://cubic-bezier.com
14pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 {
15    move |t: f32| {
16        let one_t = 1.0 - t;
17        let one_t2 = one_t * one_t;
18        let t2 = t * t;
19        let t3 = t2 * t;
20
21        // The Bezier curve function for x and y, where x0 = 0, y0 = 0, x3 = 1, y3 = 1
22        let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3;
23        let y = 3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3;
24
25        y
26    }
27}
28
29// ── Easing presets ──────────────────────────────────────────────────────────
30
31/// Cubic ease-out — fast start, slow end. Good for enter animations.
32pub fn ease_out_cubic(t: f32) -> f32 {
33    let t = t.clamp(0.0, 1.0);
34    1.0 - (1.0 - t).powi(3)
35}
36
37/// Cubic ease-in — slow start, fast end. Good for exit animations.
38pub fn ease_in_cubic(t: f32) -> f32 {
39    let t = t.clamp(0.0, 1.0);
40    t * t * t
41}
42
43/// Cubic ease-in-out — slow start and end. Good for position transitions.
44pub fn ease_in_out_cubic(t: f32) -> f32 {
45    let t = t.clamp(0.0, 1.0);
46    if t < 0.5 {
47        4.0 * t * t * t
48    } else {
49        1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
50    }
51}
52
53// ── Lerp trait ──────────────────────────────────────────────────────────────
54
55/// Trait for types that support linear interpolation.
56pub trait Lerp: Clone {
57    fn lerp(&self, target: &Self, t: f32) -> Self;
58}
59
60impl Lerp for f32 {
61    fn lerp(&self, target: &Self, t: f32) -> Self {
62        self + (target - self) * t
63    }
64}
65
66impl Lerp for Pixels {
67    fn lerp(&self, target: &Self, t: f32) -> Self {
68        let a: f32 = (*self).into();
69        let b: f32 = (*target).into();
70        px(a + (b - a) * t)
71    }
72}
73
74impl Lerp for Point<Pixels> {
75    fn lerp(&self, target: &Self, t: f32) -> Self {
76        point(
77            Lerp::lerp(&self.x, &target.x, t),
78            Lerp::lerp(&self.y, &target.y, t),
79        )
80    }
81}
82
83// ── Transition combinator ───────────────────────────────────────────────────
84
85/// A composable transition that describes animated style changes.
86///
87/// # Example
88///
89/// ```ignore
90/// Transition::new(Duration::from_millis(150))
91///     .ease(ease_out_cubic)
92///     .slide_y(px(-4.), px(0.))
93///     .fade(0.0, 1.0)
94///     .apply(element, "enter-anim")
95/// ```
96#[derive(Clone)]
97pub struct Transition {
98    pub duration: Duration,
99    easing: Rc<dyn Fn(f32) -> f32>,
100    effects: SmallVec<[TransitionEffect; 2]>,
101}
102
103#[derive(Clone, Copy)]
104enum TransitionEffect {
105    SlideY(Pixels, Pixels),
106    SlideX(Pixels, Pixels),
107    Fade(f32, f32),
108    Width(Pixels, Pixels),
109    Height(Pixels, Pixels),
110}
111
112impl Transition {
113    pub fn new(duration: Duration) -> Self {
114        Self {
115            duration,
116            easing: Rc::new(ease_out_cubic),
117            effects: SmallVec::new(),
118        }
119    }
120
121    /// Set the easing function.
122    pub fn ease(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
123        self.easing = Rc::new(easing);
124        self
125    }
126
127    /// Animate vertical offset from `from` to `to`.
128    pub fn slide_y(mut self, from: Pixels, to: Pixels) -> Self {
129        self.effects.push(TransitionEffect::SlideY(from, to));
130        self
131    }
132
133    /// Animate horizontal offset from `from` to `to`.
134    pub fn slide_x(mut self, from: Pixels, to: Pixels) -> Self {
135        self.effects.push(TransitionEffect::SlideX(from, to));
136        self
137    }
138
139    /// Animate opacity from `from` to `to`.
140    pub fn fade(mut self, from: f32, to: f32) -> Self {
141        self.effects.push(TransitionEffect::Fade(from, to));
142        self
143    }
144
145    /// Animate width from `from` to `to`.
146    pub fn width(mut self, from: Pixels, to: Pixels) -> Self {
147        self.effects.push(TransitionEffect::Width(from, to));
148        self
149    }
150
151    /// Animate height from `from` to `to`.
152    pub fn height(mut self, from: Pixels, to: Pixels) -> Self {
153        self.effects.push(TransitionEffect::Height(from, to));
154        self
155    }
156
157    /// Apply this transition to a Styled element, returning an AnimationElement.
158    pub fn apply<E: IntoElement + Styled + 'static>(
159        self,
160        element: E,
161        id: impl Into<ElementId>,
162    ) -> rgpui::AnimationElement<E> {
163        let animation = Animation::new(self.duration).with_easing({
164            let easing = self.easing.clone();
165            move |t| easing(t)
166        });
167        let effects = self.effects;
168        element.with_animation(id, animation, move |el, delta| {
169            let mut el = el;
170            for effect in &effects {
171                match effect {
172                    TransitionEffect::SlideY(from, to) => {
173                        el = el.top(Lerp::lerp(from, to, delta));
174                    }
175                    TransitionEffect::SlideX(from, to) => {
176                        el = el.left(Lerp::lerp(from, to, delta));
177                    }
178                    TransitionEffect::Fade(from, to) => {
179                        el = el.opacity(Lerp::lerp(from, to, delta));
180                    }
181                    TransitionEffect::Width(from, to) => {
182                        el = el.w(Lerp::lerp(from, to, delta));
183                    }
184                    TransitionEffect::Height(from, to) => {
185                        el = el.h(Lerp::lerp(from, to, delta));
186                    }
187                }
188            }
189            el
190        })
191    }
192}
193
194impl FluentBuilder for Transition {}