mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 12:37:08 -06:00
* 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
61 lines
3.3 KiB
Rust
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");
|
|
}
|
|
}
|