mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
123 lines
6.0 KiB
Rust
123 lines
6.0 KiB
Rust
//! Motion blur of collapsed precomps (the precomp layer's own motion; the containing comp's
|
||
//! shutter) and of animated shape content, rendered through the commands.
|
||
|
||
use effectcraft_raster::Image;
|
||
use effectcraft_time::Tick;
|
||
use serde_json::{Value, json};
|
||
|
||
use crate::Session;
|
||
use crate::render::RenderOpts;
|
||
|
||
const W: u32 = 160;
|
||
const H: u32 = 90;
|
||
|
||
fn id(v: &Value, k: &str) -> u64 {
|
||
v[k].as_u64().unwrap()
|
||
}
|
||
|
||
fn render(s: &Session, comp: u64) -> Image {
|
||
s.render(effectcraft_project::ItemId(comp), Tick::from_seconds_f64(0.5), RenderOpts::default())
|
||
}
|
||
|
||
/// Pixels with partial alpha (motion-blur smear).
|
||
fn partial(img: &Image) -> usize {
|
||
img.data.iter().filter(|p| p[3] > 0.05 && p[3] < 0.95).count()
|
||
}
|
||
|
||
fn max_diff(a: &Image, b: &Image) -> f32 {
|
||
a.data.iter().zip(&b.data).flat_map(|(p, q)| (0..4).map(move |c| (p[c] - q[c]).abs())).fold(0.0, f32::max)
|
||
}
|
||
|
||
/// "Main" holding "Pre" (a 20×20 white square in the middle), the precomp layer moving 480 px/s
|
||
/// (20 px a frame at 24 fps) with motion blur on. `inner_move`: the square moves inside Pre too
|
||
/// (with its own motion blur switch), and Pre's shutter is closed (0°).
|
||
fn scene(collapse: bool, inner_move: bool) -> (Session, u64) {
|
||
let mut s = Session::default();
|
||
let pre = id(&s.execute("comp.new", json!({"name": "Pre", "width": W, "height": H, "frameRate": 24, "duration": 2})).unwrap(), "comp");
|
||
let sq = id(&s.execute("layer.newSolid", json!({"name": "Square", "color": "#ffffff", "width": 20, "height": 20})).unwrap(), "layer");
|
||
if inner_move {
|
||
s.execute("comp.settings", json!({"shutterAngle": 0})).unwrap();
|
||
s.execute("comp.setSwitch", json!({"switch": "motionBlur", "value": true})).unwrap();
|
||
s.execute("layer.setSwitch", json!({"layers": [sq], "switch": "motionBlur", "value": true})).unwrap();
|
||
for (t, x) in [(0.0, -160.0), (1.0, 320.0)] {
|
||
s.execute("prop.addKey", json!({"layer": sq, "path": "transform/position", "time": t, "value": [x, 45.0]})).unwrap();
|
||
}
|
||
}
|
||
let main = id(&s.execute("comp.new", json!({"name": "Main", "width": W, "height": H, "frameRate": 24, "duration": 2})).unwrap(), "comp");
|
||
s.execute("comp.setSwitch", json!({"switch": "motionBlur", "value": true})).unwrap();
|
||
let l = id(&s.execute("layer.addItem", json!({"item": pre})).unwrap(), "layer");
|
||
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "motionBlur", "value": true})).unwrap();
|
||
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "collapse", "value": collapse})).unwrap();
|
||
if !inner_move {
|
||
for (t, x) in [(0.0, -160.0), (1.0, 320.0)] {
|
||
s.execute("prop.addKey", json!({"layer": l, "path": "transform/position", "time": t, "value": [x, 45.0]})).unwrap();
|
||
}
|
||
}
|
||
(s, main)
|
||
}
|
||
|
||
#[test]
|
||
fn a_collapsed_precomp_layer_blurs_with_its_own_motion() {
|
||
let (s, main) = scene(true, false);
|
||
let collapsed = render(&s, main);
|
||
// The square sits at x = 80 at 0.5 s and smears over 10 px (180° of 20 px).
|
||
assert!(partial(&collapsed) > 100, "smeared: {}", partial(&collapsed));
|
||
// As blurred as the same precomp rendered flat.
|
||
let (s, main) = scene(false, false);
|
||
let flat = render(&s, main);
|
||
assert!(max_diff(&collapsed, &flat) < 0.08, "{}", max_diff(&collapsed, &flat));
|
||
// Motion blur off for the render: sharp.
|
||
let sharp = s.render(effectcraft_project::ItemId(main), Tick::from_seconds_f64(0.5), RenderOpts { motion_blur: false, ..Default::default() });
|
||
assert!(partial(&sharp) < 10, "{}", partial(&sharp));
|
||
}
|
||
|
||
#[test]
|
||
fn collapsed_layers_use_the_containing_comps_shutter() {
|
||
// Pre's own shutter is closed; drawn into Main (180°) the square's own motion blurs.
|
||
let (s, main) = scene(true, true);
|
||
assert!(partial(&render(&s, main)) > 100, "{}", partial(&render(&s, main)));
|
||
// Rendered flat, Pre uses its own (closed) shutter.
|
||
let (s, main) = scene(false, true);
|
||
assert!(partial(&render(&s, main)) < 10, "{}", partial(&render(&s, main)));
|
||
}
|
||
|
||
/// A shape layer whose rectangle moves inside its contents (not its transform): motion blur
|
||
/// smears it like the same move made with the layer's Position.
|
||
#[test]
|
||
fn animated_shape_content_is_motion_blurred() {
|
||
let shape = |content: bool, blur: bool| {
|
||
let mut s = Session::default();
|
||
let c = id(&s.execute("comp.new", json!({"name": "S", "width": W, "height": H, "frameRate": 24, "duration": 2})).unwrap(), "comp");
|
||
s.execute("comp.setSwitch", json!({"switch": "motionBlur", "value": true})).unwrap();
|
||
let l = id(&s.execute("layer.newShape", json!({"kind": "rect", "size": [20, 20], "fill": "#ffffff", "position": [80, 45]})).unwrap(), "layer");
|
||
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "motionBlur", "value": blur})).unwrap();
|
||
let (target, base) = if content {
|
||
let layer = s.active_comp().unwrap().layer(effectcraft_project::LayerId(l)).unwrap().clone();
|
||
let mut found = None;
|
||
layer.props.group("contents").unwrap().walk("contents", &mut |_, p| {
|
||
if found.is_none() && p.match_id == "position" {
|
||
found = Some((json!({"prop": p.uid}), p.value.as_vec2()));
|
||
}
|
||
});
|
||
found.unwrap()
|
||
} else {
|
||
(json!({"path": "transform/position"}), [80.0, 45.0])
|
||
};
|
||
for (t, dx) in [(0.0, -240.0), (1.0, 240.0)] {
|
||
let mut p = target.clone();
|
||
p["layer"] = json!(l);
|
||
p["time"] = json!(t);
|
||
p["value"] = json!([base[0] + dx, base[1]]);
|
||
s.execute("prop.addKey", p).unwrap();
|
||
}
|
||
render(&s, c)
|
||
};
|
||
let content = shape(true, true);
|
||
assert!(partial(&content) > 100, "smeared: {}", partial(&content));
|
||
// Like the layer moved by its transform…
|
||
let moved = shape(false, true);
|
||
assert!(max_diff(&content, &moved) < 0.15, "{}", max_diff(&content, &moved));
|
||
// …and sharp with the layer's motion blur switch off.
|
||
assert!(partial(&shape(true, false)) < 40, "{}", partial(&shape(true, false)));
|
||
}
|