aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/wiki/Configuration:-Animations.md16
-rw-r--r--niri-config/src/animations.rs92
-rw-r--r--niri-config/src/lib.rs11
-rw-r--r--src/animation/bezier.rs60
-rw-r--r--src/animation/mod.rs8
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))
+ }
}
}
}