Files
effectcraft/crates/render/src/tests/paint.rs
T

114 lines
5.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Paint and Puppet through the full pipeline: layer cache keys, resolution, determinism.
use effectcraft_color::Label;
use effectcraft_effects::paint::{self, StrokeSpec};
use effectcraft_effects::puppet::{self, MeshOpts, PinKind};
use effectcraft_keyframe::{Keyframe, Value};
use effectcraft_project::build::{self, Ids};
use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, Project, PropGroup, Solid};
use effectcraft_time::{FrameRate, Tick};
use crate::{LayerCache, NoFootage, RenderOpts, Renderer};
fn scene(fx: impl FnOnce(&mut Ids) -> PropGroup) -> (Project, ItemId) {
let mut p = Project::default();
let comp = Comp::new(100, 80, FrameRate::FPS_30, Tick::from_seconds_f64(3.0));
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
let sid = p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color: [0.0, 0.0, 1.0], width: 60, height: 40, pixel_aspect: 1.0 }));
let mut l = build::layer(&mut p, &comp, "Solid", LayerSource::Solid { item: sid }, (60, 40), None);
let mut next = p.next_id;
let g = fx(&mut Ids(&mut next));
p.next_id = next;
l.props.sub_mut("effects").unwrap().children.push(g.into());
p.comp_mut(cid).unwrap().layers.push(l);
(p, cid)
}
fn paint_fx(ids: &mut Ids, strokes: &[StrokeSpec]) -> PropGroup {
let mut g = effectcraft_effects::instantiate(effectcraft_effects::find(paint::ID).unwrap(), ids, "Paint", [60.0, 40.0]);
for (i, s) in strokes.iter().enumerate() {
g.children.push(paint::stroke_group(ids, &format!("Brush {}", i + 1), s).into());
}
g
}
fn frame(p: &Project, cid: ItemId, t: f64, scale: f64, cache: Option<&LayerCache>) -> crate::Image {
let mut r = Renderer::new(p, &NoFootage, RenderOpts { scale, ..Default::default() });
r.cache = cache;
r.comp_frame(cid, Tick::from_seconds_f64(t))
}
#[test]
fn stroke_spans_are_part_of_the_cache_key() {
// Layer at comp (20, 20)–(80, 60). A red dab at layer (10, 10) = comp (30, 30), shown 1–2 s.
let s = StrokeSpec { points: vec![[10.5, 10.5]], color: [1.0, 0.0, 0.0, 1.0], in_time: 1.0, out_time: 2.0, size_pressure: false, ..Default::default() };
let (p, cid) = scene(|ids| paint_fx(ids, &[s]));
let cache = LayerCache::default();
for t in [0.5, 1.5, 0.5, 2.5, 1.5] {
let a = frame(&p, cid, t, 1.0, Some(&cache));
let b = frame(&p, cid, t, 1.0, None);
assert_eq!(a, b, "t={t}");
let red = a.get(30, 30)[0] > 0.99;
assert_eq!(red, (1.0..2.0).contains(&t), "t={t}");
}
assert!(cache.stats().hits > 0);
}
#[test]
fn write_on_keys_animate_end() {
let s = StrokeSpec { points: vec![[5.5, 20.5], [55.5, 20.5]], color: [1.0, 0.0, 0.0, 1.0], size_pressure: false, ..Default::default() };
let (mut p, cid) = scene(|ids| paint_fx(ids, &[s]));
{
let l = &mut p.comp_mut(cid).unwrap().layers[0];
let end = l.props.prop_mut("effects/#1/brush/stroke_options/end").unwrap();
end.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(100.0))];
}
let cache = LayerCache::default();
let at = |t: f64, x: u32| frame(&p, cid, t, 1.0, Some(&cache)).get(x as i64, 40)[0] > 0.99;
// Layer x 5..55 → comp x 25..75.
assert!(!at(0.0, 30));
assert!(at(0.5, 30) && at(0.5, 45) && !at(0.5, 65));
assert!(at(1.0, 74));
}
#[test]
fn paint_respects_resolution() {
let s = StrokeSpec { points: vec![[10.5, 10.5], [40.5, 30.5]], diameter: 8.0, color: [1.0, 0.0, 0.0, 1.0], ..Default::default() };
let (p, cid) = scene(|ids| paint_fx(ids, &[s]));
let full = frame(&p, cid, 0.0, 1.0, None);
let half = frame(&p, cid, 0.0, 0.5, None);
assert_eq!((half.width, half.height), (50, 40));
// Same spot, both resolutions.
assert!(full.get(45, 40)[0] > 0.9);
assert!(half.get(22, 20)[0] > 0.9);
}
#[test]
fn puppet_moves_pixels_and_caches() {
let (mut p, cid) = scene(|ids| {
let mut fx = effectcraft_effects::instantiate(effectcraft_effects::find(puppet::ID).unwrap(), ids, "Puppet", [60.0, 40.0]);
let mut m = puppet::mesh_group(ids, "Mesh 1", [30.0, 20.0], &MeshOpts::default());
let mut pin = puppet::pin_group(ids, "Puppet Pin 1", PinKind::Position, [30.0, 20.0]);
pin.get_mut("position").unwrap().value = Value::Vec2([30.0, 20.0]);
m.sub_mut("deform").unwrap().children.push(pin.into());
fx.children.push(m.into());
fx
});
let cache = LayerCache::default();
let still = frame(&p, cid, 0.0, 1.0, Some(&cache));
assert_eq!(still.get(50, 40), [0.0, 0.0, 1.0, 1.0]);
assert_eq!(still.get(85, 40)[3], 0.0);
{
let l = &mut p.comp_mut(cid).unwrap().layers[0];
let pos = l.props.prop_mut("effects/#1/mesh/deform/pin/position").unwrap();
pos.keys = vec![Keyframe::new(Tick::ZERO, Value::Vec2([30.0, 20.0])), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Vec2([40.0, 20.0]))];
}
let moved = frame(&p, cid, 1.0, 1.0, Some(&cache));
assert_eq!(moved, frame(&p, cid, 1.0, 1.0, None));
// The 60 px wide layer shifted right by 10: comp x 30..90.
assert_eq!(moved.get(85, 40), [0.0, 0.0, 1.0, 1.0]);
assert_eq!(moved.get(25, 40)[3], 0.0);
let half = frame(&p, cid, 1.0, 0.5, None);
assert!(half.get(42, 20)[3] > 0.99);
}