Skip to main content

rydit_anim/
lib.rs

1//! RyDit Anim - Módulo de Animación para RyDit
2//! 
3//! Implementa principios de animación de Disney:
4//! - Principio #1: Squash & Stretch (Deformación)
5//! - Principio #2: Anticipation (Anticipación)
6//! - Principio #6: Slow In & Slow Out (Easing)
7
8use rydit_core::{RyditModule, ModuleResult, ModuleError};
9use serde_json::{Value, json};
10use std::collections::HashMap;
11
12/// Módulo de Animación - 12 principios de Disney
13pub struct AnimModule;
14
15impl RyditModule for AnimModule {
16    fn name(&self) -> &'static str {
17        "anim"
18    }
19
20    fn version(&self) -> &'static str {
21        "0.7.3"
22    }
23
24    fn register(&self) -> HashMap<&'static str, &'static str> {
25        let mut cmds = HashMap::new();
26        cmds.insert("ease_in", "Easing In - comienza lento, acelera");
27        cmds.insert("ease_out", "Easing Out - comienza rápido, frena");
28        cmds.insert("ease_in_out", "Easing In-Out - combina ambos");
29        cmds.insert("squash", "Squash - aplasta (mantiene área)");
30        cmds.insert("stretch", "Stretch - estira (mantiene área)");
31        cmds.insert("anticipate", "Anticipation - retrocede antes de avanzar");
32        cmds
33    }
34
35    fn execute(&self, command: &str, params: Value) -> ModuleResult {
36        match command {
37            "ease_in" => self.ease_in(params),
38            "ease_out" => self.ease_out(params),
39            "ease_in_out" => self.ease_in_out(params),
40            "squash" => self.squash(params),
41            "stretch" => self.stretch(params),
42            "anticipate" => self.anticipate(params),
43            _ => Err(ModuleError {
44                code: "UNKNOWN_COMMAND".to_string(),
45                message: format!("Comando desconocido: {}", command),
46            }),
47        }
48    }
49}
50
51impl AnimModule {
52    /// Easing In - Comienza lento, acelera al final
53    /// Fórmula: t²
54    fn ease_in(&self, params: Value) -> ModuleResult {
55        let arr = params.as_array().ok_or_else(|| ModuleError {
56            code: "INVALID_PARAMS".to_string(),
57            message: "Params must be an array".to_string(),
58        })?;
59
60        if arr.len() != 1 {
61            return Err(ModuleError {
62                code: "INVALID_PARAMS".to_string(),
63                message: "ease_in requires 1 param: t (0.0-1.0)".to_string(),
64            });
65        }
66
67        let t = arr[0].as_f64().unwrap_or(0.0).max(0.0).min(1.0);
68        Ok(json!(t * t))
69    }
70
71    /// Easing Out - Comienza rápido, frena al final
72    /// Fórmula: t * (2 - t)
73    fn ease_out(&self, params: Value) -> ModuleResult {
74        let arr = params.as_array().ok_or_else(|| ModuleError {
75            code: "INVALID_PARAMS".to_string(),
76            message: "Params must be an array".to_string(),
77        })?;
78
79        if arr.len() != 1 {
80            return Err(ModuleError {
81                code: "INVALID_PARAMS".to_string(),
82                message: "ease_out requires 1 param: t (0.0-1.0)".to_string(),
83            });
84        }
85
86        let t = arr[0].as_f64().unwrap_or(0.0).max(0.0).min(1.0);
87        Ok(json!(t * (2.0 - t)))
88    }
89
90    /// Easing In-Out - Combina ambos, suave en extremos
91    /// Fórmula: 2t² (t<0.5) o 1-2(1-t)² (t>=0.5)
92    fn ease_in_out(&self, params: Value) -> ModuleResult {
93        let arr = params.as_array().ok_or_else(|| ModuleError {
94            code: "INVALID_PARAMS".to_string(),
95            message: "Params must be an array".to_string(),
96        })?;
97
98        if arr.len() != 1 {
99            return Err(ModuleError {
100                code: "INVALID_PARAMS".to_string(),
101                message: "ease_in_out requires 1 param: t (0.0-1.0)".to_string(),
102            });
103        }
104
105        let t = arr[0].as_f64().unwrap_or(0.0).max(0.0).min(1.0);
106        let result = if t < 0.5 {
107            2.0 * t * t
108        } else {
109            1.0 - 2.0 * (1.0 - t) * (1.0 - t)
110        };
111        Ok(json!(result))
112    }
113
114    /// Squash - Aplasta objeto (mantiene área)
115    /// Retorna: [factor, 1/factor]
116    fn squash(&self, params: Value) -> ModuleResult {
117        let arr = params.as_array().ok_or_else(|| ModuleError {
118            code: "INVALID_PARAMS".to_string(),
119            message: "Params must be an array".to_string(),
120        })?;
121
122        if arr.len() != 1 {
123            return Err(ModuleError {
124                code: "INVALID_PARAMS".to_string(),
125                message: "squash requires 1 param: factor (0.5-2.0)".to_string(),
126            });
127        }
128
129        let factor = arr[0].as_f64().unwrap_or(1.0).max(0.5).min(2.0);
130        Ok(json!([factor, 1.0 / factor]))
131    }
132
133    /// Stretch - Estira objeto (mantiene área)
134    /// Retorna: [1/factor, factor]
135    fn stretch(&self, params: Value) -> ModuleResult {
136        let arr = params.as_array().ok_or_else(|| ModuleError {
137            code: "INVALID_PARAMS".to_string(),
138            message: "Params must be an array".to_string(),
139        })?;
140
141        if arr.len() != 1 {
142            return Err(ModuleError {
143                code: "INVALID_PARAMS".to_string(),
144                message: "stretch requires 1 param: factor (0.5-2.0)".to_string(),
145            });
146        }
147
148        let factor = arr[0].as_f64().unwrap_or(1.0).max(0.5).min(2.0);
149        Ok(json!([1.0 / factor, factor]))
150    }
151
152    /// Anticipation - Retrocede antes de avanzar
153    /// Retorna: pos + dir * amount (dir = -1 si target>pos, else 1)
154    fn anticipate(&self, params: Value) -> ModuleResult {
155        let arr = params.as_array().ok_or_else(|| ModuleError {
156            code: "INVALID_PARAMS".to_string(),
157            message: "Params must be an array".to_string(),
158        })?;
159
160        if arr.len() != 3 {
161            return Err(ModuleError {
162                code: "INVALID_PARAMS".to_string(),
163                message: "anticipate requires 3 params: pos, target, amount".to_string(),
164            });
165        }
166
167        let pos = arr[0].as_f64().unwrap_or(0.0);
168        let target = arr[1].as_f64().unwrap_or(0.0);
169        let amount = arr[2].as_f64().unwrap_or(0.0);
170
171        let dir = if target > pos { -1.0 } else { 1.0 };
172        Ok(json!(pos + dir * amount))
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_anim_module_name() {
182        let module = AnimModule;
183        assert_eq!(module.name(), "anim");
184        assert_eq!(module.version(), "0.7.3");
185    }
186
187    #[test]
188    fn test_anim_register() {
189        let module = AnimModule;
190        let cmds = module.register();
191        
192        assert!(cmds.contains_key("ease_in"));
193        assert!(cmds.contains_key("ease_out"));
194        assert!(cmds.contains_key("squash"));
195        assert!(cmds.contains_key("anticipate"));
196    }
197
198    #[test]
199    fn test_ease_in() {
200        let module = AnimModule;
201        // ease_in(0.5) = 0.25
202        let params = json!([0.5]);
203        let result = module.execute("ease_in", params).unwrap();
204        assert!((result.as_f64().unwrap() - 0.25).abs() < 0.001);
205    }
206
207    #[test]
208    fn test_ease_out() {
209        let module = AnimModule;
210        // ease_out(0.5) = 0.75
211        let params = json!([0.5]);
212        let result = module.execute("ease_out", params).unwrap();
213        assert!((result.as_f64().unwrap() - 0.75).abs() < 0.001);
214    }
215
216    #[test]
217    fn test_ease_in_out() {
218        let module = AnimModule;
219        // ease_in_out(0.5) = 0.5
220        let params = json!([0.5]);
221        let result = module.execute("ease_in_out", params).unwrap();
222        assert!((result.as_f64().unwrap() - 0.5).abs() < 0.001);
223    }
224
225    #[test]
226    fn test_squash() {
227        let module = AnimModule;
228        // squash(2.0) = [2.0, 0.5]
229        let params = json!([2.0]);
230        let result = module.execute("squash", params).unwrap();
231        let arr = result.as_array().unwrap();
232        assert!((arr[0].as_f64().unwrap() - 2.0).abs() < 0.001);
233        assert!((arr[1].as_f64().unwrap() - 0.5).abs() < 0.001);
234    }
235
236    #[test]
237    fn test_stretch() {
238        let module = AnimModule;
239        // stretch(2.0) = [0.5, 2.0]
240        let params = json!([2.0]);
241        let result = module.execute("stretch", params).unwrap();
242        let arr = result.as_array().unwrap();
243        assert!((arr[0].as_f64().unwrap() - 0.5).abs() < 0.001);
244        assert!((arr[1].as_f64().unwrap() - 2.0).abs() < 0.001);
245    }
246
247    #[test]
248    fn test_anticipate() {
249        let module = AnimModule;
250        // anticipate(100, 200, 20) = 80 (retrocede)
251        let params = json!([100.0, 200.0, 20.0]);
252        let result = module.execute("anticipate", params).unwrap();
253        assert!((result.as_f64().unwrap() - 80.0).abs() < 0.001);
254    }
255
256    #[test]
257    fn test_unknown_command() {
258        let module = AnimModule;
259        let result = module.execute("unknown", json!([]));
260        
261        assert!(result.is_err());
262        let err = result.unwrap_err();
263        assert_eq!(err.code, "UNKNOWN_COMMAND");
264    }
265}