use std::fmt::Write as _;
use ffcine_core::geom::{self, Mat3};
use ffcine_render::{Border, Grade, Hook, ParamTrack, Size};
use ffcine_stabilize::hook::WARP_GLSL;
use crate::geo::Mapped;
use crate::profile::{Profile, SafeArea};
use crate::reframe::Frame;
pub const GEOMETRY_HOOK: &str = "ffcine:geometry";
pub const SAFE_AREA_HOOK: &str = "ffcine:safe-area";
const M: [&str; 9] = [
"m00", "m01", "m02", "m10", "m11", "m12", "m20", "m21", "m22",
];
pub fn apply(
grade: &mut Grade,
frames: &[Frame],
warps: &[Mat3],
source: (u32, u32),
out: &Profile,
) {
grade.size = Some(Size {
width: out.width,
height: out.height,
});
grade.place = frames.first().and_then(|f| f.place).map(|r| r.to_crop());
#[allow(clippy::cast_possible_truncation)]
let blur = frames.first().and_then(|f| f.blur).map(|r| r as f32);
grade.border = blur.map_or(Border::Black, Border::Blur);
if warps.is_empty() {
grade.crop = None;
grade.crops = frames.iter().map(|f| f.crop.to_crop()).collect();
} else {
grade.crop = None;
grade.crops.clear();
grade.hooks.insert(0, geometry_hook(frames, warps, source));
}
if let Some(area) = out.safe_area {
grade.hooks.push(safe_area_hook(&area));
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn intermediate_size(frames: &[Frame]) -> (u32, u32) {
let w = frames.iter().map(|f| f.crop.w).fold(1.0_f64, f64::max);
let ar = frames.first().map_or(1.0, |f| f.crop.aspect());
(w.ceil() as u32, (w / ar).round().max(1.0) as u32)
}
pub fn compose(frames: &[Frame], warps: &[Mat3], iw: u32, ih: u32) -> Vec<Mat3> {
let n = frames.len().max(warps.len());
(0..n)
.map(|i| {
let crop = frames.get(i).or(frames.last()).map_or(geom::IDENTITY, |f| {
f.crop.to_mat3(f64::from(iw), f64::from(ih))
});
let warp = warps.get(i).or(warps.last()).unwrap_or(&geom::IDENTITY);
geom::mul(warp, &crop)
})
.collect()
}
pub fn geometry_hook(frames: &[Frame], warps: &[Mat3], (sw, sh): (u32, u32)) -> Hook {
let (iw, ih) = intermediate_size(frames);
let mats = compose(frames, warps, iw, ih);
let mut s = String::with_capacity(4096);
for (k, name) in M.iter().enumerate() {
let init = if k % 4 == 0 { "1.0" } else { "0.0" };
let _ = write!(s, "//!PARAM {name}\n//!TYPE DYNAMIC float\n{init}\n\n");
}
let _ = write!(
s,
"//!HOOK MAIN
//!BIND HOOKED
//!WIDTH {iw}
//!HEIGHT {ih}
//!DESC ffcine reframe + warp ({sw}x{sh} -> {iw}x{ih})
#define WARP_TEX HOOKED_tex
#define WARP_PT HOOKED_pt
#define WARP_SIZE vec2({sw}.0, {sh}.0)
{WARP_GLSL}
vec4 hook() {{
return ffcine_warp(vec3(m00, m01, m02), vec3(m10, m11, m12), vec3(m20, m21, m22),
HOOKED_pos * vec2({iw}.0, {ih}.0));
}}
"
);
#[allow(clippy::cast_possible_truncation)]
let tracks = M
.iter()
.enumerate()
.map(|(k, name)| ParamTrack {
name: (*name).to_owned(),
values: mats.iter().map(|m| m[k / 3][k % 3] as f32).collect(),
})
.collect();
Hook {
name: GEOMETRY_HOOK.to_owned(),
source: s,
params: Vec::new(),
tracks,
}
}
pub fn apply_mapped(
grade: &mut Grade,
mapped: &[Mapped],
warps: &[Mat3],
source: (u32, u32),
out: &Profile,
) {
grade.size = Some(Size {
width: out.width,
height: out.height,
});
grade.place = None;
grade.border = Border::Black;
grade.crop = None;
grade.crops.clear();
grade.warp = None;
grade.hooks.insert(
0,
mapped_hook(mapped, warps, source, (out.width, out.height)),
);
if let Some(area) = out.safe_area {
grade.hooks.push(safe_area_hook(&area));
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn mapped_size(mapped: &[Mapped], (ow, oh): (u32, u32)) -> (u32, u32) {
let k = |c: usize| {
mapped
.iter()
.map(|m| m.m[0][c].hypot(m.m[1][c]))
.fold(1.0_f64, f64::max)
};
let side = |o: u32, k: f64| (f64::from(o) * k).ceil().clamp(1.0, 8192.0) as u32;
(side(ow, k(0)), side(oh, k(1)))
}
const MASK: [&str; 5] = ["mx0", "my0", "mx1", "my1", "feather"];
#[allow(clippy::cast_possible_truncation)]
pub fn mapped_hook(
mapped: &[Mapped],
warps: &[Mat3],
(sw, sh): (u32, u32),
(ow, oh): (u32, u32),
) -> Hook {
let (iw, ih) = mapped_size(mapped, (ow, oh));
let to_out = [
[f64::from(ow) / f64::from(iw), 0.0, 0.0],
[0.0, f64::from(oh) / f64::from(ih), 0.0],
[0.0, 0.0, 1.0],
];
let n = mapped.len().max(warps.len()).max(1);
let at = |i: usize| mapped.get(i).or(mapped.last());
let maps: Vec<Mat3> = (0..n)
.map(|i| at(i).map_or(to_out, |m| geom::mul(&m.m, &to_out)))
.collect();
let stab: Vec<Mat3> = (0..n)
.map(|i| *warps.get(i).or(warps.last()).unwrap_or(&geom::IDENTITY))
.collect();
let full = [0.0, 0.0, f64::from(sw), f64::from(sh), 0.0];
let masks: Vec<[f64; 5]> = (0..n)
.map(|i| {
at(i).map_or(full, |m| {
[
m.mask.x,
m.mask.y,
m.mask.x + m.mask.w,
m.mask.y + m.mask.h,
m.feather,
]
})
})
.collect();
let mut s = String::with_capacity(4096);
let mut tracks = Vec::new();
for (prefix, mats) in [("a", &maps), ("m", &stab)] {
for (k, name) in M.iter().enumerate() {
let name = format!("{prefix}{}", &name[1..]);
let init = if k % 4 == 0 { "1.0" } else { "0.0" };
let _ = write!(s, "//!PARAM {name}\n//!TYPE DYNAMIC float\n{init}\n\n");
tracks.push(ParamTrack {
name,
values: mats.iter().map(|m| m[k / 3][k % 3] as f32).collect(),
});
}
}
for (k, name) in MASK.iter().enumerate() {
let _ = write!(s, "//!PARAM {name}\n//!TYPE DYNAMIC float\n{}\n\n", full[k]);
tracks.push(ParamTrack {
name: (*name).to_owned(),
values: masks.iter().map(|m| m[k] as f32).collect(),
});
}
let _ = write!(
s,
"//!HOOK MAIN
//!BIND HOOKED
//!WIDTH {iw}
//!HEIGHT {ih}
//!DESC ffcine crop + transform ({sw}x{sh} -> {iw}x{ih})
#define WARP_TEX HOOKED_tex
#define WARP_PT HOOKED_pt
#define WARP_SIZE vec2({sw}.0, {sh}.0)
{WARP_GLSL}
vec4 hook() {{
vec3 d = vec3(HOOKED_pos * vec2({iw}.0, {ih}.0), 1.0);
vec3 h = vec3(dot(vec3(a00, a01, a02), d), dot(vec3(a10, a11, a12), d),
dot(vec3(a20, a21, a22), d));
vec2 q = h.xy / h.z;
vec2 e = min(q - vec2(mx0, my0), vec2(mx1, my1) - q);
float k = min(e.x, e.y);
float a = feather > 0.0 ? clamp(k / feather, 0.0, 1.0) : step(0.0, k);
vec4 c = ffcine_warp(vec3(m00, m01, m02), vec3(m10, m11, m12), vec3(m20, m21, m22), q);
return vec4(c.rgb * a, 1.0);
}}
"
);
Hook {
name: GEOMETRY_HOOK.to_owned(),
source: s,
params: Vec::new(),
tracks,
}
}
pub fn safe_area_hook(area: &SafeArea) -> Hook {
let SafeArea {
top,
right,
bottom,
left,
} = *area;
let source = format!(
"//!HOOK PREOUTPUT
//!BIND HOOKED
//!DESC ffcine safe area
vec4 hook() {{
vec4 c = HOOKED_texOff(vec2(0.0));
vec2 s = HOOKED_size;
vec2 p = HOOKED_pos * s;
vec2 lo = vec2({left:.5}, {top:.5}) * s;
vec2 hi = vec2({:.5}, {:.5}) * s;
float lw = max(2.0, s.y / 540.0);
bool inside = all(greaterThanEqual(p, lo)) && all(lessThan(p, hi));
vec2 d = min(abs(p - lo), abs(p - hi));
bool edge = all(greaterThanEqual(p, lo - lw)) && all(lessThan(p, hi + lw)) && min(d.x, d.y) < lw;
if (!inside)
c.rgb *= 0.5;
if (edge)
c.rgb = vec3(1.0, 0.85, 0.0);
return c;
}}
",
1.0 - right,
1.0 - bottom,
);
Hook {
name: SAFE_AREA_HOOK.to_owned(),
source,
..Hook::default()
}
}