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.
This commit is contained in:
thearturca
2025-04-13 14:07:06 +03:00
committed by LGUG2Z
parent 5e308b9131
commit 31752e422a
3 changed files with 271 additions and 65 deletions

View File

@@ -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 { pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
match style { match style {
AnimationStyle::Linear => Linear::evaluate(t), 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::EaseInBounce => EaseInBounce::evaluate(t),
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t), AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t), AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
AnimationStyle::CubicBezier(x1, y1, x2, y2) => CubicBezier { x1, y1, x2, y2 }.evaluate(t),
} }
} }

View File

@@ -1,11 +1,12 @@
use clap::ValueEnum; use clap::ValueEnum;
use serde::ser::SerializeSeq;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use strum::Display; use strum::Display;
use strum::EnumString; 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))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum AnimationStyle { pub enum AnimationStyle {
Linear, Linear,
@@ -38,4 +39,81 @@ pub enum AnimationStyle {
EaseInBounce, EaseInBounce,
EaseOutBounce, EaseOutBounce,
EaseInOutBounce, EaseInOutBounce,
#[value(skip)]
CubicBezier(f64, f64, f64, f64),
}
// Custom serde implementation
impl<'de> Deserialize<'de> for AnimationStyle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, value: &str) -> Result<Self::Value, E>
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
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::<serde::de::IgnoredAny>()?.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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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()),
}
}
} }

View File

@@ -55,74 +55,146 @@
{ {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string", "oneOf": [
"enum": [ {
"Linear", "type": "string",
"EaseInSine", "enum": [
"EaseOutSine", "Linear",
"EaseInOutSine", "EaseInSine",
"EaseInQuad", "EaseOutSine",
"EaseOutQuad", "EaseInOutSine",
"EaseInOutQuad", "EaseInQuad",
"EaseInCubic", "EaseOutQuad",
"EaseInOutCubic", "EaseInOutQuad",
"EaseInQuart", "EaseInCubic",
"EaseOutQuart", "EaseInOutCubic",
"EaseInOutQuart", "EaseInQuart",
"EaseInQuint", "EaseOutQuart",
"EaseOutQuint", "EaseInOutQuart",
"EaseInOutQuint", "EaseInQuint",
"EaseInExpo", "EaseOutQuint",
"EaseOutExpo", "EaseInOutQuint",
"EaseInOutExpo", "EaseInExpo",
"EaseInCirc", "EaseOutExpo",
"EaseOutCirc", "EaseInOutExpo",
"EaseInOutCirc", "EaseInCirc",
"EaseInBack", "EaseOutCirc",
"EaseOutBack", "EaseInOutCirc",
"EaseInOutBack", "EaseInBack",
"EaseInElastic", "EaseOutBack",
"EaseOutElastic", "EaseInOutBack",
"EaseInOutElastic", "EaseInElastic",
"EaseInBounce", "EaseOutElastic",
"EaseOutBounce", "EaseInOutElastic",
"EaseInOutBounce" "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", "oneOf": [
"enum": [ {
"Linear", "type": "string",
"EaseInSine", "enum": [
"EaseOutSine", "Linear",
"EaseInOutSine", "EaseInSine",
"EaseInQuad", "EaseOutSine",
"EaseOutQuad", "EaseInOutSine",
"EaseInOutQuad", "EaseInQuad",
"EaseInCubic", "EaseOutQuad",
"EaseInOutCubic", "EaseInOutQuad",
"EaseInQuart", "EaseInCubic",
"EaseOutQuart", "EaseInOutCubic",
"EaseInOutQuart", "EaseInQuart",
"EaseInQuint", "EaseOutQuart",
"EaseOutQuint", "EaseInOutQuart",
"EaseInOutQuint", "EaseInQuint",
"EaseInExpo", "EaseOutQuint",
"EaseOutExpo", "EaseInOutQuint",
"EaseInOutExpo", "EaseInExpo",
"EaseInCirc", "EaseOutExpo",
"EaseOutCirc", "EaseInOutExpo",
"EaseInOutCirc", "EaseInCirc",
"EaseInBack", "EaseOutCirc",
"EaseOutBack", "EaseInOutCirc",
"EaseInOutBack", "EaseInBack",
"EaseInElastic", "EaseOutBack",
"EaseOutElastic", "EaseInOutBack",
"EaseInOutElastic", "EaseInElastic",
"EaseInBounce", "EaseOutElastic",
"EaseOutBounce", "EaseInOutElastic",
"EaseInOutBounce" "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
}
] ]
} }
] ]