From 31752e422a09f1510c389ae87994d925fb547c9c Mon Sep 17 00:00:00 2001 From: thearturca Date: Sun, 13 Apr 2025 14:07:06 +0300 Subject: [PATCH] feat(animation): cubic-bezier for styles This commit adds ability to use cubic-bezier as an animation style, which allows users to customize the smoothness of animations. --- komorebi/src/animation/style.rs | 56 +++++++++ komorebi/src/core/animation.rs | 80 ++++++++++++- schema.json | 200 ++++++++++++++++++++++---------- 3 files changed, 271 insertions(+), 65 deletions(-) diff --git a/komorebi/src/animation/style.rs b/komorebi/src/animation/style.rs index 31bbdef3..949f7608 100644 --- a/komorebi/src/animation/style.rs +++ b/komorebi/src/animation/style.rs @@ -355,6 +355,61 @@ impl Ease for EaseInOutBounce { } } +pub struct CubicBezier { + pub x1: f64, + pub y1: f64, + pub x2: f64, + pub y2: f64, +} + +impl CubicBezier { + fn x(&self, s: f64) -> f64 { + 3.0 * self.x1 * s * (1.0 - s).powi(2) + 3.0 * self.x2 * s.powi(2) * (1.0 - s) + s.powi(3) + } + + fn y(&self, s: f64) -> f64 { + 3.0 * self.y1 * s * (1.0 - s).powi(2) + 3.0 * self.y2 * s.powi(2) * (1.0 - s) + s.powi(3) + } + + fn dx_ds(&self, s: f64) -> f64 { + 3.0 * self.x1 * (1.0 - s) * (1.0 - 3.0 * s) + + 3.0 * self.x2 * (2.0 * s - 3.0 * s.powi(2)) + + 3.0 * s.powi(2) + } + + fn find_s(&self, t: f64) -> f64 { + if t <= 0.0 { + return 0.0; + } + + if t >= 1.0 { + return 1.0; + } + + let mut s = t; + + for _ in 0..8 { + let x_val = self.x(s); + let dx_val = self.dx_ds(s); + if dx_val.abs() < 1e-6 { + break; + } + let delta = (x_val - t) / dx_val; + s = (s - delta).clamp(0.0, 1.0); + if delta.abs() < 1e-6 { + break; + } + } + + s + } + + fn evaluate(&self, t: f64) -> f64 { + let s = self.find_s(t.clamp(0.0, 1.0)); + self.y(s) + } +} + pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 { match style { AnimationStyle::Linear => Linear::evaluate(t), @@ -387,5 +442,6 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 { AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t), AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t), AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t), + AnimationStyle::CubicBezier(x1, y1, x2, y2) => CubicBezier { x1, y1, x2, y2 }.evaluate(t), } } diff --git a/komorebi/src/core/animation.rs b/komorebi/src/core/animation.rs index e23f0a9b..8c3aa58d 100644 --- a/komorebi/src/core/animation.rs +++ b/komorebi/src/core/animation.rs @@ -1,11 +1,12 @@ use clap::ValueEnum; +use serde::ser::SerializeSeq; use serde::Deserialize; use serde::Serialize; use strum::Display; use strum::EnumString; -#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)] +#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum AnimationStyle { Linear, @@ -38,4 +39,81 @@ pub enum AnimationStyle { EaseInBounce, EaseOutBounce, EaseInOutBounce, + #[value(skip)] + CubicBezier(f64, f64, f64, f64), +} + +// Custom serde implementation +impl<'de> Deserialize<'de> for AnimationStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct AnimationStyleVisitor; + + impl<'de> serde::de::Visitor<'de> for AnimationStyleVisitor { + type Value = AnimationStyle; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or an array of four f64 values") + } + + // Handle string variants (e.g., "EaseInOutExpo") + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + value.parse().map_err(|_| E::unknown_variant(value, &[])) + } + + // Handle CubicBezier array (e.g., [0.32, 0.72, 0.0, 1.0]) + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let x1 = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + let y1 = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + let x2 = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; + let y2 = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?; + + // Ensure no extra elements + if seq.next_element::()?.is_some() { + return Err(serde::de::Error::invalid_length(5, &self)); + } + + Ok(AnimationStyle::CubicBezier(x1, y1, x2, y2)) + } + } + + deserializer.deserialize_any(AnimationStyleVisitor) + } +} + +impl Serialize for AnimationStyle { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + // Serialize CubicBezier as an array + AnimationStyle::CubicBezier(x1, y1, x2, y2) => { + let mut seq = serializer.serialize_seq(Some(4))?; + seq.serialize_element(x1)?; + seq.serialize_element(y1)?; + seq.serialize_element(x2)?; + seq.serialize_element(y2)?; + seq.end() + } + // Serialize all other variants as strings + _ => serializer.serialize_str(&self.to_string()), + } + } } diff --git a/schema.json b/schema.json index 53482ad6..c0372524 100644 --- a/schema.json +++ b/schema.json @@ -55,74 +55,146 @@ { "type": "object", "additionalProperties": { - "type": "string", - "enum": [ - "Linear", - "EaseInSine", - "EaseOutSine", - "EaseInOutSine", - "EaseInQuad", - "EaseOutQuad", - "EaseInOutQuad", - "EaseInCubic", - "EaseInOutCubic", - "EaseInQuart", - "EaseOutQuart", - "EaseInOutQuart", - "EaseInQuint", - "EaseOutQuint", - "EaseInOutQuint", - "EaseInExpo", - "EaseOutExpo", - "EaseInOutExpo", - "EaseInCirc", - "EaseOutCirc", - "EaseInOutCirc", - "EaseInBack", - "EaseOutBack", - "EaseInOutBack", - "EaseInElastic", - "EaseOutElastic", - "EaseInOutElastic", - "EaseInBounce", - "EaseOutBounce", - "EaseInOutBounce" + "oneOf": [ + { + "type": "string", + "enum": [ + "Linear", + "EaseInSine", + "EaseOutSine", + "EaseInOutSine", + "EaseInQuad", + "EaseOutQuad", + "EaseInOutQuad", + "EaseInCubic", + "EaseInOutCubic", + "EaseInQuart", + "EaseOutQuart", + "EaseInOutQuart", + "EaseInQuint", + "EaseOutQuint", + "EaseInOutQuint", + "EaseInExpo", + "EaseOutExpo", + "EaseInOutExpo", + "EaseInCirc", + "EaseOutCirc", + "EaseInOutCirc", + "EaseInBack", + "EaseOutBack", + "EaseInOutBack", + "EaseInElastic", + "EaseOutElastic", + "EaseInOutElastic", + "EaseInBounce", + "EaseOutBounce", + "EaseInOutBounce" + ] + }, + { + "type": "object", + "required": [ + "CubicBezier" + ], + "properties": { + "CubicBezier": { + "type": "array", + "items": [ + { + "type": "number", + "format": "double" + }, + { + "type": "number", + "format": "double" + }, + { + "type": "number", + "format": "double" + }, + { + "type": "number", + "format": "double" + } + ], + "maxItems": 4, + "minItems": 4 + } + }, + "additionalProperties": false + } ] } }, { - "type": "string", - "enum": [ - "Linear", - "EaseInSine", - "EaseOutSine", - "EaseInOutSine", - "EaseInQuad", - "EaseOutQuad", - "EaseInOutQuad", - "EaseInCubic", - "EaseInOutCubic", - "EaseInQuart", - "EaseOutQuart", - "EaseInOutQuart", - "EaseInQuint", - "EaseOutQuint", - "EaseInOutQuint", - "EaseInExpo", - "EaseOutExpo", - "EaseInOutExpo", - "EaseInCirc", - "EaseOutCirc", - "EaseInOutCirc", - "EaseInBack", - "EaseOutBack", - "EaseInOutBack", - "EaseInElastic", - "EaseOutElastic", - "EaseInOutElastic", - "EaseInBounce", - "EaseOutBounce", - "EaseInOutBounce" + "oneOf": [ + { + "type": "string", + "enum": [ + "Linear", + "EaseInSine", + "EaseOutSine", + "EaseInOutSine", + "EaseInQuad", + "EaseOutQuad", + "EaseInOutQuad", + "EaseInCubic", + "EaseInOutCubic", + "EaseInQuart", + "EaseOutQuart", + "EaseInOutQuart", + "EaseInQuint", + "EaseOutQuint", + "EaseInOutQuint", + "EaseInExpo", + "EaseOutExpo", + "EaseInOutExpo", + "EaseInCirc", + "EaseOutCirc", + "EaseInOutCirc", + "EaseInBack", + "EaseOutBack", + "EaseInOutBack", + "EaseInElastic", + "EaseOutElastic", + "EaseInOutElastic", + "EaseInBounce", + "EaseOutBounce", + "EaseInOutBounce" + ] + }, + { + "type": "object", + "required": [ + "CubicBezier" + ], + "properties": { + "CubicBezier": { + "type": "array", + "items": [ + { + "type": "number", + "format": "double" + }, + { + "type": "number", + "format": "double" + }, + { + "type": "number", + "format": "double" + }, + { + "type": "number", + "format": "double" + } + ], + "maxItems": 4, + "minItems": 4 + } + }, + "additionalProperties": false + } ] } ]