aboutsummaryrefslogtreecommitdiff
path: root/src/animation
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-03-05 13:32:30 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-03-05 13:32:52 +0400
commitae89b2e514fd4c22a38c2dce258707c369ca944a (patch)
tree743448567dbc92843b46fc677b39336df3406a58 /src/animation
parent732f7f6f33595924654d23a380cc2a48bf9f3257 (diff)
downloadniri-ae89b2e514fd4c22a38c2dce258707c369ca944a.tar.gz
niri-ae89b2e514fd4c22a38c2dce258707c369ca944a.tar.bz2
niri-ae89b2e514fd4c22a38c2dce258707c369ca944a.zip
Implement spring animations
Diffstat (limited to 'src/animation')
-rw-r--r--src/animation/mod.rs151
-rw-r--r--src/animation/spring.rs137
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())
+ }
+ }
+}