1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
5#[serde(deny_unknown_fields)]
6pub struct Animation {
7 pub property: String,
8 pub keyframes: Vec<Keyframe>,
9 #[serde(default = "default_easing")]
10 pub easing: EasingType,
11 #[serde(default)]
12 pub spring: Option<SpringConfig>,
13}
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
16#[serde(deny_unknown_fields)]
17pub struct Keyframe {
18 pub time: f64,
19 pub value: KeyframeValue,
20 #[serde(default)]
22 pub easing: Option<EasingType>,
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
26#[serde(untagged)]
27pub enum KeyframeValue {
28 Number(f64),
29 Color(String),
30}
31
32impl KeyframeValue {
33 pub fn as_f64(&self) -> f64 {
34 match self {
35 KeyframeValue::Number(n) => *n,
36 KeyframeValue::Color(_) => 0.0,
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
42#[serde(rename_all = "snake_case")]
43pub enum EasingType {
44 #[default]
45 Linear,
46 EaseIn,
47 EaseOut,
48 EaseInOut,
49 EaseInQuad,
50 EaseOutQuad,
51 EaseInCubic,
52 EaseOutCubic,
53 EaseInExpo,
54 EaseOutExpo,
55 EaseInOutQuad,
56 EaseInOutExpo,
57 EaseInBack,
58 EaseOutBack,
59 EaseOutElastic,
60 Bounce,
61 Spring,
62 CubicBezier {
64 x1: f64,
65 y1: f64,
66 x2: f64,
67 y2: f64,
68 },
69 Steps(u32),
78}
79
80fn default_easing() -> EasingType {
81 EasingType::EaseOut
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
85pub struct SpringConfig {
86 #[serde(default = "default_damping")]
87 pub damping: f64,
88 #[serde(default = "default_stiffness")]
89 pub stiffness: f64,
90 #[serde(default = "default_mass")]
91 pub mass: f64,
92 #[serde(default)]
110 pub duration: Option<f64>,
111 #[serde(default)]
121 pub rest_threshold: Option<f64>,
122}
123
124impl Default for SpringConfig {
125 fn default() -> Self {
126 Self {
127 damping: 15.0,
128 stiffness: 100.0,
129 mass: 1.0,
130 duration: None,
131 rest_threshold: None,
132 }
133 }
134}
135
136fn default_damping() -> f64 {
137 15.0
138}
139fn default_stiffness() -> f64 {
140 100.0
141}
142fn default_mass() -> f64 {
143 1.0
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
148#[serde(rename_all = "snake_case")]
149pub enum AnimationPreset {
150 FadeIn,
151 FadeInUp,
152 FadeInDown,
153 FadeInLeft,
154 FadeInRight,
155 SlideInLeft,
156 SlideInRight,
157 SlideInUp,
158 SlideInDown,
159 ScaleIn,
160 BounceIn,
161 BlurIn,
162 RotateIn,
163 ElasticIn,
164 PopIn,
165 FadeOut,
166 FadeOutUp,
167 FadeOutDown,
168 SlideOutLeft,
169 SlideOutRight,
170 SlideOutUp,
171 SlideOutDown,
172 ScaleOut,
173 BounceOut,
174 BlurOut,
175 RotateOut,
176 Pulse,
177 Float,
178 Shake,
179 Spin,
180 FlipInX,
181 FlipInY,
182 FlipOutX,
183 FlipOutY,
184 TiltIn,
185 DrawIn,
186 StrokeReveal,
187 #[serde(alias = "float_3d")]
188 Float3d,
189 Typewriter,
190 WipeLeft,
191 WipeRight,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
195pub struct PresetConfig {
196 #[serde(default)]
197 pub delay: f64,
198 #[serde(default = "default_preset_duration")]
199 pub duration: f64,
200 #[serde(default, rename = "loop")]
207 pub repeat: bool,
208 #[serde(default)]
216 pub repeat_count: Option<u32>,
217 #[serde(default)]
224 pub yoyo: bool,
225 #[serde(default)]
229 pub repeat_delay: f64,
230 #[serde(default)]
232 pub overshoot: Option<f64>,
233 #[serde(default)]
236 pub spring: Option<SpringConfig>,
237 #[serde(default)]
242 pub amplitude: Option<f64>,
243}
244
245impl Default for PresetConfig {
246 fn default() -> Self {
247 Self {
248 delay: 0.0,
249 duration: 0.8,
250 repeat: false,
251 repeat_count: None,
252 yoyo: false,
253 repeat_delay: 0.0,
254 overshoot: None,
255 spring: None,
256 amplitude: None,
257 }
258 }
259}
260
261fn default_preset_duration() -> f64 {
262 0.8
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
272#[serde(deny_unknown_fields)]
273pub struct ChromaticAberrationConfig {
274 #[serde(default)]
276 pub delay: f64,
277 #[serde(default = "default_chromatic_aberration_duration")]
280 pub duration: f64,
281 #[serde(default = "default_chromatic_aberration_amount")]
285 pub amount: f32,
286 #[serde(default = "default_easing")]
288 pub easing: EasingType,
289}
290
291fn default_chromatic_aberration_duration() -> f64 {
292 0.6
293}
294
295fn default_chromatic_aberration_amount() -> f32 {
296 6.0
297}
298
299#[cfg(test)]
300mod deny_unknown_fields_tests {
301 use super::*;
302 use serde_json::json;
303
304 #[test]
305 fn animation_rejects_unknown_fields() {
306 let json = json!({
307 "property": "opacity",
308 "keyframes": [{ "time": 0.0, "value": 1.0 }],
309 "easing": "ease_out",
310 "duratoin": 5.0
311 });
312 let err = serde_json::from_value::<Animation>(json)
313 .expect_err("a typo'd field on Animation must be rejected, not silently ignored");
314 assert!(err.to_string().contains("duratoin"), "got: {err}");
315 }
316
317 #[test]
318 fn keyframe_rejects_unknown_fields() {
319 let json = json!({ "time": 0.0, "value": 1.0, "eaisng": "linear" });
320 let err = serde_json::from_value::<Keyframe>(json)
321 .expect_err("a typo'd field on Keyframe must be rejected, not silently ignored");
322 assert!(err.to_string().contains("eaisng"), "got: {err}");
323 }
324
325 #[test]
326 fn animation_still_accepts_every_known_field() {
327 let json = json!({
328 "property": "opacity",
329 "keyframes": [
330 { "time": 0.0, "value": 0.0, "easing": "linear" },
331 { "time": 1.0, "value": 1.0 }
332 ],
333 "easing": "ease_out",
334 "spring": { "damping": 10.0, "stiffness": 100.0, "mass": 1.0 }
335 });
336 let a: Animation = serde_json::from_value(json).unwrap();
337 assert_eq!(a.property, "opacity");
338 assert_eq!(a.keyframes.len(), 2);
339 assert!(a.spring.is_some());
340 }
341
342 #[test]
343 fn keyframe_still_accepts_every_known_field() {
344 let json = json!({ "time": 0.5, "value": 10.0, "easing": "ease_in" });
345 let k: Keyframe = serde_json::from_value(json).unwrap();
346 assert_eq!(k.time, 0.5);
347 assert!(k.easing.is_some());
348 }
349
350 #[test]
351 fn chromatic_aberration_config_rejects_unknown_fields() {
352 let json = json!({ "amount": 6.0, "duratoin": 0.6 });
353 let err = serde_json::from_value::<ChromaticAberrationConfig>(json)
354 .expect_err("a typo'd field must be rejected, not silently ignored");
355 assert!(err.to_string().contains("duratoin"), "got: {err}");
356 }
357
358 #[test]
359 fn chromatic_aberration_config_defaults() {
360 let json = json!({});
361 let cfg: ChromaticAberrationConfig = serde_json::from_value(json).unwrap();
362 assert_eq!(cfg.delay, 0.0);
363 assert_eq!(cfg.duration, default_chromatic_aberration_duration());
364 assert_eq!(cfg.amount, default_chromatic_aberration_amount());
365 }
366
367 #[test]
368 fn chromatic_aberration_config_accepts_issue_shape() {
369 let json = json!({ "amount": 6, "duration": 0.6 });
370 let cfg: ChromaticAberrationConfig = serde_json::from_value(json).unwrap();
371 assert_eq!(cfg.amount, 6.0);
372 assert_eq!(cfg.duration, 0.6);
373 }
374}