1pub 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
53pub 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 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 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 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 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 "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 "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 "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 "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 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 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 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 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 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}