mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Implement spring animations
This commit is contained in:
+321
-32
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
)));
|
)));
|
||||||
|
|||||||
@@ -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(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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(),
|
||||||
)));
|
)));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user