animation.rs

  1use std::time::{Duration, Instant};
  2
  3use crate::{AnyElement, App, Element, ElementId, GlobalElementId, IntoElement, Window};
  4
  5pub use easing::*;
  6use smallvec::SmallVec;
  7
  8/// An animation that can be applied to an element.
  9pub struct Animation {
 10    /// The amount of time for which this animation should run
 11    pub duration: Duration,
 12    /// Whether to repeat this animation when it finishes
 13    pub oneshot: bool,
 14    /// A function that takes a delta between 0 and 1 and returns a new delta
 15    /// between 0 and 1 based on the given easing function.
 16    pub easing: Box<dyn Fn(f32) -> f32>,
 17}
 18
 19impl Animation {
 20    /// Create a new animation with the given duration.
 21    /// By default the animation will only run once and will use a linear easing function.
 22    pub fn new(duration: Duration) -> Self {
 23        Self {
 24            duration,
 25            oneshot: true,
 26            easing: Box::new(linear),
 27        }
 28    }
 29
 30    /// Set the animation to loop when it finishes.
 31    pub fn repeat(mut self) -> Self {
 32        self.oneshot = false;
 33        self
 34    }
 35
 36    /// Set the easing function to use for this animation.
 37    /// The easing function will take a time delta between 0 and 1 and return a new delta
 38    /// between 0 and 1
 39    pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
 40        self.easing = Box::new(easing);
 41        self
 42    }
 43}
 44
 45/// An extension trait for adding the animation wrapper to both Elements and Components
 46pub trait AnimationExt {
 47    /// Render this component or element with an animation
 48    fn with_animation(
 49        self,
 50        id: impl Into<ElementId>,
 51        animation: Animation,
 52        animator: impl Fn(Self, f32) -> Self + 'static,
 53    ) -> AnimationElement<Self>
 54    where
 55        Self: Sized,
 56    {
 57        AnimationElement {
 58            id: id.into(),
 59            element: Some(self),
 60            animator: Box::new(move |this, _, value| animator(this, value)),
 61            animations: smallvec::smallvec![animation],
 62        }
 63    }
 64
 65    /// Render this component or element with a chain of animations
 66    fn with_animations(
 67        self,
 68        id: impl Into<ElementId>,
 69        animations: Vec<Animation>,
 70        animator: impl Fn(Self, usize, f32) -> Self + 'static,
 71    ) -> AnimationElement<Self>
 72    where
 73        Self: Sized,
 74    {
 75        AnimationElement {
 76            id: id.into(),
 77            element: Some(self),
 78            animator: Box::new(animator),
 79            animations: animations.into(),
 80        }
 81    }
 82}
 83
 84impl<E> AnimationExt for E {}
 85
 86/// A GPUI element that applies an animation to another element
 87pub struct AnimationElement<E> {
 88    id: ElementId,
 89    element: Option<E>,
 90    animations: SmallVec<[Animation; 1]>,
 91    animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
 92}
 93
 94impl<E> AnimationElement<E> {
 95    /// Returns a new [`AnimationElement<E>`] after applying the given function
 96    /// to the element being animated.
 97    pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
 98        self.element = self.element.map(f);
 99        self
100    }
101}
102
103impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
104    type Element = AnimationElement<E>;
105
106    fn into_element(self) -> Self::Element {
107        self
108    }
109}
110
111struct AnimationState {
112    start: Instant,
113    animation_ix: usize,
114}
115
116impl<E: IntoElement + 'static> Element for AnimationElement<E> {
117    type RequestLayoutState = AnyElement;
118    type PrepaintState = ();
119
120    fn id(&self) -> Option<ElementId> {
121        Some(self.id.clone())
122    }
123
124    fn request_layout(
125        &mut self,
126        global_id: Option<&GlobalElementId>,
127        window: &mut Window,
128        cx: &mut App,
129    ) -> (crate::LayoutId, Self::RequestLayoutState) {
130        window.with_element_state(global_id.unwrap(), |state, window| {
131            let mut state = state.unwrap_or_else(|| AnimationState {
132                start: Instant::now(),
133                animation_ix: 0,
134            });
135            let animation_ix = state.animation_ix;
136
137            let mut delta = state.start.elapsed().as_secs_f32()
138                / self.animations[animation_ix].duration.as_secs_f32();
139
140            let mut done = false;
141            if delta > 1.0 {
142                if self.animations[animation_ix].oneshot {
143                    if animation_ix >= self.animations.len() - 1 {
144                        done = true;
145                    } else {
146                        state.start = Instant::now();
147                        state.animation_ix += 1;
148                    }
149                    delta = 1.0;
150                } else {
151                    delta %= 1.0;
152                }
153            }
154            let delta = (self.animations[animation_ix].easing)(delta);
155
156            debug_assert!(
157                (0.0..=1.0).contains(&delta),
158                "delta should always be between 0 and 1"
159            );
160
161            let element = self.element.take().expect("should only be called once");
162            let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
163
164            if !done {
165                window.request_animation_frame();
166            }
167
168            ((element.request_layout(window, cx), element), state)
169        })
170    }
171
172    fn prepaint(
173        &mut self,
174        _id: Option<&GlobalElementId>,
175        _bounds: crate::Bounds<crate::Pixels>,
176        element: &mut Self::RequestLayoutState,
177        window: &mut Window,
178        cx: &mut App,
179    ) -> Self::PrepaintState {
180        element.prepaint(window, cx);
181    }
182
183    fn paint(
184        &mut self,
185        _id: Option<&GlobalElementId>,
186        _bounds: crate::Bounds<crate::Pixels>,
187        element: &mut Self::RequestLayoutState,
188        _: &mut Self::PrepaintState,
189        window: &mut Window,
190        cx: &mut App,
191    ) {
192        element.paint(window, cx);
193    }
194}
195
196mod easing {
197    use std::f32::consts::PI;
198
199    /// The linear easing function, or delta itself
200    pub fn linear(delta: f32) -> f32 {
201        delta
202    }
203
204    /// The quadratic easing function, delta * delta
205    pub fn quadratic(delta: f32) -> f32 {
206        delta * delta
207    }
208
209    /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
210    pub fn ease_in_out(delta: f32) -> f32 {
211        if delta < 0.5 {
212            2.0 * delta * delta
213        } else {
214            let x = -2.0 * delta + 2.0;
215            1.0 - x * x / 2.0
216        }
217    }
218
219    /// The Quint ease-out function, which starts quickly and decelerates to a stop
220    pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
221        move |delta| 1.0 - (1.0 - delta).powi(5)
222    }
223
224    /// Apply the given easing function, first in the forward direction and then in the reverse direction
225    pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
226        move |delta| {
227            if delta < 0.5 {
228                easing(delta * 2.0)
229            } else {
230                easing((1.0 - delta) * 2.0)
231            }
232        }
233    }
234
235    /// A custom easing function for pulsating alpha that slows down as it approaches 0.1
236    pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
237        let range = max - min;
238
239        move |delta| {
240            // Use a combination of sine and cubic functions for a more natural breathing rhythm
241            let t = (delta * 2.0 * PI).sin();
242            let breath = (t * t * t + t) / 2.0;
243
244            // Map the breath to our desired alpha range
245            let normalized_alpha = (breath + 1.0) / 2.0;
246
247            min + (normalized_alpha * range)
248        }
249    }
250}