Skip to main content

i_slint_core/
animations.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4#![warn(missing_docs)]
5//! The animation system
6
7use alloc::boxed::Box;
8use core::cell::Cell;
9#[cfg(not(feature = "std"))]
10use num_traits::Float;
11
12pub(crate) mod simulations;
13
14mod cubic_bezier {
15    //! This is a copy from lyon_algorithms::geom::cubic_bezier implementation
16    //! (from lyon_algorithms 0.17)
17    type S = f32;
18    use euclid::default::Point2D as Point;
19    #[allow(unused)]
20    use num_traits::Float;
21    trait Scalar {
22        const ONE: f32 = 1.;
23        const THREE: f32 = 3.;
24        const HALF: f32 = 0.5;
25        const SIX: f32 = 6.;
26        const NINE: f32 = 9.;
27        fn value(v: f32) -> f32 {
28            v
29        }
30    }
31    impl Scalar for f32 {}
32    pub struct CubicBezierSegment {
33        pub from: Point<S>,
34        pub ctrl1: Point<S>,
35        pub ctrl2: Point<S>,
36        pub to: Point<S>,
37    }
38
39    impl CubicBezierSegment {
40        /// Sample the x coordinate of the curve at t (expecting t between 0 and 1).
41        pub fn x(&self, t: S) -> S {
42            let t2 = t * t;
43            let t3 = t2 * t;
44            let one_t = S::ONE - t;
45            let one_t2 = one_t * one_t;
46            let one_t3 = one_t2 * one_t;
47
48            self.from.x * one_t3
49                + self.ctrl1.x * S::THREE * one_t2 * t
50                + self.ctrl2.x * S::THREE * one_t * t2
51                + self.to.x * t3
52        }
53
54        /// Sample the y coordinate of the curve at t (expecting t between 0 and 1).
55        pub fn y(&self, t: S) -> S {
56            let t2 = t * t;
57            let t3 = t2 * t;
58            let one_t = S::ONE - t;
59            let one_t2 = one_t * one_t;
60            let one_t3 = one_t2 * one_t;
61
62            self.from.y * one_t3
63                + self.ctrl1.y * S::THREE * one_t2 * t
64                + self.ctrl2.y * S::THREE * one_t * t2
65                + self.to.y * t3
66        }
67
68        #[inline]
69        fn derivative_coefficients(&self, t: S) -> (S, S, S, S) {
70            let t2 = t * t;
71            (
72                -S::THREE * t2 + S::SIX * t - S::THREE,
73                S::NINE * t2 - S::value(12.0) * t + S::THREE,
74                -S::NINE * t2 + S::SIX * t,
75                S::THREE * t2,
76            )
77        }
78
79        /// Sample the x coordinate of the curve's derivative at t (expecting t between 0 and 1).
80        pub fn dx(&self, t: S) -> S {
81            let (c0, c1, c2, c3) = self.derivative_coefficients(t);
82            self.from.x * c0 + self.ctrl1.x * c1 + self.ctrl2.x * c2 + self.to.x * c3
83        }
84    }
85
86    impl CubicBezierSegment {
87        // This is actually in the Monotonic<CubicBezierSegment<S>> impl
88        pub fn solve_t_for_x(&self, x: S, t_range: core::ops::Range<S>, tolerance: S) -> S {
89            debug_assert!(t_range.start <= t_range.end);
90            let from = self.x(t_range.start);
91            let to = self.x(t_range.end);
92            if x <= from {
93                return t_range.start;
94            }
95            if x >= to {
96                return t_range.end;
97            }
98
99            // Newton's method.
100            let mut t = (x - from) / (to - from);
101            let mut degenerate = false;
102            for _ in 0..8 {
103                let x2 = self.x(t);
104                let dx = self.dx(t);
105
106                if dx <= S::EPSILON {
107                    degenerate = true;
108                    break;
109                }
110
111                let step = (x2 - x) / dx;
112                t -= step;
113
114                if S::abs(step) <= tolerance {
115                    return t.max(t_range.start).min(t_range.end);
116                }
117            }
118
119            if !degenerate {
120                return t.max(t_range.start).min(t_range.end);
121            }
122
123            // Fall back to binary search.
124            let mut min = t_range.start;
125            let mut max = t_range.end;
126            let mut t = S::HALF;
127
128            while min < max {
129                let x2 = self.x(t);
130
131                if S::abs(x2 - x) < tolerance {
132                    return t;
133                }
134
135                if x > x2 {
136                    min = t;
137                } else {
138                    max = t;
139                }
140
141                t = (max - min) * S::HALF + min;
142            }
143
144            t
145        }
146    }
147}
148
149/// The representation of an easing curve, for animations
150#[repr(C, u32)]
151#[derive(Debug, Clone, Copy, PartialEq, Default)]
152pub enum EasingCurve {
153    /// The linear curve
154    #[default]
155    Linear,
156    /// A Cubic bezier curve, with its 4 parameters
157    CubicBezier([f32; 4]),
158    /// Easing curve as defined at: <https://easings.net/#easeInElastic>
159    EaseInElastic,
160    /// Easing curve as defined at: <https://easings.net/#easeOutElastic>
161    EaseOutElastic,
162    /// Easing curve as defined at: <https://easings.net/#easeInOutElastic>
163    EaseInOutElastic,
164    /// Easing curve as defined at: <https://easings.net/#easeInBounce>
165    EaseInBounce,
166    /// Easing curve as defined at: <https://easings.net/#easeOutBounce>
167    EaseOutBounce,
168    /// Easing curve as defined at: <https://easings.net/#easeInOutBounce>
169    EaseInOutBounce,
170    /// A spring animation, configured via `PropertyAnimation`'s `duration`, and the passed in
171    /// `bounce`
172    Spring(f32),
173    // Custom(Box<dyn Fn(f32) -> f32>),
174}
175
176/// Represent an instant, in milliseconds since the AnimationDriver's initial_instant
177#[repr(transparent)]
178#[derive(Copy, Clone, Debug, Default, PartialEq, Ord, PartialOrd, Eq)]
179pub struct Instant(pub u64);
180
181impl core::ops::Sub<Instant> for Instant {
182    type Output = core::time::Duration;
183    fn sub(self, other: Self) -> core::time::Duration {
184        core::time::Duration::from_millis(self.0 - other.0)
185    }
186}
187
188impl core::ops::Sub<core::time::Duration> for Instant {
189    type Output = Instant;
190    fn sub(self, other: core::time::Duration) -> Instant {
191        Self(self.0 - other.as_millis() as u64)
192    }
193}
194
195impl core::ops::Add<core::time::Duration> for Instant {
196    type Output = Instant;
197    fn add(self, other: core::time::Duration) -> Instant {
198        Self(self.0 + other.as_millis() as u64)
199    }
200}
201
202impl core::ops::AddAssign<core::time::Duration> for Instant {
203    fn add_assign(&mut self, other: core::time::Duration) {
204        self.0 += other.as_millis() as u64;
205    }
206}
207
208impl core::ops::SubAssign<core::time::Duration> for Instant {
209    fn sub_assign(&mut self, other: core::time::Duration) {
210        self.0 -= other.as_millis() as u64;
211    }
212}
213
214impl Instant {
215    /// Returns the amount of time elapsed since an other instant.
216    ///
217    /// Equivalent to `self - earlier`
218    pub fn duration_since(self, earlier: Instant) -> core::time::Duration {
219        self - earlier
220    }
221
222    /// Wrapper around [`std::time::Instant::now()`] that delegates to the backend
223    /// and allows working in no_std environments.
224    ///
225    /// Takes the context rather than reaching for an ambient one because the origin is the
226    /// platform's start time: instants from different contexts are not comparable, so the
227    /// caller has to say which clock it means.
228    pub fn now(ctx: &crate::SlintContext) -> Self {
229        Self(ctx.platform().duration_since_start().as_millis() as u64)
230    }
231
232    /// Return the number of milliseconds this `Instant` is after the backend has started
233    pub fn as_millis(&self) -> u64 {
234        self.0
235    }
236}
237
238/// The AnimationDriver
239pub struct AnimationDriver {
240    /// Indicate whether there are any active animations that require a future call to update_animations.
241    active_animations: Cell<bool>,
242    global_instant: core::pin::Pin<Box<crate::Property<Instant>>>,
243}
244
245impl Default for AnimationDriver {
246    fn default() -> Self {
247        AnimationDriver {
248            active_animations: Cell::default(),
249            global_instant: Box::pin(crate::Property::new_named(
250                Instant::default(),
251                "i_slint_core::AnimationDriver::global_instant",
252            )),
253        }
254    }
255}
256
257impl AnimationDriver {
258    /// Iterates through all animations based on the new time tick and updates their state. This should be called by
259    /// the windowing system driver for every frame.
260    pub fn update_animations(&self, new_tick: Instant) {
261        let current_tick = self.global_instant.as_ref().get_untracked();
262        assert!(current_tick <= new_tick, "The platform's clock is not monotonic!");
263        if current_tick != new_tick {
264            self.active_animations.set(false);
265            self.global_instant.as_ref().set(new_tick);
266        }
267    }
268
269    /// Returns true if there are any active or ready animations. This is used by the windowing system to determine
270    /// if a new animation frame is required or not. Returns false otherwise.
271    pub fn has_active_animations(&self) -> bool {
272        self.active_animations.get()
273    }
274
275    /// Tell the driver that there are active animations
276    pub fn set_has_active_animations(&self) {
277        self.active_animations.set(true);
278    }
279    /// The current instant that is to be used for animation
280    /// using this function register the current binding as a dependency
281    pub fn current_tick(&self) -> Instant {
282        self.global_instant.as_ref().get()
283    }
284}
285
286crate::thread_local!(
287/// This is the default instance of the animation driver that's used to advance all property animations
288/// at the same time.
289pub static CURRENT_ANIMATION_DRIVER : AnimationDriver = AnimationDriver::default()
290);
291
292/// The current instant that is to be used for animation
293/// using this function register the current binding as a dependency
294pub fn current_tick() -> Instant {
295    CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick())
296}
297
298/// Same as [`current_tick`], but also register that one should be running animation
299/// on next frame
300pub fn animation_tick() -> u64 {
301    CURRENT_ANIMATION_DRIVER.with(|driver| {
302        driver.set_has_active_animations();
303        driver.current_tick().0
304    })
305}
306
307fn ease_out_bounce_curve(value: f32) -> f32 {
308    const N1: f32 = 7.5625;
309    const D1: f32 = 2.75;
310
311    if value < 1.0 / D1 {
312        N1 * value * value
313    } else if value < 2.0 / D1 {
314        let value = value - (1.5 / D1);
315        N1 * value * value + 0.75
316    } else if value < 2.5 / D1 {
317        let value = value - (2.25 / D1);
318        N1 * value * value + 0.9375
319    } else {
320        let value = value - (2.625 / D1);
321        N1 * value * value + 0.984375
322    }
323}
324
325/// How close to the target position/velocity a `SpringSimulation` must get before it is
326/// considered settled and snaps to rest. This is the "settling duration" and is distinct from the
327/// user-facing `duration` that fixes the natural frequency
328const SPRING_SETTLE_POSITION_EPSILON: f32 = 0.001;
329const SPRING_SETTLE_VELOCITY_EPSILON: f32 = 0.05;
330
331/// Evaluates a mass/stiffness/damping spring at `elapsed_secs`, returning `(progress, settled)`.
332pub fn spring_settle_progress(
333    regime: &simulations::spring::SpringRegime,
334    elapsed_secs: f32,
335) -> (f32, bool) {
336    let (rel_pos, rel_vel) = regime.evaluate(elapsed_secs);
337    let settled = rel_pos.abs() < SPRING_SETTLE_POSITION_EPSILON
338        && rel_vel.abs() < SPRING_SETTLE_VELOCITY_EPSILON;
339    (1.0 + rel_pos, settled)
340}
341
342/// The damping ratio (zeta) required for a spring to settle within 9x `duration` is a fixed
343/// value, since zeta is proportional to bounce and duration.
344///
345/// The spring runs at its literal bounce for every iteration except the last, where it gets
346/// clamped to this value if it hasn't settled by then -- hence needing to settle within 9x
347/// (not some other multiple of) `duration`. Found empirically: the actual value is ~0.8803, but
348/// 0.87 is used to leave some floating-point leeway for comparisons.
349const SPRING_SETTLE_ZETA: f32 = 1.0 - 0.87;
350
351/// Set the spring to settle within 10x duration
352pub fn spring_settle_within(
353    regime: &simulations::spring::SpringRegime,
354    elapsed_secs: f32,
355    w_n: f32,
356) -> simulations::spring::SpringRegime {
357    let (rel_pos, rel_vel) = regime.evaluate(elapsed_secs);
358    let zeta = regime.zeta().max(SPRING_SETTLE_ZETA);
359    simulations::spring::SpringRegime::new(rel_pos, rel_vel, w_n, zeta)
360}
361
362/// map a value between 0 and 1 to another value between 0 and 1 according to the curve
363pub fn easing_curve(curve: &EasingCurve, value: f32) -> f32 {
364    match curve {
365        EasingCurve::Linear => value,
366        EasingCurve::CubicBezier([a, b, c, d]) => {
367            if !(0.0..=1.0).contains(a) && !(0.0..=1.0).contains(c) {
368                return value;
369            };
370            let curve = cubic_bezier::CubicBezierSegment {
371                from: (0., 0.).into(),
372                ctrl1: (*a, *b).into(),
373                ctrl2: (*c, *d).into(),
374                to: (1., 1.).into(),
375            };
376            curve.y(curve.solve_t_for_x(value, 0.0..1.0, 0.01))
377        }
378        EasingCurve::EaseInElastic => {
379            const C4: f32 = 2.0 * core::f32::consts::PI / 3.0;
380
381            if value == 0.0 {
382                0.0
383            } else if value == 1.0 {
384                1.0
385            } else {
386                -f32::powf(2.0, 10.0 * value - 10.0) * f32::sin((value * 10.0 - 10.75) * C4)
387            }
388        }
389        EasingCurve::EaseOutElastic => {
390            let c4 = (2.0 * core::f32::consts::PI) / 3.0;
391
392            if value == 0.0 {
393                0.0
394            } else if value == 1.0 {
395                1.0
396            } else {
397                2.0f32.powf(-10.0 * value) * ((value * 10.0 - 0.75) * c4).sin() + 1.0
398            }
399        }
400        EasingCurve::EaseInOutElastic => {
401            const C5: f32 = 2.0 * core::f32::consts::PI / 4.5;
402
403            if value == 0.0 {
404                0.0
405            } else if value == 1.0 {
406                1.0
407            } else if value < 0.5 {
408                -(f32::powf(2.0, 20.0 * value - 10.0) * f32::sin((20.0 * value - 11.125) * C5))
409                    / 2.0
410            } else {
411                (f32::powf(2.0, -20.0 * value + 10.0) * f32::sin((20.0 * value - 11.125) * C5))
412                    / 2.0
413                    + 1.0
414            }
415        }
416        EasingCurve::EaseInBounce => 1.0 - ease_out_bounce_curve(1.0 - value),
417        EasingCurve::EaseOutBounce => ease_out_bounce_curve(value),
418        EasingCurve::EaseInOutBounce => {
419            if value < 0.5 {
420                (1.0 - ease_out_bounce_curve(1.0 - 2.0 * value)) / 2.0
421            } else {
422                (1.0 + ease_out_bounce_curve(2.0 * value - 1.0)) / 2.0
423            }
424        }
425        EasingCurve::Spring(_) => {
426            panic!("Springs are handled separately");
427        }
428    }
429}
430
431/*
432#[test]
433fn easing_test() {
434    fn test_curve(name: &str, curve: &EasingCurve) {
435        let mut img = image::ImageBuffer::new(500, 500);
436        let white = image::Rgba([255 as u8, 255 as u8, 255 as u8, 255 as u8]);
437
438        for x in 0..img.width() {
439            let t = (x as f32) / (img.width() as f32);
440            let y = easing_curve(curve, t);
441            let y = (y * (img.height() as f32)) as u32;
442            let y = y.min(img.height() - 1);
443            *img.get_pixel_mut(x, img.height() - 1 - y) = white;
444        }
445
446        img.save(
447            std::path::PathBuf::from(std::env::var_os("HOME").unwrap())
448                .join(format!("{}.png", name)),
449        )
450        .unwrap();
451    }
452
453    test_curve("linear", &EasingCurve::Linear);
454    test_curve("linear2", &EasingCurve::CubicBezier([0.0, 0.0, 1.0, 1.0]));
455    test_curve("ease", &EasingCurve::CubicBezier([0.25, 0.1, 0.25, 1.0]));
456    test_curve("ease_in", &EasingCurve::CubicBezier([0.42, 0.0, 1.0, 1.0]));
457    test_curve("ease_in_out", &EasingCurve::CubicBezier([0.42, 0.0, 0.58, 1.0]));
458    test_curve("ease_out", &EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]));
459}
460*/
461
462/// Update the global animation time to `now`.
463///
464/// The driver is per-thread while `now` comes from whichever context is driving it, so a
465/// thread running several contexts with different clock origins would see the tick jump.
466/// Per-context animation drivers would mean reaching a context from every binding
467/// evaluation, which is a much larger change.
468pub fn update_animations(now: Instant) {
469    CURRENT_ANIMATION_DRIVER.with(|driver| {
470        #[allow(unused_mut)]
471        let mut duration = now.0;
472        #[cfg(feature = "std")]
473        if let Ok(val) = std::env::var("SLINT_SLOW_ANIMATIONS") {
474            let factor = val.parse().unwrap_or(2).max(1);
475            duration /= factor;
476        };
477        driver.update_animations(Instant(duration))
478    });
479}