#![allow(clippy::needless_pass_by_value)]
use ffcine_core::geom;
use ffcine_render::Grade;
use serde_json::{Value, json};
use crate::pass::{self, GEOMETRY_HOOK, SAFE_AREA_HOOK};
use crate::{Fit, Profile, Rect, Reframe, Shot};
const UHD: Shot = Shot {
width: 3840,
height: 2160,
frames: 120,
rate: 24000.0 / 1001.0,
start: 0.0,
};
fn reframe(v: Value, profile: Option<&str>) -> Reframe {
Reframe::parse(v.as_object().unwrap(), profile).unwrap()
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
#[test]
fn static_zoom_is_the_prototype_crop_unrounded() {
let r = reframe(json!({"zoom": 1.3, "zoom_center": [0.4, 0.6]}), None);
let f = r.frame(0, &UHD, &Profile::preset("landscape").unwrap());
let (w, h) = (3840.0 / 1.3, 2160.0 / 1.3);
assert!(close(f.crop.w, w) && close(f.crop.h, h));
assert!(close(f.crop.x, (3840.0 - w) * 0.4) && close(f.crop.y, (2160.0 - h) * 0.6));
assert!(f.place.is_none());
assert!(!r.is_animated());
}
#[test]
fn push_is_smooth_and_keeps_aspect() {
let r = reframe(
json!({"push": [1.0, 1.12], "zoom_center": [0.5, 0.4]}),
None,
);
let out = Profile::preset("hd").unwrap();
let frames = r.frames(&UHD, &out);
assert_eq!(frames.len(), 120);
assert!(close(frames[0].crop.w, 3840.0));
assert!(close(frames[119].crop.w, 3840.0 / 1.12));
let mut fractional = 0;
for w in frames.windows(3) {
let [a, b, c] = [w[0].crop, w[1].crop, w[2].crop];
assert!(b.w < a.w && c.w < b.w);
assert!((b.aspect() - 16.0 / 9.0).abs() < 1e-12);
assert!(b.x >= 0.0 && b.x + b.w <= 3840.0 + 1e-9);
let d2w = (c.w - b.w) - (b.w - a.w);
let d2x = (c.x - b.x) - (b.x - a.x);
assert!(d2w.abs() < 0.2 && d2x.abs() < 0.2, "{d2w} {d2x}");
assert!(close(b.x, (3840.0 - b.w) * 0.5) && close(b.y, (2160.0 - b.h) * 0.4));
if b.w.fract() != 0.0 {
fractional += 1;
}
}
assert!(fractional > 100);
}
#[test]
fn punches_cap_and_cut_on_frames() {
let r = reframe(
json!({"zoom": 1.1, "punch": [
{"at": 1.0, "to": 2.0, "zoom": 2.0, "center": [0.2, 0.3]},
{"at": 3.0}
]}),
None,
);
let out = Profile::preset("landscape").unwrap();
let at = |i| r.frame(i, &UHD, &out).crop;
let a = 24; assert!(close(at(a - 1).w, 3840.0 / 1.1));
let p = at(a);
assert!(close(p.w, 3840.0 / (1.4 * 1.1)));
assert!(close(p.x, (3840.0 - p.w) * 0.2));
assert!(close(at(48).w, 3840.0 / 1.1));
let q = at(100);
assert!(close(q.w, 3840.0 / (1.25 * 1.1)) && close(q.x, (3840.0 - q.w) * 0.5));
}
#[test]
fn vertical_windows_slide_and_pan() {
let reel = Profile::preset("reel").unwrap();
let feed = Profile::preset("feed").unwrap();
let r = reframe(json!({"crop_x": 0.3}), None);
let f = r.frame(0, &UHD, &reel).crop;
assert!(close(f.w, 1215.0) && close(f.h, 2160.0) && close(f.x, (3840.0 - 1215.0) * 0.3));
let f = r.frame(0, &UHD, &feed).crop;
assert!(close(f.w, 1728.0) && close(f.x, (3840.0 - 1728.0) * 0.3));
let r = reframe(json!({"crop_x": [0.2, 0.8]}), None);
assert!(r.is_animated());
let x = |i| r.frame(i, &UHD, &reel).crop.x / (3840.0 - 1215.0);
assert!(close(x(0), 0.2));
assert!(close(x(60), 0.5));
assert!(x(119) > 0.79 && x(119) < 0.8);
let proto = reframe(json!({"crop_x": [[1.0, 0.2], [3.0, 0.9]]}), None);
let rt = |s: f64| json!({"OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": s * 24.0});
let keys = reframe(
json!({"crop_x": [
{"t": rt(1.0), "v": 0.2, "interp": "ease"},
{"t": rt(3.0), "v": 0.9, "interp": "linear"}
]}),
None,
);
for i in [0, 30, 48, 60, 100] {
assert!(close(
proto.frame(i, &UHD, &reel).crop.x,
keys.frame(i, &UHD, &reel).crop.x
));
}
let f = r.frame(10, &UHD, &Profile::preset("hd").unwrap()).crop;
assert_eq!(f, Rect::frame(3840.0, 2160.0));
}
#[test]
fn keyframed_zoom_and_centre() {
let rt = |s: f64| json!({"OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": s * 24.0});
let r = reframe(
json!({
"zoom": [{"t": rt(0.0), "v": 1.0, "interp": "ease"}, {"t": rt(4.0), "v": 1.5}],
"zoom_center": [{"t": rt(0.0), "v": [0.5, 0.5]}, {"t": rt(4.0), "v": [0.8, 0.2]}]
}),
None,
);
assert!(r.is_animated());
let out = Profile::preset("landscape").unwrap();
let shot = Shot { rate: 24.0, ..UHD };
let f = r.frame(48, &shot, &out).crop; assert!(close(f.w, 3840.0 / 1.25));
assert!(close(f.x, (3840.0 - f.w) * 0.65) && close(f.y, (2160.0 - f.h) * 0.35));
let late = Shot { start: 2.0, ..shot };
assert_eq!(r.frame(0, &late, &out).crop, f);
let proto = reframe(json!({"crop_x": [[0.0, 0.0], [1.0, 1.0]]}), None);
let reel = Profile::preset("reel").unwrap();
assert_eq!(proto.frame(0, &late, &reel).crop.x, 0.0);
}
#[test]
fn profiles_override_per_deliverable() {
let v = json!({
"zoom": 1.1, "crop_x": 0.5,
"profiles": {"reel": {"crop_x": 0.2}, "feed": {"crop_x": 0.7, "zoom": 1.0}}
});
let reel = reframe(v.clone(), Some("reel"));
let feed = reframe(v.clone(), Some("feed"));
let base = reframe(v, Some("landscape"));
let rp = Profile::preset("reel").unwrap();
let fp = Profile::preset("feed").unwrap();
let r = reel.frame(0, &UHD, &rp).crop;
let aspect = Rect::frame(3840.0, 2160.0).fit_aspect(9.0 / 16.0, [0.2, 0.5]);
assert_eq!(r, aspect.zoom(1.1, [0.5, 0.5]));
let f = feed.frame(0, &UHD, &fp).crop;
assert!(close(f.w, 1728.0) && close(f.x, (3840.0 - 1728.0) * 0.7));
let centred = Rect::frame(3840.0, 2160.0).fit_aspect(9.0 / 16.0, [0.5, 0.5]);
assert_eq!(base.frame(0, &UHD, &rp).crop, centred.zoom(1.1, [0.5, 0.5]));
}
#[test]
fn contain_places_the_whole_picture() {
let r = reframe(json!({"fit": "contain", "fit_y": 0.42}), None);
assert_eq!(r.fit, Fit::Contain);
let f = r.frame(0, &UHD, &Profile::preset("reel").unwrap());
assert_eq!(f.crop, Rect::frame(3840.0, 2160.0));
let p = f.place.unwrap();
assert!(close(p.w, 1080.0) && close(p.h, 608.0));
assert!(close(p.y, (1920.0f64 * 0.42 - 607.5 / 2.0).round()));
assert_eq!(reframe(json!({"mode": "fit"}), None).fit, Fit::Contain);
}
#[test]
fn warp_composes_with_the_window() {
let reel = Profile::preset("reel").unwrap();
let r = reframe(json!({"crop_x": [0.2, 0.8], "push": [1.0, 1.1]}), None);
let frames = r.frames(&UHD, &reel);
let (iw, ih) = pass::intermediate_size(&frames);
assert_eq!((iw, ih), (1215, 2160));
let warps: Vec<_> = (0..120)
.map(|i| geom::similarity([1920.0, 1080.0], f64::from(i) * 0.1, -2.0, 0.01, 1.03))
.collect();
let mats = pass::compose(&frames, &warps, iw, ih);
assert_eq!(mats.len(), 120);
for (i, m) in mats.iter().enumerate() {
let q = [300.5, 1700.25];
let c = frames[i].crop;
let in_window = [
c.x + q[0] * c.w / f64::from(iw),
c.y + q[1] * c.h / f64::from(ih),
];
let want = geom::apply(&warps[i], in_window);
let got = geom::apply(m, q);
assert!(close(got[0], want[0]) && close(got[1], want[1]));
}
let id = pass::compose(&frames, &[geom::IDENTITY], iw, ih);
assert_eq!(id[7], frames[7].crop.to_mat3(f64::from(iw), f64::from(ih)));
}
#[test]
fn apply_to_grade() {
let mut reel = Profile::preset("reel").unwrap();
reel.safe_area = Some(reel.default_safe_area());
let r = reframe(json!({"crop_x": [0.2, 0.8]}), None);
let frames = r.frames(&UHD, &reel);
let mut g = Grade::default();
pass::apply(&mut g, &frames, &[], (3840, 2160), &reel);
assert_eq!(g.size.map(|s| (s.width, s.height)), Some((1080, 1920)));
assert_eq!(g.crops.len(), 120);
assert_eq!(g.crop_at(500), g.crops.last().copied());
assert_eq!(g.hooks.len(), 1);
assert_eq!(g.hooks[0].name, SAFE_AREA_HOOK);
let mut g = Grade::default();
pass::apply(
&mut g,
&frames,
&[geom::translation(3.0, 1.0)],
(3840, 2160),
&reel,
);
assert!(g.crops.is_empty() && g.crop.is_none());
let h = &g.hooks[0];
assert_eq!(h.name, GEOMETRY_HOOK);
assert_eq!(h.tracks.len(), 9);
assert!(h.tracks.iter().all(|t| t.values.len() == 120));
let m02 = &h.tracks[2];
assert!((f64::from(m02.values[0]) - (frames[0].crop.x + 3.0)).abs() < 1e-3);
assert!(h.source.contains("//!WIDTH 1215\n//!HEIGHT 2160\n"));
assert_eq!(h.source.matches("//!PARAM m").count(), 9);
for line in h.source.lines().chain(g.hooks[1].source.lines()) {
if let Some(i) = line.find("//!") {
assert_eq!(i, 0, "{line}");
}
}
}
#[test]
fn reads_the_effect_from_a_clip() {
use ffcine_timeline::schema::{Effect, GenericEffect};
let params = json!({"zoom": 1.2, "profiles": {"reel": {"crop_x": 0.1}}});
let e = Effect::Effect(GenericEffect::ffcine(
"ffcine.geo.reframe/1",
params.as_object().unwrap().clone(),
));
let r = Reframe::from_effects(&[e], Some("reel")).unwrap().unwrap();
assert!(r.zoom.is_some());
assert!(matches!(r.crop_x, crate::Slide::Keyed(crate::Keyed::Const(x)) if close(x, 0.1)));
assert!(Reframe::from_effects(&[], None).unwrap().is_none());
}