Skip to main content

ry_anim/
science_anim.rs

1//! Animaciones Científicas
2//!
3//! Generadores de animaciones basadas en fenómenos científicos reales:
4//! química, biología, zoología, botánica, física histórica.
5//!
6//! ## Animaciones implementadas
7//!
8//! - Chemical Reactions (cristalización, crecimiento)
9//! - Biological Cycles (división celular, curvas de crecimiento)
10//! - Fauna Movement (ciclos de caminata, patrones de vuelo)
11//! - Flora Growth (L-systems animados)
12//! - Tusi Couple (pareja de Tusi - animación histórica)
13//! - Pendulum Waves (ondas de péndulos)
14//! - Wave Interference (interferencia de ondas)
15
16use serde_json::{json, Value};
17
18// ============================================================================
19// CHEMICAL REACTIONS — Cristalización
20// ============================================================================
21
22/// Chemical Crystallization — Simula crecimiento de cristales
23///
24/// # Args
25/// - cx, cy: centro de cristalización
26/// - num_crystals: número de cristales (5-30)
27/// - max_radius: radio máximo de crecimiento
28/// - t: tiempo de animación (0.0-1.0)
29/// - growth_rate: velocidad de crecimiento (0.5-3.0)
30///
31/// # Retorna
32/// Array de cristales [{x, y, size, angle, alpha}, ...]
33pub fn chemical_crystallization(cx: f64, cy: f64, num_crystals: usize,
34                                 max_radius: f64, t: f64, growth_rate: f64) -> Vec<Value> {
35    let num_crystals = num_crystals.clamp(5, 40);
36    let t = t.clamp(0.0, 1.0);
37    let growth_rate = growth_rate.clamp(0.3, 5.0);
38    let mut result = Vec::new();
39
40    for i in 0..num_crystals {
41        let angle = (i as f64 / num_crystals as f64) * std::f64::consts::PI * 2.0 + i as f64 * 0.5;
42        // Crecimiento no lineal — cristales externos crecen más lento
43        let dist_factor = i as f64 / num_crystals as f64;
44        let radius = max_radius * t.powf(growth_rate) * (1.0 - dist_factor * 0.5);
45        let x = cx + angle.cos() * radius;
46        let y = cy + angle.sin() * radius;
47        let size = 3.0 + 8.0 * t.powf(growth_rate * 0.8);
48        let alpha = (t * 2.0).min(1.0);
49
50        result.push(json!({
51            "type": "crystal",
52            "x": x, "y": y,
53            "size": size,
54            "angle": angle,
55            "alpha": alpha,
56            "color": "#AADDFF"
57        }));
58    }
59
60    result
61}
62
63// ============================================================================
64// BIOLOGICAL CYCLES — División celular
65// ============================================================================
66
67/// Cell Division — Simula división celular con crecimiento
68///
69/// # Args
70/// - cx, cy: centro
71/// - initial_radius: radio de la célula inicial
72/// - division_time: tiempo en el que ocurre la división (1.0 = primera división)
73/// - max_divisions: número máximo de divisiones (1-6)
74/// - t: tiempo de animación
75///
76/// # Retorna
77/// Array de células [{x, y, radius, dividing, alpha}, ...]
78pub fn cell_division(cx: f64, cy: f64, initial_radius: f64,
79                     division_time: f64, max_divisions: usize, t: f64) -> Vec<Value> {
80    let max_divisions = max_divisions.clamp(1, 8);
81    let mut cells = vec![(cx, cy, initial_radius, 0usize)];
82
83    for div in 0..max_divisions {
84        let div_t = division_time * (div as f64 + 1.0);
85        if t >= div_t {
86            let progress = ((t - div_t) / division_time).min(1.0);
87            let mut new_cells = Vec::new();
88
89            for (ccx, ccy, r, _) in &cells {
90                if progress > 0.5 {
91                    // División completada — crear 2 células hijas
92                    let offset = r * 0.3 * (progress - 0.5) * 2.0;
93                    let new_r = r * 0.7;
94                    new_cells.push((*ccx - offset, *ccy, new_r, div + 1));
95                    new_cells.push((*ccx + offset, *ccy, new_r, div + 1));
96                } else {
97                    // Célula creciendo antes de dividirse
98                    let new_r = r * (1.0 + progress * 0.5);
99                    new_cells.push((*ccx, *ccy, new_r, div));
100                }
101            }
102
103            cells = new_cells;
104        }
105    }
106
107    cells.iter().map(|(x, y, r, _)| json!({
108        "type": "cell",
109        "x": x, "y": y,
110        "radius": r,
111        "alpha": 0.8,
112        "color": "#44DD88"
113    })).collect()
114}
115
116// ============================================================================
117// FAUNA MOVEMENT — Ciclo de caminata (walk cycle)
118// ============================================================================
119
120/// Walk Cycle — Genera posiciones de patas para ciclo de caminata
121///
122/// # Args
123/// - cx, cy: posición del cuerpo
124/// - body_size: tamaño del cuerpo
125/// - num_legs: número de patas (2, 4, 6, 8)
126/// - stride: largo del paso
127/// - t: tiempo del ciclo (0.0-1.0 = un paso completo)
128/// - leg_phase_offset: desfase entre patas (0.0-1.0)
129///
130/// # Retorna
131/// Array de puntos [{type: "body" | "leg", x, y, angle}, ...]
132pub fn walk_cycle(cx: f64, cy: f64, body_size: f64, num_legs: usize,
133                  stride: f64, t: f64, leg_phase_offset: f64) -> Vec<Value> {
134    let num_legs = num_legs.clamp(2, 8);
135    let t = t % 1.0;
136    let mut result = Vec::new();
137
138    // Cuerpo con ligero balanceo
139    let body_y = cy + 3.0 * (t * std::f64::consts::PI * 4.0).sin();
140    result.push(json!({
141        "type": "body",
142        "x": cx, "y": body_y,
143        "size": body_size,
144        "color": "#CC8844"
145    }));
146
147    // Patas
148    for i in 0..num_legs {
149        let phase = t + (i as f64 / num_legs as f64) * leg_phase_offset;
150        let leg_angle = (phase * std::f64::consts::PI * 2.0).sin() * stride;
151        let _side = if i < num_legs / 2 { 1.0 } else { -1.0 };
152        let leg_idx = i % (num_legs / 2);
153        let leg_x = cx + (leg_idx as f64 - (num_legs / 4) as f64) * body_size * 0.4;
154
155        let foot_x = leg_x + leg_angle;
156        let foot_y = body_y + body_size + leg_angle.abs() * 0.3;
157
158        result.push(json!({
159            "type": "leg",
160            "x1": leg_x, "y1": body_y,
161            "x2": foot_x, "y2": foot_y,
162            "color": "#AA6633"
163        }));
164    }
165
166    result
167}
168
169// ============================================================================
170// FAUNA MOVEMENT — Patrón de vuelo (flapping wings)
171// ============================================================================
172
173/// Flight Pattern — Simula aleteo de aves
174///
175/// # Args
176/// - cx, cy: posición del cuerpo
177/// - wingspan: envergadura
178/// - flap_speed: velocidad del aleteo
179/// - t: tiempo
180///
181/// # Retorna
182/// Array de líneas [{type: "body" | "wing", x1, y1, x2, y2}, ...]
183pub fn flight_pattern(cx: f64, cy: f64, wingspan: f64, flap_speed: f64, t: f64) -> Vec<Value> {
184    let flap = (t * flap_speed * std::f64::consts::PI * 2.0).sin();
185    let wing_angle = flap * 0.6; // ±60 grados
186
187    let mut result = Vec::new();
188
189    // Cuerpo
190    result.push(json!({
191        "type": "body",
192        "x": cx, "y": cy,
193        "size": 10.0,
194        "color": "#4488CC"
195    }));
196
197    // Ala izquierda
198    let left_wing_x = cx - wingspan / 2.0 * wing_angle.cos();
199    let left_wing_y = cy - wingspan / 2.0 * wing_angle.sin();
200    result.push(json!({
201        "type": "wing",
202        "x1": cx, "y1": cy,
203        "x2": left_wing_x, "y2": left_wing_y,
204        "color": "#66AADD"
205    }));
206
207    // Ala derecha
208    let right_wing_x = cx + wingspan / 2.0 * wing_angle.cos();
209    let right_wing_y = cy - wingspan / 2.0 * wing_angle.sin();
210    result.push(json!({
211        "type": "wing",
212        "x1": cx, "y1": cy,
213        "x2": right_wing_x, "y2": right_wing_y,
214        "color": "#66AADD"
215    }));
216
217    result
218}
219
220// ============================================================================
221// FLORA GROWTH — L-System animado (árbol simple)
222// ============================================================================
223
224/// L-System Tree — Árbol fractal animado
225///
226/// # Args
227/// - base_x, base_y: base del tronco
228/// - trunk_length: largo del tronco
229/// - branch_angle: ángulo de ramificación (radianes)
230/// - length_ratio: ratio de reducción por nivel (0.6-0.8)
231/// - max_depth: profundidad máxima de recursión (3-8)
232/// - t: tiempo de animación (0.0 = solo tronco, 1.0 = árbol completo)
233///
234/// # Retorna
235/// Array de segmentos [{x1, y1, x2, y2, depth, color}, ...]
236pub fn lsystem_tree(base_x: f64, base_y: f64, trunk_length: f64,
237                     branch_angle: f64, length_ratio: f64, max_depth: usize, t: f64) -> Vec<Value> {
238    let max_depth = max_depth.clamp(2, 10);
239    let t = t.clamp(0.0, 1.0);
240    let mut result = Vec::new();
241
242    fn grow_branch(x: f64, y: f64, angle: f64, length: f64,
243                   depth: usize, max_depth: usize, t: f64,
244                   branch_angle: f64, length_ratio: f64,
245                   result: &mut Vec<Value>) {
246        if depth == 0 || t < (depth as f64 / max_depth as f64) {
247            return;
248        }
249
250        let progress = ((t - (depth as f64 / max_depth as f64)) * max_depth as f64).min(1.0);
251        let current_length = length * progress;
252
253        let end_x = x + angle.cos() * current_length;
254        let end_y = y + angle.sin() * current_length;
255
256        // Color: tronco marrón, ramas verdes
257        let color = if depth > max_depth / 2 { "#8B4513" } else { "#228B22" };
258
259        result.push(json!({
260            "type": "branch",
261            "x1": x, "y1": y,
262            "x2": end_x, "y2": end_y,
263            "depth": depth,
264            "color": color
265        }));
266
267        if depth > 1 {
268            grow_branch(end_x, end_y, angle - branch_angle,
269                       length * length_ratio, depth - 1, max_depth, t,
270                       branch_angle, length_ratio, result);
271            grow_branch(end_x, end_y, angle + branch_angle,
272                       length * length_ratio, depth - 1, max_depth, t,
273                       branch_angle, length_ratio, result);
274        }
275    }
276
277    grow_branch(base_x, base_y, -std::f64::consts::PI / 2.0,
278               trunk_length, max_depth, max_depth, t, branch_angle, length_ratio, &mut result);
279
280    result
281}
282
283// ============================================================================
284// TUSI COUPLE — Pareja de Tusi (animación histórica)
285// ============================================================================
286
287/// Tusi Couple — Círculo pequeño rodando dentro de uno grande
288///
289/// Genera movimiento lineal a partir de movimiento circular.
290/// Inventado por Nasir al-Din al-Tusi (~1250 d.C.)
291///
292/// # Args
293/// - cx, cy: centro
294/// - large_radius: radio del círculo grande
295/// - t: tiempo de animación
296///
297/// # Retorna
298/// Array de elementos [{type: "large_circle" | "small_circle" | "point" | "trace", ...}, ...]
299pub fn tusi_couple(cx: f64, cy: f64, large_radius: f64, t: f64) -> Vec<Value> {
300    let small_radius = large_radius / 2.0;
301    let mut result = Vec::new();
302
303    // Círculo grande
304    result.push(json!({
305        "type": "large_circle",
306        "x": cx, "y": cy,
307        "radius": large_radius,
308        "color": "#444466"
309    }));
310
311    // Centro del círculo pequeño (orbitando)
312    let small_cx = cx + small_radius * (t * 2.0).cos();
313    let small_cy = cy + small_radius * (t * 2.0).sin();
314
315    // Círculo pequeño
316    result.push(json!({
317        "type": "small_circle",
318        "x": small_cx, "y": small_cy,
319        "radius": small_radius,
320        "color": "#666688"
321    }));
322
323    // Punto en el borde del círculo pequeño (se mueve linealmente!)
324    let point_x = small_cx + small_radius * (-t * 2.0).cos();
325    let point_y = small_cy + small_radius * (-t * 2.0).sin();
326
327    result.push(json!({
328        "type": "point",
329        "x": point_x, "y": point_y,
330        "size": 6.0,
331        "color": "#FF4444"
332    }));
333
334    // Línea de traza (movimiento lineal)
335    result.push(json!({
336        "type": "trace_line",
337        "x1": cx - large_radius, "y1": cy,
338        "x2": cx + large_radius, "y2": cy,
339        "color": "#FF8888"
340    }));
341
342    result
343}
344
345// ============================================================================
346// PENDULUM WAVES — Ondas de péndulos
347// ============================================================================
348
349/// Pendulum Waves — Múltiples péndulos con frecuencias ligeramente distintas
350///
351/// # Args
352/// - base_x, base_y: línea de soporte
353/// - num_pendulums: número de péndulos (8-20)
354/// - pendulum_length: largo de cada péndulo
355/// - freq_spread: diferencia de frecuencia entre péndulos
356/// - t: tiempo
357///
358/// # Retorna
359/// Array de péndulos [{x1, y1, x2, y2, bob_x, bob_y}, ...]
360pub fn pendulum_waves(base_x: f64, base_y: f64, num_pendulums: usize,
361                      pendulum_length: f64, freq_spread: f64, t: f64) -> Vec<Value> {
362    let num_pendulums = num_pendulums.clamp(6, 30);
363    let mut result = Vec::new();
364    let spacing = (base_x * 2.0 / num_pendulums as f64).min(40.0);
365
366    for i in 0..num_pendulums {
367        let px = base_x + i as f64 * spacing - (num_pendulums as f64 * spacing / 2.0);
368        let freq = 1.0 + i as f64 * freq_spread;
369        let angle = 0.5 * (t * freq * std::f64::consts::PI * 2.0).sin();
370
371        let bob_x = px + pendulum_length * angle.sin();
372        let bob_y = base_y + pendulum_length * angle.cos();
373
374        // Color basado en la posición (efecto arcoíris)
375        let hue = (i as f64 / num_pendulums as f64 + t * 0.1) % 1.0;
376        let color = format!("hsl({}, 80%, 60%)", (hue * 360.0) as usize);
377
378        result.push(json!({
379            "type": "pendulum",
380            "x1": px, "y1": base_y,
381            "x2": bob_x, "y2": bob_y,
382            "bob_x": bob_x, "bob_y": bob_y,
383            "color": color
384        }));
385    }
386
387    result
388}
389
390// ============================================================================
391// WAVE INTERFERENCE — Interferencia de ondas
392// ============================================================================
393
394/// Wave Interference — Dos fuentes de ondas interfiriéndose
395///
396/// # Args
397/// - cx1, cy1: centro de fuente 1
398/// - cx2, cy2: centro de fuente 2
399/// - wavelength: longitud de onda
400/// - amplitude: amplitud
401/// - grid_resolution: resolución de la cuadrícula (10-50)
402/// - t: tiempo
403///
404/// # Retorna
405/// Array de puntos de la cuadrícula [{x, y, amplitude, color}, ...]
406pub fn wave_interference(cx1: f64, cy1: f64, cx2: f64, cy2: f64,
407                           wavelength: f64, amplitude: f64,
408                           grid_resolution: usize, t: f64) -> Vec<Value> {
409    let grid_resolution = grid_resolution.clamp(8, 60);
410    let mut result = Vec::new();
411
412    let width = 600.0;
413    let height = 400.0;
414    let start_x = 100.0;
415    let start_y = 100.0;
416    let step_x = width / grid_resolution as f64;
417    let step_y = height / grid_resolution as f64;
418
419    for iy in 0..=grid_resolution {
420        for ix in 0..=grid_resolution {
421            let x = start_x + ix as f64 * step_x;
422            let y = start_y + iy as f64 * step_y;
423
424            // Distancia a cada fuente
425            let d1 = ((x - cx1) * (x - cx1) + (y - cy1) * (y - cy1)).sqrt();
426            let d2 = ((x - cx2) * (x - cx2) + (y - cy2) * (y - cy2)).sqrt();
427
428            // Ondas individuales
429            let w1 = amplitude * (d1 * std::f64::consts::PI * 2.0 / wavelength - t * 3.0).sin();
430            let w2 = amplitude * (d2 * std::f64::consts::PI * 2.0 / wavelength - t * 3.0).sin();
431
432            // Interferencia
433            let combined = (w1 + w2) / 2.0;
434            let normalized = (combined / amplitude + 1.0) / 2.0; // 0.0-1.0
435
436            // Color: azul (destructiva) → blanco → rojo (constructiva)
437            let color = if normalized < 0.5 {
438                let t = normalized * 2.0;
439                format!("rgb({}, {}, {})", (t * 100.0) as u8, (t * 100.0) as u8, 255)
440            } else {
441                let t = (normalized - 0.5) * 2.0;
442                format!("rgb(255, {}, {})", (255.0 - t * 155.0) as u8, (255.0 - t * 200.0) as u8)
443            };
444
445            result.push(json!({
446                "type": "wave_point",
447                "x": x, "y": y,
448                "amplitude": combined,
449                "color": color
450            }));
451        }
452    }
453
454    // Fuentes
455    result.push(json!({ "type": "source", "x": cx1, "y": cy1, "color": "#00FFFF" }));
456    result.push(json!({ "type": "source", "x": cx2, "y": cy2, "color": "#FF00FF" }));
457
458    result
459}
460
461// ============================================================================
462// TESTS
463// ============================================================================
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn test_chemical_crystallization() {
471        let result = chemical_crystallization(400.0, 300.0, 12, 100.0, 0.5, 1.5);
472        assert_eq!(result.len(), 12);
473        assert!(result[0].get("type").is_some());
474    }
475
476    #[test]
477    fn test_cell_division() {
478        let result = cell_division(400.0, 300.0, 30.0, 1.0, 3, 2.5);
479        assert!(result.len() >= 2); // Al menos 2 células después de dos divisiones
480    }
481
482    #[test]
483    fn test_walk_cycle() {
484        let result = walk_cycle(400.0, 300.0, 20.0, 4, 15.0, 0.3, 0.25);
485        assert!(result.len() > 4); // Cuerpo + 4 patas
486    }
487
488    #[test]
489    fn test_flight_pattern() {
490        let result = flight_pattern(400.0, 300.0, 80.0, 5.0, 0.25);
491        assert_eq!(result.len(), 3); // Cuerpo + 2 alas
492    }
493
494    #[test]
495    fn test_lsystem_tree() {
496        let result = lsystem_tree(400.0, 500.0, 80.0, 0.5, 0.7, 4, 1.0);
497        assert!(!result.is_empty());
498        assert!(result.len() >= 3); // Tronco + al menos 2 ramas
499    }
500
501    #[test]
502    fn test_tusi_couple() {
503        let result = tusi_couple(400.0, 300.0, 100.0, 0.5);
504        assert!(result.len() >= 4); // large_circle, small_circle, point, trace_line
505    }
506
507    #[test]
508    fn test_pendulum_waves() {
509        let result = pendulum_waves(400.0, 100.0, 12, 100.0, 0.05, 0.5);
510        assert_eq!(result.len(), 12);
511    }
512
513    #[test]
514    fn test_wave_interference() {
515        let result = wave_interference(250.0, 300.0, 550.0, 300.0, 40.0, 1.0, 15, 0.5);
516        assert!(!result.is_empty());
517        // Debe tener puntos de onda + 2 fuentes
518        assert!(result.len() > 2);
519    }
520}