Skip to main content

ry_anim/
lib.rs

1//! RyDit Anim - Módulo de Animación para Ry-Dit
2//!
3//! Implementa principios de animación de Disney:
4//! - Principio #1: Squash & Stretch (Deformación)
5//! - Principio #2: Anticipation (Anticipación)
6//! - Principio #3: Staging (en progreso)
7//! - Principio #4: Follow Through & Overlapping Action
8//! - Principio #5: Straight Ahead vs Pose-to-Pose
9//! - Principio #6: Slow In & Slow Out (Easing)
10//! - Principio #7: Arcs
11//! - Principio #8: Secondary Action
12//! - Principio #9: Timing
13//! - Principio #10: Exaggeration
14//! - Principio #11: Solid Drawing
15//! - Principio #12: Appeal
16
17pub mod particles;
18pub mod disney;
19pub mod illusions;
20pub mod effects;
21pub mod science_anim;
22pub mod action_assets;
23
24pub use disney::{
25    appeal, arc_path, exaggerate, follow_through, overlapping_action, pose_to_pose,
26    secondary_action, solid_rotation, timing,
27};
28
29pub use illusions::{
30    cafe_wall, motion_induced_blindness, pulsing_star, rotating_snakes,
31    troxler_fading, zollner_effect,
32};
33
34pub use effects::{
35    bloom_effect, chromatic_aberration, morph_shapes, motion_blur,
36    neon_glow, particle_trails,
37};
38
39pub use science_anim::{
40    cell_division, chemical_crystallization, flight_pattern,
41    lsystem_tree, pendulum_waves, tusi_couple, walk_cycle, wave_interference,
42};
43
44pub use action_assets::{
45    animation_blend, animation_state_machine, frame_animation,
46    sprite_events, sprite_flip, sprite_sheet_parse,
47};
48
49use ry_core::{ModuleError, ModuleResult, RyditModule};
50use serde_json::{json, Value};
51use std::collections::HashMap;
52
53/// Módulo de Animación - 12 principios de Disney
54pub struct AnimModule;
55
56impl RyditModule for AnimModule {
57    fn name(&self) -> &'static str { "anim" }
58    fn version(&self) -> &'static str { "0.12.0" }
59
60    fn register(&self) -> HashMap<&'static str, &'static str> {
61        let mut cmds = HashMap::new();
62        cmds.insert("ease_in", "Easing In - comienza lento, acelera");
63        cmds.insert("ease_out", "Easing Out - comienza rápido, frena");
64        cmds.insert("ease_in_out", "Easing In-Out - combina ambos");
65        cmds.insert("squash", "Squash - aplasta (mantiene área)");
66        cmds.insert("stretch", "Stretch - estira (mantiene área)");
67        cmds.insert("anticipate", "Anticipation - retrocede antes de avanzar");
68        cmds.insert("follow_through", "Follow Through - partes siguen moviéndose");
69        cmds.insert("overlapping_action", "Overlapping Action - partes a distintas velocidades");
70        cmds.insert("arc_path", "Arcs - trayectoria curva entre puntos");
71        cmds.insert("secondary_action", "Secondary Action - movimiento secundario");
72        cmds.insert("timing", "Timing - interpolación entre keyframes");
73        cmds.insert("exaggerate", "Exaggeration - exagerar movimientos");
74        cmds.insert("solid_rotation", "Solid Drawing - rotación 3D con perspectiva");
75        cmds.insert("appeal", "Appeal - hacer forma más atractiva");
76        cmds.insert("pose_to_pose", "Pose-to-Pose - interpolación entre poses clave");
77        // ✅ v0.9.0: Ilusiones ópticas animadas
78        cmds.insert("rotating_snakes", "Rotating Snakes - ilusión de movimiento circular");
79        cmds.insert("cafe_wall", "Cafe Wall - líneas paralelas que parecen inclinadas");
80        cmds.insert("troxler_fading", "Troxler Fading - desvanecimiento por fijación");
81        cmds.insert("pulsing_star", "Pulsing Star - estrella que pulsa");
82        cmds.insert("zollner_effect", "Zöllner Effect - líneas que parecen no ser paralelas");
83        cmds.insert("motion_blindness", "Motion-Induced Blindness - puntos que desaparecen");
84        // ✅ v0.10.0: Efectos especiales
85        cmds.insert("neon_glow", "Neon Glow - resplandor neón configurable");
86        cmds.insert("motion_blur", "Motion Blur - desenfoque de movimiento");
87        cmds.insert("chromatic_aberration", "Chromatic Aberration - separación RGB");
88        cmds.insert("bloom_effect", "Bloom - brillo difuso en zonas claras");
89        cmds.insert("particle_trails", "Particle Trails - estelas de partículas");
90        cmds.insert("morph_shapes", "Morphing - transición entre formas");
91        // ✅ v0.11.0: Ciencia animada
92        cmds.insert("chemical_crystallization", "Chemical - cristalización animada");
93        cmds.insert("cell_division", "Biological - división celular");
94        cmds.insert("walk_cycle", "Fauna - ciclo de caminata");
95        cmds.insert("flight_pattern", "Fauna - aleteo de aves");
96        cmds.insert("lsystem_tree", "Flora - árbol L-System animado");
97        cmds.insert("tusi_couple", "Historical - Pareja de Tusi");
98        cmds.insert("pendulum_waves", "Physics - ondas de péndulos");
99        cmds.insert("wave_interference", "Physics - interferencia de ondas");
100        // ✅ v0.12.0: Action Assets (Sprite Animation)
101        cmds.insert("frame_animation", "Sprites - animacion cuadro por cuadro");
102        cmds.insert("sprite_sheet_parse", "Sprites - parsear hoja de sprites");
103        cmds.insert("animation_state", "Sprites - maquina de estados");
104        cmds.insert("animation_blend", "Sprites - transicion entre estados");
105        cmds.insert("sprite_events", "Sprites - eventos de animacion");
106        cmds.insert("sprite_flip", "Sprites - voltear sprite");
107        cmds
108    }
109
110    fn execute(&self, command: &str, params: Value) -> ModuleResult {
111        let _invalid = || ModuleError { code: "INVALID_PARAMS".to_string(), message: format!("Parámetros inválidos para {}", command) };
112        match command {
113            "ease_in" => self.ease_in(params),
114            "ease_out" => self.ease_out(params),
115            "ease_in_out" => self.ease_in_out(params),
116            "squash" => self.squash(params),
117            "stretch" => self.stretch(params),
118            "anticipate" => self.anticipate(params),
119            "follow_through" => self.follow_through(params),
120            "overlapping_action" => self.overlapping_action(params),
121            "arc_path" => self.arc_path(params),
122            "secondary_action" => self.secondary_action(params),
123            "timing" => self.timing(params),
124            "exaggerate" => self.exaggerate(params),
125            "solid_rotation" => self.solid_rotation(params),
126            "appeal" => self.appeal(params),
127            "pose_to_pose" => self.pose_to_pose(params),
128            // ✅ v0.9.0: Ilusiones ópticas
129            "rotating_snakes" => self.rotating_snakes(params),
130            "cafe_wall" => self.cafe_wall(params),
131            "troxler_fading" => self.troxler_fading(params),
132            "pulsing_star" => self.pulsing_star(params),
133            "zollner_effect" => self.zollner_effect(params),
134            "motion_blindness" => self.motion_blindness(params),
135            // ✅ v0.10.0: Efectos especiales
136            "neon_glow" => self.neon_glow(params),
137            "motion_blur" => self.motion_blur(params),
138            "chromatic_aberration" => self.chromatic_aberration(params),
139            "bloom_effect" => self.bloom_effect(params),
140            "particle_trails" => self.particle_trails(params),
141            "morph_shapes" => self.morph_shapes(params),
142            // ✅ v0.11.0: Ciencia animada
143            "chemical_crystallization" => self.chemical_crystallization(params),
144            "cell_division" => self.cell_division(params),
145            "walk_cycle" => self.walk_cycle(params),
146            "flight_pattern" => self.flight_pattern(params),
147            "lsystem_tree" => self.lsystem_tree(params),
148            "tusi_couple" => self.tusi_couple(params),
149            "pendulum_waves" => self.pendulum_waves(params),
150            "wave_interference" => self.wave_interference(params),
151            // ✅ v0.12.0: Action Assets
152            "frame_animation" => self.frame_animation(params),
153            "sprite_sheet_parse" => self.sprite_sheet_parse(params),
154            "animation_state" => self.animation_state(params),
155            "animation_blend" => self.animation_blend(params),
156            "sprite_events" => self.sprite_events(params),
157            "sprite_flip" => self.sprite_flip(params),
158            _ => Err(ModuleError { code: "UNKNOWN_COMMAND".to_string(), message: format!("Comando desconocido: {}", command) }),
159        }
160    }
161}
162
163impl AnimModule {
164    fn ease_in(&self, p: Value) -> ModuleResult {
165        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "ease_in requiere [t]".to_string() })?;
166        if a.len() != 1 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "ease_in requiere 1 param".to_string() }); }
167        let t = a[0].as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
168        Ok(json!(t * t))
169    }
170
171    fn ease_out(&self, p: Value) -> ModuleResult {
172        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "ease_out requiere [t]".to_string() })?;
173        if a.len() != 1 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "ease_out requiere 1 param".to_string() }); }
174        let t = a[0].as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
175        Ok(json!(t * (2.0 - t)))
176    }
177
178    fn ease_in_out(&self, p: Value) -> ModuleResult {
179        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "ease_in_out requiere [t]".to_string() })?;
180        if a.len() != 1 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "ease_in_out requiere 1 param".to_string() }); }
181        let t = a[0].as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
182        let r = if t < 0.5 { 2.0 * t * t } else { 1.0 - 2.0 * (1.0 - t) * (1.0 - t) };
183        Ok(json!(r))
184    }
185
186    fn squash(&self, p: Value) -> ModuleResult {
187        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "squash requiere [factor]".to_string() })?;
188        if a.len() != 1 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "squash requiere 1 param".to_string() }); }
189        let f = a[0].as_f64().unwrap_or(1.0).clamp(0.5, 2.0);
190        Ok(json!([f, 1.0 / f]))
191    }
192
193    fn stretch(&self, p: Value) -> ModuleResult {
194        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "stretch requiere [factor]".to_string() })?;
195        if a.len() != 1 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "stretch requiere 1 param".to_string() }); }
196        let f = a[0].as_f64().unwrap_or(1.0).clamp(0.5, 2.0);
197        Ok(json!([1.0 / f, f]))
198    }
199
200    fn anticipate(&self, p: Value) -> ModuleResult {
201        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "anticipate requiere [pos, target, amount]".to_string() })?;
202        if a.len() != 3 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "anticipate requiere 3 params".to_string() }); }
203        let pos = a[0].as_f64().unwrap_or(0.0);
204        let target = a[1].as_f64().unwrap_or(0.0);
205        let amount = a[2].as_f64().unwrap_or(0.0);
206        let dir = if target > pos { -1.0 } else { 1.0 };
207        Ok(json!(pos + dir * amount))
208    }
209
210    // ===== v0.8.0: 9 PRINCIPIOS NUEVOS =====
211
212    fn follow_through(&self, p: Value) -> ModuleResult {
213        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "follow_through requiere [amp, decay, freq, t]".to_string() })?;
214        if a.len() != 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "follow_through requiere 4 params".to_string() }); }
215        Ok(json!(disney::follow_through(a[0].as_f64().unwrap_or(1.0), a[1].as_f64().unwrap_or(1.0), a[2].as_f64().unwrap_or(5.0), a[3].as_f64().unwrap_or(0.0))))
216    }
217
218    fn overlapping_action(&self, p: Value) -> ModuleResult {
219        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "overlapping_action requiere [base, offsets, t]".to_string() })?;
220        if a.len() != 3 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "overlapping_action requiere 3 params".to_string() }); }
221        let base = a[0].as_f64().unwrap_or(0.0);
222        let offsets: Vec<(f64, f64)> = a[1].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0)))).collect()).unwrap_or_default();
223        let t = a[2].as_f64().unwrap_or(0.0);
224        Ok(json!(disney::overlapping_action(base, &offsets, t)))
225    }
226
227    fn arc_path(&self, p: Value) -> ModuleResult {
228        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "arc_path requiere [sx, sy, ex, ey, curvature, t]".to_string() })?;
229        if a.len() != 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "arc_path requiere 6 params".to_string() }); }
230        let (x, y) = disney::arc_path((a[0].as_f64().unwrap_or(0.0), a[1].as_f64().unwrap_or(0.0)), (a[2].as_f64().unwrap_or(10.0), a[3].as_f64().unwrap_or(0.0)), a[4].as_f64().unwrap_or(5.0), a[5].as_f64().unwrap_or(0.0));
231        Ok(json!([x, y]))
232    }
233
234    fn secondary_action(&self, p: Value) -> ModuleResult {
235        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "secondary_action requiere [primary, offset, amp, t]".to_string() })?;
236        if a.len() != 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "secondary_action requiere 4 params".to_string() }); }
237        let (pr, sc) = disney::secondary_action(a[0].as_f64().unwrap_or(0.0), a[1].as_f64().unwrap_or(0.2), a[2].as_f64().unwrap_or(0.5), a[3].as_f64().unwrap_or(0.0));
238        Ok(json!([pr, sc]))
239    }
240
241    fn timing(&self, p: Value) -> ModuleResult {
242        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "timing requiere [keyframes, frame]".to_string() })?;
243        if a.len() != 2 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "timing requiere 2 params".to_string() }); }
244        let kfs: Vec<(f64, f64)> = a[0].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0)))).collect()).unwrap_or_default();
245        Ok(json!(disney::timing(&kfs, a[1].as_f64().unwrap_or(0.0))))
246    }
247
248    fn exaggerate(&self, p: Value) -> ModuleResult {
249        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "exaggerate requiere [base, factor, t]".to_string() })?;
250        if a.len() != 3 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "exaggerate requiere 3 params".to_string() }); }
251        Ok(json!(disney::exaggerate(a[0].as_f64().unwrap_or(0.0), a[1].as_f64().unwrap_or(1.5), a[2].as_f64().unwrap_or(0.0))))
252    }
253
254    fn solid_rotation(&self, p: Value) -> ModuleResult {
255        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "solid_rotation requiere [x,y,z,rx,ry,rz,fov]".to_string() })?;
256        if a.len() != 7 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "solid_rotation requiere 7 params".to_string() }); }
257        let (x, y, s) = disney::solid_rotation((a[0].as_f64().unwrap_or(0.0), a[1].as_f64().unwrap_or(0.0), a[2].as_f64().unwrap_or(0.0)), (a[3].as_f64().unwrap_or(0.0), a[4].as_f64().unwrap_or(0.0), a[5].as_f64().unwrap_or(0.0)), a[6].as_f64().unwrap_or(60.0));
258        Ok(json!([x, y, s]))
259    }
260
261    fn appeal(&self, p: Value) -> ModuleResult {
262        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "appeal requiere [w, h, charm, t]".to_string() })?;
263        if a.len() != 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "appeal requiere 4 params".to_string() }); }
264        let (w, h, r) = disney::appeal((a[0].as_f64().unwrap_or(10.0), a[1].as_f64().unwrap_or(10.0)), a[2].as_f64().unwrap_or(0.5), a[3].as_f64().unwrap_or(0.0));
265        Ok(json!([w, h, r]))
266    }
267
268    fn pose_to_pose(&self, p: Value) -> ModuleResult {
269        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "pose_to_pose requiere [kfs, time]".to_string() })?;
270        if a.len() != 2 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "pose_to_pose requiere 2 params".to_string() }); }
271        let kfs: Vec<(f64, f64, f64, f64, f64)> = a[0].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0), p[2].as_f64().unwrap_or(0.0), p[3].as_f64().unwrap_or(1.0), p[4].as_f64().unwrap_or(0.0)))).collect()).unwrap_or_default();
272        let (x, y, s, r) = disney::pose_to_pose(&kfs, a[1].as_f64().unwrap_or(0.0));
273        Ok(json!([x, y, s, r]))
274    }
275
276    // ===== v0.9.0: ILUSIONES ÓPTICAS =====
277
278    fn rotating_snakes(&self, p: Value) -> ModuleResult {
279        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "rotating_snakes requiere [cx, cy, radius, segments, t]".to_string() })?;
280        if a.len() < 5 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "rotating_snakes requiere 5+ params".to_string() }); }
281        let colors: Vec<String> = a.get(5).and_then(|v| v.as_array()).map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()).unwrap_or_default();
282        Ok(json!(illusions::rotating_snakes(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(100.0), a[3].as_f64().unwrap_or(16.0) as usize, a[4].as_f64().unwrap_or(0.0), &colors)))
283    }
284
285    fn cafe_wall(&self, p: Value) -> ModuleResult {
286        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "cafe_wall requiere [sx, sy, rows, cols, bw, bh, mortar, t]".to_string() })?;
287        if a.len() < 8 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "cafe_wall requiere 8 params".to_string() }); }
288        Ok(json!(illusions::cafe_wall(a[0].as_f64().unwrap_or(0.0), a[1].as_f64().unwrap_or(0.0), a[2].as_f64().unwrap_or(8.0) as usize, a[3].as_f64().unwrap_or(12.0) as usize, a[4].as_f64().unwrap_or(30.0), a[5].as_f64().unwrap_or(15.0), a[6].as_f64().unwrap_or(2.0), a[7].as_f64().unwrap_or(0.0))))
289    }
290
291    fn troxler_fading(&self, p: Value) -> ModuleResult {
292        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "troxler_fading requiere [cx, cy, num, radius, size, t]".to_string() })?;
293        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "troxler_fading requiere 6 params".to_string() }); }
294        Ok(json!(illusions::troxler_fading(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(12.0) as usize, a[3].as_f64().unwrap_or(100.0), a[4].as_f64().unwrap_or(10.0), a[5].as_f64().unwrap_or(0.0))))
295    }
296
297    fn pulsing_star(&self, p: Value) -> ModuleResult {
298        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "pulsing_star requiere [cx, cy, outer, inner, points, t]".to_string() })?;
299        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "pulsing_star requiere 6 params".to_string() }); }
300        Ok(json!(illusions::pulsing_star(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(50.0), a[3].as_f64().unwrap_or(25.0), a[4].as_f64().unwrap_or(5.0) as usize, a[5].as_f64().unwrap_or(0.0))))
301    }
302
303    fn zollner_effect(&self, p: Value) -> ModuleResult {
304        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "zollner_effect requiere [sx, sy, len, spacing, lines, tick_len, angle, t]".to_string() })?;
305        if a.len() < 8 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "zollner_effect requiere 8 params".to_string() }); }
306        Ok(json!(illusions::zollner_effect(a[0].as_f64().unwrap_or(50.0), a[1].as_f64().unwrap_or(50.0), a[2].as_f64().unwrap_or(700.0), a[3].as_f64().unwrap_or(40.0), a[4].as_f64().unwrap_or(10.0) as usize, a[5].as_f64().unwrap_or(15.0), a[6].as_f64().unwrap_or(0.5), a[7].as_f64().unwrap_or(0.0))))
307    }
308
309    fn motion_blindness(&self, p: Value) -> ModuleResult {
310        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "motion_blindness requiere [cx, cy, grid, spacing, size, t]".to_string() })?;
311        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "motion_blindness requiere 6 params".to_string() }); }
312        Ok(json!(illusions::motion_induced_blindness(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(10.0) as usize, a[3].as_f64().unwrap_or(30.0), a[4].as_f64().unwrap_or(5.0), a[5].as_f64().unwrap_or(0.0))))
313    }
314
315    // ===== v0.10.0: EFECTOS ESPECIALES =====
316
317    fn neon_glow(&self, p: Value) -> ModuleResult {
318        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "neon_glow requiere [cx, cy, radius, layers, spread, intensity, color, t]".to_string() })?;
319        if a.len() < 8 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "neon_glow requiere 8 params".to_string() }); }
320        let color = a.get(6).and_then(|v| v.as_str()).unwrap_or("#FF00FF");
321        Ok(json!(effects::neon_glow(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(20.0), a[3].as_f64().unwrap_or(5.0) as usize, a[4].as_f64().unwrap_or(2.0), a[5].as_f64().unwrap_or(0.8), color, a[7].as_f64().unwrap_or(0.0))))
322    }
323
324    fn motion_blur(&self, p: Value) -> ModuleResult {
325        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "motion_blur requiere [prev_positions, cx, cy, intensity, fade]".to_string() })?;
326        if a.len() < 5 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "motion_blur requiere 5 params".to_string() }); }
327        let prev: Vec<(f64, f64)> = a[0].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0)))).collect()).unwrap_or_default();
328        Ok(json!(effects::motion_blur(&prev, (a[1].as_f64().unwrap_or(0.0), a[2].as_f64().unwrap_or(0.0)), a[3].as_f64().unwrap_or(0.8), a[4].as_f64().unwrap_or(0.8))))
329    }
330
331    fn chromatic_aberration(&self, p: Value) -> ModuleResult {
332        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "chromatic_aberration requiere [cx, cy, radius, sep, t, shape]".to_string() })?;
333        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "chromatic_aberration requiere 6 params".to_string() }); }
334        let shape = a.get(5).and_then(|v| v.as_str()).unwrap_or("circle");
335        Ok(json!(effects::chromatic_aberration(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(30.0), a[3].as_f64().unwrap_or(10.0), a[4].as_f64().unwrap_or(0.0), shape)))
336    }
337
338    fn bloom_effect(&self, p: Value) -> ModuleResult {
339        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "bloom_effect requiere [sources, radius, intensity, t]".to_string() })?;
340        if a.len() < 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "bloom_effect requiere 4 params".to_string() }); }
341        let sources: Vec<(f64, f64, f64, f64)> = a[0].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0), p[2].as_f64().unwrap_or(1.0), p[3].as_f64().unwrap_or(10.0)))).collect()).unwrap_or_default();
342        Ok(json!(effects::bloom_effect(&sources, a[1].as_f64().unwrap_or(50.0), a[2].as_f64().unwrap_or(0.8), a[3].as_f64().unwrap_or(0.0))))
343    }
344
345    fn particle_trails(&self, p: Value) -> ModuleResult {
346        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "particle_trails requiere [positions, length, fade, color]".to_string() })?;
347        if a.len() < 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "particle_trails requiere 4 params".to_string() }); }
348        let positions: Vec<(f64, f64, f64, f64)> = a[0].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0), p[2].as_f64().unwrap_or(0.0), p[3].as_f64().unwrap_or(0.0)))).collect()).unwrap_or_default();
349        let color = a.get(3).and_then(|v| v.as_str()).unwrap_or("#FFAA00");
350        Ok(json!(effects::particle_trails(&positions, a[1].as_f64().unwrap_or(10.0) as usize, a[2].as_f64().unwrap_or(0.85), color)))
351    }
352
353    fn morph_shapes(&self, p: Value) -> ModuleResult {
354        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "morph_shapes requiere [shape_a, shape_b, t, easing]".to_string() })?;
355        if a.len() < 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "morph_shapes requiere 4 params".to_string() }); }
356        let sa: Vec<(f64, f64)> = a[0].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0)))).collect()).unwrap_or_default();
357        let sb: Vec<(f64, f64)> = a[1].as_array().map(|arr| arr.iter().filter_map(|v| v.as_array().map(|p| (p[0].as_f64().unwrap_or(0.0), p[1].as_f64().unwrap_or(0.0)))).collect()).unwrap_or_default();
358        let easing = a.get(3).and_then(|v| v.as_str()).unwrap_or("linear");
359        Ok(json!(effects::morph_shapes(&sa, &sb, a[2].as_f64().unwrap_or(0.0), easing)))
360    }
361
362    // ===== v0.11.0: CIENCIA ANIMADA =====
363
364    fn chemical_crystallization(&self, p: Value) -> ModuleResult {
365        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "chemical_crystallization requiere [cx, cy, num, radius, t, growth]".to_string() })?;
366        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "chemical_crystallization requiere 6 params".to_string() }); }
367        Ok(json!(science_anim::chemical_crystallization(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(12.0) as usize, a[3].as_f64().unwrap_or(100.0), a[4].as_f64().unwrap_or(0.0), a[5].as_f64().unwrap_or(1.5))))
368    }
369
370    fn cell_division(&self, p: Value) -> ModuleResult {
371        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "cell_division requiere [cx, cy, radius, div_time, max_div, t]".to_string() })?;
372        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "cell_division requiere 6 params".to_string() }); }
373        Ok(json!(science_anim::cell_division(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(30.0), a[3].as_f64().unwrap_or(1.0), a[4].as_f64().unwrap_or(3.0) as usize, a[5].as_f64().unwrap_or(0.0))))
374    }
375
376    fn walk_cycle(&self, p: Value) -> ModuleResult {
377        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "walk_cycle requiere [cx, cy, body, legs, stride, t, phase]".to_string() })?;
378        if a.len() < 7 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "walk_cycle requiere 7 params".to_string() }); }
379        Ok(json!(science_anim::walk_cycle(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(20.0), a[3].as_f64().unwrap_or(4.0) as usize, a[4].as_f64().unwrap_or(15.0), a[5].as_f64().unwrap_or(0.0), a[6].as_f64().unwrap_or(0.25))))
380    }
381
382    fn flight_pattern(&self, p: Value) -> ModuleResult {
383        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "flight_pattern requiere [cx, cy, wingspan, flap_speed, t]".to_string() })?;
384        if a.len() < 5 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "flight_pattern requiere 5 params".to_string() }); }
385        Ok(json!(science_anim::flight_pattern(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(80.0), a[3].as_f64().unwrap_or(5.0), a[4].as_f64().unwrap_or(0.0))))
386    }
387
388    fn lsystem_tree(&self, p: Value) -> ModuleResult {
389        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "lsystem_tree requiere [bx, by, trunk, angle, ratio, depth, t]".to_string() })?;
390        if a.len() < 7 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "lsystem_tree requiere 7 params".to_string() }); }
391        Ok(json!(science_anim::lsystem_tree(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(500.0), a[2].as_f64().unwrap_or(80.0), a[3].as_f64().unwrap_or(0.5), a[4].as_f64().unwrap_or(0.7), a[5].as_f64().unwrap_or(4.0) as usize, a[6].as_f64().unwrap_or(0.0))))
392    }
393
394    fn tusi_couple(&self, p: Value) -> ModuleResult {
395        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "tusi_couple requiere [cx, cy, large_radius, t]".to_string() })?;
396        if a.len() < 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "tusi_couple requiere 4 params".to_string() }); }
397        Ok(json!(science_anim::tusi_couple(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(100.0), a[3].as_f64().unwrap_or(0.0))))
398    }
399
400    fn pendulum_waves(&self, p: Value) -> ModuleResult {
401        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "pendulum_waves requiere [base_x, base_y, num, length, freq_spread, t]".to_string() })?;
402        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "pendulum_waves requiere 6 params".to_string() }); }
403        Ok(json!(science_anim::pendulum_waves(a[0].as_f64().unwrap_or(400.0), a[1].as_f64().unwrap_or(100.0), a[2].as_f64().unwrap_or(12.0) as usize, a[3].as_f64().unwrap_or(100.0), a[4].as_f64().unwrap_or(0.05), a[5].as_f64().unwrap_or(0.0))))
404    }
405
406    fn wave_interference(&self, p: Value) -> ModuleResult {
407        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "wave_interference requiere [cx1, cy1, cx2, cy2, wavelength, amp, grid, t]".to_string() })?;
408        if a.len() < 8 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "wave_interference requiere 8 params".to_string() }); }
409        Ok(json!(science_anim::wave_interference(a[0].as_f64().unwrap_or(250.0), a[1].as_f64().unwrap_or(300.0), a[2].as_f64().unwrap_or(550.0), a[3].as_f64().unwrap_or(300.0), a[4].as_f64().unwrap_or(40.0), a[5].as_f64().unwrap_or(1.0), a[6].as_f64().unwrap_or(15.0) as usize, a[7].as_f64().unwrap_or(0.0))))
410    }
411
412    // ===== v0.12.0: ACTION ASSETS =====
413
414    fn frame_animation(&self, p: Value) -> ModuleResult {
415        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "frame_animation requiere [frames, duration, t, loop_mode]".to_string() })?;
416        if a.len() < 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "frame_animation requiere 4 params".to_string() }); }
417        let loop_mode = a.get(3).and_then(|v| v.as_str()).unwrap_or("loop");
418        Ok(json!(action_assets::frame_animation(a[0].as_f64().unwrap_or(4.0) as usize, a[1].as_f64().unwrap_or(0.25), a[2].as_f64().unwrap_or(0.0), loop_mode)))
419    }
420
421    fn sprite_sheet_parse(&self, p: Value) -> ModuleResult {
422        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "sprite_sheet_parse requiere [sw, sh, fw, fh, idx, cols]".to_string() })?;
423        if a.len() < 6 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "sprite_sheet_parse requiere 6 params".to_string() }); }
424        Ok(json!(action_assets::sprite_sheet_parse(a[0].as_f64().unwrap_or(256.0), a[1].as_f64().unwrap_or(256.0), a[2].as_f64().unwrap_or(64.0), a[3].as_f64().unwrap_or(64.0), a[4].as_f64().unwrap_or(0.0) as usize, a[5].as_f64().unwrap_or(0.0) as usize)))
425    }
426
427    fn animation_state(&self, p: Value) -> ModuleResult {
428        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "animation_state requiere [state, states[], durations[], t, trigger]".to_string() })?;
429        if a.len() < 5 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "animation_state requiere 5 params".to_string() }); }
430        let state = a[0].as_str().unwrap_or("idle");
431        let states: Vec<String> = a[1].as_array().map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()).unwrap_or(vec!["idle".to_string()]);
432        let durations: Vec<f64> = a[2].as_array().map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect()).unwrap_or(vec![1.0]);
433        let t = a[3].as_f64().unwrap_or(0.0);
434        let trigger = a.get(4).and_then(|v| v.as_str()).unwrap_or("");
435        Ok(json!(action_assets::animation_state_machine(state, &states, &durations, t, trigger)))
436    }
437
438    fn animation_blend(&self, p: Value) -> ModuleResult {
439        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "animation_blend requiere [prog_a, prog_b, factor, duration, t]".to_string() })?;
440        if a.len() < 5 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "animation_blend requiere 5 params".to_string() }); }
441        Ok(json!(action_assets::animation_blend(a[0].as_f64().unwrap_or(0.0), a[1].as_f64().unwrap_or(1.0), a[2].as_f64().unwrap_or(1.0), a[3].as_f64().unwrap_or(1.0), a[4].as_f64().unwrap_or(0.0))))
442    }
443
444    fn sprite_events(&self, p: Value) -> ModuleResult {
445        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "sprite_events requiere [type, frame, total, state, progress]".to_string() })?;
446        if a.len() < 5 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "sprite_events requiere 5 params".to_string() }); }
447        let etype = a[0].as_str().unwrap_or("frame_change");
448        let frame = a[1].as_f64().unwrap_or(0.0) as usize;
449        let total = a[2].as_f64().unwrap_or(4.0) as usize;
450        let state = a[3].as_str().unwrap_or("idle");
451        let progress = a[4].as_f64().unwrap_or(0.0);
452        Ok(json!(action_assets::sprite_events(etype, frame, total, state, progress)))
453    }
454
455    fn sprite_flip(&self, p: Value) -> ModuleResult {
456        let a = p.as_array().ok_or_else(|| ModuleError { code: "INVALID_PARAMS".to_string(), message: "sprite_flip requiere [hflip, vflip, ox, oy]".to_string() })?;
457        if a.len() < 4 { return Err(ModuleError { code: "INVALID_PARAMS".to_string(), message: "sprite_flip requiere 4 params".to_string() }); }
458        let hflip = a[0].as_f64().map(|v| v != 0.0).unwrap_or(false);
459        let vflip = a[1].as_f64().map(|v| v != 0.0).unwrap_or(false);
460        Ok(json!(action_assets::sprite_flip(hflip, vflip, a[2].as_f64().unwrap_or(0.5), a[3].as_f64().unwrap_or(0.5))))
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn test_anim_module_name() {
470        let m = AnimModule;
471        assert_eq!(m.name(), "anim");
472        assert_eq!(m.version(), "0.12.0");
473    }
474
475    #[test]
476    fn test_anim_register() {
477        let m = AnimModule;
478        let cmds = m.register();
479        assert!(cmds.contains_key("ease_in"));
480        assert!(cmds.contains_key("follow_through"));
481        assert!(cmds.contains_key("arc_path"));
482        assert!(cmds.contains_key("pose_to_pose"));
483    }
484
485    #[test]
486    fn test_ease_in() {
487        let m = AnimModule;
488        let r = m.execute("ease_in", json!([0.5])).unwrap();
489        assert!((r.as_f64().unwrap() - 0.25).abs() < 0.001);
490    }
491
492    #[test]
493    fn test_ease_out() {
494        let m = AnimModule;
495        let r = m.execute("ease_out", json!([0.5])).unwrap();
496        assert!((r.as_f64().unwrap() - 0.75).abs() < 0.001);
497    }
498
499    #[test]
500    fn test_ease_in_out() {
501        let m = AnimModule;
502        let r = m.execute("ease_in_out", json!([0.5])).unwrap();
503        assert!((r.as_f64().unwrap() - 0.5).abs() < 0.001);
504    }
505
506    #[test]
507    fn test_squash() {
508        let m = AnimModule;
509        let r = m.execute("squash", json!([2.0])).unwrap();
510        let arr = r.as_array().unwrap();
511        assert!((arr[0].as_f64().unwrap() - 2.0).abs() < 0.001);
512        assert!((arr[1].as_f64().unwrap() - 0.5).abs() < 0.001);
513    }
514
515    #[test]
516    fn test_stretch() {
517        let m = AnimModule;
518        let r = m.execute("stretch", json!([2.0])).unwrap();
519        let arr = r.as_array().unwrap();
520        assert!((arr[0].as_f64().unwrap() - 0.5).abs() < 0.001);
521        assert!((arr[1].as_f64().unwrap() - 2.0).abs() < 0.001);
522    }
523
524    #[test]
525    fn test_anticipate() {
526        let m = AnimModule;
527        let r = m.execute("anticipate", json!([100.0, 200.0, 20.0])).unwrap();
528        assert!((r.as_f64().unwrap() - 80.0).abs() < 0.001);
529    }
530
531    #[test]
532    fn test_follow_through() {
533        let m = AnimModule;
534        let r = m.execute("follow_through", json!([1.0, 1.0, 5.0, 0.1])).unwrap();
535        assert!(r.as_f64().unwrap().abs() > 0.0);
536    }
537
538    #[test]
539    fn test_arc_path() {
540        let m = AnimModule;
541        let r = m.execute("arc_path", json!([0.0, 0.0, 10.0, 0.0, 5.0, 0.5])).unwrap();
542        let arr = r.as_array().unwrap();
543        assert!((arr[0].as_f64().unwrap() - 5.0).abs() < 0.1);
544        assert!(arr[1].as_f64().unwrap() > 0.0);
545    }
546
547    #[test]
548    fn test_secondary_action() {
549        let m = AnimModule;
550        let r = m.execute("secondary_action", json!([1.0, 0.2, 0.5, 0.5])).unwrap();
551        let arr = r.as_array().unwrap();
552        assert_eq!(arr.len(), 2);
553    }
554
555    #[test]
556    fn test_timing() {
557        let m = AnimModule;
558        let r = m.execute("timing", json!([[[0.0, 0.0], [10.0, 100.0]], 5.0])).unwrap();
559        let v = r.as_f64().unwrap();
560        assert!(v > 40.0 && v < 60.0);
561    }
562
563    #[test]
564    fn test_exaggerate() {
565        let m = AnimModule;
566        let r = m.execute("exaggerate", json!([1.0, 2.0, 0.5])).unwrap();
567        assert!((r.as_f64().unwrap() - 1.0).abs() < 0.01);
568    }
569
570    #[test]
571    fn test_solid_rotation() {
572        let m = AnimModule;
573        let r = m.execute("solid_rotation", json!([0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 60.0])).unwrap();
574        let arr = r.as_array().unwrap();
575        assert!((arr[0].as_f64().unwrap() - 0.0).abs() < 0.01);
576        assert!(arr[2].as_f64().unwrap() > 0.0);
577    }
578
579    #[test]
580    fn test_appeal() {
581        let m = AnimModule;
582        let r = m.execute("appeal", json!([10.0, 10.0, 0.5, 1.0])).unwrap();
583        let arr = r.as_array().unwrap();
584        assert!(arr[0].as_f64().unwrap() > 10.0);
585    }
586
587    #[test]
588    fn test_pose_to_pose() {
589        let m = AnimModule;
590        let r = m.execute("pose_to_pose", json!([[[0.0, 0.0, 0.0, 1.0, 0.0], [1.0, 10.0, 5.0, 1.5, 0.5]], 0.5])).unwrap();
591        let arr = r.as_array().unwrap();
592        assert!(arr[0].as_f64().unwrap() > 2.0 && arr[0].as_f64().unwrap() < 8.0);
593    }
594
595    #[test]
596    fn test_unknown_command() {
597        let m = AnimModule;
598        assert!(m.execute("unknown", json!([])).is_err());
599    }
600}