1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
11#[serde(deny_unknown_fields)]
12pub struct Animation {
13 pub property: String,
14 pub keyframes: Vec<Keyframe>,
15 #[serde(default = "default_easing")]
16 pub easing: EasingType,
17 #[serde(default)]
18 pub spring: Option<SpringConfig>,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct Keyframe {
24 pub time: f64,
25 pub value: KeyframeValue,
26 #[serde(default)]
28 pub easing: Option<EasingType>,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
32#[serde(untagged)]
33pub enum KeyframeValue {
34 Number(f64),
35 Color(String),
36}
37
38impl KeyframeValue {
39 pub fn as_f64(&self) -> f64 {
40 match self {
41 KeyframeValue::Number(n) => *n,
42 KeyframeValue::Color(_) => 0.0,
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
48#[serde(rename_all = "snake_case")]
49pub enum EasingType {
50 #[default]
51 Linear,
52 EaseIn,
53 EaseOut,
54 EaseInOut,
55 EaseInQuad,
56 EaseOutQuad,
57 EaseInCubic,
58 EaseOutCubic,
59 EaseInExpo,
60 EaseOutExpo,
61 EaseInOutQuad,
62 EaseInOutExpo,
63 EaseInBack,
64 EaseOutBack,
65 EaseOutElastic,
66 Bounce,
67 Spring,
68 CubicBezier {
70 x1: f64,
71 y1: f64,
72 x2: f64,
73 y2: f64,
74 },
75}
76
77fn default_easing() -> EasingType {
78 EasingType::EaseOut
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
82pub struct SpringConfig {
83 #[serde(default = "default_damping")]
84 pub damping: f64,
85 #[serde(default = "default_stiffness")]
86 pub stiffness: f64,
87 #[serde(default = "default_mass")]
88 pub mass: f64,
89 #[serde(default)]
107 pub duration: Option<f64>,
108 #[serde(default)]
118 pub rest_threshold: Option<f64>,
119}
120
121impl Default for SpringConfig {
122 fn default() -> Self {
123 Self {
124 damping: 15.0,
125 stiffness: 100.0,
126 mass: 1.0,
127 duration: None,
128 rest_threshold: None,
129 }
130 }
131}
132
133fn default_damping() -> f64 {
134 15.0
135}
136fn default_stiffness() -> f64 {
137 100.0
138}
139fn default_mass() -> f64 {
140 1.0
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
145#[serde(rename_all = "snake_case")]
146pub enum AnimationPreset {
147 FadeIn,
149 FadeInUp,
150 FadeInDown,
151 FadeInLeft,
152 FadeInRight,
153 SlideInLeft,
154 SlideInRight,
155 SlideInUp,
156 SlideInDown,
157 ScaleIn,
158 BounceIn,
159 BlurIn,
160 RotateIn,
161 ElasticIn,
162 PopIn,
163 FadeOut,
165 FadeOutUp,
166 FadeOutDown,
167 SlideOutLeft,
168 SlideOutRight,
169 SlideOutUp,
170 SlideOutDown,
171 ScaleOut,
172 BounceOut,
173 BlurOut,
174 RotateOut,
175 Pulse,
177 Float,
178 Shake,
179 Spin,
180 FlipInX,
182 FlipInY,
183 FlipOutX,
184 FlipOutY,
185 TiltIn,
186 DrawIn,
188 StrokeReveal,
189 #[serde(alias = "float_3d")]
191 Float3d,
192 Typewriter,
194 WipeLeft,
195 WipeRight,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
199pub struct PresetConfig {
200 #[serde(default)]
201 pub delay: f64,
202 #[serde(default = "default_preset_duration")]
203 pub duration: f64,
204 #[serde(default, rename = "loop")]
206 pub repeat: bool,
207 #[serde(default)]
209 pub overshoot: Option<f64>,
210 #[serde(default)]
213 pub spring: Option<SpringConfig>,
214 #[serde(default)]
219 pub amplitude: Option<f64>,
220}
221
222impl Default for PresetConfig {
223 fn default() -> Self {
224 Self {
225 delay: 0.0,
226 duration: 0.8,
227 repeat: false,
228 overshoot: None,
229 spring: None,
230 amplitude: None,
231 }
232 }
233}
234
235fn default_preset_duration() -> f64 {
236 0.8
237}
238
239#[cfg(test)]
240mod deny_unknown_fields_tests {
241 use super::*;
242 use serde_json::json;
243
244 #[test]
253 fn animation_rejects_unknown_fields() {
254 let json = json!({
255 "property": "opacity",
256 "keyframes": [{ "time": 0.0, "value": 1.0 }],
257 "easing": "ease_out",
258 "duratoin": 5.0
259 });
260 let err = serde_json::from_value::<Animation>(json)
261 .expect_err("a typo'd field on Animation must be rejected, not silently ignored");
262 assert!(err.to_string().contains("duratoin"), "got: {err}");
263 }
264
265 #[test]
266 fn keyframe_rejects_unknown_fields() {
267 let json = json!({ "time": 0.0, "value": 1.0, "eaisng": "linear" });
268 let err = serde_json::from_value::<Keyframe>(json)
269 .expect_err("a typo'd field on Keyframe must be rejected, not silently ignored");
270 assert!(err.to_string().contains("eaisng"), "got: {err}");
271 }
272
273 #[test]
274 fn animation_still_accepts_every_known_field() {
275 let json = json!({
276 "property": "opacity",
277 "keyframes": [
278 { "time": 0.0, "value": 0.0, "easing": "linear" },
279 { "time": 1.0, "value": 1.0 }
280 ],
281 "easing": "ease_out",
282 "spring": { "damping": 10.0, "stiffness": 100.0, "mass": 1.0 }
283 });
284 let a: Animation = serde_json::from_value(json).unwrap();
285 assert_eq!(a.property, "opacity");
286 assert_eq!(a.keyframes.len(), 2);
287 assert!(a.spring.is_some());
288 }
289
290 #[test]
291 fn keyframe_still_accepts_every_known_field() {
292 let json = json!({ "time": 0.5, "value": 10.0, "easing": "ease_in" });
293 let k: Keyframe = serde_json::from_value(json).unwrap();
294 assert_eq!(k.time, 0.5);
295 assert!(k.easing.is_some());
296 }
297}