| 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 | |
| 7 | use alloc::boxed::Box; |
| 8 | use core::cell::Cell; |
| 9 | #[cfg (not(feature = "std" ))] |
| 10 | use num_traits::Float; |
| 11 | |
| 12 | mod 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)] |
| 144 | pub 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)] |
| 168 | pub struct Instant(pub u64); |
| 169 | |
| 170 | impl 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 | |
| 177 | impl 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 | |
| 184 | impl 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 | |
| 191 | impl 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 | |
| 197 | impl 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 | |
| 203 | impl 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 |
| 230 | pub 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 | |
| 236 | impl 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 | |
| 248 | impl 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 | |
| 275 | crate::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. |
| 278 | pub 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 |
| 283 | pub 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 |
| 289 | pub 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 | |
| 296 | fn 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 |
| 315 | pub 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] |
| 382 | fn 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 |
| 412 | pub 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 | |