mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
380 lines
11 KiB
Rust
380 lines
11 KiB
Rust
use std::time::Duration;
|
|
|
|
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
|
use keyframe::EasingFunction;
|
|
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)]
|
|
pub struct Animation {
|
|
from: f64,
|
|
to: f64,
|
|
initial_velocity: f64,
|
|
is_off: bool,
|
|
duration: Duration,
|
|
/// Time until the animation first reaches `to`.
|
|
///
|
|
/// Best effort; not always exactly precise.
|
|
clamped_duration: Duration,
|
|
start_time: Duration,
|
|
current_time: Duration,
|
|
kind: Kind,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum Kind {
|
|
Easing {
|
|
curve: Curve,
|
|
},
|
|
Spring(Spring),
|
|
Deceleration {
|
|
initial_velocity: f64,
|
|
deceleration_rate: f64,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum Curve {
|
|
Linear,
|
|
EaseOutQuad,
|
|
EaseOutCubic,
|
|
EaseOutExpo,
|
|
}
|
|
|
|
impl Animation {
|
|
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
|
|
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
|
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
|
|
|
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
|
if config.off {
|
|
rv.is_off = true;
|
|
return rv;
|
|
}
|
|
|
|
rv.replace_config(config);
|
|
rv
|
|
}
|
|
|
|
pub fn replace_config(&mut self, config: niri_config::Animation) {
|
|
self.is_off = config.off;
|
|
if config.off {
|
|
self.duration = Duration::ZERO;
|
|
self.clamped_duration = Duration::ZERO;
|
|
return;
|
|
}
|
|
|
|
let start_time = self.start_time;
|
|
let current_time = self.current_time;
|
|
|
|
match config.kind {
|
|
niri_config::AnimationKind::Spring(p) => {
|
|
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
|
|
|
|
let spring = Spring {
|
|
from: self.from,
|
|
to: self.to,
|
|
initial_velocity: self.initial_velocity,
|
|
params,
|
|
};
|
|
*self = Self::spring(spring);
|
|
}
|
|
niri_config::AnimationKind::Easing(p) => {
|
|
*self = Self::ease(
|
|
self.from,
|
|
self.to,
|
|
self.initial_velocity,
|
|
u64::from(p.duration_ms),
|
|
Curve::from(p.curve),
|
|
);
|
|
}
|
|
}
|
|
|
|
self.start_time = start_time;
|
|
self.current_time = current_time;
|
|
}
|
|
|
|
/// Restarts the animation using the previous config.
|
|
pub fn restarted(self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
|
if self.is_off {
|
|
return self;
|
|
}
|
|
|
|
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
|
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
|
|
|
match self.kind {
|
|
Kind::Easing { curve } => Self::ease(
|
|
from,
|
|
to,
|
|
initial_velocity,
|
|
self.duration.as_millis() as u64,
|
|
curve,
|
|
),
|
|
Kind::Spring(spring) => {
|
|
let spring = Spring {
|
|
from: self.from,
|
|
to: self.to,
|
|
initial_velocity: self.initial_velocity,
|
|
params: spring.params,
|
|
};
|
|
Self::spring(spring)
|
|
}
|
|
Kind::Deceleration {
|
|
initial_velocity,
|
|
deceleration_rate,
|
|
} => {
|
|
let threshold = 0.001; // FIXME
|
|
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn ease(from: f64, to: f64, initial_velocity: 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,
|
|
initial_velocity,
|
|
is_off: false,
|
|
duration,
|
|
// Our current curves never overshoot.
|
|
clamped_duration: duration,
|
|
start_time: now,
|
|
current_time: now,
|
|
kind,
|
|
}
|
|
}
|
|
|
|
pub fn spring(spring: Spring) -> Self {
|
|
let _span = tracy_client::span!("Animation::spring");
|
|
|
|
// 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 clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
|
let kind = Kind::Spring(spring);
|
|
|
|
Self {
|
|
from: spring.from,
|
|
to: spring.to,
|
|
initial_velocity: spring.initial_velocity,
|
|
is_off: false,
|
|
duration,
|
|
clamped_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_s = if initial_velocity == 0. {
|
|
0.
|
|
} else {
|
|
let coeff = 1000. * deceleration_rate.ln();
|
|
(-coeff * threshold / initial_velocity.abs()).ln() / coeff
|
|
};
|
|
let duration = Duration::from_secs_f64(duration_s);
|
|
|
|
let to = from - initial_velocity / (1000. * deceleration_rate.ln());
|
|
|
|
let kind = Kind::Deceleration {
|
|
initial_velocity,
|
|
deceleration_rate,
|
|
};
|
|
|
|
Self {
|
|
from,
|
|
to,
|
|
initial_velocity,
|
|
is_off: false,
|
|
duration,
|
|
clamped_duration: duration,
|
|
start_time: now,
|
|
current_time: now,
|
|
kind,
|
|
}
|
|
}
|
|
|
|
pub fn set_current_time(&mut self, time: Duration) {
|
|
if self.duration.is_zero() {
|
|
self.current_time = time;
|
|
return;
|
|
}
|
|
|
|
let end_time = self.start_time + self.duration;
|
|
if end_time <= self.current_time {
|
|
return;
|
|
}
|
|
|
|
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
|
if slowdown <= f64::EPSILON {
|
|
// Zero slowdown will cause the animation to end right away.
|
|
self.current_time = end_time;
|
|
return;
|
|
}
|
|
|
|
// We can't change current_time (since the incoming time values are always real-time), so
|
|
// apply the slowdown by shifting the start time to compensate.
|
|
if self.current_time <= time {
|
|
let delta = time - self.current_time;
|
|
|
|
let max_delta = end_time - self.current_time;
|
|
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
|
|
if slowdown <= min_slowdown {
|
|
// Our slowdown value will cause the animation to end right away.
|
|
self.current_time = end_time;
|
|
return;
|
|
}
|
|
|
|
let adjusted_delta = delta.div_f64(slowdown);
|
|
if adjusted_delta >= delta {
|
|
self.start_time -= adjusted_delta - delta;
|
|
} else {
|
|
self.start_time += delta - adjusted_delta;
|
|
}
|
|
} else {
|
|
let delta = self.current_time - time;
|
|
|
|
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
|
|
if slowdown <= min_slowdown {
|
|
// Current time was about to jump to before the animation had started; let's just
|
|
// cancel the animation in this case.
|
|
self.current_time = end_time;
|
|
return;
|
|
}
|
|
|
|
let adjusted_delta = delta.div_f64(slowdown);
|
|
if adjusted_delta >= delta {
|
|
self.start_time += adjusted_delta - delta;
|
|
} else {
|
|
self.start_time -= delta - adjusted_delta;
|
|
}
|
|
}
|
|
|
|
self.current_time = time;
|
|
}
|
|
|
|
pub fn is_done(&self) -> bool {
|
|
self.current_time >= self.start_time + self.duration
|
|
}
|
|
|
|
pub fn is_clamped_done(&self) -> bool {
|
|
self.current_time >= self.start_time + self.clamped_duration
|
|
}
|
|
|
|
pub fn value(&self) -> f64 {
|
|
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) => {
|
|
let value = spring.value_at(passed);
|
|
|
|
// Protect against numerical instability.
|
|
let range = (self.to - self.from) * 10.;
|
|
let a = self.from - range;
|
|
let b = self.to + range;
|
|
if self.from <= self.to {
|
|
value.clamp(a, b)
|
|
} else {
|
|
value.clamp(b, a)
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a value that stops at the target value after first reaching it.
|
|
///
|
|
/// Best effort; not always exactly precise.
|
|
pub fn clamped_value(&self) -> f64 {
|
|
if self.is_clamped_done() {
|
|
return self.to;
|
|
}
|
|
|
|
self.value()
|
|
}
|
|
|
|
pub fn to(&self) -> f64 {
|
|
self.to
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn from(&self) -> f64 {
|
|
self.from
|
|
}
|
|
|
|
pub fn offset(&mut self, offset: f64) {
|
|
self.from += offset;
|
|
self.to += offset;
|
|
|
|
if let Kind::Spring(spring) = &mut self.kind {
|
|
spring.from += offset;
|
|
spring.to += offset;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Curve {
|
|
pub fn y(self, x: f64) -> f64 {
|
|
match self {
|
|
Curve::Linear => x,
|
|
Curve::EaseOutQuad => EaseOutQuad.y(x),
|
|
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
|
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<niri_config::AnimationCurve> for Curve {
|
|
fn from(value: niri_config::AnimationCurve) -> Self {
|
|
match value {
|
|
niri_config::AnimationCurve::Linear => Curve::Linear,
|
|
niri_config::AnimationCurve::EaseOutQuad => Curve::EaseOutQuad,
|
|
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
|
|
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
|
|
}
|
|
}
|
|
}
|