Files
Marcos Paulo cd43ea488e
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: notescraft-lottie player crate, egui lottie_icon painter, icon_sheet review example, first icon animation set
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:42:57 -03:00

87 lines
3.6 KiB
Rust

//! Primitive evaluation and combined resource boundaries.
use notescraft_lottie::{Animation, Error, Item};
use serde_json::{Value, json};
fn fixture() -> Value {
serde_json::from_str(include_str!("fixtures/stroke.json")).expect("valid fixture")
}
#[test]
fn rounded_rectangle_and_ellipse_emit_closed_pixel_contours() {
for primitive in [
json!({"ty":"rc","p":{"a":0,"k":[12,12]},"s":{"a":0,"k":[20,10]},"r":{"a":0,"k":2}}),
json!({"ty":"el","p":{"a":0,"k":[12,12]},"s":{"a":0,"k":[20,10]}}),
] {
let mut input = fixture();
input["layers"][0]["shapes"][0] = primitive;
let animation = Animation::parse(&input.to_string()).expect("primitive");
let frame = animation.frame_at(0.0);
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
let line = stroke.polylines.first().expect("contour");
assert!(line.closed);
assert!(line.points.len() > 8);
assert!(line.points.iter().all(|[x, y]| (2.0..=22.0).contains(x) && (7.0..=17.0).contains(y)));
}
}
#[test]
fn animated_ellipse_size_and_position_interpolate() {
let mut input = fixture();
input["layers"][0]["shapes"][0] = json!({
"ty":"el",
"p":{"a":1,"k":[{"t":0,"s":[10,10]},{"t":10,"s":[20,20]}]},
"s":{"a":1,"k":[{"t":0,"s":[10,10]},{"t":10,"s":[20,20]}]}
});
let animation = Animation::parse(&input.to_string()).expect("ellipse");
let frame = animation.frame_at(5.0);
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
let [x, y] = stroke.polylines.first().and_then(|line| line.points.first()).expect("top vertex");
assert!((*x - 15.0).abs() < 0.001);
assert!((*y - 7.5).abs() < 0.001);
}
#[test]
fn vertex_budget_counts_all_paths_not_only_each_path() {
let mut input = fixture();
let mut path = input["layers"][0]["shapes"][0].clone();
path["ks"]["k"] = json!({"v":vec![[0,0];2050],"i":vec![[0,0];2050],"o":vec![[0,0];2050],"c":false});
input["layers"][0]["shapes"] = json!([path, path]);
assert!(matches!(Animation::parse(&input.to_string()), Err(Error::Unsupported {what,..}) if what.contains("4096")));
}
#[test]
fn exactly_eight_group_levels_are_accepted() {
let mut input = fixture();
let mut shapes = input["layers"][0]["shapes"].take();
for _ in 0..8 {
shapes.as_array_mut().expect("shapes").push(json!({"ty":"tr"}));
shapes = json!([{"ty":"gr","it":shapes}]);
}
input["layers"][0]["shapes"] = shapes;
assert!(Animation::parse(&input.to_string()).is_ok());
}
#[test]
fn masks_and_unimplemented_trim_modes_are_explicitly_unsupported() {
let mut input = fixture();
input["layers"][0]["hasMask"] = json!(true);
assert!(matches!(Animation::parse(&input.to_string()),Err(Error::Unsupported{what,..}) if what.contains("Mask")));
input["layers"][0]["hasMask"] = json!(false);
input["layers"][0]["shapes"].as_array_mut().expect("shapes").push(json!({"ty":"tm","m":2}));
assert!(matches!(Animation::parse(&input.to_string()),Err(Error::Unsupported{what,..}) if what.contains("trim")));
}
#[test]
fn point_budget_includes_every_stroke_and_fill_copy() {
let mut input = fixture();
let path = json!({"ty":"sh","ks":{"a":0,"k":{
"v":[[0,0],[100,0]],"i":[[0,0],[0,100]],"o":[[0,100],[0,0]],"c":false
}}});
let stroke = input["layers"][0]["shapes"][1].clone();
let mut shapes = vec![path];
shapes.extend(std::iter::repeat_n(stroke, 1000));
input["layers"][0]["shapes"] = json!(shapes);
assert!(matches!(Animation::parse(&input.to_string()),Err(Error::Unsupported{what,..}) if what.contains("20000")));
}