Skip to content

Commit 31752e4

Browse files
thearturcaLGUG2Z
authored andcommitted
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.
1 parent 5e308b9 commit 31752e4

File tree

3 files changed

+271
-65
lines changed

3 files changed

+271
-65
lines changed

komorebi/src/animation/style.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,61 @@ impl Ease for EaseInOutBounce {
355355
}
356356
}
357357

358+
pub struct CubicBezier {
359+
pub x1: f64,
360+
pub y1: f64,
361+
pub x2: f64,
362+
pub y2: f64,
363+
}
364+
365+
impl CubicBezier {
366+
fn x(&self, s: f64) -> f64 {
367+
3.0 * self.x1 * s * (1.0 - s).powi(2) + 3.0 * self.x2 * s.powi(2) * (1.0 - s) + s.powi(3)
368+
}
369+
370+
fn y(&self, s: f64) -> f64 {
371+
3.0 * self.y1 * s * (1.0 - s).powi(2) + 3.0 * self.y2 * s.powi(2) * (1.0 - s) + s.powi(3)
372+
}
373+
374+
fn dx_ds(&self, s: f64) -> f64 {
375+
3.0 * self.x1 * (1.0 - s) * (1.0 - 3.0 * s)
376+
+ 3.0 * self.x2 * (2.0 * s - 3.0 * s.powi(2))
377+
+ 3.0 * s.powi(2)
378+
}
379+
380+
fn find_s(&self, t: f64) -> f64 {
381+
if t <= 0.0 {
382+
return 0.0;
383+
}
384+
385+
if t >= 1.0 {
386+
return 1.0;
387+
}
388+
389+
let mut s = t;
390+
391+
for _ in 0..8 {
392+
let x_val = self.x(s);
393+
let dx_val = self.dx_ds(s);
394+
if dx_val.abs() < 1e-6 {
395+
break;
396+
}
397+
let delta = (x_val - t) / dx_val;
398+
s = (s - delta).clamp(0.0, 1.0);
399+
if delta.abs() < 1e-6 {
400+
break;
401+
}
402+
}
403+
404+
s
405+
}
406+
407+
fn evaluate(&self, t: f64) -> f64 {
408+
let s = self.find_s(t.clamp(0.0, 1.0));
409+
self.y(s)
410+
}
411+
}
412+
358413
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
359414
match style {
360415
AnimationStyle::Linear => Linear::evaluate(t),
@@ -387,5 +442,6 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
387442
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
388443
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
389444
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
445+
AnimationStyle::CubicBezier(x1, y1, x2, y2) => CubicBezier { x1, y1, x2, y2 }.evaluate(t),
390446
}
391447
}

komorebi/src/core/animation.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use clap::ValueEnum;
22

3+
use serde::ser::SerializeSeq;
34
use serde::Deserialize;
45
use serde::Serialize;
56
use strum::Display;
67
use strum::EnumString;
78

8-
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
9+
#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)]
910
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1011
pub enum AnimationStyle {
1112
Linear,
@@ -38,4 +39,81 @@ pub enum AnimationStyle {
3839
EaseInBounce,
3940
EaseOutBounce,
4041
EaseInOutBounce,
42+
#[value(skip)]
43+
CubicBezier(f64, f64, f64, f64),
44+
}
45+
46+
// Custom serde implementation
47+
impl<'de> Deserialize<'de> for AnimationStyle {
48+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
49+
where
50+
D: serde::Deserializer<'de>,
51+
{
52+
struct AnimationStyleVisitor;
53+
54+
impl<'de> serde::de::Visitor<'de> for AnimationStyleVisitor {
55+
type Value = AnimationStyle;
56+
57+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
58+
formatter.write_str("a string or an array of four f64 values")
59+
}
60+
61+
// Handle string variants (e.g., "EaseInOutExpo")
62+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
63+
where
64+
E: serde::de::Error,
65+
{
66+
value.parse().map_err(|_| E::unknown_variant(value, &[]))
67+
}
68+
69+
// Handle CubicBezier array (e.g., [0.32, 0.72, 0.0, 1.0])
70+
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
71+
where
72+
A: serde::de::SeqAccess<'de>,
73+
{
74+
let x1 = seq
75+
.next_element()?
76+
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
77+
let y1 = seq
78+
.next_element()?
79+
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
80+
let x2 = seq
81+
.next_element()?
82+
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
83+
let y2 = seq
84+
.next_element()?
85+
.ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
86+
87+
// Ensure no extra elements
88+
if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
89+
return Err(serde::de::Error::invalid_length(5, &self));
90+
}
91+
92+
Ok(AnimationStyle::CubicBezier(x1, y1, x2, y2))
93+
}
94+
}
95+
96+
deserializer.deserialize_any(AnimationStyleVisitor)
97+
}
98+
}
99+
100+
impl Serialize for AnimationStyle {
101+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102+
where
103+
S: serde::Serializer,
104+
{
105+
match self {
106+
// Serialize CubicBezier as an array
107+
AnimationStyle::CubicBezier(x1, y1, x2, y2) => {
108+
let mut seq = serializer.serialize_seq(Some(4))?;
109+
seq.serialize_element(x1)?;
110+
seq.serialize_element(y1)?;
111+
seq.serialize_element(x2)?;
112+
seq.serialize_element(y2)?;
113+
seq.end()
114+
}
115+
// Serialize all other variants as strings
116+
_ => serializer.serialize_str(&self.to_string()),
117+
}
118+
}
41119
}

schema.json

