Implement spring animations

This commit is contained in:
Ivan Molodetskikh
2024-03-05 13:32:30 +04:00
parent 732f7f6f33
commit ae89b2e514
8 changed files with 649 additions and 54 deletions
+321 -32
View File
@@ -527,20 +527,86 @@ impl Default for Animations {
} }
} }
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Animation { pub struct Animation {
#[knuffel(child)]
pub off: bool, pub off: bool,
#[knuffel(child, unwrap(argument))] pub kind: AnimationKind,
pub duration_ms: Option<u32>,
#[knuffel(child, unwrap(argument))]
pub curve: Option<AnimationCurve>,
} }
impl Animation { impl Animation {
pub const fn unfilled() -> Self { pub const fn unfilled() -> Self {
Self { Self {
off: false, off: false,
kind: AnimationKind::Easing(EasingParams::unfilled()),
}
}
pub const fn default() -> Self {
Self {
off: false,
kind: AnimationKind::Easing(EasingParams::default()),
}
}
pub const fn default_workspace_switch() -> Self {
Self {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 1000,
epsilon: 0.0001,
}),
}
}
pub const fn default_horizontal_view_movement() -> Self {
Self {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 800,
epsilon: 0.0001,
}),
}
}
pub const fn default_config_notification_open_close() -> Self {
Self {
off: false,
kind: AnimationKind::Spring(SpringParams {
damping_ratio: 0.6,
stiffness: 1000,
epsilon: 0.001,
}),
}
}
pub const fn default_window_open() -> Self {
Self {
off: false,
kind: AnimationKind::Easing(EasingParams {
duration_ms: Some(150),
curve: Some(AnimationCurve::EaseOutExpo),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnimationKind {
Easing(EasingParams),
Spring(SpringParams),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EasingParams {
pub duration_ms: Option<u32>,
pub curve: Option<AnimationCurve>,
}
impl EasingParams {
pub const fn unfilled() -> Self {
Self {
duration_ms: None, duration_ms: None,
curve: None, curve: None,
} }
@@ -548,31 +614,10 @@ impl Animation {
pub const fn default() -> Self { pub const fn default() -> Self {
Self { Self {
off: false,
duration_ms: Some(250), duration_ms: Some(250),
curve: Some(AnimationCurve::EaseOutCubic), curve: Some(AnimationCurve::EaseOutCubic),
} }
} }
pub const fn default_workspace_switch() -> Self {
Self::default()
}
pub const fn default_horizontal_view_movement() -> Self {
Self::default()
}
pub const fn default_config_notification_open_close() -> Self {
Self::default()
}
pub const fn default_window_open() -> Self {
Self {
duration_ms: Some(150),
curve: Some(AnimationCurve::EaseOutExpo),
..Self::default()
}
}
} }
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)] #[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
@@ -581,6 +626,13 @@ pub enum AnimationCurve {
EaseOutExpo, EaseOutExpo,
} }
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpringParams {
pub damping_ratio: f64,
pub stiffness: u32,
pub epsilon: f64,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)] #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)]
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>); pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
@@ -1012,6 +1064,229 @@ where
} }
} }
fn parse_arg_node<S: knuffel::traits::ErrorSpan, T: knuffel::traits::DecodeScalar<S>>(
name: &str,
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<T, DecodeError<S>> {
let mut iter_args = node.arguments.iter();
let val = iter_args.next().ok_or_else(|| {
DecodeError::missing(node, format!("additional argument `{name}` is required"))
})?;
let value = knuffel::traits::DecodeScalar::decode(val, ctx)?;
if let Some(val) = iter_args.next() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"unexpected argument",
));
}
for name in node.properties.keys() {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name.escape_default()),
));
}
for child in node.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
Ok(value)
}
impl<S> knuffel::Decode<S> for Animation
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
expect_only_children(node, ctx);
let mut off = false;
let mut easing_params = EasingParams::unfilled();
let mut spring_params = None;
for child in node.children() {
match &**child.node_name {
"off" => {
knuffel::decode::check_flag_node(child, ctx);
if off {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `off`, single node expected",
));
} else {
off = true;
}
}
"spring" => {
if easing_params != EasingParams::unfilled() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
"cannot set both spring and easing parameters at once",
));
}
if spring_params.is_some() {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `spring`, single node expected",
));
}
spring_params = Some(SpringParams::decode_node(child, ctx)?);
}
"duration-ms" => {
if spring_params.is_some() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
"cannot set both spring and easing parameters at once",
));
}
if easing_params.duration_ms.is_some() {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `duration-ms`, single node expected",
));
}
easing_params.duration_ms = Some(parse_arg_node("duration-ms", child, ctx)?);
}
"curve" => {
if spring_params.is_some() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
"cannot set both spring and easing parameters at once",
));
}
if easing_params.curve.is_some() {
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"node",
"duplicate node `curve`, single node expected",
));
}
easing_params.curve = Some(parse_arg_node("curve", child, ctx)?);
}
name_str => {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", name_str.escape_default()),
));
}
}
}
let kind = if let Some(spring_params) = spring_params {
AnimationKind::Spring(spring_params)
} else {
AnimationKind::Easing(easing_params)
};
Ok(Self { off, kind })
}
}
impl<S> knuffel::Decode<S> for SpringParams
where
S: knuffel::traits::ErrorSpan,
{
fn decode_node(
node: &knuffel::ast::SpannedNode<S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<Self, DecodeError<S>> {
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
if let Some(val) = node.arguments.first() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"unexpected argument",
));
}
for child in node.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
let mut damping_ratio = None;
let mut stiffness = None;
let mut epsilon = None;
for (name, val) in &node.properties {
match &***name {
"damping-ratio" => {
damping_ratio = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
}
"stiffness" => {
stiffness = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
}
"epsilon" => {
epsilon = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
}
name_str => {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name_str.escape_default()),
));
}
}
}
let damping_ratio = damping_ratio
.ok_or_else(|| DecodeError::missing(node, "property `damping-ratio` is required"))?;
let stiffness = stiffness
.ok_or_else(|| DecodeError::missing(node, "property `stiffness` is required"))?;
let epsilon =
epsilon.ok_or_else(|| DecodeError::missing(node, "property `epsilon` is required"))?;
if !(0.1..=10.).contains(&damping_ratio) {
ctx.emit_error(DecodeError::conversion(
node,
"damping-ratio must be between 0.1 and 10.0",
));
}
if stiffness < 1 {
ctx.emit_error(DecodeError::conversion(node, "stiffness must be >= 1"));
}
if !(0.00001..=0.1).contains(&epsilon) {
ctx.emit_error(DecodeError::conversion(
node,
"epsilon must be between 0.00001 and 0.1",
));
}
Ok(SpringParams {
damping_ratio,
stiffness,
epsilon,
})
}
}
impl<S> knuffel::Decode<S> for Binds impl<S> knuffel::Decode<S> for Binds
where where
S: knuffel::traits::ErrorSpan, S: knuffel::traits::ErrorSpan,
@@ -1345,12 +1620,16 @@ mod tests {
animations { animations {
slowdown 2.0 slowdown 2.0
workspace-switch { off; } workspace-switch {
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
}
horizontal-view-movement { horizontal-view-movement {
duration-ms 100 duration-ms 100
curve "ease-out-expo" curve "ease-out-expo"
} }
window-open { off; }
} }
environment { environment {
@@ -1507,12 +1786,22 @@ mod tests {
animations: Animations { animations: Animations {
slowdown: 2., slowdown: 2.,
workspace_switch: Animation { workspace_switch: Animation {
off: true, off: false,
..Animation::unfilled() kind: AnimationKind::Spring(SpringParams {
damping_ratio: 1.,
stiffness: 1000,
epsilon: 0.0001,
}),
}, },
horizontal_view_movement: Animation { horizontal_view_movement: Animation {
duration_ms: Some(100), off: false,
curve: Some(AnimationCurve::EaseOutExpo), kind: AnimationKind::Easing(EasingParams {
duration_ms: Some(100),
curve: Some(AnimationCurve::EaseOutExpo),
}),
},
window_open: Animation {
off: true,
..Animation::unfilled() ..Animation::unfilled()
}, },
..Default::default() ..Default::default()
+35 -7
View File
@@ -254,18 +254,48 @@ animations {
// slowdown 3.0 // slowdown 3.0
// You can configure all individual animations. // You can configure all individual animations.
// Available settings are the same for all of them: // Available settings are the same for all of them.
// - off disables the animation. // - off disables the animation.
//
// Niri supports two animation types: easing and spring.
//
// Easing has the following settings:
// - duration-ms sets the duration of the animation in milliseconds. // - duration-ms sets the duration of the animation in milliseconds.
// - curve sets the easing curve. Currently, available curves // - curve sets the easing curve. Currently, available curves
// are "ease-out-cubic" and "ease-out-expo". // are "ease-out-cubic" and "ease-out-expo".
//
// Spring animations work better with touchpad gestures, because they
// take into account the velocity of your fingers as you release the swipe.
// The parameters are less obvious and generally should be tuned
// with trial and error. Notably, you cannot directly set the duration.
// You can use this app to help visualize how the spring parameters
// change the animation: https://flathub.org/apps/app.drey.Elastic
//
// A spring animation is configured like this:
// - spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
//
// The damping ratio goes from 0.1 to 10.0 and has the following properties:
// - below 1.0: underdamped spring, will oscillate in the end.
// - above 1.0: overdamped spring, won't oscillate.
// - 1.0: critically damped spring, comes to rest in minimum possible time
// without oscillations.
//
// However, even with damping ratio = 1.0 the spring animation may oscillate
// if "launched" with enough velocity from a touchpad swipe.
//
// Lower stiffness will result in a slower animation more prone to oscillation.
//
// Set epsilon to a lower value if the animation "jumps" in the end.
//
// The spring mass is hardcoded to 1.0 and cannot be changed. Instead, change
// stiffness proportionally. E.g. increasing mass by 2x is the same as
// decreasing stiffness by 2x.
// Animation when switching workspaces up and down, // Animation when switching workspaces up and down,
// including after the touchpad gesture. // including after the touchpad gesture.
workspace-switch { workspace-switch {
// off // off
// duration-ms 250 // spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
// curve "ease-out-cubic"
} }
// All horizontal camera view movement: // All horizontal camera view movement:
@@ -275,8 +305,7 @@ animations {
// - And so on. // - And so on.
horizontal-view-movement { horizontal-view-movement {
// off // off
// duration-ms 250 // spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
// curve "ease-out-cubic"
} }
// Window opening animation. Note that this one has different defaults. // Window opening animation. Note that this one has different defaults.
@@ -290,8 +319,7 @@ animations {
// open/close animation. // open/close animation.
config-notification-open-close { config-notification-open-close {
// off // off
// duration-ms 250 // spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
// curve "ease-out-cubic"
} }
} }
+143 -14
View File
@@ -6,6 +6,9 @@ use portable_atomic::{AtomicF64, Ordering};
use crate::utils::get_monotonic_time; use crate::utils::get_monotonic_time;
mod spring;
pub use spring::{Spring, SpringParams};
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.); pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
#[derive(Debug)] #[derive(Debug)]
@@ -15,7 +18,19 @@ pub struct Animation {
duration: Duration, duration: Duration,
start_time: Duration, start_time: Duration,
current_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)] #[derive(Debug, Clone, Copy)]
@@ -28,21 +43,63 @@ impl Animation {
pub fn new( pub fn new(
from: f64, from: f64,
to: f64, to: f64,
initial_velocity: f64,
config: niri_config::Animation, config: niri_config::Animation,
default: niri_config::Animation, default: niri_config::Animation,
) -> Self { ) -> 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 // 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. // same frame cycle should have the same start time to be synchronized.
let now = get_monotonic_time(); let now = get_monotonic_time();
let duration_ms = if config.off { let duration = Duration::from_millis(duration_ms);
0 let kind = Kind::Easing { curve };
} else {
config.duration_ms.unwrap_or(default.duration_ms.unwrap())
};
let duration = Duration::from_millis(u64::from(duration_ms));
let curve = Curve::from(config.curve.unwrap_or(default.curve.unwrap()));
Self { Self {
from, from,
@@ -50,7 +107,60 @@ impl Animation {
duration, duration,
start_time: now, start_time: now,
current_time: now, current_time: now,
curve, 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_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,
duration,
start_time: now,
current_time: now,
kind,
} }
} }
@@ -118,10 +228,29 @@ impl Animation {
} }
pub fn value(&self) -> f64 { pub fn value(&self) -> f64 {
let passed = (self.current_time - self.start_time).as_secs_f64(); if self.is_done() {
let total = self.duration.as_secs_f64(); return self.to;
let x = (passed / total).clamp(0., 1.); }
self.curve.y(x) * (self.to - self.from) + self.from
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 { pub fn to(&self) -> f64 {
+137
View File
@@ -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())
}
}
}
+4
View File
@@ -94,6 +94,7 @@ impl<W: LayoutElement> Monitor<W> {
return; return;
} }
// FIXME: also compute and use current velocity.
let current_idx = self let current_idx = self
.workspace_switch .workspace_switch
.as_ref() .as_ref()
@@ -105,6 +106,7 @@ impl<W: LayoutElement> Monitor<W> {
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new( self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
current_idx, current_idx,
idx as f64, idx as f64,
0.,
self.options.animations.workspace_switch, self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(), niri_config::Animation::default_workspace_switch(),
))); )));
@@ -781,6 +783,7 @@ impl<W: LayoutElement> Monitor<W> {
return true; return true;
} }
let velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT; let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
let min = gesture.center_idx.saturating_sub(1) as f64; 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( self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
gesture.current_idx, gesture.current_idx,
new_idx as f64, new_idx as f64,
velocity,
self.options.animations.workspace_switch, self.options.animations.workspace_switch,
niri_config::Animation::default_workspace_switch(), niri_config::Animation::default_workspace_switch(),
))); )));
+1
View File
@@ -111,6 +111,7 @@ impl<W: LayoutElement> Tile<W> {
self.open_animation = Some(Animation::new( self.open_animation = Some(Animation::new(
0., 0.,
1., 1.,
0.,
self.options.animations.window_open, self.options.animations.window_open,
niri_config::Animation::default_window_open(), niri_config::Animation::default_window_open(),
)); ));
+4
View File
@@ -436,9 +436,11 @@ impl<W: LayoutElement> Workspace<W> {
return; return;
} }
// FIXME: also compute and use current velocity.
self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new( self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new(
self.view_offset as f64, self.view_offset as f64,
new_view_offset as f64, new_view_offset as f64,
0.,
self.options.animations.horizontal_view_movement, self.options.animations.horizontal_view_movement,
niri_config::Animation::default_horizontal_view_movement(), niri_config::Animation::default_horizontal_view_movement(),
))); )));
@@ -1272,6 +1274,7 @@ impl<W: LayoutElement> Workspace<W> {
// effort and bug potential. // effort and bug potential.
let norm_factor = self.working_area.size.w as f64 / VIEW_GESTURE_WORKING_AREA_MOVEMENT; 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 pos = gesture.tracker.pos() * norm_factor;
let current_view_offset = pos + gesture.delta_from_tracker; 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( self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new(
current_view_offset + delta, current_view_offset + delta,
target_view_offset as f64, target_view_offset as f64,
velocity,
self.options.animations.horizontal_view_movement, self.options.animations.horizontal_view_movement,
niri_config::Animation::default_horizontal_view_movement(), niri_config::Animation::default_horizontal_view_movement(),
))); )));
+4 -1
View File
@@ -62,6 +62,7 @@ impl ConfigErrorNotification {
Animation::new( Animation::new(
from, from,
to, to,
0.,
c.animations.config_notification_open_close, c.animations.config_notification_open_close,
niri_config::Animation::default_config_notification_open_close(), niri_config::Animation::default_config_notification_open_close(),
) )
@@ -118,7 +119,9 @@ impl ConfigErrorNotification {
} }
State::Hiding(anim) => { State::Hiding(anim) => {
anim.set_current_time(target_presentation_time); 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; self.state = State::Hidden;
} }
} }