Skip to main content

glassy_ui/motion/
transition.rs

1use std::rc::Rc;
2use std::time::Duration;
3
4use gpui::{ease_in_out, ease_out_quint, linear, quadratic, Animation};
5
6/// Named easing curves (motion.dev / CSS-like).
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub enum Ease {
9    Linear,
10    EaseIn,
11    #[default]
12    EaseOut,
13    EaseInOut,
14    EaseOutQuint,
15}
16
17impl Ease {
18    pub(crate) fn function(self) -> Rc<dyn Fn(f32) -> f32> {
19        match self {
20            Self::Linear => Rc::new(linear),
21            Self::EaseIn => Rc::new(quadratic),
22            Self::EaseOut => Rc::new(ease_out_cubic),
23            Self::EaseInOut => Rc::new(ease_in_out),
24            Self::EaseOutQuint => Rc::new(ease_out_quint()),
25        }
26    }
27}
28
29pub fn ease_out_cubic(t: f32) -> f32 {
30    let t = t.clamp(0.0, 1.0);
31    1.0 - (1.0 - t).powi(3)
32}
33
34pub fn ease_in_cubic(t: f32) -> f32 {
35    let t = t.clamp(0.0, 1.0);
36    t * t * t
37}
38
39pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 {
40    move |t: f32| {
41        let t = t.clamp(0.0, 1.0);
42        let one_t = 1.0 - t;
43        let one_t2 = one_t * one_t;
44        let t2 = t * t;
45        let t3 = t2 * t;
46        let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3;
47        3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3
48    }
49}
50
51pub(crate) fn spring_out(t: f32, bounce: f32) -> f32 {
52    let t = t.clamp(0.0, 1.0);
53    if bounce <= 0.001 {
54        return ease_out_cubic(t);
55    }
56    let freq = 1.0 + bounce * 2.5;
57    let decay = (-5.0 * t).exp();
58    let wave = (t * freq * std::f32::consts::TAU).sin();
59    (1.0 - decay + decay * wave * bounce * 0.45).clamp(0.0, 1.0 + bounce * 0.2)
60}
61
62#[derive(Clone)]
63enum TransitionKind {
64    Tween {
65        duration: Duration,
66        ease: Ease,
67    },
68    Spring {
69        duration: Duration,
70        bounce: f32,
71    },
72    SpringPhysics {
73        stiffness: f32,
74        damping: f32,
75        mass: f32,
76    },
77}
78
79/// motion.dev-style `transition` config.
80#[derive(Clone)]
81pub struct Transition {
82    kind: TransitionKind,
83    delay: Duration,
84    repeat: bool,
85    /// Delay before animating each child in a [`crate::Stagger`] group.
86    pub(crate) stagger_children: Duration,
87    /// Extra delay before the first staggered child starts.
88    pub(crate) delay_children: Duration,
89}
90
91impl Default for Transition {
92    fn default() -> Self {
93        Self::tween(Duration::from_millis(300)).ease(Ease::EaseOut)
94    }
95}
96
97impl Transition {
98    pub fn tween(duration: Duration) -> Self {
99        Self {
100            kind: TransitionKind::Tween {
101                duration,
102                ease: Ease::EaseOut,
103            },
104            delay: Duration::ZERO,
105            repeat: false,
106            stagger_children: Duration::ZERO,
107            delay_children: Duration::ZERO,
108        }
109    }
110
111    pub fn spring() -> Self {
112        Self {
113            kind: TransitionKind::Spring {
114                duration: Duration::from_millis(500),
115                bounce: 0.25,
116            },
117            delay: Duration::ZERO,
118            repeat: false,
119            stagger_children: Duration::ZERO,
120            delay_children: Duration::ZERO,
121        }
122    }
123
124    pub fn ease(mut self, ease: Ease) -> Self {
125        if let TransitionKind::Tween {
126            ease: ref mut e, ..
127        } = self.kind
128        {
129            *e = ease;
130        }
131        self
132    }
133
134    pub fn duration(mut self, duration: Duration) -> Self {
135        match &mut self.kind {
136            TransitionKind::Tween {
137                duration: ref mut d,
138                ..
139            }
140            | TransitionKind::Spring {
141                duration: ref mut d,
142                ..
143            } => *d = duration,
144            TransitionKind::SpringPhysics { .. } => {
145                self.kind = TransitionKind::Spring {
146                    duration,
147                    bounce: 0.25,
148                };
149            }
150        }
151        self
152    }
153
154    pub fn bounce(mut self, bounce: f32) -> Self {
155        let bounce = bounce.clamp(0.0, 1.0);
156        match &mut self.kind {
157            TransitionKind::Spring {
158                bounce: ref mut b, ..
159            } => *b = bounce,
160            _ => {
161                let duration = self.resolved_duration();
162                self.kind = TransitionKind::Spring { duration, bounce };
163            }
164        }
165        self
166    }
167
168    pub fn stiffness(mut self, stiffness: f32) -> Self {
169        self.ensure_physics();
170        if let TransitionKind::SpringPhysics {
171            stiffness: ref mut s,
172            ..
173        } = self.kind
174        {
175            *s = stiffness.max(1.0);
176        }
177        self
178    }
179
180    pub fn damping(mut self, damping: f32) -> Self {
181        self.ensure_physics();
182        if let TransitionKind::SpringPhysics {
183            damping: ref mut d, ..
184        } = self.kind
185        {
186            *d = damping.max(0.0);
187        }
188        self
189    }
190
191    pub fn mass(mut self, mass: f32) -> Self {
192        self.ensure_physics();
193        if let TransitionKind::SpringPhysics {
194            mass: ref mut m, ..
195        } = self.kind
196        {
197            *m = mass.max(0.01);
198        }
199        self
200    }
201
202    pub fn delay(mut self, delay: Duration) -> Self {
203        self.delay = delay;
204        self
205    }
206
207    pub fn repeat(mut self) -> Self {
208        self.repeat = true;
209        self
210    }
211
212    /// Stagger delay between children (motion.dev `staggerChildren`).
213    pub fn stagger_children(mut self, stagger: Duration) -> Self {
214        self.stagger_children = stagger;
215        self
216    }
217
218    /// Delay before the first staggered child (motion.dev `delayChildren`).
219    pub fn delay_children(mut self, delay: Duration) -> Self {
220        self.delay_children = delay;
221        self
222    }
223
224    fn ensure_physics(&mut self) {
225        if !matches!(self.kind, TransitionKind::SpringPhysics { .. }) {
226            self.kind = TransitionKind::SpringPhysics {
227                stiffness: 300.0,
228                damping: 25.0,
229                mass: 1.0,
230            };
231        }
232    }
233
234    pub(crate) fn resolved_duration(&self) -> Duration {
235        match self.kind {
236            TransitionKind::Tween { duration, .. } | TransitionKind::Spring { duration, .. } => {
237                duration
238            }
239            TransitionKind::SpringPhysics {
240                stiffness,
241                damping,
242                mass,
243            } => {
244                let omega = (stiffness / mass).sqrt();
245                let zeta = damping / (2.0 * (stiffness * mass).sqrt());
246                let seconds = if zeta < 1.0 {
247                    4.0 / (zeta * omega).max(0.1)
248                } else {
249                    4.0 / omega.max(0.1)
250                };
251                Duration::from_secs_f32(seconds.clamp(0.12, 2.0))
252            }
253        }
254    }
255
256    fn resolved_bounce(&self) -> f32 {
257        match self.kind {
258            TransitionKind::Spring { bounce, .. } => bounce,
259            TransitionKind::SpringPhysics {
260                stiffness,
261                damping,
262                mass,
263            } => {
264                let zeta = damping / (2.0 * (stiffness * mass).sqrt()).max(0.001);
265                (1.0 - zeta.clamp(0.0, 1.0)) * 0.5
266            }
267            TransitionKind::Tween { .. } => 0.0,
268        }
269    }
270
271    pub(crate) fn with_extra_delay(mut self, extra: Duration) -> Self {
272        self.delay = self.delay.saturating_add(extra);
273        self
274    }
275
276    pub(crate) fn into_animation(self) -> Animation {
277        let duration = self.resolved_duration().saturating_add(self.delay);
278        let bounce = self.resolved_bounce();
279        let delay_frac = if duration.as_secs_f32() > 0.0 {
280            self.delay.as_secs_f32() / duration.as_secs_f32()
281        } else {
282            0.0
283        };
284
285        let easing: Rc<dyn Fn(f32) -> f32> = match self.kind {
286            TransitionKind::Tween { ease, .. } => {
287                let ease = ease.function();
288                Rc::new(move |t: f32| gated_ease(t, delay_frac, |local| ease(local)))
289            }
290            TransitionKind::Spring { .. } | TransitionKind::SpringPhysics { .. } => {
291                Rc::new(move |t: f32| gated_ease(t, delay_frac, |local| spring_out(local, bounce)))
292            }
293        };
294
295        let mut animation = Animation::new(duration).with_easing(move |t| easing(t));
296        if self.repeat {
297            animation = animation.repeat();
298        }
299        animation
300    }
301}
302
303fn gated_ease(t: f32, delay_frac: f32, ease: impl Fn(f32) -> f32) -> f32 {
304    if t < delay_frac {
305        0.0
306    } else {
307        let local = ((t - delay_frac) / (1.0 - delay_frac).max(0.0001)).clamp(0.0, 1.0);
308        ease(local)
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::{ease_out_cubic, spring_out};
315
316    #[test]
317    fn ease_out_ends_at_one() {
318        assert!((ease_out_cubic(1.0) - 1.0).abs() < 0.001);
319        assert!(ease_out_cubic(0.0).abs() < 0.001);
320    }
321
322    #[test]
323    fn spring_out_settles() {
324        assert!(spring_out(0.0, 0.25).abs() < 0.05);
325        assert!((spring_out(1.0, 0.25) - 1.0).abs() < 0.05);
326    }
327}