diff options
| -rw-r--r-- | docs/wiki/Configuration:-Animations.md | 16 | ||||
| -rw-r--r-- | niri-config/src/animations.rs | 92 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 11 | ||||
| -rw-r--r-- | src/animation/bezier.rs | 60 | ||||
| -rw-r--r-- | src/animation/mod.rs | 8 |
5 files changed, 181 insertions, 6 deletions
diff --git a/docs/wiki/Configuration:-Animations.md b/docs/wiki/Configuration:-Animations.md index 0012aa83..224f99cd 100644 --- a/docs/wiki/Configuration:-Animations.md +++ b/docs/wiki/Configuration:-Animations.md @@ -84,14 +84,24 @@ animations { } ``` -Currently, niri only supports four curves: +Currently, niri only supports five curves. +You can get a feel for them on pages like [easings.net](https://easings.net/). - `ease-out-quad` <sup>Since: 0.1.5</sup> - `ease-out-cubic` - `ease-out-expo` - `linear` <sup>Since: 0.1.6</sup> - -You can get a feel for them on pages like [easings.net](https://easings.net/). +- `cubic-bezier` <sup>Since: next release</sup> + A custom [cubic Bézier curve](https://www.w3.org/TR/css-easing-1/#cubic-bezier-easing-functions). You need to set 4 numbers defining the control points of the curve, for example: + ```kdl + animations { + window-open { + // Same as CSS cubic-bezier(0.05, 0.7, 0.1, 1) + curve "cubic-bezier" 0.05 0.7 0.1 1 + } + } + ``` + You can tweak the cubic-bezier parameters on pages like [easings.co](https://easings.co?curve=0.05,0.7,0.1,1). #### Spring diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index 90ff0bf2..d265026e 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -69,12 +69,13 @@ pub struct EasingParams { pub curve: Curve, } -#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Curve { Linear, EaseOutQuad, EaseOutCubic, EaseOutExpo, + CubicBezier(f64, f64, f64, f64), } #[derive(Debug, Clone, Copy, PartialEq)] @@ -540,7 +541,94 @@ impl Animation { )); } - easing_params.curve = Some(parse_arg_node("curve", child, ctx)?); + let mut iter_args = child.arguments.iter(); + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing(child, "additional argument `curve` is required") + })?; + let animation_curve_string: String = + knuffel::traits::DecodeScalar::decode(val, ctx)?; + + let animation_curve = match animation_curve_string.as_str() { + "linear" => Some(Curve::Linear), + "ease-out-quad" => Some(Curve::EaseOutQuad), + "ease-out-cubic" => Some(Curve::EaseOutCubic), + "ease-out-expo" => Some(Curve::EaseOutExpo), + "cubic-bezier" => { + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing( + child, + "missing x1 coordinate for cubic Bézier curve control point", + ) + })?; + // the X axis represents time frame so it cannot be negative + // or larger than 1 + let x1: FloatOrInt<0, 1> = + knuffel::traits::DecodeScalar::decode(val, ctx)?; + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing( + child, + "missing y1 coordinate for cubic Bézier curve control point", + ) + })?; + let y1: FloatOrInt<{ i32::MIN }, { i32::MAX }> = + knuffel::traits::DecodeScalar::decode(val, ctx)?; + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing( + child, + "missing x2 coordinate for cubic Bézier curve control point", + ) + })?; + let x2: FloatOrInt<0, 1> = + knuffel::traits::DecodeScalar::decode(val, ctx)?; + let val = iter_args.next().ok_or_else(|| { + DecodeError::missing( + child, + "missing y2 coordinate for cubic Bézier curve control point", + ) + })?; + let y2: FloatOrInt<{ i32::MIN }, { i32::MAX }> = + knuffel::traits::DecodeScalar::decode(val, ctx)?; + + Some(Curve::CubicBezier(x1.0, y1.0, x2.0, y2.0)) + } + unexpected_curve => { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + format!( + "unexpected animation curve `{unexpected_curve}`. \ + Niri only supports five animation curves: \ + `ease-out-quad`, `ease-out-cubic`, `ease-out-expo`, `linear` and `cubic-bezier`." + ), + )); + + None + } + }; + + if let Some(val) = iter_args.next() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "unexpected argument", + )); + } + for name in child.properties.keys() { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name.escape_default()), + )); + } + for child in child.children() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + + easing_params.curve = animation_curve; } name_str => { if !process_children(child, ctx)? { diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index ce821c5e..8e07839e 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -441,6 +441,10 @@ mod tests { } window-open { off; } + + window-close { + curve "cubic-bezier" 0.05 0.7 0.1 1 + } } gestures { @@ -1038,7 +1042,12 @@ mod tests { kind: Easing( EasingParams { duration_ms: 150, - curve: EaseOutQuad, + curve: CubicBezier( + 0.05, + 0.7, + 0.1, + 1.0, + ), }, ), }, diff --git a/src/animation/bezier.rs b/src/animation/bezier.rs new file mode 100644 index 00000000..17facfda --- /dev/null +++ b/src/animation/bezier.rs @@ -0,0 +1,60 @@ +use keyframe::EasingFunction; + +#[derive(Debug, Clone, Copy)] +pub struct CubicBezier { + x1: f64, + y1: f64, + x2: f64, + y2: f64, +} + +impl CubicBezier { + pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self { + Self { x1, y1, x2, y2 } + } + + // Based on libadwaita (LGPL-2.1-or-later): + // https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.7.6/src/adw-easing.c?ref_type=tags#L469-531 + + fn x_for_t(&self, t: f64) -> f64 { + let omt = 1. - t; + 3. * omt * omt * t * self.x1 + 3. * omt * t * t * self.x2 + t * t * t + } + + fn y_for_t(&self, t: f64) -> f64 { + let omt = 1. - t; + 3. * omt * omt * t * self.y1 + 3. * omt * t * t * self.y2 + t * t * t + } + + fn t_for_x(&self, x: f64) -> f64 { + let mut min_t = 0.; + let mut max_t = 1.; + + for _ in 0..=30 { + let guess_t = (min_t + max_t) / 2.; + let guess_x = self.x_for_t(guess_t); + + if x < guess_x { + max_t = guess_t; + } else { + min_t = guess_t; + } + } + + (min_t + max_t) / 2. + } +} + +impl EasingFunction for CubicBezier { + fn y(&self, x: f64) -> f64 { + if x <= f64::EPSILON { + return 0.; + } + + if 1. - f64::EPSILON <= x { + return 1.; + } + + self.y_for_t(self.t_for_x(x)) + } +} diff --git a/src/animation/mod.rs b/src/animation/mod.rs index a8de3275..73a79c5e 100644 --- a/src/animation/mod.rs +++ b/src/animation/mod.rs @@ -3,6 +3,9 @@ use std::time::Duration; use keyframe::functions::{EaseOutCubic, EaseOutQuad}; use keyframe::EasingFunction; +mod bezier; +use bezier::CubicBezier; + mod spring; pub use spring::{Spring, SpringParams}; @@ -43,6 +46,7 @@ pub enum Curve { EaseOutQuad, EaseOutCubic, EaseOutExpo, + CubicBezier(CubicBezier), } impl Animation { @@ -342,6 +346,7 @@ impl Curve { Curve::EaseOutQuad => EaseOutQuad.y(x), Curve::EaseOutCubic => EaseOutCubic.y(x), Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x), + Curve::CubicBezier(b) => b.y(x), } } } @@ -353,6 +358,9 @@ impl From<niri_config::animations::Curve> for Curve { niri_config::animations::Curve::EaseOutQuad => Curve::EaseOutQuad, niri_config::animations::Curve::EaseOutCubic => Curve::EaseOutCubic, niri_config::animations::Curve::EaseOutExpo => Curve::EaseOutExpo, + niri_config::animations::Curve::CubicBezier(x1, y1, x2, y2) => { + Curve::CubicBezier(CubicBezier::new(x1, y1, x2, y2)) + } } } } |
