diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-03-05 13:32:30 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-03-05 13:32:52 +0400 |
| commit | ae89b2e514fd4c22a38c2dce258707c369ca944a (patch) | |
| tree | 743448567dbc92843b46fc677b39336df3406a58 /src/animation | |
| parent | 732f7f6f33595924654d23a380cc2a48bf9f3257 (diff) | |
| download | niri-ae89b2e514fd4c22a38c2dce258707c369ca944a.tar.gz niri-ae89b2e514fd4c22a38c2dce258707c369ca944a.tar.bz2 niri-ae89b2e514fd4c22a38c2dce258707c369ca944a.zip | |
Implement spring animations
Diffstat (limited to 'src/animation')
| -rw-r--r-- | src/animation/mod.rs | 151 | ||||
| -rw-r--r-- | src/animation/spring.rs | 137 |
2 files changed, 277 insertions, 11 deletions
diff --git a/src/animation/mod.rs b/src/animation/mod.rs index 1b928435..7984b0d9 100644 --- a/src/animation/mod.rs +++ b/src/animation/mod.rs @@ -6,6 +6,9 @@ use portable_atomic::{AtomicF64, Ordering}; use crate::utils::get_monotonic_time; +mod spring; +pub use spring::{Spring, SpringParams}; + pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.); #[derive(Debug)] @@ -15,7 +18,19 @@ pub struct Animation { duration: Duration, start_time: Duration, current_time: Duration, - curve: Curve, + kind: Kind, +} + +#[derive(Debug, Clone, Copy)] +enum Kind { + Easing { + curve: Curve, + }, + Spring(Spring), + Deceleration { + initial_velocity: f64, + deceleration_rate: f64, + }, } #[derive(Debug, Clone, Copy)] @@ -28,21 +43,116 @@ impl Animation { pub fn new( from: f64, to: f64, + initial_velocity: f64, config: niri_config::Animation, default: niri_config::Animation, ) -> Self { + if config.off { + return Self::ease(from, to, 0, Curve::EaseOutCubic); + } + + // Resolve defaults. + let (kind, easing_defaults) = match (config.kind, default.kind) { + // Configured spring. + (configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None), + // Configured nothing, defaults spring. + ( + niri_config::AnimationKind::Easing(easing), + defaults @ niri_config::AnimationKind::Spring(_), + ) if easing == niri_config::EasingParams::unfilled() => (defaults, None), + // Configured easing or nothing, defaults easing. + ( + configured @ niri_config::AnimationKind::Easing(_), + niri_config::AnimationKind::Easing(defaults), + ) => (configured, Some(defaults)), + // Configured easing, defaults spring. + ( + configured @ niri_config::AnimationKind::Easing(_), + niri_config::AnimationKind::Spring(_), + ) => (configured, None), + }; + + match kind { + niri_config::AnimationKind::Spring(p) => { + let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon); + + let spring = Spring { + from, + to, + initial_velocity, + params, + }; + Self::spring(spring) + } + niri_config::AnimationKind::Easing(p) => { + let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default()); + let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap(); + let curve = Curve::from(p.curve.or(defaults.curve).unwrap()); + Self::ease(from, to, u64::from(duration_ms), curve) + } + } + } + + pub fn ease(from: f64, to: f64, duration_ms: u64, curve: Curve) -> Self { + // FIXME: ideally we shouldn't use current time here because animations started within the + // same frame cycle should have the same start time to be synchronized. + let now = get_monotonic_time(); + + let duration = Duration::from_millis(duration_ms); + let kind = Kind::Easing { curve }; + + Self { + from, + to, + duration, + start_time: now, + current_time: now, + kind, + } + } + + pub fn spring(spring: Spring) -> Self { + // FIXME: ideally we shouldn't use current time here because animations started within the + // same frame cycle should have the same start time to be synchronized. + let now = get_monotonic_time(); + + let duration = spring.duration(); + let kind = Kind::Spring(spring); + + Self { + from: spring.from, + to: spring.to, + duration, + start_time: now, + current_time: now, + kind, + } + } + + pub fn decelerate( + from: f64, + initial_velocity: f64, + deceleration_rate: f64, + threshold: f64, + ) -> Self { // FIXME: ideally we shouldn't use current time here because animations started within the // same frame cycle should have the same start time to be synchronized. let now = get_monotonic_time(); - let duration_ms = if config.off { - 0 + let duration_s = if initial_velocity == 0. { + 0. } else { - config.duration_ms.unwrap_or(default.duration_ms.unwrap()) + let coeff = 1000. * deceleration_rate.ln(); + (-coeff * threshold / initial_velocity.abs()).ln() / coeff }; - let duration = Duration::from_millis(u64::from(duration_ms)); + let duration = Duration::from_secs_f64(duration_s); + + let to = from - initial_velocity / (1000. * deceleration_rate.ln()); - let curve = Curve::from(config.curve.unwrap_or(default.curve.unwrap())); + let kind = Kind::Deceleration { + initial_velocity, + deceleration_rate, + }; Self { from, @@ -50,7 +160,7 @@ impl Animation { duration, start_time: now, current_time: now, - curve, + kind, } } @@ -118,10 +228,29 @@ impl Animation { } pub fn value(&self) -> f64 { - let passed = (self.current_time - self.start_time).as_secs_f64(); - let total = self.duration.as_secs_f64(); - let x = (passed / total).clamp(0., 1.); - self.curve.y(x) * (self.to - self.from) + self.from + if self.is_done() { + return self.to; + } + + let passed = self.current_time - self.start_time; + + match self.kind { + Kind::Easing { curve } => { + let passed = passed.as_secs_f64(); + let total = self.duration.as_secs_f64(); + let x = (passed / total).clamp(0., 1.); + curve.y(x) * (self.to - self.from) + self.from + } + Kind::Spring(spring) => spring.value_at(passed), + Kind::Deceleration { + initial_velocity, + deceleration_rate, + } => { + let passed = passed.as_secs_f64(); + let coeff = 1000. * deceleration_rate.ln(); + self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity + } + } } pub fn to(&self) -> f64 { diff --git a/src/animation/spring.rs b/src/animation/spring.rs new file mode 100644 index 00000000..6e100f05 --- /dev/null +++ b/src/animation/spring.rs @@ -0,0 +1,137 @@ +use std::time::Duration; + +#[derive(Debug, Clone, Copy)] +pub struct SpringParams { + pub damping: f64, + pub mass: f64, + pub stiffness: f64, + pub epsilon: f64, +} + +#[derive(Debug, Clone, Copy)] +pub struct Spring { + pub from: f64, + pub to: f64, + pub initial_velocity: f64, + pub params: SpringParams, +} + +impl SpringParams { + pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self { + let damping_ratio = damping_ratio.max(0.); + let stiffness = stiffness.max(0.); + let epsilon = epsilon.max(0.); + + let mass = 1.; + let critical_damping = 2. * (mass * stiffness).sqrt(); + let damping = damping_ratio * critical_damping; + + Self { + damping, + mass, + stiffness, + epsilon, + } + } +} + +impl Spring { + pub fn value_at(&self, t: Duration) -> f64 { + self.oscillate(t.as_secs_f64()) + } + + // Based on libadwaita (LGPL-2.1-or-later): + // https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c, + // which itself is based on (MIT): + // https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m + /// Computes and returns the duration until the spring is at rest. + pub fn duration(&self) -> Duration { + const DELTA: f64 = 0.001; + + let beta = self.params.damping / (2. * self.params.mass); + + if beta.abs() <= f64::EPSILON || beta < 0. { + return Duration::MAX; + } + + let omega0 = (self.params.stiffness / self.params.mass).sqrt(); + + // As first ansatz for the overdamped solution, + // and general estimation for the oscillating ones + // we take the value of the envelope when it's < epsilon. + let mut x0 = -self.params.epsilon.ln() / beta; + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 { + return Duration::from_secs_f64(x0); + } + + // Since the overdamped solution decays way slower than the envelope + // we need to use the value of the oscillation itself. + // Newton's root finding method is a good candidate in this particular case: + // https://en.wikipedia.org/wiki/Newton%27s_method + let mut y0 = self.oscillate(x0); + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + let mut x1 = (self.to - y0 + m * x0) / m; + let mut y1 = self.oscillate(x1); + + let mut i = 0; + while (self.to - y1).abs() > self.params.epsilon { + if i > 1000 { + return Duration::ZERO; + } + + x0 = x1; + y0 = y1; + + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + x1 = (self.to - y0 + m * x0) / m; + y1 = self.oscillate(x1); + i += 1; + } + + Duration::from_secs_f64(x1) + } + + /// Returns the spring position at a given time in seconds. + fn oscillate(&self, t: f64) -> f64 { + let b = self.params.damping; + let m = self.params.mass; + let k = self.params.stiffness; + let v0 = self.initial_velocity; + + let beta = b / (2. * m); + let omega0 = (k / m).sqrt(); + + let x0 = self.from - self.to; + + let envelope = (-beta * t).exp(); + + // Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x) + // for the differential equation m*ẍ+b*ẋ+kx = 0 + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) { + // Critically damped. + self.to + envelope * (x0 + (beta * x0 + v0) * t) + } else if beta < omega0 { + // Underdamped. + let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt(); + + self.to + + envelope + * (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin()) + } else { + // Overdamped. + let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt(); + + self.to + + envelope + * (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh()) + } + } +} |
