crater/serde/threejs/
animation.rs

1use crate::csg::trajectory::Trajectories;
2use burn::prelude::*;
3use serde_json::json;
4use std::path::Path;
5
6/// Animation type determines coloring strategy and data source
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum AnimationType {
9    /// Use custom color/opacity pairs for each trajectory
10    CustomColors,
11    /// Derive colors from raycast results (hit/miss, region-based)
12    RaycastResults,
13    /// Use default uniform coloring scheme
14    Uniform,
15    /// Color by trajectory index with optional grouping
16    Indexed { group_size: Option<usize> },
17    /// Color by distance/time along trajectory
18    Gradient {
19        start_color: String,
20        end_color: String,
21    },
22}
23
24/// Configuration for trajectory animation generation
25#[derive(Debug, Clone)]
26pub struct TrajectoryAnimationConfig {
27    /// Duration of the animation in milliseconds
28    pub duration_ms: u32,
29    /// Frame rate for the animation
30    pub fps: u32,
31    /// Color scheme for trajectories (hex colors)
32    pub colors: Vec<String>,
33    /// Camera position for the 3D view
34    pub camera_position: [f32; 3],
35    /// Camera target (look-at point)
36    pub camera_target: [f32; 3],
37    /// Background color (hex)
38    pub background_color: String,
39    /// Size of trajectory dots
40    pub dot_size: f32,
41    /// Maximum number of trajectories to animate (for performance)
42    pub max_trajectories: Option<usize>,
43    /// Opacity for failed trajectories
44    pub failed_opacity: f32,
45    /// Animation type determining coloring strategy
46    pub animation_type: AnimationType,
47}
48
49/// Color source for trajectory animations
50#[derive(Clone)]
51pub enum ColorSource<'a, B: Backend, const N: usize> {
52    /// Custom color/opacity pairs
53    Custom(&'a [(String, f32)]),
54    /// Raycast result for hit/miss coloring
55    Raycast(&'a crate::analysis::prelude::RayCastResult<B, N>),
56    /// Use configuration colors uniformly
57    Config,
58}
59
60impl Default for TrajectoryAnimationConfig {
61    fn default() -> Self {
62        Self {
63            duration_ms: 3000,
64            fps: 60,
65            colors: vec![
66                "#ff6b6b".to_string(), // Red
67                "#4ecdc4".to_string(), // Teal
68                "#45b7d1".to_string(), // Blue
69                "#96ceb4".to_string(), // Green
70                "#feca57".to_string(), // Yellow
71                "#ff9ff3".to_string(), // Pink
72                "#54a0ff".to_string(), // Light blue
73            ],
74            camera_position: [10.0, 10.0, 10.0],
75            camera_target: [0.0, 0.0, 0.0],
76            background_color: "#1a1a1a".to_string(),
77            dot_size: 0.02,
78            max_trajectories: Some(100_000), // Limit for performance
79            failed_opacity: 0.3,
80            animation_type: AnimationType::Uniform,
81        }
82    }
83}
84
85impl AnimationType {
86    /// Create a gradient animation type with predefined color schemes
87    pub fn gradient_preset(preset: &str) -> Self {
88        match preset {
89            "fire" => Self::Gradient {
90                start_color: "#ff4757".to_string(),
91                end_color: "#ffa502".to_string(),
92            },
93            "ocean" => Self::Gradient {
94                start_color: "#3742fa".to_string(),
95                end_color: "#2ed573".to_string(),
96            },
97            "sunset" => Self::Gradient {
98                start_color: "#ff6b9d".to_string(),
99                end_color: "#ffa726".to_string(),
100            },
101            "forest" => Self::Gradient {
102                start_color: "#00d2d3".to_string(),
103                end_color: "#54a0ff".to_string(),
104            },
105            _ => Self::Gradient {
106                start_color: "#ff6b6b".to_string(),
107                end_color: "#4ecdc4".to_string(),
108            },
109        }
110    }
111}
112
113/// Generate trajectory colors based on the specified animation type and color source
114pub fn generate_trajectory_colors<B: Backend, const N: usize>(
115    trajectories: &Trajectories<B, N>,
116    color_source: ColorSource<'_, B, N>,
117    config: &TrajectoryAnimationConfig,
118) -> Result<Vec<(String, f32)>, Box<dyn std::error::Error>> {
119    if trajectories.is_empty() {
120        return Ok(Vec::new());
121    }
122
123    let first_step = &trajectories[0];
124    let trajectory_count = first_step.shape().dims::<2>()[0];
125
126    match (color_source, &config.animation_type) {
127        (ColorSource::Custom(colors), AnimationType::CustomColors) => {
128            if colors.len() != trajectory_count {
129                return Err(format!(
130                    "Custom color count ({}) does not match trajectory count ({})",
131                    colors.len(),
132                    trajectory_count
133                )
134                .into());
135            }
136            Ok(colors.to_vec())
137        }
138        (ColorSource::Raycast(raycast_result), AnimationType::RaycastResults) => {
139            compute_raycast_colors(raycast_result, config)
140        }
141        (_, AnimationType::Uniform) => {
142            let mut colors = Vec::with_capacity(trajectory_count);
143            for i in 0..trajectory_count {
144                let color = config.colors[i % config.colors.len()].clone();
145                colors.push((color, 1.0));
146            }
147            Ok(colors)
148        }
149        (_, AnimationType::Indexed { group_size }) => {
150            let mut colors = Vec::with_capacity(trajectory_count);
151            let group_size = group_size.unwrap_or(config.colors.len());
152            for i in 0..trajectory_count {
153                let color_idx = (i / group_size) % config.colors.len();
154                let color = config.colors[color_idx].clone();
155                colors.push((color, 1.0));
156            }
157            Ok(colors)
158        }
159        (
160            _,
161            AnimationType::Gradient {
162                start_color,
163                end_color,
164            },
165        ) => generate_gradient_colors(trajectory_count, start_color, end_color),
166        _ => Err("Incompatible color source and animation type combination".into()),
167    }
168}
169
170/// Generate gradient colors between two hex colors
171fn generate_gradient_colors(
172    count: usize,
173    start_hex: &str,
174    end_hex: &str,
175) -> Result<Vec<(String, f32)>, Box<dyn std::error::Error>> {
176    let start_rgb = hex_to_rgb(start_hex)?;
177    let end_rgb = hex_to_rgb(end_hex)?;
178
179    let mut colors = Vec::with_capacity(count);
180    for i in 0..count {
181        let t = if count > 1 {
182            i as f32 / (count - 1) as f32
183        } else {
184            0.0
185        };
186        let r = (start_rgb[0] as f32 * (1.0 - t) + end_rgb[0] as f32 * t) as u8;
187        let g = (start_rgb[1] as f32 * (1.0 - t) + end_rgb[1] as f32 * t) as u8;
188        let b = (start_rgb[2] as f32 * (1.0 - t) + end_rgb[2] as f32 * t) as u8;
189        let hex = format!("#{:02x}{:02x}{:02x}", r, g, b);
190        colors.push((hex, 1.0));
191    }
192    Ok(colors)
193}
194
195/// Convert hex color to RGB
196fn hex_to_rgb(hex: &str) -> Result<[u8; 3], Box<dyn std::error::Error>> {
197    let hex = hex.trim_start_matches('#');
198    if hex.len() != 6 {
199        return Err("Invalid hex color format".into());
200    }
201
202    let r = u8::from_str_radix(&hex[0..2], 16)?;
203    let g = u8::from_str_radix(&hex[2..4], 16)?;
204    let b = u8::from_str_radix(&hex[4..6], 16)?;
205    Ok([r, g, b])
206}
207
208/// Core unified trajectory animation generation function
209pub fn generate_trajectory_animation<B: Backend, const N: usize>(
210    trajectories: &Trajectories<B, N>,
211    color_source: ColorSource<'_, B, N>,
212    config: &TrajectoryAnimationConfig,
213    output_path: &Path,
214) -> Result<(), Box<dyn std::error::Error>> {
215    if trajectories.is_empty() {
216        return Err("No trajectories to animate".into());
217    }
218
219    // Generate colors based on animation type and source
220    let colors = generate_trajectory_colors(trajectories, color_source, config)?;
221
222    // Generate trajectory data for rendering
223    let trajectories_data = convert_trajectories_to_json::<B, N>(trajectories, &colors, config)?;
224
225    // Generate and write HTML
226    let html_content = generate_threejs_html(&trajectories_data, config)?;
227    std::fs::write(output_path, html_content)?;
228
229    Ok(())
230}
231
232/// Convert trajectories to JSON format for Three.js rendering
233fn convert_trajectories_to_json<B: Backend, const N: usize>(
234    trajectories: &Trajectories<B, N>,
235    colors: &[(String, f32)],
236    config: &TrajectoryAnimationConfig,
237) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
238    let first_step = &trajectories[0];
239    let trajectory_count = first_step.shape().dims::<2>()[0];
240    let actual_trajectory_count = config
241        .max_trajectories
242        .unwrap_or(trajectory_count)
243        .min(trajectory_count);
244
245    // Pre-process all trajectory data efficiently
246    let step_data_vecs: Vec<Vec<f32>> = trajectories
247        .iter()
248        .map(|step| {
249            let step_data = step.to_data();
250            step_data.iter::<f32>().collect()
251        })
252        .collect();
253
254    // Build trajectory JSON data
255    let mut trajectories_data = Vec::with_capacity(actual_trajectory_count);
256
257    for traj_idx in 0..actual_trajectory_count {
258        let mut path = Vec::with_capacity(trajectories.len());
259
260        for step_vec in &step_data_vecs {
261            let position = if N == 2 {
262                [step_vec[traj_idx * N], step_vec[traj_idx * N + 1], 0.0]
263            } else {
264                [
265                    step_vec[traj_idx * N],
266                    step_vec[traj_idx * N + 1],
267                    step_vec[traj_idx * N + 2],
268                ]
269            };
270
271            // Validate coordinates
272            if position.iter().all(|&x| x.is_finite() && x.abs() < 1000.0) {
273                path.push(position);
274            }
275        }
276
277        if !path.is_empty() {
278            let (color, opacity) = &colors[traj_idx.min(colors.len() - 1)];
279
280            trajectories_data.push(json!({
281                "path": path,
282                "color": color,
283                "opacity": opacity
284            }));
285        }
286    }
287
288    Ok(trajectories_data)
289}
290
291/// Generate a Three.js-based animation from trajectories with custom coloring (legacy)
292pub fn generate_trajectory_animation_with_colors<B: Backend, const N: usize>(
293    trajectories: &Trajectories<B, N>,
294    colors: &[(String, f32)], // (color_hex, opacity) pairs
295    config: &TrajectoryAnimationConfig,
296    output_path: &Path,
297) -> Result<(), Box<dyn std::error::Error>> {
298    let mut custom_config = config.clone();
299    custom_config.animation_type = AnimationType::CustomColors;
300
301    generate_trajectory_animation(
302        trajectories,
303        ColorSource::<B, N>::Custom(colors),
304        &custom_config,
305        output_path,
306    )
307}
308
309/// Compute colors from raycast results according to hit/miss status and region indices
310pub fn compute_raycast_colors<B: Backend, const N: usize>(
311    raycast_result: &crate::analysis::prelude::RayCastResult<B, N>,
312    config: &TrajectoryAnimationConfig,
313) -> Result<Vec<(String, f32)>, Box<dyn std::error::Error>> {
314    use crate::analysis::prelude::RAY_FAIL_VALUE;
315
316    let ray_count = raycast_result.rays().ray_count();
317    let distances = raycast_result.distances().to_data();
318    let region_indices = raycast_result.region_indices().to_data();
319    let extensions = raycast_result.extensions().to_data();
320
321    let distances_vec: Vec<f32> = distances.iter::<f32>().collect();
322    let region_indices_vec: Vec<f32> = region_indices.iter::<f32>().collect();
323    let extensions_vec: Vec<f32> = extensions.iter::<f32>().collect();
324
325    let mut colors = Vec::with_capacity(ray_count);
326
327    for ray_idx in 0..ray_count {
328        let distance = distances_vec[ray_idx];
329        let region_idx = region_indices_vec[ray_idx] as usize;
330
331        // Check if extension coordinates are valid (not RAY_FAIL_VALUE)
332        let extension_start = ray_idx * N;
333        let extension_is_valid = (0..N).all(|i| {
334            let coord = extensions_vec[extension_start + i];
335            coord.is_finite() && coord.abs() < 1000.0 && coord != RAY_FAIL_VALUE
336        });
337
338        let is_hit = extension_is_valid
339            && distance.is_finite()
340            && distance != RAY_FAIL_VALUE
341            && distance < 1000.0;
342
343        let (color, opacity) = if is_hit {
344            // Use region-based coloring for hits
345            let color = config.colors[region_idx % config.colors.len()].clone();
346            (color, 1.0)
347        } else {
348            // Gray color for failed rays with reduced opacity
349            ("#666666".to_string(), config.failed_opacity)
350        };
351
352        colors.push((color, opacity));
353    }
354
355    Ok(colors)
356}
357
358/// Generate a Three.js-based animation from trajectories with raycast context for coloring (legacy)
359pub fn generate_trajectory_animation_with_raycast<B: Backend, const N: usize>(
360    trajectories: &Trajectories<B, N>,
361    raycast_result: &crate::analysis::prelude::RayCastResult<B, N>,
362    config: &TrajectoryAnimationConfig,
363    output_path: &Path,
364) -> Result<(), Box<dyn std::error::Error>> {
365    let mut raycast_config = config.clone();
366    raycast_config.animation_type = AnimationType::RaycastResults;
367
368    generate_trajectory_animation(
369        trajectories,
370        ColorSource::<B, N>::Raycast(raycast_result),
371        &raycast_config,
372        output_path,
373    )
374}
375
376/// Generate a Three.js-based animation from trajectories with uniform coloring (legacy method)
377pub fn generate_trajectory_animation_uniform<B: Backend, const N: usize>(
378    trajectories: &Trajectories<B, N>,
379    config: &TrajectoryAnimationConfig,
380    output_path: &Path,
381) -> Result<(), Box<dyn std::error::Error>> {
382    let mut uniform_config = config.clone();
383    uniform_config.animation_type = AnimationType::Uniform;
384
385    generate_trajectory_animation(
386        trajectories,
387        ColorSource::<B, N>::Config,
388        &uniform_config,
389        output_path,
390    )
391}
392
393fn generate_threejs_html(
394    trajectories_data: &[serde_json::Value],
395    config: &TrajectoryAnimationConfig,
396) -> Result<String, Box<dyn std::error::Error>> {
397    let trajectories_json = serde_json::to_string(trajectories_data)?;
398
399    // Load HTML template and replace placeholders
400    let template = include_str!("animation_template.html");
401    let html = template
402        .replace("BACKGROUND_COLOR_PLACEHOLDER", &config.background_color)
403        .replace(
404            "TRAJECTORY_COUNT_PLACEHOLDER",
405            &trajectories_data.len().to_string(),
406        )
407        .replace("DURATION_MS_PLACEHOLDER", &config.duration_ms.to_string())
408        .replace("FPS_PLACEHOLDER", &config.fps.to_string())
409        .replace(
410            "CAMERA_POSITION_PLACEHOLDER",
411            &format!("{:?}", config.camera_position),
412        )
413        .replace(
414            "CAMERA_TARGET_PLACEHOLDER",
415            &format!("{:?}", config.camera_target),
416        )
417        .replace("DOT_SIZE_PLACEHOLDER", &config.dot_size.to_string())
418        .replace("TRAJECTORIES_JSON_PLACEHOLDER", &trajectories_json);
419
420    Ok(html)
421}
422
423/// Convenience function to create an animation with default uniform coloring
424pub fn create_default_trajectory_animation<B: Backend, const N: usize>(
425    trajectories: &Trajectories<B, N>,
426    output_path: &Path,
427) -> Result<(), Box<dyn std::error::Error>> {
428    let config = TrajectoryAnimationConfig::default();
429    generate_trajectory_animation(
430        trajectories,
431        ColorSource::<B, N>::Config,
432        &config,
433        output_path,
434    )
435}
436
437/// Convenience function to create a gradient animation
438pub fn create_gradient_animation<B: Backend, const N: usize>(
439    trajectories: &Trajectories<B, N>,
440    gradient_preset: &str,
441    output_path: &Path,
442) -> Result<(), Box<dyn std::error::Error>> {
443    let config = TrajectoryAnimationConfig {
444        animation_type: AnimationType::gradient_preset(gradient_preset),
445        ..Default::default()
446    };
447
448    generate_trajectory_animation(
449        trajectories,
450        ColorSource::<B, N>::Config,
451        &config,
452        output_path,
453    )
454}
455
456/// Convenience function to create a raycast-colored animation
457pub fn create_raycast_animation<B: Backend, const N: usize>(
458    trajectories: &Trajectories<B, N>,
459    raycast_result: &crate::analysis::prelude::RayCastResult<B, N>,
460    output_path: &Path,
461) -> Result<(), Box<dyn std::error::Error>> {
462    let config = TrajectoryAnimationConfig {
463        animation_type: AnimationType::RaycastResults,
464        ..Default::default()
465    };
466
467    generate_trajectory_animation(
468        trajectories,
469        ColorSource::<B, N>::Raycast(raycast_result),
470        &config,
471        output_path,
472    )
473}
474
475/// Convenience function to create an indexed/grouped animation
476pub fn create_indexed_animation<B: Backend, const N: usize>(
477    trajectories: &Trajectories<B, N>,
478    group_size: Option<usize>,
479    output_path: &Path,
480) -> Result<(), Box<dyn std::error::Error>> {
481    let config = TrajectoryAnimationConfig {
482        animation_type: AnimationType::Indexed { group_size },
483        ..Default::default()
484    };
485
486    generate_trajectory_animation(
487        trajectories,
488        ColorSource::<B, N>::Config,
489        &config,
490        output_path,
491    )
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_animation_config_default() {
500        let config = TrajectoryAnimationConfig::default();
501        assert_eq!(config.duration_ms, 3000);
502        assert_eq!(config.fps, 60);
503        assert!(!config.colors.is_empty());
504        assert_eq!(config.animation_type, AnimationType::Uniform);
505    }
506
507    #[test]
508    fn test_animation_type_gradient_presets() {
509        let fire = AnimationType::gradient_preset("fire");
510        match fire {
511            AnimationType::Gradient {
512                start_color,
513                end_color,
514            } => {
515                assert_eq!(start_color, "#ff4757");
516                assert_eq!(end_color, "#ffa502");
517            }
518            _ => panic!("Expected gradient type"),
519        }
520
521        let unknown = AnimationType::gradient_preset("unknown");
522        match unknown {
523            AnimationType::Gradient {
524                start_color,
525                end_color,
526            } => {
527                assert_eq!(start_color, "#ff6b6b");
528                assert_eq!(end_color, "#4ecdc4");
529            }
530            _ => panic!("Expected gradient type"),
531        }
532    }
533
534    #[test]
535    fn test_hex_to_rgb() {
536        assert_eq!(hex_to_rgb("#ff0000").unwrap(), [255, 0, 0]);
537        assert_eq!(hex_to_rgb("00ff00").unwrap(), [0, 255, 0]);
538        assert_eq!(hex_to_rgb("#0000ff").unwrap(), [0, 0, 255]);
539        assert!(hex_to_rgb("invalid").is_err());
540    }
541
542    #[test]
543    fn test_generate_gradient_colors() {
544        let colors = generate_gradient_colors(3, "#ff0000", "#0000ff").unwrap();
545        assert_eq!(colors.len(), 3);
546        assert_eq!(colors[0].0, "#ff0000");
547        assert_eq!(colors[2].0, "#0000ff");
548        assert_eq!(colors[0].1, 1.0); // opacity
549    }
550}