mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
Implement spring animations
This commit is contained in:
+319
-30
@@ -527,20 +527,86 @@ impl Default for Animations {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Animation {
|
||||
#[knuffel(child)]
|
||||
pub off: bool,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub duration_ms: Option<u32>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub curve: Option<AnimationCurve>,
|
||||
pub kind: AnimationKind,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub const fn unfilled() -> Self {
|
||||
Self {
|
||||
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,
|
||||
curve: None,
|
||||
}
|
||||
@@ -548,31 +614,10 @@ impl Animation {
|
||||
|
||||
pub const fn default() -> Self {
|
||||
Self {
|
||||
off: false,
|
||||
duration_ms: Some(250),
|
||||
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)]
|
||||
@@ -581,6 +626,13 @@ pub enum AnimationCurve {
|
||||
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)]
|
||||
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
|
||||
where
|
||||
S: knuffel::traits::ErrorSpan,
|
||||
@@ -1345,12 +1620,16 @@ mod tests {
|
||||
animations {
|
||||
slowdown 2.0
|
||||
|
||||
workspace-switch { off; }
|
||||
workspace-switch {
|
||||
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||
}
|
||||
|
||||
horizontal-view-movement {
|
||||
duration-ms 100
|
||||
curve "ease-out-expo"
|
||||
}
|
||||
|
||||
window-open { off; }
|
||||
}
|
||||
|
||||
environment {
|
||||
@@ -1507,12 +1786,22 @@ mod tests {
|
||||
animations: Animations {
|
||||
slowdown: 2.,
|
||||
workspace_switch: Animation {
|
||||
off: true,
|
||||
..Animation::unfilled()
|
||||
off: false,
|
||||
kind: AnimationKind::Spring(SpringParams {
|
||||
damping_ratio: 1.,
|
||||
stiffness: 1000,
|
||||
epsilon: 0.0001,
|
||||
}),
|
||||
},
|
||||
horizontal_view_movement: Animation {
|
||||
off: false,
|
||||
kind: AnimationKind::Easing(EasingParams {
|
||||
duration_ms: Some(100),
|
||||
curve: Some(AnimationCurve::EaseOutExpo),
|
||||
}),
|
||||
},
|
||||
window_open: Animation {
|
||||
off: true,
|
||||
..Animation::unfilled()
|
||||
},
|
||||
..Default::default()
|
||||
|
||||
@@ -254,18 +254,48 @@ animations {
|
||||
// slowdown 3.0
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Niri supports two animation types: easing and spring.
|
||||
//
|
||||
// Easing has the following settings:
|
||||
// - duration-ms sets the duration of the animation in milliseconds.
|
||||
// - curve sets the easing curve. Currently, available curves
|
||||
// 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,
|
||||
// including after the touchpad gesture.
|
||||
workspace-switch {
|
||||
// off
|
||||
// duration-ms 250
|
||||
// curve "ease-out-cubic"
|
||||
// spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||
}
|
||||
|
||||
// All horizontal camera view movement:
|
||||
@@ -275,8 +305,7 @@ animations {
|
||||
// - And so on.
|
||||
horizontal-view-movement {
|
||||
// off
|
||||
// duration-ms 250
|
||||
// curve "ease-out-cubic"
|
||||
// spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||
}
|
||||
|
||||
// Window opening animation. Note that this one has different defaults.
|
||||
@@ -290,8 +319,7 @@ animations {
|
||||
// open/close animation.
|
||||
config-notification-open-close {
|
||||
// off
|
||||
// duration-ms 250
|
||||
// curve "ease-out-cubic"
|
||||
// spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+140
-11
@@ -6,6 +6,9 @@ use portable_atomic::{AtomicF64, Ordering};
|
||||
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
mod spring;
|
||||
pub use spring::{Spring, SpringParams};
|
||||
|
||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -15,7 +18,19 @@ pub struct Animation {
|
||||
duration: Duration,
|
||||
start_time: Duration,
|
||||
current_time: Duration,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Kind {
|
||||
Easing {
|
||||
curve: Curve,
|
||||
},
|
||||
Spring(Spring),
|
||||
Deceleration {
|
||||
initial_velocity: f64,
|
||||
deceleration_rate: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -28,21 +43,63 @@ impl Animation {
|
||||
pub fn new(
|
||||
from: f64,
|
||||
to: f64,
|
||||
initial_velocity: f64,
|
||||
config: niri_config::Animation,
|
||||
default: niri_config::Animation,
|
||||
) -> 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
|
||||
// same frame cycle should have the same start time to be synchronized.
|
||||
let now = get_monotonic_time();
|
||||
|
||||
let duration_ms = if config.off {
|
||||
0
|
||||
} 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()));
|
||||
let duration = Duration::from_millis(duration_ms);
|
||||
let kind = Kind::Easing { curve };
|
||||
|
||||
Self {
|
||||
from,
|
||||
@@ -50,7 +107,60 @@ impl Animation {
|
||||
duration,
|
||||
start_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 {
|
||||
let passed = (self.current_time - self.start_time).as_secs_f64();
|
||||
if self.is_done() {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
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.);
|
||||
self.curve.y(x) * (self.to - self.from) + self.from
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// FIXME: also compute and use current velocity.
|
||||
let current_idx = self
|
||||
.workspace_switch
|
||||
.as_ref()
|
||||
@@ -105,6 +106,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||
current_idx,
|
||||
idx as f64,
|
||||
0.,
|
||||
self.options.animations.workspace_switch,
|
||||
niri_config::Animation::default_workspace_switch(),
|
||||
)));
|
||||
@@ -781,6 +783,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return true;
|
||||
}
|
||||
|
||||
let velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
|
||||
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
|
||||
|
||||
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(
|
||||
gesture.current_idx,
|
||||
new_idx as f64,
|
||||
velocity,
|
||||
self.options.animations.workspace_switch,
|
||||
niri_config::Animation::default_workspace_switch(),
|
||||
)));
|
||||
|
||||
@@ -111,6 +111,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.open_animation = Some(Animation::new(
|
||||
0.,
|
||||
1.,
|
||||
0.,
|
||||
self.options.animations.window_open,
|
||||
niri_config::Animation::default_window_open(),
|
||||
));
|
||||
|
||||
@@ -436,9 +436,11 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: also compute and use current velocity.
|
||||
self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new(
|
||||
self.view_offset as f64,
|
||||
new_view_offset as f64,
|
||||
0.,
|
||||
self.options.animations.horizontal_view_movement,
|
||||
niri_config::Animation::default_horizontal_view_movement(),
|
||||
)));
|
||||
@@ -1272,6 +1274,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
// effort and bug potential.
|
||||
|
||||
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 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(
|
||||
current_view_offset + delta,
|
||||
target_view_offset as f64,
|
||||
velocity,
|
||||
self.options.animations.horizontal_view_movement,
|
||||
niri_config::Animation::default_horizontal_view_movement(),
|
||||
)));
|
||||
|
||||
@@ -62,6 +62,7 @@ impl ConfigErrorNotification {
|
||||
Animation::new(
|
||||
from,
|
||||
to,
|
||||
0.,
|
||||
c.animations.config_notification_open_close,
|
||||
niri_config::Animation::default_config_notification_open_close(),
|
||||
)
|
||||
@@ -118,7 +119,9 @@ impl ConfigErrorNotification {
|
||||
}
|
||||
State::Hiding(anim) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user