mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
feat: cubic-bezier curve for animation (#2059)
* feat: bezier curve for animation * fixes --------- Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
@@ -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-quad` <sup>Since: 0.1.5</sup>
|
||||||
- `ease-out-cubic`
|
- `ease-out-cubic`
|
||||||
- `ease-out-expo`
|
- `ease-out-expo`
|
||||||
- `linear` <sup>Since: 0.1.6</sup>
|
- `linear` <sup>Since: 0.1.6</sup>
|
||||||
|
- `cubic-bezier` <sup>Since: next release</sup>
|
||||||
You can get a feel for them on pages like [easings.net](https://easings.net/).
|
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
|
#### Spring
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,13 @@ pub struct EasingParams {
|
|||||||
pub curve: Curve,
|
pub curve: Curve,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum Curve {
|
pub enum Curve {
|
||||||
Linear,
|
Linear,
|
||||||
EaseOutQuad,
|
EaseOutQuad,
|
||||||
EaseOutCubic,
|
EaseOutCubic,
|
||||||
EaseOutExpo,
|
EaseOutExpo,
|
||||||
|
CubicBezier(f64, f64, f64, f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[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 => {
|
name_str => {
|
||||||
if !process_children(child, ctx)? {
|
if !process_children(child, ctx)? {
|
||||||
|
|||||||
+10
-1
@@ -441,6 +441,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window-open { off; }
|
window-open { off; }
|
||||||
|
|
||||||
|
window-close {
|
||||||
|
curve "cubic-bezier" 0.05 0.7 0.1 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gestures {
|
gestures {
|
||||||
@@ -1038,7 +1042,12 @@ mod tests {
|
|||||||
kind: Easing(
|
kind: Easing(
|
||||||
EasingParams {
|
EasingParams {
|
||||||
duration_ms: 150,
|
duration_ms: 150,
|
||||||
curve: EaseOutQuad,
|
curve: CubicBezier(
|
||||||
|
0.05,
|
||||||
|
0.7,
|
||||||
|
0.1,
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ use std::time::Duration;
|
|||||||
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
||||||
use keyframe::EasingFunction;
|
use keyframe::EasingFunction;
|
||||||
|
|
||||||
|
mod bezier;
|
||||||
|
use bezier::CubicBezier;
|
||||||
|
|
||||||
mod spring;
|
mod spring;
|
||||||
pub use spring::{Spring, SpringParams};
|
pub use spring::{Spring, SpringParams};
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ pub enum Curve {
|
|||||||
EaseOutQuad,
|
EaseOutQuad,
|
||||||
EaseOutCubic,
|
EaseOutCubic,
|
||||||
EaseOutExpo,
|
EaseOutExpo,
|
||||||
|
CubicBezier(CubicBezier),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Animation {
|
impl Animation {
|
||||||
@@ -342,6 +346,7 @@ impl Curve {
|
|||||||
Curve::EaseOutQuad => EaseOutQuad.y(x),
|
Curve::EaseOutQuad => EaseOutQuad.y(x),
|
||||||
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
||||||
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * 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::EaseOutQuad => Curve::EaseOutQuad,
|
||||||
niri_config::animations::Curve::EaseOutCubic => Curve::EaseOutCubic,
|
niri_config::animations::Curve::EaseOutCubic => Curve::EaseOutCubic,
|
||||||
niri_config::animations::Curve::EaseOutExpo => Curve::EaseOutExpo,
|
niri_config::animations::Curve::EaseOutExpo => Curve::EaseOutExpo,
|
||||||
|
niri_config::animations::Curve::CubicBezier(x1, y1, x2, y2) => {
|
||||||
|
Curve::CubicBezier(CubicBezier::new(x1, y1, x2, y2))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user