Files
effectcraft/crates/host/tests/it/background_presets.rs
T
aiongg 0173abde8c CI: dev-profile gate, one test binary per crate, nextest, line tables only (#2235)
* Dev profile: line tables only for workspace crates, no debug info for dependencies

* xtask ci: dev-profile gate, nextest, tests without examples, wasm --keep-going

* One integration-test binary per crate

* xtask ci: run effectcraft-gpu's shared-device tests in one cargo test process

* xtask ci: run every stage and report all failures

* xtask ci: name the stage type (clippy type_complexity)

* xtask ci: run every test pass and report all failures

* cargo fmt
2026-10-23 12:59:51 -04:00

61 lines
3.3 KiB
Rust

//! The Backgrounds animation presets through the wired session (with the expression engine):
//! each one renders a non-flat, finite, deterministic frame that keeps moving, and undoes.
use effectcraft_engine::render::RenderOpts;
use effectcraft_time::Tick;
use serde_json::json;
type Frame = Vec<[f32; 4]>;
/// The frame at `secs`, its mean luma, luma standard deviation and whether any channel is not finite.
fn frame(s: &effectcraft_engine::Session, secs: f64) -> (Frame, f64, f64, bool) {
let Some(cid) = s.active_comp_id() else { panic!("no active comp") };
let img = s.render(cid, Tick::from_seconds_f64(secs), RenderOpts { scale: 1.0, ..Default::default() });
let bad = img.data.iter().any(|p| p.iter().any(|c| !c.is_finite()));
let l: Vec<f64> = img.data.iter().map(|p| 0.2126 * p[0] as f64 + 0.7152 * p[1] as f64 + 0.0722 * p[2] as f64).collect();
let n = l.len().max(1) as f64;
let mean = l.iter().sum::<f64>() / n;
let sd = (l.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n).sqrt();
(img.data.clone(), mean, sd, bad)
}
fn diff(a: &[[f32; 4]], b: &[[f32; 4]]) -> f64 {
a.iter().zip(b).map(|(p, q)| (0..3).map(|i| (p[i] - q[i]).abs() as f64).sum::<f64>()).sum()
}
#[test]
fn every_background_preset_renders_moves_and_undoes() {
let mut s = effectcraft_host::session();
s.execute("comp.new", json!({"name": "BG", "width": 640, "height": 360, "frameRate": 30, "duration": 8})).unwrap();
let solid = s.execute("layer.newSolid", json!({"name": "S", "color": "#ffffff"})).unwrap()["layer"].as_u64().unwrap();
let list = s.execute("anim.builtinPresets", json!({"category": "Backgrounds"})).unwrap();
let list = list.as_array().unwrap().clone();
assert!(list.len() >= 15, "{}", list.len());
for p in &list {
let id = p["id"].as_str().unwrap();
s.execute("time.set", json!({"time": 0.0})).unwrap();
let r = s.execute("anim.applyBuiltinPreset", json!({"layers": [solid], "preset": id})).unwrap();
assert_eq!(r["skipped"], json!([]), "{id}");
let mut frames = Vec::new();
for t in [0.0, 1.3, 3.7] {
let (px, mean, sd, nan) = frame(&s, t);
if let Some(dir) = std::env::var_os("EC_PRESET_DUMP") {
// Contact-sheet helper for reviewing the looks by eye (not part of the check).
let mut ppm = b"P6\n640 360\n255\n".to_vec();
ppm.extend(px.iter().flat_map(|p| (0..3).map(|i| (p[i].clamp(0.0, 1.0) * 255.0) as u8)));
let _ = std::fs::write(std::path::Path::new(&dir).join(format!("{id}_{t}.ppm")), ppm);
}
assert!(!nan, "{id} @ {t}: NaN or infinite pixel");
assert!(sd > 0.015, "{id} @ {t}: flat frame (mean {mean:.3}, sd {sd:.4})");
let (again, ..) = frame(&s, t);
assert!(px == again, "{id} @ {t}: render is not deterministic");
frames.push(px);
}
assert!(diff(&frames[0], &frames[1]) > 100.0, "{id}: frame does not move between 0 s and 1.3 s");
assert!(diff(&frames[1], &frames[2]) > 100.0, "{id}: frame does not move between 1.3 s and 3.7 s");
s.execute("edit.undo", json!({})).unwrap();
let fx = s.active_comp().map_or(1, |c| c.layers[0].effects().map_or(0, |fx| fx.groups().count()));
assert_eq!(fx, 0, "{id}: undo removes the effects");
}
}