diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/animation/mod.rs | 151 | ||||
| -rw-r--r-- | src/animation/spring.rs | 137 | ||||
| -rw-r--r-- | src/layout/monitor.rs | 4 | ||||
| -rw-r--r-- | src/layout/tile.rs | 1 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 4 | ||||
| -rw-r--r-- | src/ui/config_error_notification.rs | 5 |
6 files changed, 290 insertions, 12 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()) + } + } +} diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index ac9f3ff9..f2e4c30e 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -94,6 +94,7 @@ impl<W: LayoutElement> Monitor<W> { return; } + // FIXME: also compute and use current velocity. let current_idx = self .workspace_switch .as_ref() @@ -105,6 +106,7 @@ impl<W: LayoutElement> Monitor<W> { self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( current_idx, idx as f64, + 0., self.options.animations.workspace_switch, niri_config::Animation::default_workspace_switch(), ))); @@ -781,6 +783,7 @@ impl<W: LayoutElement> Monitor<W> { return true; } + let velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT; let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT; let min = gesture.center_idx.saturating_sub(1) as f64; @@ -792,6 +795,7 @@ impl<W: LayoutElement> Monitor<W> { self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( gesture.current_idx, new_idx as f64, + velocity, self.options.animations.workspace_switch, niri_config::Animation::default_workspace_switch(), ))); diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 816c92e1..505a6ccd 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -111,6 +111,7 @@ impl<W: LayoutElement> Tile<W> { self.open_animation = Some(Animation::new( 0., 1., + 0., self.options.animations.window_open, niri_config::Animation::default_window_open(), )); diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 5475862c..ac31cdfb 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -436,9 +436,11 @@ impl<W: LayoutElement> Workspace<W> { return; } + // FIXME: also compute and use current velocity. self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( self.view_offset as f64, new_view_offset as f64, + 0., self.options.animations.horizontal_view_movement, niri_config::Animation::default_horizontal_view_movement(), ))); @@ -1272,6 +1274,7 @@ impl<W: LayoutElement> Workspace<W> { // effort and bug potential. let norm_factor = self.working_area.size.w as f64 / VIEW_GESTURE_WORKING_AREA_MOVEMENT; + let velocity = gesture.tracker.velocity() * norm_factor; let pos = gesture.tracker.pos() * norm_factor; let current_view_offset = pos + gesture.delta_from_tracker; @@ -1420,6 +1423,7 @@ impl<W: LayoutElement> Workspace<W> { self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( current_view_offset + delta, target_view_offset as f64, + velocity, self.options.animations.horizontal_view_movement, niri_config::Animation::default_horizontal_view_movement(), ))); diff --git a/src/ui/config_error_notification.rs b/src/ui/config_error_notification.rs index 12d92b85..b9e68e8e 100644 --- a/src/ui/config_error_notification.rs +++ b/src/ui/config_error_notification.rs @@ -62,6 +62,7 @@ impl ConfigErrorNotification { Animation::new( from, to, + 0., c.animations.config_notification_open_close, niri_config::Animation::default_config_notification_open_close(), ) @@ -118,7 +119,9 @@ impl ConfigErrorNotification { } State::Hiding(anim) => { anim.set_current_time(target_presentation_time); - if anim.is_done() { + // HACK: prevent bounciness on hiding. This is better done with a clamp property on + // the spring animation. + if anim.is_done() || anim.value() <= 0. { self.state = State::Hidden; } } |