Lines changed: 136 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -55,74 +55,146 @@
5555
{
5656
"type": "object",
5757
"additionalProperties": {
58-
"type": "string",
59-
"enum": [
60-
"Linear",
61-
"EaseInSine",
62-
"EaseOutSine",
63-
"EaseInOutSine",
64-
"EaseInQuad",
65-
"EaseOutQuad",
66-
"EaseInOutQuad",
67-
"EaseInCubic",
68-
"EaseInOutCubic",
69-
"EaseInQuart",
70-
"EaseOutQuart",
71-
"EaseInOutQuart",
72-
"EaseInQuint",
73-
"EaseOutQuint",
74-
"EaseInOutQuint",
75-
"EaseInExpo",
76-
"EaseOutExpo",
77-
"EaseInOutExpo",
78-
"EaseInCirc",
79-
"EaseOutCirc",
80-
"EaseInOutCirc",
81-
"EaseInBack",
82-
"EaseOutBack",
83-
"EaseInOutBack",
84-
"EaseInElastic",
85-
"EaseOutElastic",
86-
"EaseInOutElastic",
87-
"EaseInBounce",
88-
"EaseOutBounce",
89-
"EaseInOutBounce"
58+
"oneOf": [
59+
{
60+
"type": "string",
61+
"enum": [
62+
"Linear",
63+
"EaseInSine",
64+
"EaseOutSine",
65+
"EaseInOutSine",
66+
"EaseInQuad",
67+
"EaseOutQuad",
68+
"EaseInOutQuad",
69+
"EaseInCubic",
70+
"EaseInOutCubic",
71+
"EaseInQuart",
72+
"EaseOutQuart",
73+
"EaseInOutQuart",
74+
"EaseInQuint",
75+
"EaseOutQuint",
76+
"EaseInOutQuint",
77+
"EaseInExpo",
78+
"EaseOutExpo",
79+
"EaseInOutExpo",
80+
"EaseInCirc",
81+
"EaseOutCirc",
82+
"EaseInOutCirc",
83+
"EaseInBack",
84+
"EaseOutBack",
85+
"EaseInOutBack",
86+
"EaseInElastic",
87+
"EaseOutElastic",
88+
"EaseInOutElastic",
89+
"EaseInBounce",
90+
"EaseOutBounce",
91+
"EaseInOutBounce"
92+
]
93+
},
94+
{
95+
"type": "object",
96+
"required": [
97+
"CubicBezier"
98+
],
99+
"properties": {
100+
"CubicBezier": {
101+
"type": "array",
102+
"items": [
103+
{
104+
"type": "number",
105+
"format": "double"
106+
},
107+
{
108+
"type": "number",
109+
"format": "double"
110+
},
111+
{
112+
"type": "number",
113+
"format": "double"
114+
},
115+
{
116+
"type": "number",
117+
"format": "double"
118+
}
119+
],
120+
"maxItems": 4,
121+
"minItems": 4
122+
}
123+
},
124+
"additionalProperties": false
125+
}
90126
]
91127
}
92128
},
93129
{
94-
"type": "string",
95-
"enum": [
96-
"Linear",
97-
"EaseInSine",
98-
"EaseOutSine",
99-
"EaseInOutSine",
100-
"EaseInQuad",
101-
"EaseOutQuad",
102-
"EaseInOutQuad",
103-
"EaseInCubic",
104-
"EaseInOutCubic",
105-
"EaseInQuart",
106-
"EaseOutQuart",
107-
"EaseInOutQuart",
108-
"EaseInQuint",
109-
"EaseOutQuint",
110-
"EaseInOutQuint",
111-
"EaseInExpo",
112-
"EaseOutExpo",
113-
"EaseInOutExpo",
114-
"EaseInCirc",
115-
"EaseOutCirc",
116-
"EaseInOutCirc",
117-
"EaseInBack",
118-
"EaseOutBack",
119-
"EaseInOutBack",
120-
"EaseInElastic",
121-
"EaseOutElastic",
122-
"EaseInOutElastic",
123-
"EaseInBounce",
124-
"EaseOutBounce",
125-
"EaseInOutBounce"
130+
"oneOf": [
131+
{
132+
"type": "string",
133+
"enum": [
134+
"Linear",
135+
"EaseInSine",
136+
"EaseOutSine",
137+
"EaseInOutSine",
138+
"EaseInQuad",
139+
"EaseOutQuad",
140+
"EaseInOutQuad",
141+
"EaseInCubic",
142+
"EaseInOutCubic",
143+
"EaseInQuart",
144+
"EaseOutQuart",
145+
"EaseInOutQuart",
146+
"EaseInQuint",
147+
"EaseOutQuint",
148+
"EaseInOutQuint",
149+
"EaseInExpo",
150+
"EaseOutExpo",
151+
"EaseInOutExpo",
152+
"EaseInCirc",
153+
"EaseOutCirc",
154+
"EaseInOutCirc",
155+
"EaseInBack",
156+
"EaseOutBack",
157+
"EaseInOutBack",
158+
"EaseInElastic",
159+
"EaseOutElastic",
160+
"EaseInOutElastic",
161+
"EaseInBounce",
162+
"EaseOutBounce",
163+
"EaseInOutBounce"
164+
]
165+
},
166+
{
167+
"type": "object",
168+
"required": [
169+
"CubicBezier"
170+
],
171+
"properties": {
172+
"CubicBezier": {
173+
"type": "array",
174+
"items": [
175+
{
176+
"type": "number",
177+
"format": "double"
178+
},
179+
{
180+
"type": "number",
181+
"format": "double"
182+
},
183+
{
184+
"type": "number",
185+
"format": "double"
186+
},
187+
{
188+
"type": "number",
189+
"format": "double"
190+
}
191+
],
192+
"maxItems": 4,
193+
"minItems": 4
194+
}
195+
},
196+
"additionalProperties": false
197+
}
126198
]
127199
}
128200
]

0 commit comments

Comments
 (0)