animation.rs

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