use crate::models::{Animation, PaletteCycle, Sprite};
use crate::renderer::render_sprite;
use image::RgbaImage;
use std::collections::HashMap;
pub fn apply_cycle_step(
original_palette: &HashMap<String, String>,
cycle: &PaletteCycle,
step: usize,
) -> HashMap<String, String> {
let mut result = original_palette.clone();
let tokens = &cycle.tokens;
let len = tokens.len();
if len == 0 {
return result;
}
let original_colors: Vec<Option<String>> =
tokens.iter().map(|token| original_palette.get(token).cloned()).collect();
for (i, token) in tokens.iter().enumerate() {
let source_idx = (i + step) % len;
if let Some(ref color) = original_colors[source_idx] {
result.insert(token.clone(), color.clone());
}
}
result
}
pub fn apply_cycles_step(
original_palette: &HashMap<String, String>,
cycles: &[PaletteCycle],
step: usize,
) -> HashMap<String, String> {
let mut result = original_palette.clone();
for cycle in cycles {
let cycle_len = cycle.cycle_length();
if cycle_len > 0 {
let cycle_step = step % cycle_len;
result = apply_cycle_step(&result, cycle, cycle_step);
}
}
result
}
pub fn calculate_total_frames(cycles: &[PaletteCycle]) -> usize {
if cycles.is_empty() {
return 1;
}
let mut total = 1usize;
for cycle in cycles {
let len = cycle.cycle_length();
if len > 0 {
total = lcm(total, len);
}
}
total.max(1)
}
fn lcm(a: usize, b: usize) -> usize {
if a == 0 || b == 0 {
return 0;
}
(a * b) / gcd(a, b)
}
fn gcd(mut a: usize, mut b: usize) -> usize {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
pub fn generate_cycle_frames(
sprite: &Sprite,
base_palette: &HashMap<String, String>,
animation: &Animation,
) -> (Vec<RgbaImage>, Vec<String>) {
let mut frames = Vec::new();
let mut all_warnings = Vec::new();
let cycles = animation.palette_cycles();
let total_frames = calculate_total_frames(cycles);
for step in 0..total_frames {
let cycled_palette = apply_cycles_step(base_palette, cycles, step);
let (image, warnings) = render_sprite(sprite, &cycled_palette);
for w in warnings {
let msg = w.message.clone();
if !all_warnings.contains(&msg) {
all_warnings.push(msg);
}
}
frames.push(image);
}
(frames, all_warnings)
}
pub fn get_cycle_duration(animation: &Animation) -> u32 {
for cycle in animation.palette_cycles() {
if let Some(duration) = cycle.duration {
return duration;
}
}
animation.duration_ms()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Duration, PaletteRef};
fn make_palette(colors: &[(&str, &str)]) -> HashMap<String, String> {
colors.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn test_apply_cycle_step_rotation() {
let palette = make_palette(&[("{w1}", "#001"), ("{w2}", "#002"), ("{w3}", "#003")]);
let cycle = PaletteCycle {
tokens: vec!["{w1}".to_string(), "{w2}".to_string(), "{w3}".to_string()],
duration: None,
};
let result0 = apply_cycle_step(&palette, &cycle, 0);
assert_eq!(result0.get("{w1}"), Some(&"#001".to_string()));
assert_eq!(result0.get("{w2}"), Some(&"#002".to_string()));
assert_eq!(result0.get("{w3}"), Some(&"#003".to_string()));
let result1 = apply_cycle_step(&palette, &cycle, 1);
assert_eq!(result1.get("{w1}"), Some(&"#002".to_string()));
assert_eq!(result1.get("{w2}"), Some(&"#003".to_string()));
assert_eq!(result1.get("{w3}"), Some(&"#001".to_string()));
let result2 = apply_cycle_step(&palette, &cycle, 2);
assert_eq!(result2.get("{w1}"), Some(&"#003".to_string()));
assert_eq!(result2.get("{w2}"), Some(&"#001".to_string()));
assert_eq!(result2.get("{w3}"), Some(&"#002".to_string()));
let result3 = apply_cycle_step(&palette, &cycle, 3);
assert_eq!(result3.get("{w1}"), Some(&"#001".to_string()));
assert_eq!(result3.get("{w2}"), Some(&"#002".to_string()));
assert_eq!(result3.get("{w3}"), Some(&"#003".to_string()));
}
#[test]
fn test_apply_cycle_preserves_other_colors() {
let palette = make_palette(&[("{w1}", "#001"), ("{w2}", "#002"), ("{static}", "#999")]);
let cycle =
PaletteCycle { tokens: vec!["{w1}".to_string(), "{w2}".to_string()], duration: None };
let result = apply_cycle_step(&palette, &cycle, 1);
assert_eq!(result.get("{w1}"), Some(&"#002".to_string()));
assert_eq!(result.get("{w2}"), Some(&"#001".to_string()));
assert_eq!(result.get("{static}"), Some(&"#999".to_string()));
}
#[test]
fn test_apply_cycle_empty_cycle() {
let palette = make_palette(&[("{a}", "#111")]);
let cycle = PaletteCycle { tokens: vec![], duration: None };
let result = apply_cycle_step(&palette, &cycle, 5);
assert_eq!(result.get("{a}"), Some(&"#111".to_string()));
}
#[test]
fn test_apply_cycles_multiple() {
let palette = make_palette(&[
("{w1}", "#001"),
("{w2}", "#002"),
("{f1}", "#F01"),
("{f2}", "#F02"),
("{f3}", "#F03"),
]);
let cycles = vec![
PaletteCycle {
tokens: vec!["{w1}".to_string(), "{w2}".to_string()],
duration: Some(200),
},
PaletteCycle {
tokens: vec!["{f1}".to_string(), "{f2}".to_string(), "{f3}".to_string()],
duration: Some(100),
},
];
let result = apply_cycles_step(&palette, &cycles, 1);
assert_eq!(result.get("{w1}"), Some(&"#002".to_string()));
assert_eq!(result.get("{w2}"), Some(&"#001".to_string()));
assert_eq!(result.get("{f1}"), Some(&"#F02".to_string()));
assert_eq!(result.get("{f2}"), Some(&"#F03".to_string()));
assert_eq!(result.get("{f3}"), Some(&"#F01".to_string()));
}
#[test]
fn test_calculate_total_frames() {
let cycles1 = vec![PaletteCycle {
tokens: vec!["{a}".to_string(), "{b}".to_string(), "{c}".to_string()],
duration: None,
}];
assert_eq!(calculate_total_frames(&cycles1), 3);
let cycles2 = vec![
PaletteCycle { tokens: vec!["{a}".to_string(), "{b}".to_string()], duration: None },
PaletteCycle {
tokens: vec!["{c}".to_string(), "{d}".to_string(), "{e}".to_string()],
duration: None,
},
];
assert_eq!(calculate_total_frames(&cycles2), 6);
let cycles_empty: Vec<PaletteCycle> = vec![];
assert_eq!(calculate_total_frames(&cycles_empty), 1);
}
#[test]
fn test_lcm_gcd() {
assert_eq!(gcd(12, 8), 4);
assert_eq!(gcd(7, 3), 1);
assert_eq!(lcm(3, 4), 12);
assert_eq!(lcm(6, 8), 24);
}
#[test]
fn test_generate_cycle_frames_basic() {
let sprite = Sprite {
name: "water".to_string(),
size: Some([2, 1]),
palette: PaletteRef::Inline(HashMap::new()),
grid: vec!["{w1}{w2}".to_string()],
metadata: None,
..Default::default()
};
let palette = make_palette(&[("{w1}", "#0000FF"), ("{w2}", "#00FFFF")]);
let anim = Animation {
name: "water_cycle".to_string(),
frames: vec!["water".to_string()],
duration: Some(Duration::Milliseconds(100)),
r#loop: Some(true),
palette_cycle: Some(vec![PaletteCycle {
tokens: vec!["{w1}".to_string(), "{w2}".to_string()],
duration: Some(150),
}]),
tags: None,
frame_metadata: None,
attachments: None,
..Default::default()
};
let (frames, warnings) = generate_cycle_frames(&sprite, &palette, &anim);
assert_eq!(frames.len(), 2);
assert!(warnings.is_empty());
let frame0 = &frames[0];
assert_eq!(frame0.get_pixel(0, 0).0, [0, 0, 255, 255]); assert_eq!(frame0.get_pixel(1, 0).0, [0, 255, 255, 255]);
let frame1 = &frames[1];
assert_eq!(frame1.get_pixel(0, 0).0, [0, 255, 255, 255]); assert_eq!(frame1.get_pixel(1, 0).0, [0, 0, 255, 255]); }
#[test]
fn test_get_cycle_duration() {
let anim1 = Animation {
name: "test".to_string(),
frames: vec!["f".to_string()],
duration: Some(Duration::Milliseconds(100)),
r#loop: None,
palette_cycle: Some(vec![PaletteCycle {
tokens: vec!["{a}".to_string()],
duration: Some(150),
}]),
tags: None,
frame_metadata: None,
attachments: None,
..Default::default()
};
assert_eq!(get_cycle_duration(&anim1), 150);
let anim2 = Animation {
name: "test".to_string(),
frames: vec!["f".to_string()],
duration: Some(Duration::Milliseconds(100)),
r#loop: None,
palette_cycle: Some(vec![PaletteCycle {
tokens: vec!["{a}".to_string()],
duration: None,
}]),
tags: None,
frame_metadata: None,
attachments: None,
..Default::default()
};
assert_eq!(get_cycle_duration(&anim2), 100);
let anim3 = Animation {
name: "test".to_string(),
frames: vec!["f".to_string()],
duration: Some(Duration::Milliseconds(100)),
r#loop: None,
palette_cycle: None,
tags: None,
frame_metadata: None,
attachments: None,
..Default::default()
};
assert_eq!(get_cycle_duration(&anim3), 100);
}
}