88 lines
3.7 KiB
Rust
88 lines
3.7 KiB
Rust
//! Canonical Bodymovin vector forms and explicit endpoint validation.
|
|
|
|
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 two_dimensional_layers_accept_neutral_z_components() {
|
|
let mut input = fixture();
|
|
input["layers"][0]["ks"]["a"]["k"] = json!([0, 0, 0]);
|
|
input["layers"][0]["ks"]["p"]["k"] = json!([10, 20, 0]);
|
|
input["layers"][0]["ks"]["s"]["k"] = json!([200, 200, 100]);
|
|
let animation = Animation::parse(&input.to_string()).expect("2D transform with neutral z");
|
|
let frame = animation.frame_at(0.0);
|
|
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
|
assert_eq!(stroke.polylines.first().and_then(|line| line.points.first()), Some(&[14.0, 28.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn nonneutral_z_is_rejected_as_three_dimensional() {
|
|
let mut input = fixture();
|
|
input["layers"][0]["ks"]["p"]["k"] = json!([0, 0, 1]);
|
|
assert!(matches!(Animation::parse(&input.to_string()), Err(Error::Unsupported { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn final_shape_transform_applies_to_root_geometry() {
|
|
let mut input = fixture();
|
|
input["layers"][0]["shapes"].as_array_mut().expect("shapes").push(json!({"ty":"tr", "p":{"a":0,"k":[3,4]}}));
|
|
let animation = Animation::parse(&input.to_string()).expect("root transform");
|
|
let frame = animation.frame_at(0.0);
|
|
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
|
assert_eq!(stroke.polylines.first().and_then(|line| line.points.first()), Some(&[5.0, 8.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_keyframe_endpoints_are_validated_and_terminal_start_can_be_omitted() {
|
|
let mut input = fixture();
|
|
input["layers"][0]["ks"]["p"] = json!({"a":1,"k":[{"t":0,"s":[0,0],"e":[10,0]},{"t":10}]});
|
|
assert!(Animation::parse(&input.to_string()).is_ok());
|
|
input["layers"][0]["ks"]["p"]["k"][1]["s"] = json!([20, 0]);
|
|
assert!(Animation::parse(&input.to_string()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn lock_shackle_vertices_and_tangents_animate_with_fixed_topology() {
|
|
let animation = Animation::parse(include_str!("fixtures/animated-shackle.json")).expect("animated shackle");
|
|
for (time, endpoint) in [(0.0, [16.0, 12.0]), (5.0, [18.0, 10.0]), (10.0, [20.0, 8.0])] {
|
|
let frame = animation.frame_at(time);
|
|
let Some(Item::Stroke(stroke)) = frame.items.first() else { panic!("stroke") };
|
|
let line = stroke.polylines.first().expect("shackle contour");
|
|
assert_eq!(line.points.first(), Some(&[8.0, 12.0]));
|
|
for (&actual, expected) in line.points.last().expect("endpoint").iter().zip(endpoint) {
|
|
assert!((actual - expected).abs() < 0.001);
|
|
}
|
|
assert!(line.points.len() > 4);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn animated_path_vertex_count_changes_return_error() {
|
|
let mut value: Value = serde_json::from_str(include_str!("fixtures/animated-shackle.json")).expect("fixture");
|
|
for key in ["v", "i", "o"] {
|
|
value["layers"][0]["shapes"][0]["ks"]["k"][1]["s"][0][key].as_array_mut().expect("vertices").pop();
|
|
}
|
|
assert!(Animation::parse(&value.to_string()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn every_fixture_byte_can_be_mutated_without_panicking() {
|
|
for source in [include_str!("fixtures/stroke.json"), include_str!("fixtures/animated-shackle.json")] {
|
|
for index in 0..source.len() {
|
|
let mut bytes = source.as_bytes().to_vec();
|
|
bytes[index] ^= 0x5a;
|
|
let input = String::from_utf8_lossy(&bytes);
|
|
let result = std::panic::catch_unwind(|| {
|
|
if let Ok(animation) = Animation::parse(&input) {
|
|
let _ = animation.frame_at(5.0);
|
|
}
|
|
});
|
|
assert!(result.is_ok(), "byte {index}");
|
|
}
|
|
}
|
|
}
|