Skip to main content

ry_anim/
effects.rs

1//! Efectos Especiales para Animación
2//!
3//! Efectos visuales que mejoran la apariencia de animaciones y juegos.
4//!
5//! ## Efectos implementados
6//!
7//! - Neon Glow: Resplandor neón configurable
8//! - Motion Blur: Desenfoque de movimiento
9//! - Chromatic Aberration: Separación RGB en bordes
10//! - Bloom: Brillo difuso en zonas claras
11//! - Particle Trails: Estelas de partículas
12//! - Morphing: Transición suave entre formas
13
14use serde_json::{json, Value};
15
16// ============================================================================
17// NEON GLOW — Resplandor neón
18// ============================================================================
19
20/// Neon Glow — Genera capas de resplandor alrededor de un objeto
21///
22/// # Args
23/// - cx, cy: centro del objeto
24/// - base_radius: radio del objeto base
25/// - glow_layers: número de capas de glow (3-8)
26/// - glow_spread: cuánto se expande cada capa (1.5-3.0)
27/// - intensity: intensidad del glow (0.3-1.0)
28/// - base_color: color base en hex
29/// - t: tiempo para animación de pulso
30///
31/// # Retorna
32/// Array de círculos [{x, y, radius, color, alpha}, ...]
33pub fn neon_glow(cx: f64, cy: f64, base_radius: f64, glow_layers: usize,
34                 glow_spread: f64, intensity: f64, base_color: &str, t: f64) -> Vec<Value> {
35    let glow_layers = glow_layers.clamp(3, 12);
36    let glow_spread = glow_spread.clamp(1.2, 4.0);
37    let intensity = intensity.clamp(0.1, 1.0);
38    let pulse = 1.0 + 0.1 * (t * 3.0).sin(); // Pulso suave
39
40    let mut result = Vec::new();
41
42    // Capas de glow (de afuera hacia adentro)
43    for i in (1..=glow_layers).rev() {
44        let radius = base_radius * glow_spread.powi(i as i32) * pulse;
45        let alpha = (intensity / (i as f64)).min(1.0) * 0.4;
46
47        result.push(json!({
48            "type": "glow_ring",
49            "x": cx, "y": cy,
50            "radius": radius,
51            "color": base_color,
52            "alpha": alpha
53        }));
54    }
55
56    // Objeto central (brillante)
57    result.push(json!({
58        "type": "core",
59        "x": cx, "y": cy,
60        "radius": base_radius * pulse,
61        "color": "#FFFFFF",
62        "alpha": 1.0
63    }));
64
65    result
66}
67
68// ============================================================================
69// MOTION BLUR — Desenfoque de movimiento
70// ============================================================================
71
72/// Motion Blur — Genera estelas de movimiento basadas en velocidad
73///
74/// # Args
75/// - prev_positions: array de posiciones anteriores [(x,y), ...]
76/// - current_pos: posición actual (x, y)
77/// - blur_intensity: cuántas copias generar (0.3-1.0)
78/// - fade_rate: qué tan rápido se desvanecen las copias (0.5-0.95)
79///
80/// # Retorna
81/// Array de copias [{x, y, alpha, scale}, ...]
82pub fn motion_blur(prev_positions: &[(f64, f64)], current_pos: (f64, f64),
83                   blur_intensity: f64, fade_rate: f64) -> Vec<Value> {
84    let blur_intensity = blur_intensity.clamp(0.1, 1.0);
85    let fade_rate = fade_rate.clamp(0.3, 0.98);
86    let max_blurs = (blur_intensity * 20.0) as usize;
87
88    let mut result = Vec::new();
89
90    // Posición actual (sin blur, alpha completo)
91    result.push(json!({
92        "type": "sharp",
93        "x": current_pos.0,
94        "y": current_pos.1,
95        "alpha": 1.0,
96        "scale": 1.0
97    }));
98
99    // Copias borrosas de posiciones anteriores
100    let blurs = prev_positions.len().min(max_blurs);
101    for i in 0..blurs {
102        let (px, py) = prev_positions[prev_positions.len() - 1 - i];
103        let alpha = fade_rate.powi((i + 1) as i32);
104        let scale = 1.0 + 0.05 * (i as f64); // Las copias son ligeramente más grandes
105
106        result.push(json!({
107            "type": "blur_copy",
108            "x": px,
109            "y": py,
110            "alpha": alpha,
111            "scale": scale
112        }));
113    }
114
115    result
116}
117
118// ============================================================================
119// CHROMATIC ABERRATION — Separación RGB
120// ============================================================================
121
122/// Chromatic Aberration — Separa un objeto en canales R, G, B
123///
124/// # Args
125/// - cx, cy: centro original
126/// - radius: tamaño del objeto
127/// - separation: cuánto separar los canales (0-20px)
128/// - t: tiempo para animación
129/// - shape_type: "circle", "rect", "star"
130///
131/// # Retorna
132/// Array de canales [{channel, x, y, radius/size, color}, ...]
133pub fn chromatic_aberration(cx: f64, cy: f64, radius: f64, separation: f64,
134                            t: f64, shape_type: &str) -> Vec<Value> {
135    let sep = separation.clamp(0.0, 30.0);
136    // Dirección de separación basada en ángulo animado
137    let angle = t * 0.5;
138    let dx = angle.cos() * sep;
139    let dy = angle.sin() * sep;
140
141    let mut result = Vec::new();
142
143    // Canal Rojo (desplazado)
144    result.push(json!({
145        "type": shape_type,
146        "channel": "red",
147        "x": cx - dx, "y": cy - dy,
148        "radius": radius,
149        "color": "#FF0000"
150    }));
151
152    // Canal Verde (centro)
153    result.push(json!({
154        "type": shape_type,
155        "channel": "green",
156        "x": cx, "y": cy,
157        "radius": radius,
158        "color": "#00FF00"
159    }));
160
161    // Canal Azul (desplazado opuesto)
162    result.push(json!({
163        "type": shape_type,
164        "channel": "blue",
165        "x": cx + dx, "y": cy + dy,
166        "radius": radius,
167        "color": "#0000FF"
168    }));
169
170    result
171}
172
173// ============================================================================
174// BLOOM — Brillo difuso
175// ============================================================================
176
177/// Bloom — Efecto de brillo difuso en zonas claras
178///
179/// # Args
180/// - sources: array de fuentes de luz [{x, y, intensity, radius}, ...]
181/// - bloom_radius: radio del bloom
182/// - bloom_intensity: intensidad del bloom
183/// - t: tiempo para parpadeo
184///
185/// # Retorna
186/// Array de halos de bloom [{x, y, radius, alpha, color}, ...]
187pub fn bloom_effect(sources: &[(f64, f64, f64, f64)], bloom_radius: f64,
188                    bloom_intensity: f64, t: f64) -> Vec<Value> {
189    let bloom_radius = bloom_radius.max(5.0);
190    let bloom_intensity = bloom_intensity.clamp(0.1, 1.0);
191
192    let mut result = Vec::new();
193
194    for (i, (x, y, intensity, radius)) in sources.iter().enumerate() {
195        let flicker = 1.0 + 0.1 * ((t * 4.0 + i as f64) as f64).sin();
196        let effective_intensity = intensity * bloom_intensity * flicker;
197        let effective_radius = radius + bloom_radius * effective_intensity;
198
199        // Halo exterior
200        result.push(json!({
201            "type": "bloom_halo",
202            "x": x, "y": y,
203            "radius": effective_radius,
204            "alpha": effective_intensity * 0.5,
205            "color": "#FFFFAA"
206        }));
207
208        // Centro brillante
209        result.push(json!({
210            "type": "bloom_core",
211            "x": x, "y": y,
212            "radius": radius,
213            "alpha": intensity,
214            "color": "#FFFFFF"
215        }));
216    }
217
218    result
219}
220
221// ============================================================================
222// PARTICLE TRAILS — Estelas de partículas
223// ============================================================================
224
225/// Particle Trails — Genera estela detrás de partículas en movimiento
226///
227/// # Args
228/// - positions: array de posiciones actuales [(x, y, vx, vy), ...]
229/// - trail_length: largo de la estela (5-30)
230/// - trail_fade: qué tan rápido se desvanece (0.7-0.98)
231/// - trail_color: color de la estela
232///
233/// # Retorna
234/// Array de puntos de estela [{x, y, size, alpha, color}, ...]
235pub fn particle_trails(positions: &[(f64, f64, f64, f64)], trail_length: usize,
236                       trail_fade: f64, trail_color: &str) -> Vec<Value> {
237    let trail_length = trail_length.clamp(3, 50);
238    let trail_fade = trail_fade.clamp(0.5, 0.99);
239
240    let mut result = Vec::new();
241
242    for (px, py, vx, vy) in positions {
243        let speed = (vx * vx + vy * vy).sqrt();
244
245        for i in 0..trail_length {
246            let trail_x = px - vx * (i as f64) * 0.3;
247            let trail_y = py - vy * (i as f64) * 0.3;
248            let alpha = trail_fade.powi((i + 1) as i32);
249            let size = (3.0 - i as f64 * 0.1).max(0.5);
250
251            result.push(json!({
252                "type": "trail_dot",
253                "x": trail_x,
254                "y": trail_y,
255                "size": size * (speed / 5.0).min(2.0),
256                "alpha": alpha,
257                "color": trail_color
258            }));
259        }
260
261        // Partícula principal
262        result.push(json!({
263            "type": "particle",
264            "x": px, "y": py,
265            "size": 4.0,
266            "alpha": 1.0,
267            "color": "#FFFFFF"
268        }));
269    }
270
271    result
272}
273
274// ============================================================================
275// MORPHING — Transición entre formas
276// ============================================================================
277
278/// Morphing — Transición suave entre dos formas
279///
280/// # Args
281/// - shape_a: puntos de la forma A [{x, y}, ...]
282/// - shape_b: puntos de la forma B [{x, y}, ...]
283/// - t: progreso de la transición (0.0 = forma A, 1.0 = forma B)
284/// - easing: función de easing ("linear", "ease_in", "ease_out", "ease_in_out")
285///
286/// # Retorna
287/// Array de puntos interpolados [{x, y}, ...]
288pub fn morph_shapes(shape_a: &[(f64, f64)], shape_b: &[(f64, f64)],
289                    t: f64, easing: &str) -> Vec<Value> {
290    let t = t.clamp(0.0, 1.0);
291
292    // Aplicar easing
293    let et = match easing {
294        "ease_in" => t * t,
295        "ease_out" => t * (2.0 - t),
296        "ease_in_out" => if t < 0.5 { 2.0 * t * t } else { 1.0 - 2.0 * (1.0 - t) * (1.0 - t) },
297        _ => t, // linear
298    };
299
300    let len = shape_a.len().max(shape_b.len());
301    let mut result = Vec::new();
302
303    for i in 0..len {
304        let ax = shape_a.get(i % shape_a.len()).map(|p| p.0).unwrap_or(0.0);
305        let ay = shape_a.get(i % shape_a.len()).map(|p| p.1).unwrap_or(0.0);
306        let bx = shape_b.get(i % shape_b.len()).map(|p| p.0).unwrap_or(0.0);
307        let by = shape_b.get(i % shape_b.len()).map(|p| p.1).unwrap_or(0.0);
308
309        let x = ax + (bx - ax) * et;
310        let y = ay + (by - ay) * et;
311
312        result.push(json!({ "x": x, "y": y }));
313    }
314
315    result
316}
317
318// ============================================================================
319// TESTS
320// ============================================================================
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_neon_glow() {
328        let result = neon_glow(400.0, 300.0, 20.0, 5, 2.0, 0.8, "#FF00FF", 0.5);
329        assert_eq!(result.len(), 6); // 5 capas glow + 1 core
330        assert!(result[0].get("radius").is_some());
331    }
332
333    #[test]
334    fn test_motion_blur() {
335        let prev = vec![(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)];
336        let result = motion_blur(&prev, (30.0, 0.0), 0.8, 0.8);
337        assert!(result.len() > 1); // Al menos la posición actual + copias
338    }
339
340    #[test]
341    fn test_chromatic_aberration() {
342        let result = chromatic_aberration(400.0, 300.0, 30.0, 10.0, 0.5, "circle");
343        assert_eq!(result.len(), 3); // R, G, B
344        let red = &result[0];
345        let blue = &result[2];
346        assert!(red.get("x").unwrap().as_f64().unwrap() < blue.get("x").unwrap().as_f64().unwrap());
347    }
348
349    #[test]
350    fn test_bloom_effect() {
351        let sources = vec![(400.0, 300.0, 1.0, 10.0)];
352        let result = bloom_effect(&sources, 50.0, 0.8, 0.5);
353        assert!(result.len() >= 2); // Halo + core
354    }
355
356    #[test]
357    fn test_particle_trails() {
358        let positions = vec![(100.0, 100.0, 5.0, 0.0)];
359        let result = particle_trails(&positions, 10, 0.85, "#FFAA00");
360        assert!(result.len() > 10); // 10 trail dots + 1 particle
361    }
362
363    #[test]
364    fn test_morph_shapes() {
365        let shape_a = vec![(0.0, 0.0), (100.0, 0.0), (50.0, 100.0)]; // Triángulo
366        let shape_b = vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]; // Cuadrado
367        let result = morph_shapes(&shape_a, &shape_b, 0.5, "linear");
368        assert_eq!(result.len(), 4); // Max de los dos shapes
369    }
370
371    #[test]
372    fn test_morph_easing() {
373        let a = vec![(0.0, 0.0)];
374        let b = vec![(100.0, 0.0)];
375
376        let linear = morph_shapes(&a, &b, 0.5, "linear");
377        let ease_in = morph_shapes(&a, &b, 0.5, "ease_in");
378
379        // ease_in debe estar más cerca de A (más lento al inicio)
380        assert!(linear[0].get("x").unwrap().as_f64().unwrap() > ease_in[0].get("x").unwrap().as_f64().unwrap());
381    }
382}