pub mod build;
pub mod cli;
pub mod css;
pub mod exports;
use image::RgbaImage;
use pixelsrc::models::{Animation, Composition, PaletteRef, TtpObject};
use pixelsrc::output::scale_image;
use pixelsrc::palette_cycle::calculate_total_frames;
use pixelsrc::parser::parse_stream;
use pixelsrc::registry::{PaletteRegistry, SpriteRegistry};
use pixelsrc::renderer::render_resolved;
use pixelsrc::spritesheet::render_spritesheet;
use pixelsrc::validate::{Severity, Validator};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::io::Cursor;
pub mod sprites;
mod imports;
#[derive(Debug, Clone)]
pub struct RenderInfo {
pub width: u32,
pub height: u32,
pub frame_count: usize,
pub palette_name: Option<String>,
pub color_count: usize,
pub sha256: String,
}
#[derive(Debug, Clone)]
pub struct SpritesheetInfo {
pub width: u32,
pub height: u32,
pub frame_count: usize,
pub frame_width: u32,
pub frame_height: u32,
pub cols: Option<u32>,
pub sha256: String,
}
pub fn parse_content(jsonl: &str) -> (PaletteRegistry, SpriteRegistry, HashMap<String, Animation>) {
let cursor = Cursor::new(jsonl);
let parse_result = parse_stream(cursor);
let mut palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let mut animations: HashMap<String, Animation> = HashMap::new();
for obj in parse_result.objects {
match obj {
TtpObject::Palette(p) => palette_registry.register(p),
TtpObject::Sprite(s) => sprite_registry.register_sprite(s),
TtpObject::Variant(v) => sprite_registry.register_variant(v),
TtpObject::Animation(a) => {
animations.insert(a.name.clone(), a);
}
TtpObject::Composition(_) => {}
TtpObject::Particle(_) => {}
TtpObject::Transform(_) => {}
}
}
(palette_registry, sprite_registry, animations)
}
pub fn parse_compositions(
jsonl: &str,
) -> (PaletteRegistry, SpriteRegistry, HashMap<String, Composition>) {
let cursor = Cursor::new(jsonl);
let parse_result = parse_stream(cursor);
let mut palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let mut compositions: HashMap<String, Composition> = HashMap::new();
for obj in parse_result.objects {
match obj {
TtpObject::Palette(p) => palette_registry.register(p),
TtpObject::Sprite(s) => sprite_registry.register_sprite(s),
TtpObject::Variant(v) => sprite_registry.register_variant(v),
TtpObject::Composition(c) => {
compositions.insert(c.name.clone(), c);
}
TtpObject::Animation(_) => {}
TtpObject::Particle(_) => {}
TtpObject::Transform(_) => {}
}
}
(palette_registry, sprite_registry, compositions)
}
#[derive(Debug, Clone)]
pub struct CompositionInfo {
pub name: String,
pub width: Option<u32>,
pub height: Option<u32>,
pub layer_count: usize,
pub blend_modes: Vec<Option<String>>,
pub sprite_keys: Vec<String>,
}
pub fn capture_composition_info(jsonl: &str, composition_name: &str) -> CompositionInfo {
let (_, _, compositions) = parse_compositions(jsonl);
let comp = compositions
.get(composition_name)
.unwrap_or_else(|| panic!("Composition '{composition_name}' not found"));
let (width, height) = match comp.size {
Some([w, h]) => (Some(w), Some(h)),
None => (None, None),
};
let blend_modes: Vec<Option<String>> =
comp.layers.iter().map(|layer| layer.blend.clone()).collect();
let sprite_keys: Vec<String> = comp.sprites.keys().cloned().collect();
CompositionInfo {
name: comp.name.clone(),
width,
height,
layer_count: comp.layers.len(),
blend_modes,
sprite_keys,
}
}
pub fn assert_layer_blend_mode(
jsonl: &str,
composition_name: &str,
layer_index: usize,
expected_blend: Option<&str>,
) {
let info = capture_composition_info(jsonl, composition_name);
assert!(
layer_index < info.layer_count,
"Layer index {} out of bounds for composition '{}' with {} layers",
layer_index,
composition_name,
info.layer_count
);
let actual_blend = info.blend_modes[layer_index].as_deref();
assert_eq!(
actual_blend, expected_blend,
"Blend mode mismatch for layer {} of composition '{}': expected {:?}, got {:?}",
layer_index, composition_name, expected_blend, actual_blend
);
}
pub fn assert_composition_sprites_resolve(jsonl: &str, composition_name: &str) {
let (palette_registry, sprite_registry, compositions) = parse_compositions(jsonl);
let comp = compositions
.get(composition_name)
.unwrap_or_else(|| panic!("Composition '{composition_name}' not found"));
for (key, sprite_name_opt) in &comp.sprites {
if let Some(sprite_name) = sprite_name_opt {
sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|e| {
panic!(
"Failed to resolve sprite '{sprite_name}' (key '{key}') in composition '{composition_name}': {e}"
)
});
}
}
}
pub fn capture_render_info(jsonl: &str, sprite_name: &str) -> RenderInfo {
let (palette_registry, sprite_registry, _) = parse_content(jsonl);
let resolved = sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Failed to resolve sprite '{sprite_name}'"));
let (image, _warnings) = render_resolved(&resolved);
let mut png_bytes = Vec::new();
image
.write_to(&mut Cursor::new(&mut png_bytes), image::ImageOutputFormat::Png)
.expect("Failed to encode PNG");
let mut hasher = Sha256::new();
hasher.update(&png_bytes);
let hash = format!("{:x}", hasher.finalize());
let original_palette_name = if let Some(orig_sprite) = sprite_registry.get_sprite(sprite_name) {
match &orig_sprite.palette {
PaletteRef::Named(name) => Some(name.clone()),
PaletteRef::Inline(_) => None,
}
} else {
None
};
RenderInfo {
width: image.width(),
height: image.height(),
frame_count: 1,
palette_name: original_palette_name,
color_count: resolved.palette.len(),
sha256: hash,
}
}
pub fn capture_scaled_render_info(jsonl: &str, sprite_name: &str, scale_factor: u8) -> RenderInfo {
let (palette_registry, sprite_registry, _) = parse_content(jsonl);
let resolved = sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Failed to resolve sprite '{sprite_name}'"));
let (image, _warnings) = render_resolved(&resolved);
let scaled = scale_image(image, scale_factor);
let mut png_bytes = Vec::new();
scaled
.write_to(&mut Cursor::new(&mut png_bytes), image::ImageOutputFormat::Png)
.expect("Failed to encode scaled PNG");
let mut hasher = Sha256::new();
hasher.update(&png_bytes);
let hash = format!("{:x}", hasher.finalize());
let original_palette_name = if let Some(orig_sprite) = sprite_registry.get_sprite(sprite_name) {
match &orig_sprite.palette {
PaletteRef::Named(name) => Some(name.clone()),
PaletteRef::Inline(_) => None,
}
} else {
None
};
RenderInfo {
width: scaled.width(),
height: scaled.height(),
frame_count: 1,
palette_name: original_palette_name,
color_count: resolved.palette.len(),
sha256: hash,
}
}
pub fn assert_dimensions(jsonl: &str, sprite_name: &str, width: u32, height: u32) {
let info = capture_render_info(jsonl, sprite_name);
assert_eq!(
info.width, width,
"Width mismatch for sprite '{}': expected {}, got {}",
sprite_name, width, info.width
);
assert_eq!(
info.height, height,
"Height mismatch for sprite '{}': expected {}, got {}",
sprite_name, height, info.height
);
}
pub fn assert_output_hash(jsonl: &str, sprite_name: &str, expected_sha256: &str) {
let info = capture_render_info(jsonl, sprite_name);
if info.sha256 == expected_sha256 {
return; }
eprintln!("Note: Hash mismatch for sprite '{sprite_name}' (platform PNG difference likely)");
eprintln!(" Expected: {expected_sha256}");
eprintln!(" Got: {}", info.sha256);
eprintln!(
" Fallback: verifying dimensions ({}x{}) and {} colors",
info.width, info.height, info.color_count
);
assert!(
info.width > 0 && info.height > 0,
"Sprite '{}' rendered with invalid dimensions: {}x{}",
sprite_name,
info.width,
info.height
);
}
pub fn assert_frame_count(jsonl: &str, animation_name: &str, expected_count: usize) {
let (_, _, animations) = parse_content(jsonl);
let animation = animations
.get(animation_name)
.unwrap_or_else(|| panic!("Animation '{animation_name}' not found"));
assert_eq!(
animation.frames.len(),
expected_count,
"Frame count mismatch for animation '{}': expected {}, got {}",
animation_name,
expected_count,
animation.frames.len()
);
}
pub fn assert_validates(jsonl: &str, should_pass: bool) {
let mut validator = Validator::new();
for (line_idx, line) in jsonl.lines().enumerate() {
validator.validate_line(line_idx + 1, line);
}
let has_errors = validator.has_errors();
if should_pass {
assert!(
!has_errors,
"Validation should pass but has {} error(s):\n{}",
validator.error_count(),
format_validation_issues(&validator)
);
} else {
assert!(
has_errors,
"Validation should fail but passed with {} warning(s)",
validator.warning_count()
);
}
}
fn format_validation_issues(validator: &Validator) -> String {
validator
.issues()
.iter()
.filter(|i| matches!(i.severity, Severity::Error))
.map(|issue| format!(" Line {}: [{}] {}", issue.line, issue.issue_type, issue.message))
.collect::<Vec<_>>()
.join("\n")
}
pub fn assert_color_count(jsonl: &str, sprite_name: &str, expected_count: usize) {
let info = capture_render_info(jsonl, sprite_name);
assert_eq!(
info.color_count, expected_count,
"Color count mismatch for sprite '{}': expected {}, got {}",
sprite_name, expected_count, info.color_count
);
}
pub fn assert_uses_palette(jsonl: &str, sprite_name: &str, palette_name: &str) {
let info = capture_render_info(jsonl, sprite_name);
assert_eq!(
info.palette_name.as_deref(),
Some(palette_name),
"Sprite '{}' expected to use palette '{}', but uses {:?}",
sprite_name,
palette_name,
info.palette_name
);
}
fn render_animation_frames(
animation: &Animation,
sprite_registry: &SpriteRegistry,
palette_registry: &PaletteRegistry,
) -> Vec<RgbaImage> {
animation
.frames
.iter()
.map(|frame_name| {
let resolved = sprite_registry
.resolve(frame_name, palette_registry, false)
.unwrap_or_else(|_| panic!("Failed to resolve frame sprite '{frame_name}'"));
let (image, _warnings) = render_resolved(&resolved);
image
})
.collect()
}
pub fn capture_spritesheet_info(
jsonl: &str,
animation_name: &str,
cols: Option<u32>,
) -> SpritesheetInfo {
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
let animation = animations
.get(animation_name)
.unwrap_or_else(|| panic!("Animation '{animation_name}' not found"));
let frame_images = render_animation_frames(animation, &sprite_registry, &palette_registry);
let frame_width = frame_images.iter().map(|f| f.width()).max().unwrap_or(1);
let frame_height = frame_images.iter().map(|f| f.height()).max().unwrap_or(1);
let sheet = render_spritesheet(&frame_images, cols);
let mut png_bytes = Vec::new();
sheet
.write_to(&mut Cursor::new(&mut png_bytes), image::ImageOutputFormat::Png)
.expect("Failed to encode spritesheet PNG");
let mut hasher = Sha256::new();
hasher.update(&png_bytes);
let hash = format!("{:x}", hasher.finalize());
SpritesheetInfo {
width: sheet.width(),
height: sheet.height(),
frame_count: animation.frames.len(),
frame_width,
frame_height,
cols,
sha256: hash,
}
}
pub fn assert_spritesheet_dimensions(
jsonl: &str,
animation_name: &str,
cols: Option<u32>,
expected_width: u32,
expected_height: u32,
) {
let info = capture_spritesheet_info(jsonl, animation_name, cols);
assert_eq!(
info.width, expected_width,
"Spritesheet width mismatch for animation '{}': expected {}, got {}",
animation_name, expected_width, info.width
);
assert_eq!(
info.height, expected_height,
"Spritesheet height mismatch for animation '{}': expected {}, got {}",
animation_name, expected_height, info.height
);
}
pub fn assert_spritesheet_frame_size(
jsonl: &str,
animation_name: &str,
expected_frame_width: u32,
expected_frame_height: u32,
) {
let info = capture_spritesheet_info(jsonl, animation_name, None);
assert_eq!(
info.frame_width, expected_frame_width,
"Frame width mismatch for animation '{}': expected {}, got {}",
animation_name, expected_frame_width, info.frame_width
);
assert_eq!(
info.frame_height, expected_frame_height,
"Frame height mismatch for animation '{}': expected {}, got {}",
animation_name, expected_frame_height, info.frame_height
);
}
#[derive(Debug, Clone)]
pub struct GifInfo {
pub frame_count: usize,
pub frame_width: u32,
pub frame_height: u32,
pub loops: bool,
pub duration_ms: u32,
}
pub fn capture_gif_info(jsonl: &str, animation_name: &str) -> GifInfo {
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
let animation = animations
.get(animation_name)
.unwrap_or_else(|| panic!("Animation '{animation_name}' not found"));
let frame_images = render_animation_frames(animation, &sprite_registry, &palette_registry);
let frame_width = frame_images.iter().map(|f| f.width()).max().unwrap_or(1);
let frame_height = frame_images.iter().map(|f| f.height()).max().unwrap_or(1);
GifInfo {
frame_count: animation.frames.len(),
frame_width,
frame_height,
loops: animation.loops(),
duration_ms: animation.duration_ms(),
}
}
pub fn assert_gif_frame_count(jsonl: &str, animation_name: &str, expected_count: usize) {
let info = capture_gif_info(jsonl, animation_name);
assert_eq!(
info.frame_count, expected_count,
"GIF frame count mismatch for animation '{}': expected {}, got {}",
animation_name, expected_count, info.frame_count
);
}
pub fn assert_gif_frame_dimensions(
jsonl: &str,
animation_name: &str,
expected_width: u32,
expected_height: u32,
) {
let info = capture_gif_info(jsonl, animation_name);
assert_eq!(
info.frame_width, expected_width,
"GIF frame width mismatch for animation '{}': expected {}, got {}",
animation_name, expected_width, info.frame_width
);
assert_eq!(
info.frame_height, expected_height,
"GIF frame height mismatch for animation '{}': expected {}, got {}",
animation_name, expected_height, info.frame_height
);
}
#[derive(Debug, Clone)]
pub struct PaletteCycleInfo {
pub cycle_count: usize,
pub total_frames: usize,
pub cycle_lengths: Vec<usize>,
pub cycle_durations: Vec<Option<u32>>,
pub cycle_tokens: Vec<Vec<String>>,
}
pub fn capture_palette_cycle_info(jsonl: &str, animation_name: &str) -> PaletteCycleInfo {
let (_, _, animations) = parse_content(jsonl);
let animation = animations
.get(animation_name)
.unwrap_or_else(|| panic!("Animation '{animation_name}' not found"));
let cycles = animation.palette_cycles();
let total_frames = calculate_total_frames(cycles);
PaletteCycleInfo {
cycle_count: cycles.len(),
total_frames,
cycle_lengths: cycles.iter().map(|c| c.cycle_length()).collect(),
cycle_durations: cycles.iter().map(|c| c.duration).collect(),
cycle_tokens: cycles.iter().map(|c| c.tokens.clone()).collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_capture_render_info_minimal() {
let jsonl = r##"{"type": "sprite", "name": "dot", "palette": {"{_}": "#00000000", "{x}": "#FF0000"}, "grid": ["{x}"]}"##;
let info = capture_render_info(jsonl, "dot");
assert_eq!(info.width, 1);
assert_eq!(info.height, 1);
assert_eq!(info.frame_count, 1);
assert_eq!(info.color_count, 2);
assert!(!info.sha256.is_empty());
assert_eq!(info.sha256.len(), 64); }
#[test]
fn test_assert_dimensions_pass() {
let jsonl = r##"{"type": "sprite", "name": "square", "palette": {"{_}": "#00000000", "{x}": "#FF0000"}, "grid": ["{x}{x}", "{x}{x}"]}"##;
assert_dimensions(jsonl, "square", 2, 2);
}
#[test]
#[should_panic(expected = "Width mismatch")]
fn test_assert_dimensions_fail_width() {
let jsonl = r##"{"type": "sprite", "name": "square", "palette": {"{_}": "#00000000", "{x}": "#FF0000"}, "grid": ["{x}{x}", "{x}{x}"]}"##;
assert_dimensions(jsonl, "square", 3, 2);
}
#[test]
fn test_assert_frame_count() {
let jsonl = r##"{"type": "sprite", "name": "f1", "palette": {"{x}": "#FF0000"}, "grid": ["{x}"]}
{"type": "sprite", "name": "f2", "palette": {"{x}": "#00FF00"}, "grid": ["{x}"]}
{"type": "sprite", "name": "f3", "palette": {"{x}": "#0000FF"}, "grid": ["{x}"]}
{"type": "animation", "name": "blink", "frames": ["f1", "f2", "f3"], "duration": 100}"##;
assert_frame_count(jsonl, "blink", 3);
}
#[test]
fn test_assert_validates_valid() {
let jsonl = r##"{"type": "palette", "name": "mono", "colors": {"{_}": "#00000000", "{x}": "#FF0000"}}
{"type": "sprite", "name": "dot", "palette": "mono", "grid": ["{x}"]}"##;
assert_validates(jsonl, true);
}
#[test]
fn test_assert_validates_invalid() {
let jsonl = "{not valid json}";
assert_validates(jsonl, false);
}
#[test]
fn test_assert_output_hash_fallback() {
let jsonl = r##"{"type": "sprite", "name": "dot", "palette": {"{x}": "#FF0000"}, "grid": ["{x}"]}"##;
assert_output_hash(
jsonl,
"dot",
"0000000000000000000000000000000000000000000000000000000000000000",
);
}
#[test]
fn test_named_palette() {
let jsonl = r##"{"type": "palette", "name": "colors", "colors": {"{_}": "#00000000", "{r}": "#FF0000", "{g}": "#00FF00"}}
{"type": "sprite", "name": "test", "palette": "colors", "grid": ["{r}{g}", "{g}{r}"]}"##;
let info = capture_render_info(jsonl, "test");
assert_eq!(info.width, 2);
assert_eq!(info.height, 2);
assert_eq!(info.color_count, 3);
assert_eq!(info.palette_name, Some("colors".to_string()));
}
#[test]
fn test_assert_color_count() {
let jsonl = r##"{"type": "sprite", "name": "rgb", "palette": {"{r}": "#FF0000", "{g}": "#00FF00", "{b}": "#0000FF"}, "grid": ["{r}{g}{b}"]}"##;
assert_color_count(jsonl, "rgb", 3);
}
#[test]
fn test_assert_uses_palette() {
let jsonl = r##"{"type": "palette", "name": "mypalette", "colors": {"{x}": "#FF0000"}}
{"type": "sprite", "name": "test", "palette": "mypalette", "grid": ["{x}"]}"##;
assert_uses_palette(jsonl, "test", "mypalette");
}
#[test]
fn test_spritesheet_horizontal() {
let jsonl = include_str!("../../examples/demos/exports/spritesheet_horizontal.jsonl");
assert_validates(jsonl, true);
assert_frame_count(jsonl, "walk_cycle", 4);
let info = capture_spritesheet_info(jsonl, "walk_cycle", None);
assert_eq!(info.width, 32, "Horizontal spritesheet should be 4 frames × 8px = 32px wide");
assert_eq!(info.height, 8, "Horizontal spritesheet should be 8px tall (single row)");
assert_eq!(info.frame_count, 4);
assert_eq!(info.frame_width, 8);
assert_eq!(info.frame_height, 8);
}
#[test]
fn test_spritesheet_grid() {
let jsonl = include_str!("../../examples/demos/exports/spritesheet_grid.jsonl");
assert_validates(jsonl, true);
assert_frame_count(jsonl, "coin_spin", 6);
let info = capture_spritesheet_info(jsonl, "coin_spin", Some(3));
assert_eq!(info.width, 18, "Grid spritesheet should be 3 cols × 6px = 18px wide");
assert_eq!(info.height, 12, "Grid spritesheet should be 2 rows × 6px = 12px tall");
assert_eq!(info.frame_count, 6);
}
#[test]
fn test_spritesheet_padding() {
let jsonl = include_str!("../../examples/demos/exports/spritesheet_padding.jsonl");
assert_validates(jsonl, true);
assert_frame_count(jsonl, "plant_grow", 4);
let info = capture_spritesheet_info(jsonl, "plant_grow", None);
assert_eq!(info.frame_width, 10, "Frame cells should be padded to max width (10px)");
assert_eq!(info.frame_height, 10, "Frame cells should be padded to max height (10px)");
assert_eq!(info.width, 40, "Padded spritesheet should be 4 frames × 10px = 40px wide");
assert_eq!(info.height, 10, "Padded spritesheet should be 10px tall");
}
#[test]
fn test_spritesheet_grid_2x2() {
let jsonl = r##"{"type": "sprite", "name": "f1", "palette": {"{.}": "#00000000", "{r}": "#FF0000"}, "grid": ["{r}{r}", "{r}{r}"]}
{"type": "sprite", "name": "f2", "palette": {"{.}": "#00000000", "{g}": "#00FF00"}, "grid": ["{g}{g}", "{g}{g}"]}
{"type": "sprite", "name": "f3", "palette": {"{.}": "#00000000", "{b}": "#0000FF"}, "grid": ["{b}{b}", "{b}{b}"]}
{"type": "sprite", "name": "f4", "palette": {"{.}": "#00000000", "{y}": "#FFFF00"}, "grid": ["{y}{y}", "{y}{y}"]}
{"type": "animation", "name": "colors", "fps": 4, "frames": ["f1", "f2", "f3", "f4"]}"##;
assert_spritesheet_dimensions(jsonl, "colors", Some(2), 4, 4);
}
#[test]
fn test_spritesheet_frame_size_detection() {
let jsonl = r##"{"type": "sprite", "name": "small", "palette": {"{x}": "#FF0000"}, "grid": ["{x}"]}
{"type": "sprite", "name": "large", "palette": {"{x}": "#00FF00"}, "grid": ["{x}{x}{x}", "{x}{x}{x}", "{x}{x}{x}"]}
{"type": "animation", "name": "mixed", "fps": 2, "frames": ["small", "large"]}"##;
assert_spritesheet_frame_size(jsonl, "mixed", 3, 3);
assert_spritesheet_dimensions(jsonl, "mixed", None, 6, 3);
}
#[test]
fn test_png_basic() {
let jsonl = include_str!("../../examples/demos/exports/png_basic.jsonl");
assert_validates(jsonl, true);
let info = capture_render_info(jsonl, "pixel_art");
assert_eq!(info.width, 4, "PNG should be 4 pixels wide");
assert_eq!(info.height, 4, "PNG should be 4 pixels tall");
assert_eq!(info.color_count, 5, "Should have 5 colors (transparent + 4 colors)");
assert_eq!(info.frame_count, 1, "Static sprite should have 1 frame");
}
#[test]
fn test_png_scaled() {
let jsonl = include_str!("../../examples/demos/exports/png_scaled.jsonl");
assert_validates(jsonl, true);
let info_1x = capture_render_info(jsonl, "scalable");
assert_eq!(info_1x.width, 3, "1x scale should be 3 pixels wide");
assert_eq!(info_1x.height, 3, "1x scale should be 3 pixels tall");
let info_2x = capture_scaled_render_info(jsonl, "scalable", 2);
assert_eq!(info_2x.width, 6, "2x scale should be 6 pixels wide (3 × 2)");
assert_eq!(info_2x.height, 6, "2x scale should be 6 pixels tall (3 × 2)");
let info_4x = capture_scaled_render_info(jsonl, "scalable", 4);
assert_eq!(info_4x.width, 12, "4x scale should be 12 pixels wide (3 × 4)");
assert_eq!(info_4x.height, 12, "4x scale should be 12 pixels tall (3 × 4)");
let info_8x = capture_scaled_render_info(jsonl, "scalable", 8);
assert_eq!(info_8x.width, 24, "8x scale should be 24 pixels wide (3 × 8)");
assert_eq!(info_8x.height, 24, "8x scale should be 24 pixels tall (3 × 8)");
assert_eq!(
info_1x.color_count, info_2x.color_count,
"Color count should be preserved at 2x"
);
assert_eq!(
info_1x.color_count, info_4x.color_count,
"Color count should be preserved at 4x"
);
assert_eq!(
info_1x.color_count, info_8x.color_count,
"Color count should be preserved at 8x"
);
}
#[test]
fn test_gif_animated() {
let jsonl = include_str!("../../examples/demos/exports/gif_animated.jsonl");
assert_validates(jsonl, true);
assert_frame_count(jsonl, "star_blink", 4);
let info = capture_gif_info(jsonl, "star_blink");
assert_eq!(info.frame_count, 4, "GIF should have 4 frames");
assert_eq!(info.frame_width, 5, "Frame width should be 5 pixels");
assert_eq!(info.frame_height, 5, "Frame height should be 5 pixels");
assert!(info.loops, "Animation should loop");
assert_eq!(info.duration_ms, 250, "Frame duration should be 250ms");
}
#[test]
fn test_gif_no_loop() {
let jsonl = r##"{"type": "sprite", "name": "f1", "palette": {"{x}": "#FF0000"}, "grid": ["{x}"]}
{"type": "sprite", "name": "f2", "palette": {"{x}": "#00FF00"}, "grid": ["{x}"]}
{"type": "animation", "name": "once", "duration": "500ms", "loop": false, "frames": ["f1", "f2"]}"##;
let info = capture_gif_info(jsonl, "once");
assert_eq!(info.frame_count, 2);
assert!(!info.loops, "Animation should not loop");
assert_eq!(info.duration_ms, 500, "Frame duration should be 500ms");
}
#[test]
fn test_css_keyframes_percentage() {
let jsonl = include_str!("../../examples/demos/css/keyframes/percentage.jsonl");
assert_validates(jsonl, true);
let (_, _, animations) = parse_content(jsonl);
let anim = animations.get("fade_walk").expect("Animation 'fade_walk' not found");
assert!(anim.is_css_keyframes(), "Should use CSS keyframes format");
let keyframes = anim.keyframes.as_ref().unwrap();
assert_eq!(keyframes.len(), 3, "Should have 3 keyframes (0%, 50%, 100%)");
assert!(keyframes.contains_key("0%"), "Should have 0% keyframe");
assert!(keyframes.contains_key("50%"), "Should have 50% keyframe");
assert!(keyframes.contains_key("100%"), "Should have 100% keyframe");
let kf_0 = &keyframes["0%"];
assert_eq!(kf_0.sprite.as_deref(), Some("walk_1"));
assert_eq!(kf_0.opacity, Some(0.0));
let kf_50 = &keyframes["50%"];
assert_eq!(kf_50.sprite.as_deref(), Some("walk_2"));
assert_eq!(kf_50.opacity, Some(1.0));
assert_eq!(anim.timing_function.as_deref(), Some("ease-in-out"));
}
#[test]
fn test_css_keyframes_from_to() {
let jsonl = include_str!("../../examples/demos/css/keyframes/from_to.jsonl");
assert_validates(jsonl, true);
let (_, _, animations) = parse_content(jsonl);
let anim = animations.get("fade_in").expect("Animation 'fade_in' not found");
assert!(anim.is_css_keyframes(), "Should use CSS keyframes format");
let keyframes = anim.keyframes.as_ref().unwrap();
assert_eq!(keyframes.len(), 2, "Should have 2 keyframes (from, to)");
assert!(keyframes.contains_key("from"), "Should have 'from' keyframe");
assert!(keyframes.contains_key("to"), "Should have 'to' keyframe");
let kf_from = &keyframes["from"];
assert_eq!(kf_from.sprite.as_deref(), Some("dot"));
assert_eq!(kf_from.opacity, Some(0.0));
let kf_to = &keyframes["to"];
assert_eq!(kf_to.sprite.as_deref(), Some("dot"));
assert_eq!(kf_to.opacity, Some(1.0));
assert_eq!(anim.duration_ms(), 1000);
}
#[test]
fn test_css_keyframes_sprite_changes() {
let jsonl = include_str!("../../examples/demos/css/keyframes/sprite_changes.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
let anim = animations.get("jump_cycle").expect("Animation 'jump_cycle' not found");
assert!(anim.is_css_keyframes(), "Should use CSS keyframes format");
let keyframes = anim.keyframes.as_ref().unwrap();
assert_eq!(keyframes.len(), 4, "Should have 4 keyframes (0%, 25%, 75%, 100%)");
assert_eq!(keyframes["0%"].sprite.as_deref(), Some("char_idle"));
assert_eq!(keyframes["25%"].sprite.as_deref(), Some("char_jump"));
assert_eq!(keyframes["75%"].sprite.as_deref(), Some("char_land"));
assert_eq!(keyframes["100%"].sprite.as_deref(), Some("char_idle"));
for sprite_name in ["char_idle", "char_jump", "char_land"] {
sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Sprite '{sprite_name}' should resolve"));
}
assert_eq!(anim.duration_ms(), 800);
}
#[test]
fn test_css_keyframes_transforms() {
let jsonl = include_str!("../../examples/demos/css/keyframes/transforms.jsonl");
assert_validates(jsonl, true);
let (_, _, animations) = parse_content(jsonl);
let spin = animations.get("spin").expect("Animation 'spin' not found");
assert!(spin.is_css_keyframes(), "spin should use CSS keyframes format");
let spin_kf = spin.keyframes.as_ref().unwrap();
assert_eq!(spin_kf.len(), 2);
assert_eq!(spin_kf["0%"].transform.as_deref(), Some("rotate(0deg)"));
assert_eq!(spin_kf["100%"].transform.as_deref(), Some("rotate(360deg)"));
assert_eq!(spin.timing_function.as_deref(), Some("linear"));
let pulse = animations.get("pulse").expect("Animation 'pulse' not found");
assert!(pulse.is_css_keyframes(), "pulse should use CSS keyframes format");
let pulse_kf = pulse.keyframes.as_ref().unwrap();
assert_eq!(pulse_kf.len(), 3);
assert_eq!(pulse_kf["0%"].transform.as_deref(), Some("scale(1)"));
assert_eq!(pulse_kf["0%"].opacity, Some(1.0));
assert_eq!(pulse_kf["50%"].transform.as_deref(), Some("scale(1.5)"));
assert_eq!(pulse_kf["50%"].opacity, Some(0.5));
assert_eq!(pulse_kf["100%"].transform.as_deref(), Some("scale(1)"));
assert_eq!(pulse_kf["100%"].opacity, Some(1.0));
assert_eq!(pulse.timing_function.as_deref(), Some("ease-in-out"));
}
#[test]
fn test_css_timing_named() {
let jsonl = include_str!("../../examples/demos/css/timing/named.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
for sprite_name in ["box_left", "box_center", "box_right"] {
sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Sprite '{sprite_name}' should resolve"));
}
let linear = animations.get("linear_slide").expect("Animation 'linear_slide' not found");
assert!(linear.is_css_keyframes(), "linear_slide should use CSS keyframes");
assert_eq!(linear.timing_function.as_deref(), Some("linear"));
assert_eq!(linear.duration_ms(), 500);
let ease = animations.get("ease_slide").expect("Animation 'ease_slide' not found");
assert_eq!(ease.timing_function.as_deref(), Some("ease"));
let ease_in = animations.get("ease_in_slide").expect("Animation 'ease_in_slide' not found");
assert_eq!(ease_in.timing_function.as_deref(), Some("ease-in"));
let ease_out =
animations.get("ease_out_slide").expect("Animation 'ease_out_slide' not found");
assert_eq!(ease_out.timing_function.as_deref(), Some("ease-out"));
let ease_in_out =
animations.get("ease_in_out_slide").expect("Animation 'ease_in_out_slide' not found");
assert_eq!(ease_in_out.timing_function.as_deref(), Some("ease-in-out"));
}
#[test]
fn test_css_timing_cubic_bezier() {
let jsonl = include_str!("../../examples/demos/css/timing/cubic_bezier.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
for sprite_name in ["ball_top", "ball_middle", "ball_bottom"] {
sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Sprite '{sprite_name}' should resolve"));
}
let bounce = animations.get("bounce_fall").expect("Animation 'bounce_fall' not found");
assert!(bounce.is_css_keyframes(), "bounce_fall should use CSS keyframes");
assert_eq!(bounce.timing_function.as_deref(), Some("cubic-bezier(0.5, 0, 0.5, 1)"));
assert_eq!(bounce.duration_ms(), 800);
let kf = bounce.keyframes.as_ref().unwrap();
assert_eq!(kf.len(), 3, "bounce_fall should have 3 keyframes");
assert!(kf.contains_key("0%"));
assert!(kf.contains_key("50%"));
assert!(kf.contains_key("100%"));
let snap = animations.get("snap_ease").expect("Animation 'snap_ease' not found");
assert_eq!(snap.timing_function.as_deref(), Some("cubic-bezier(0.68, -0.55, 0.27, 1.55)"));
let smooth = animations.get("smooth_decel").expect("Animation 'smooth_decel' not found");
assert_eq!(smooth.timing_function.as_deref(), Some("cubic-bezier(0.25, 0.1, 0.25, 1.0)"));
}
#[test]
fn test_css_timing_steps() {
let jsonl = include_str!("../../examples/demos/css/timing/steps.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
for sprite_name in ["step1", "step2", "step3", "step4"] {
sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Sprite '{sprite_name}' should resolve"));
}
let steps4 = animations.get("steps_4").expect("Animation 'steps_4' not found");
assert!(steps4.is_css_keyframes(), "steps_4 should use CSS keyframes");
assert_eq!(steps4.timing_function.as_deref(), Some("steps(4)"));
assert_eq!(steps4.duration_ms(), 1000);
let kf = steps4.keyframes.as_ref().unwrap();
assert_eq!(kf.len(), 5, "steps_4 should have 5 keyframes");
let jump_start =
animations.get("steps_jump_start").expect("Animation 'steps_jump_start' not found");
assert_eq!(jump_start.timing_function.as_deref(), Some("steps(4, jump-start)"));
let jump_end =
animations.get("steps_jump_end").expect("Animation 'steps_jump_end' not found");
assert_eq!(jump_end.timing_function.as_deref(), Some("steps(4, jump-end)"));
let step_start =
animations.get("step_start_instant").expect("Animation 'step_start_instant' not found");
assert_eq!(step_start.timing_function.as_deref(), Some("step-start"));
assert_eq!(step_start.duration_ms(), 500);
let step_end =
animations.get("step_end_delayed").expect("Animation 'step_end_delayed' not found");
assert_eq!(step_end.timing_function.as_deref(), Some("step-end"));
}
#[test]
fn test_css_variables_definition() {
let jsonl = include_str!("../../examples/demos/css/variables/definition.jsonl");
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("theme_colors"));
sprite_registry
.resolve("theme_example", &palette_registry, false)
.expect("Sprite 'theme_example' should resolve");
}
#[test]
fn test_css_variables_resolution() {
let jsonl = include_str!("../../examples/demos/css/variables/resolution.jsonl");
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("var_resolution"));
sprite_registry
.resolve("resolved_colors", &palette_registry, false)
.expect("Sprite 'resolved_colors' should resolve with var() references");
}
#[test]
fn test_css_variables_fallbacks() {
let jsonl = include_str!("../../examples/demos/css/variables/fallbacks.jsonl");
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("simple_fallback"));
assert!(palette_registry.contains("nested_fallback"));
assert!(palette_registry.contains("color_mix_fallback"));
sprite_registry
.resolve("fallback_demo", &palette_registry, false)
.expect("Sprite 'fallback_demo' should resolve with fallback");
sprite_registry
.resolve("nested_fallback_result", &palette_registry, false)
.expect("Sprite 'nested_fallback_result' should resolve with nested fallbacks");
sprite_registry
.resolve("mix_fallback_result", &palette_registry, false)
.expect("Sprite 'mix_fallback_result' should resolve with color-mix fallback");
}
#[test]
fn test_css_variables_chaining() {
let jsonl = include_str!("../../examples/demos/css/variables/chaining.jsonl");
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("basic_chain"));
assert!(palette_registry.contains("deep_chain"));
assert!(palette_registry.contains("color_mix_chain"));
sprite_registry
.resolve("chain_result", &palette_registry, false)
.expect("Sprite 'chain_result' should resolve with basic chain");
sprite_registry
.resolve("deep_chain_result", &palette_registry, false)
.expect("Sprite 'deep_chain_result' should resolve with deep chain");
sprite_registry
.resolve("shaded_box", &palette_registry, false)
.expect("Sprite 'shaded_box' should resolve with color-mix chain");
}
#[test]
fn test_css_colors_hex() {
let jsonl = include_str!("../../examples/demos/css/colors/hex.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("hex_short"));
assert!(palette_registry.contains("hex_full"));
assert!(palette_registry.contains("hex_alpha"));
sprite_registry
.resolve("rgb_short", &palette_registry, false)
.expect("Sprite 'rgb_short' should resolve");
sprite_registry
.resolve("alpha_gradient", &palette_registry, false)
.expect("Sprite 'alpha_gradient' should resolve");
}
#[test]
fn test_css_colors_rgb() {
let jsonl = include_str!("../../examples/demos/css/colors/rgb.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("rgb_basic"));
assert!(palette_registry.contains("rgba_alpha"));
sprite_registry
.resolve("rgb_demo", &palette_registry, false)
.expect("Sprite 'rgb_demo' should resolve");
sprite_registry
.resolve("rgba_gradient", &palette_registry, false)
.expect("Sprite 'rgba_gradient' should resolve");
}
#[test]
fn test_css_colors_hsl() {
let jsonl = include_str!("../../examples/demos/css/colors/hsl.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("hsl_basic"));
assert!(palette_registry.contains("hsl_saturation"));
assert!(palette_registry.contains("hsl_lightness"));
sprite_registry
.resolve("hsl_demo", &palette_registry, false)
.expect("Sprite 'hsl_demo' should resolve");
sprite_registry
.resolve("saturation_demo", &palette_registry, false)
.expect("Sprite 'saturation_demo' should resolve");
}
#[test]
fn test_css_colors_oklch() {
let jsonl = include_str!("../../examples/demos/css/colors/oklch.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("oklch_basic"));
assert!(palette_registry.contains("oklch_lightness"));
assert!(palette_registry.contains("oklch_chroma"));
sprite_registry
.resolve("oklch_demo", &palette_registry, false)
.expect("Sprite 'oklch_demo' should resolve");
}
#[test]
fn test_css_colors_hwb() {
let jsonl = include_str!("../../examples/demos/css/colors/hwb.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("hwb_basic"));
assert!(palette_registry.contains("hwb_whiteness"));
assert!(palette_registry.contains("hwb_blackness"));
sprite_registry
.resolve("hwb_demo", &palette_registry, false)
.expect("Sprite 'hwb_demo' should resolve");
}
#[test]
fn test_css_colors_named() {
let jsonl = include_str!("../../examples/demos/css/colors/named.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("named_basic"));
assert!(palette_registry.contains("named_warm"));
assert!(palette_registry.contains("named_cool"));
assert!(palette_registry.contains("named_neutral"));
sprite_registry
.resolve("named_demo", &palette_registry, false)
.expect("Sprite 'named_demo' should resolve");
sprite_registry
.resolve("grayscale", &palette_registry, false)
.expect("Sprite 'grayscale' should resolve");
}
#[test]
fn test_css_colors_color_mix() {
let jsonl = include_str!("../../examples/demos/css/colors/color_mix.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, _animations) = parse_content(jsonl);
assert!(palette_registry.contains("color_mix_basic"));
assert!(palette_registry.contains("shadows_oklch"));
assert!(palette_registry.contains("highlights_srgb"));
assert!(palette_registry.contains("skin_tones"));
sprite_registry
.resolve("shaded_square", &palette_registry, false)
.expect("Sprite 'shaded_square' should resolve with color-mix");
}
#[test]
fn test_css_transforms_translate() {
let jsonl = include_str!("../../examples/demos/css/transforms/translate.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
sprite_registry
.resolve("arrow_right", &palette_registry, false)
.expect("Sprite 'arrow_right' should resolve");
sprite_registry
.resolve("arrow_base", &palette_registry, false)
.expect("Sprite 'arrow_base' should resolve");
let slide_right = animations.get("slide_right").expect("Animation 'slide_right' not found");
assert!(slide_right.is_css_keyframes(), "slide_right should use CSS keyframes");
let kf = slide_right.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("translate(0, 0)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("translate(8px, 0)"));
let slide_down = animations.get("slide_down").expect("Animation 'slide_down' not found");
let kf = slide_down.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("translateY(0)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("translateY(4px)"));
let slide_diagonal =
animations.get("slide_diagonal").expect("Animation 'slide_diagonal' not found");
let kf = slide_diagonal.keyframes.as_ref().unwrap();
assert_eq!(kf.len(), 3, "slide_diagonal should have 3 keyframes");
assert_eq!(kf["50%"].transform.as_deref(), Some("translate(4px, 4px)"));
}
#[test]
fn test_css_transforms_rotate() {
let jsonl = include_str!("../../examples/demos/css/transforms/rotate.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
sprite_registry
.resolve("L_shape", &palette_registry, false)
.expect("Sprite 'L_shape' should resolve");
sprite_registry
.resolve("arrow_up", &palette_registry, false)
.expect("Sprite 'arrow_up' should resolve");
let rotate_90 = animations.get("rotate_90").expect("Animation 'rotate_90' not found");
assert!(rotate_90.is_css_keyframes(), "rotate_90 should use CSS keyframes");
let kf = rotate_90.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("rotate(0deg)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("rotate(90deg)"));
let rotate_180 = animations.get("rotate_180").expect("Animation 'rotate_180' not found");
let kf = rotate_180.keyframes.as_ref().unwrap();
assert_eq!(kf["100%"].transform.as_deref(), Some("rotate(180deg)"));
let rotate_270 = animations.get("rotate_270").expect("Animation 'rotate_270' not found");
let kf = rotate_270.keyframes.as_ref().unwrap();
assert_eq!(kf["100%"].transform.as_deref(), Some("rotate(270deg)"));
let spin_full = animations.get("spin_full").expect("Animation 'spin_full' not found");
let kf = spin_full.keyframes.as_ref().unwrap();
assert_eq!(kf.len(), 5, "spin_full should have 5 keyframes (0%, 25%, 50%, 75%, 100%)");
assert_eq!(kf["25%"].transform.as_deref(), Some("rotate(90deg)"));
assert_eq!(kf["50%"].transform.as_deref(), Some("rotate(180deg)"));
assert_eq!(kf["75%"].transform.as_deref(), Some("rotate(270deg)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("rotate(360deg)"));
}
#[test]
fn test_css_transforms_scale() {
let jsonl = include_str!("../../examples/demos/css/transforms/scale.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
sprite_registry
.resolve("dot", &palette_registry, false)
.expect("Sprite 'dot' should resolve");
sprite_registry
.resolve("square", &palette_registry, false)
.expect("Sprite 'square' should resolve");
let scale_up = animations.get("scale_up").expect("Animation 'scale_up' not found");
assert!(scale_up.is_css_keyframes(), "scale_up should use CSS keyframes");
let kf = scale_up.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("scale(1)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("scale(4)"));
let scale_xy = animations.get("scale_xy").expect("Animation 'scale_xy' not found");
let kf = scale_xy.keyframes.as_ref().unwrap();
assert_eq!(kf["50%"].transform.as_deref(), Some("scale(2, 1)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("scale(2, 2)"));
let scale_x = animations.get("scale_x_only").expect("Animation 'scale_x_only' not found");
let kf = scale_x.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("scaleX(1)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("scaleX(3)"));
let scale_y = animations.get("scale_y_only").expect("Animation 'scale_y_only' not found");
let kf = scale_y.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("scaleY(1)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("scaleY(3)"));
let pulse = animations.get("pulse_scale").expect("Animation 'pulse_scale' not found");
let kf = pulse.keyframes.as_ref().unwrap();
assert_eq!(kf["50%"].transform.as_deref(), Some("scale(2)"));
assert_eq!(kf["50%"].opacity, Some(0.6));
}
#[test]
fn test_css_transforms_flip() {
let jsonl = include_str!("../../examples/demos/css/transforms/flip.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
sprite_registry
.resolve("face_right", &palette_registry, false)
.expect("Sprite 'face_right' should resolve");
sprite_registry
.resolve("arrow_left", &palette_registry, false)
.expect("Sprite 'arrow_left' should resolve");
let flip_h =
animations.get("flip_horizontal").expect("Animation 'flip_horizontal' not found");
assert!(flip_h.is_css_keyframes(), "flip_horizontal should use CSS keyframes");
let kf = flip_h.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("scaleX(1)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("scaleX(-1)"));
let flip_v = animations.get("flip_vertical").expect("Animation 'flip_vertical' not found");
let kf = flip_v.keyframes.as_ref().unwrap();
assert_eq!(kf["0%"].transform.as_deref(), Some("scaleY(1)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("scaleY(-1)"));
let flip_both = animations.get("flip_both").expect("Animation 'flip_both' not found");
let kf = flip_both.keyframes.as_ref().unwrap();
assert_eq!(kf.len(), 3, "flip_both should have 3 keyframes");
assert_eq!(kf["50%"].transform.as_deref(), Some("scale(-1, 1)"));
assert_eq!(kf["100%"].transform.as_deref(), Some("scale(-1, -1)"));
let mirror = animations.get("mirror_walk").expect("Animation 'mirror_walk' not found");
let kf = mirror.keyframes.as_ref().unwrap();
assert_eq!(kf.len(), 4, "mirror_walk should have 4 keyframes");
assert_eq!(kf["50%"].transform.as_deref(), Some("translate(8px, 0) scaleX(1)"));
assert_eq!(kf["51%"].transform.as_deref(), Some("translate(8px, 0) scaleX(-1)"));
}
#[test]
fn test_atlas_aseprite() {
let jsonl = include_str!("../../examples/demos/exports/atlas_aseprite.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
assert!(palette_registry.contains("character"));
for sprite_name in [
"char_idle_1",
"char_idle_2",
"char_run_1",
"char_run_2",
"char_run_3",
"char_run_4",
"item_gem",
] {
sprite_registry
.resolve(sprite_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Sprite '{sprite_name}' should resolve"));
}
let info = capture_render_info(jsonl, "char_idle_1");
assert_eq!(info.width, 4, "Character sprite should be 4 pixels wide");
assert_eq!(info.height, 4, "Character sprite should be 4 pixels tall");
let gem_info = capture_render_info(jsonl, "item_gem");
assert_eq!(gem_info.width, 3, "Gem sprite should be 3 pixels wide");
assert_eq!(gem_info.height, 3, "Gem sprite should be 3 pixels tall");
let idle = animations.get("idle").expect("Animation 'idle' not found");
assert_eq!(idle.frames.len(), 2, "Idle animation should have 2 frames");
assert_eq!(idle.duration_ms(), 500, "Idle animation should have 500ms duration");
let run = animations.get("run").expect("Animation 'run' not found");
assert_eq!(run.frames.len(), 4, "Run animation should have 4 frames");
assert_eq!(run.duration_ms(), 100, "Run animation should have 100ms duration");
}
#[test]
fn test_recolor_export() {
let jsonl = include_str!("../../examples/demos/exports/recolor_export.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
assert!(palette_registry.contains("slime_green"));
let base_info = capture_render_info(jsonl, "slime_base");
assert_eq!(base_info.width, 5, "Slime sprite should be 5 pixels wide");
assert_eq!(base_info.height, 5, "Slime sprite should be 5 pixels tall");
assert_eq!(
base_info.color_count, 5,
"Base slime should have 5 colors (including transparent)"
);
for variant_name in ["slime_red", "slime_blue", "slime_gold"] {
sprite_registry
.resolve(variant_name, &palette_registry, false)
.unwrap_or_else(|_| panic!("Variant '{variant_name}' should resolve"));
let variant_info = capture_render_info(jsonl, variant_name);
assert_eq!(
variant_info.width, base_info.width,
"Variant '{variant_name}' should match base width"
);
assert_eq!(
variant_info.height, base_info.height,
"Variant '{variant_name}' should match base height"
);
}
let squash_info = capture_render_info(jsonl, "slime_squash");
assert_eq!(squash_info.width, 5, "Squash sprite should be 5 pixels wide");
assert_eq!(squash_info.height, 5, "Squash sprite should be 5 pixels tall");
let bounce = animations.get("slime_bounce").expect("Animation 'slime_bounce' not found");
assert_eq!(bounce.frames.len(), 2, "Bounce animation should have 2 frames");
assert_eq!(bounce.duration_ms(), 300, "Bounce animation should have 300ms duration");
let red_info = capture_render_info(jsonl, "slime_red");
let blue_info = capture_render_info(jsonl, "slime_blue");
let gold_info = capture_render_info(jsonl, "slime_gold");
assert_ne!(base_info.sha256, red_info.sha256, "Red variant should differ from base");
assert_ne!(base_info.sha256, blue_info.sha256, "Blue variant should differ from base");
assert_ne!(base_info.sha256, gold_info.sha256, "Gold variant should differ from base");
assert_ne!(red_info.sha256, blue_info.sha256, "Red and blue variants should differ");
}
#[test]
fn test_palette_cycle_single() {
let jsonl = include_str!("../../examples/demos/palette_cycling/single_cycle.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
sprite_registry
.resolve("wave", &palette_registry, false)
.expect("Sprite 'wave' should resolve");
let anim = animations.get("wave_cycle").expect("Animation 'wave_cycle' not found");
let cycles = anim.palette_cycles();
assert_eq!(cycles.len(), 1, "Should have 1 palette cycle");
let cycle = &cycles[0];
assert_eq!(cycle.tokens.len(), 4, "Cycle should have 4 tokens");
assert_eq!(cycle.tokens[0], "{c1}");
assert_eq!(cycle.tokens[3], "{c4}");
assert_eq!(cycle.duration, Some(200), "Cycle duration should be 200ms");
let info = capture_palette_cycle_info(jsonl, "wave_cycle");
assert_eq!(info.cycle_count, 1);
assert_eq!(info.total_frames, 4, "4 tokens = 4 frames for single cycle");
assert_eq!(info.cycle_lengths, vec![4]);
}
#[test]
fn test_palette_cycle_multiple() {
let jsonl = include_str!("../../examples/demos/palette_cycling/multiple_cycles.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
sprite_registry
.resolve("waterfire", &palette_registry, false)
.expect("Sprite 'waterfire' should resolve");
let anim = animations.get("dual_cycle").expect("Animation 'dual_cycle' not found");
let cycles = anim.palette_cycles();
assert_eq!(cycles.len(), 2, "Should have 2 palette cycles");
let water_cycle = &cycles[0];
assert_eq!(water_cycle.tokens.len(), 3, "Water cycle should have 3 tokens");
assert!(water_cycle.tokens.iter().all(|t| t.starts_with("{w")));
assert_eq!(water_cycle.duration, Some(300), "Water cycle duration should be 300ms");
let fire_cycle = &cycles[1];
assert_eq!(fire_cycle.tokens.len(), 3, "Fire cycle should have 3 tokens");
assert!(fire_cycle.tokens.iter().all(|t| t.starts_with("{f")));
assert_eq!(fire_cycle.duration, Some(200), "Fire cycle duration should be 200ms");
let info = capture_palette_cycle_info(jsonl, "dual_cycle");
assert_eq!(info.cycle_count, 2);
assert_eq!(info.total_frames, 3, "LCM(3,3) = 3 total frames");
assert_eq!(info.cycle_lengths, vec![3, 3]);
}
#[test]
fn test_palette_cycle_timing() {
let jsonl = include_str!("../../examples/demos/palette_cycling/cycle_timing.jsonl");
assert_validates(jsonl, true);
let (_, _, animations) = parse_content(jsonl);
let fast_anim = animations.get("fast_cycle").expect("Animation 'fast_cycle' not found");
let fast_cycles = fast_anim.palette_cycles();
assert_eq!(fast_cycles.len(), 1);
assert_eq!(fast_cycles[0].duration, Some(50), "Fast cycle should be 50ms");
assert_eq!(fast_cycles[0].tokens.len(), 3);
let slow_anim = animations.get("slow_cycle").expect("Animation 'slow_cycle' not found");
let slow_cycles = slow_anim.palette_cycles();
assert_eq!(slow_cycles.len(), 1);
assert_eq!(slow_cycles[0].duration, Some(500), "Slow cycle should be 500ms");
assert_eq!(slow_cycles[0].tokens.len(), 3);
let fast_info = capture_palette_cycle_info(jsonl, "fast_cycle");
let slow_info = capture_palette_cycle_info(jsonl, "slow_cycle");
assert_eq!(
fast_info.total_frames, slow_info.total_frames,
"Same token count = same frames"
);
assert_eq!(fast_info.total_frames, 3);
assert_eq!(fast_info.cycle_durations, vec![Some(50)]);
assert_eq!(slow_info.cycle_durations, vec![Some(500)]);
}
#[test]
fn test_palette_cycle_ping_pong() {
let jsonl = include_str!("../../examples/demos/palette_cycling/ping_pong.jsonl");
assert_validates(jsonl, true);
let (palette_registry, sprite_registry, animations) = parse_content(jsonl);
sprite_registry
.resolve("glow", &palette_registry, false)
.expect("Sprite 'glow' should resolve");
let anim = animations.get("ping_pong_glow").expect("Animation 'ping_pong_glow' not found");
let cycles = anim.palette_cycles();
assert_eq!(cycles.len(), 1, "Should have 1 palette cycle");
let cycle = &cycles[0];
assert_eq!(cycle.tokens.len(), 8, "Ping-pong cycle should have 8 tokens (5 + 3 reverse)");
assert_eq!(cycle.tokens[0], "{p1}", "Start at p1");
assert_eq!(cycle.tokens[4], "{p5}", "Peak at p5 (middle)");
assert_eq!(cycle.tokens[5], "{p4}", "Reverse: p4");
assert_eq!(cycle.tokens[6], "{p3}", "Reverse: p3");
assert_eq!(cycle.tokens[7], "{p2}", "Reverse: p2 (ends before p1 to avoid double)");
let info = capture_palette_cycle_info(jsonl, "ping_pong_glow");
assert_eq!(info.total_frames, 8, "8 tokens = 8 frames");
assert_eq!(info.cycle_durations, vec![Some(100)]);
}
}