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
12mod cubic_bezier {
13 //! This is a copy from lyon_algorithms::geom::cubic_bezier implementation
14 //! (from lyon_algorithms 0.17)
15 type S = f32;
16 use euclid::default::Point2D as Point;
17 #[allow(unused)]
18 use num_traits::Float;
19 trait Scalar {
20 const ONE: f32 = 1.;
21 const THREE: f32 = 3.;
22 const HALF: f32 = 0.5;
23 const SIX: f32 = 6.;
24 const NINE: f32 = 9.;
25 fn value(v: f32) -> f32 {
26 v
27 }
28 }
29 impl Scalar for f32 {}
30 pub struct CubicBezierSegment {
31 pub from: Point<S>,
32 pub ctrl1: Point<S>,
33 pub ctrl2: Point<S>,
34 pub to: Point<S>,
35 }
36
37 impl CubicBezierSegment {
38 /// Sample the x coordinate of the curve at t (expecting t between 0 and 1).
39 pub fn x(&self, t: S) -> S {
40 let t2 = t * t;
41 let t3 = t2 * t;
42 let one_t = S::ONE - t;
43 let one_t2 = one_t * one_t;
44 let one_t3 = one_t2 * one_t;
45
46 self.from.x * one_t3
47 + self.ctrl1.x * S::THREE * one_t2 * t
48 + self.ctrl2.x * S::THREE * one_t * t2
49 + self.to.x * t3
50 }
51
52 /// Sample the y coordinate of the curve at t (expecting t between 0 and 1).
53 pub fn y(&self, t: S) -> S {
54 let t2 = t * t;
55 let t3 = t2 * t;
56 let one_t = S::ONE - t;
57 let one_t2 = one_t * one_t;
58 let one_t3 = one_t2 * one_t;
59
60 self.from.y * one_t3
61 + self.ctrl1.y * S::THREE * one_t2 * t
62 + self.ctrl2.y * S::THREE * one_t * t2
63 + self.to.y * t3
64 }
65
66 #[inline]
67 fn derivative_coefficients(&self, t: S) -> (S, S, S, S) {
68 let t2 = t * t;
69 (
70 -S::THREE * t2 + S::SIX * t - S::THREE,
71 S::NINE * t2 - S::value(12.0) * t + S::THREE,
72 -S::NINE * t2 + S::SIX * t,
73 S::THREE * t2,
74 )
75 }
76
77 /// Sample the x coordinate of the curve's derivative at t (expecting t between 0 and 1).
78 pub fn dx(&self, t: S) -> S {
79 let (c0, c1, c2, c3) = self.derivative_coefficients(t);
80 self.from.x * c0 + self.ctrl1.x * c1 + self.ctrl2.x * c2 + self.to.x * c3
81 }
82 }
83
84 impl CubicBezierSegment {
85 // This is actually in the Monotonic<CubicBezierSegment<S>> impl
86 pub fn solve_t_for_x(&self, x: S, t_range: core::ops::Range<S>, tolerance: S) -> S {
87 debug_assert!(t_range.start <= t_range.end);
88 let from = self.x(t_range.start);
89 let to = self.x(t_range.end);
90 if x <= from {
91 return t_range.start;
92 }
93 if x >= to {
94 return t_range.end;
95 }
96
97 // Newton's method.
98 let mut t = x - from / (to - from);
99 for _ in 0..8 {
100 let x2 = self.x(t);
101
102 if S::abs(x2 - x) <= tolerance {
103 return t;
104 }
105
106 let dx = self.dx(t);
107
108 if dx <= S::EPSILON {
109 break;
110 }
111
112 t -= (x2 - x) / dx;
113 }
114
115 // Fall back to binary search.
116 let mut min = t_range.start;
117 let mut max = t_range.end;
118 let mut t = S::HALF;
119
120 while min < max {
121 let x2 = self.x(t);
122
123 if S::abs(x2 - x) < tolerance {
124 return t;
125 }
126
127 if x > x2 {
128 min = t;
129 } else {
130 max = t;
131 }
132
133 t = (max - min) * S::HALF + min;
134 }
135
136 t
137 }
138 }
139}
140
141/// The representation of an easing curve, for animations
142#[repr(C, u32)]
143#[derive(Debug, Clone, Copy, PartialEq, Default)]
144pub enum EasingCurve {
145 /// The linear curve
146 #[default]
147 Linear,
148 /// A Cubic bezier curve, with its 4 parameters
149 CubicBezier([f32; 4]),
150 /// Easing curve as defined at: <https://easings.net/#easeInElastic>
151 EaseInElastic,
152 /// Easing curve as defined at: <https://easings.net/#easeOutElastic>
153 EaseOutElastic,
154 /// Easing curve as defined at: <https://easings.net/#easeInOutElastic>
155 EaseInOutElastic,
156 /// Easing curve as defined at: <https://easings.net/#easeInBounce>
157 EaseInBounce,
158 /// Easing curve as defined at: <https://easings.net/#easeOutBounce>
159 EaseOutBounce,
160 /// Easing curve as defined at: <https://easings.net/#easeInOutBounce>
161 EaseInOutBounce,
162 // Custom(Box<dyn Fn(f32) -> f32>),
163}
164
165/// Represent an instant, in milliseconds since the AnimationDriver's initial_instant
166#[repr(transparent)]
167#[derive(Copy, Clone, Debug, Default, PartialEq, Ord, PartialOrd, Eq)]
168pub struct Instant(pub u64);
169
170impl core::ops::Sub<Instant> for Instant {
171 type Output = core::time::Duration;
172 fn sub(self, other: Self) -> core::time::Duration {
173 core::time::Duration::from_millis(self.0 - other.0)
174 }
175}
176
177impl core::ops::Sub<core::time::Duration> for Instant {
178 type Output = Instant;
179 fn sub(self, other: core::time::Duration) -> Instant {
180 Self(self.0 - other.as_millis() as u64)
181 }
182}
183
184impl core::ops::Add<core::time::Duration> for Instant {
185 type Output = Instant;
186 fn add(self, other: core::time::Duration) -> Instant {
187 Self(self.0 + other.as_millis() as u64)
188 }
189}
190
191impl core::ops::AddAssign<core::time::Duration> for Instant {
192 fn add_assign(&mut self, other: core::time::Duration) {
193 self.0 += other.as_millis() as u64;
194 }
195}
196
197impl core::ops::SubAssign<core::time::Duration> for Instant {
198 fn sub_assign(&mut self, other: core::time::Duration) {
199 self.0 -= other.as_millis() as u64;
200 }
201}
202
203impl Instant {
204 /// Returns the amount of time elapsed since an other instant.
205 ///
206 /// Equivalent to `self - earlier`
207 pub fn duration_since(self, earlier: Instant) -> core::time::Duration {
208 self - earlier
209 }
210
211 /// Wrapper around [`std::time::Instant::now()`] that delegates to the backend
212 /// and allows working in no_std environments.
213 pub fn now() -> Self {
214 Self(Self::duration_since_start().as_millis() as u64)
215 }
216
217 fn duration_since_start() -> core::time::Duration {
218 crate::context::GLOBAL_CONTEXT
219 .with(|p| p.get().map(|p| p.platform().duration_since_start()))
220 .unwrap_or_default()
221 }
222
223 /// Return the number of milliseconds this `Instant` is after the backend has started
224 pub fn as_millis(&self) -> u64 {
225 self.0
226 }
227}
228
229/// The AnimationDriver
230pub struct AnimationDriver {
231 /// Indicate whether there are any active animations that require a future call to update_animations.
232 active_animations: Cell<bool>,
233 global_instant: core::pin::Pin<Box<crate::Property<Instant>>>,
234}
235
236impl Default for AnimationDriver {
237 fn default() -> Self {
238 AnimationDriver {
239 active_animations: Cell::default(),
240 global_instant: Box::pin(crate::Property::new_named(
241 value:Instant::default(),
242 _name:"i_slint_core::AnimationDriver::global_instant",
243 )),
244 }
245 }
246}
247
248impl AnimationDriver {
249 /// Iterates through all animations based on the new time tick and updates their state. This should be called by
250 /// the windowing system driver for every frame.
251 pub fn update_animations(&self, new_tick: Instant) {
252 if self.global_instant.as_ref().get_untracked() != new_tick {
253 self.active_animations.set(false);
254 self.global_instant.as_ref().set(new_tick);
255 }
256 }
257
258 /// Returns true if there are any active or ready animations. This is used by the windowing system to determine
259 /// if a new animation frame is required or not. Returns false otherwise.
260 pub fn has_active_animations(&self) -> bool {
261 self.active_animations.get()
262 }
263
264 /// Tell the driver that there are active animations
265 pub fn set_has_active_animations(&self) {
266 self.active_animations.set(true);
267 }
268 /// The current instant that is to be used for animation
269 /// using this function register the current binding as a dependency
270 pub fn current_tick(&self) -> Instant {
271 self.global_instant.as_ref().get()
272 }
273}
274
275crate::thread_local!(
276/// This is the default instance of the animation driver that's used to advance all property animations
277/// at the same time.
278pub static CURRENT_ANIMATION_DRIVER : AnimationDriver = AnimationDriver::default()
279);
280
281/// The current instant that is to be used for animation
282/// using this function register the current binding as a dependency
283pub fn current_tick() -> Instant {
284 CURRENT_ANIMATION_DRIVER.with(|driver: &AnimationDriver| driver.current_tick())
285}
286
287/// Same as [`current_tick`], but also register that one should be running animation
288/// on next frame
289pub fn animation_tick() -> u64 {
290 CURRENT_ANIMATION_DRIVER.with(|driver: &AnimationDriver| {
291 driver.set_has_active_animations();
292 driver.current_tick().0
293 })
294}
295
296fn ease_out_bounce_curve(value: f32) -> f32 {
297 const N1: f32 = 7.5625;
298 const D1: f32 = 2.75;
299
300 if value < 1.0 / D1 {
301 N1 * value * value
302 } else if value < 2.0 / D1 {
303 let value: f32 = value - (1.5 / D1);
304 N1 * value * value + 0.75
305 } else if value < 2.5 / D1 {
306 let value: f32 = value - (2.25 / D1);
307 N1 * value * value + 0.9375
308 } else {
309 let value: f32 = value - (2.625 / D1);
310 N1 * value * value + 0.984375
311 }
312}
313
314/// map a value between 0 and 1 to another value between 0 and 1 according to the curve
315pub fn easing_curve(curve: &EasingCurve, value: f32) -> f32 {
316 match curve {
317 EasingCurve::Linear => value,
318 EasingCurve::CubicBezier([a, b, c, d]) => {
319 if !(0.0..=1.0).contains(a) && !(0.0..=1.0).contains(c) {
320 return value;
321 };
322 let curve = cubic_bezier::CubicBezierSegment {
323 from: (0., 0.).into(),
324 ctrl1: (*a, *b).into(),
325 ctrl2: (*c, *d).into(),
326 to: (1., 1.).into(),
327 };
328 curve.y(curve.solve_t_for_x(value, 0.0..1.0, 0.01))
329 }
330 EasingCurve::EaseInElastic => {
331 const C4: f32 = 2.0 * core::f32::consts::PI / 3.0;
332
333 if value == 0.0 {
334 0.0
335 } else if value == 1.0 {
336 1.0
337 } else {
338 -f32::powf(2.0, 10.0 * value - 10.0) * f32::sin((value * 10.0 - 10.75) * C4)
339 }
340 }
341 EasingCurve::EaseOutElastic => {
342 let c4 = (2.0 * core::f32::consts::PI) / 3.0;
343
344 if value == 0.0 {
345 0.0
346 } else if value == 1.0 {
347 1.0
348 } else {
349 2.0f32.powf(-10.0 * value) * ((value * 10.0 - 0.75) * c4).sin() + 1.0
350 }
351 }
352 EasingCurve::EaseInOutElastic => {
353 const C5: f32 = 2.0 * core::f32::consts::PI / 4.5;
354
355 if value == 0.0 {
356 0.0
357 } else if value == 1.0 {
358 1.0
359 } else if value < 0.5 {
360 -(f32::powf(2.0, 20.0 * value - 10.0) * f32::sin((20.0 * value - 11.125) * C5))
361 / 2.0
362 } else {
363 (f32::powf(2.0, -20.0 * value + 10.0) * f32::sin((20.0 * value - 11.125) * C5))
364 / 2.0
365 + 1.0
366 }
367 }
368 EasingCurve::EaseInBounce => 1.0 - ease_out_bounce_curve(1.0 - value),
369 EasingCurve::EaseOutBounce => ease_out_bounce_curve(value),
370 EasingCurve::EaseInOutBounce => {
371 if value < 0.5 {
372 (1.0 - ease_out_bounce_curve(1.0 - 2.0 * value)) / 2.0
373 } else {
374 (1.0 + ease_out_bounce_curve(2.0 * value - 1.0)) / 2.0
375 }
376 }
377 }
378}
379
380/*
381#[test]
382fn easing_test() {
383 fn test_curve(name: &str, curve: &EasingCurve) {
384 let mut img = image::ImageBuffer::new(500, 500);
385 let white = image::Rgba([255 as u8, 255 as u8, 255 as u8, 255 as u8]);
386
387 for x in 0..img.width() {
388 let t = (x as f32) / (img.width() as f32);
389 let y = easing_curve(curve, t);
390 let y = (y * (img.height() as f32)) as u32;
391 let y = y.min(img.height() - 1);
392 *img.get_pixel_mut(x, img.height() - 1 - y) = white;
393 }
394
395 img.save(
396 std::path::PathBuf::from(std::env::var_os("HOME").unwrap())
397 .join(format!("{}.png", name)),
398 )
399 .unwrap();
400 }
401
402 test_curve("linear", &EasingCurve::Linear);
403 test_curve("linear2", &EasingCurve::CubicBezier([0.0, 0.0, 1.0, 1.0]));
404 test_curve("ease", &EasingCurve::CubicBezier([0.25, 0.1, 0.25, 1.0]));
405 test_curve("ease_in", &EasingCurve::CubicBezier([0.42, 0.0, 1.0, 1.0]));
406 test_curve("ease_in_out", &EasingCurve::CubicBezier([0.42, 0.0, 0.58, 1.0]));
407 test_curve("ease_out", &EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]));
408}
409*/
410
411/// Update the global animation time to the current time
412pub fn update_animations() {
413 CURRENT_ANIMATION_DRIVER.with(|driver: &AnimationDriver| {
414 #[allow(unused_mut)]
415 let mut duration: u64 = Instant::duration_since_start().as_millis() as u64;
416 #[cfg(feature = "std")]
417 if let Ok(val: String) = std::env::var(key:"SLINT_SLOW_ANIMATIONS") {
418 let factor: u64 = val.parse().unwrap_or(default:2);
419 duration /= factor;
420 };
421 driver.update_animations(new_tick:Instant(duration))
422 });
423}
424