1use std::fmt::Write as _;
7
8use ffcine_core::geom::{self, Mat3};
9use ffcine_render::{Border, Grade, Hook, ParamTrack, Size};
10use ffcine_stabilize::hook::WARP_GLSL;
11
12use crate::geo::Mapped;
13use crate::profile::{Profile, SafeArea};
14use crate::reframe::Frame;
15
16pub const GEOMETRY_HOOK: &str = "ffcine:geometry";
18pub const SAFE_AREA_HOOK: &str = "ffcine:safe-area";
19
20const M: [&str; 9] = [
21 "m00", "m01", "m02", "m10", "m11", "m12", "m20", "m21", "m22",
22];
23
24pub fn apply(
34 grade: &mut Grade,
35 frames: &[Frame],
36 warps: &[Mat3],
37 source: (u32, u32),
38 out: &Profile,
39) {
40 grade.size = Some(Size {
41 width: out.width,
42 height: out.height,
43 });
44 grade.place = frames.first().and_then(|f| f.place).map(|r| r.to_crop());
45 #[allow(clippy::cast_possible_truncation)]
46 let blur = frames.first().and_then(|f| f.blur).map(|r| r as f32);
47 grade.border = blur.map_or(Border::Black, Border::Blur);
48 if warps.is_empty() {
49 grade.crop = None;
50 grade.crops = frames.iter().map(|f| f.crop.to_crop()).collect();
51 } else {
52 grade.crop = None;
53 grade.crops.clear();
54 grade.hooks.insert(0, geometry_hook(frames, warps, source));
55 }
56 if let Some(area) = out.safe_area {
57 grade.hooks.push(safe_area_hook(&area));
58 }
59}
60
61#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
63pub fn intermediate_size(frames: &[Frame]) -> (u32, u32) {
64 let w = frames.iter().map(|f| f.crop.w).fold(1.0_f64, f64::max);
65 let ar = frames.first().map_or(1.0, |f| f.crop.aspect());
66 (w.ceil() as u32, (w / ar).round().max(1.0) as u32)
67}
68
69pub fn compose(frames: &[Frame], warps: &[Mat3], iw: u32, ih: u32) -> Vec<Mat3> {
72 let n = frames.len().max(warps.len());
73 (0..n)
74 .map(|i| {
75 let crop = frames.get(i).or(frames.last()).map_or(geom::IDENTITY, |f| {
76 f.crop.to_mat3(f64::from(iw), f64::from(ih))
77 });
78 let warp = warps.get(i).or(warps.last()).unwrap_or(&geom::IDENTITY);
79 geom::mul(warp, &crop)
80 })
81 .collect()
82}
83
84pub fn geometry_hook(frames: &[Frame], warps: &[Mat3], (sw, sh): (u32, u32)) -> Hook {
87 let (iw, ih) = intermediate_size(frames);
88 let mats = compose(frames, warps, iw, ih);
89 let mut s = String::with_capacity(4096);
90 for (k, name) in M.iter().enumerate() {
91 let init = if k % 4 == 0 { "1.0" } else { "0.0" };
92 let _ = write!(s, "//!PARAM {name}\n//!TYPE DYNAMIC float\n{init}\n\n");
93 }
94 let _ = write!(
95 s,
96 "//!HOOK MAIN
97//!BIND HOOKED
98//!WIDTH {iw}
99//!HEIGHT {ih}
100//!DESC ffcine reframe + warp ({sw}x{sh} -> {iw}x{ih})
101#define WARP_TEX HOOKED_tex
102#define WARP_PT HOOKED_pt
103#define WARP_SIZE vec2({sw}.0, {sh}.0)
104{WARP_GLSL}
105vec4 hook() {{
106 return ffcine_warp(vec3(m00, m01, m02), vec3(m10, m11, m12), vec3(m20, m21, m22),
107 HOOKED_pos * vec2({iw}.0, {ih}.0));
108}}
109"
110 );
111 #[allow(clippy::cast_possible_truncation)]
112 let tracks = M
113 .iter()
114 .enumerate()
115 .map(|(k, name)| ParamTrack {
116 name: (*name).to_owned(),
117 values: mats.iter().map(|m| m[k / 3][k % 3] as f32).collect(),
118 })
119 .collect();
120 Hook {
121 name: GEOMETRY_HOOK.to_owned(),
122 source: s,
123 params: Vec::new(),
124 tracks,
125 }
126}
127
128pub fn apply_mapped(
134 grade: &mut Grade,
135 mapped: &[Mapped],
136 warps: &[Mat3],
137 source: (u32, u32),
138 out: &Profile,
139) {
140 grade.size = Some(Size {
141 width: out.width,
142 height: out.height,
143 });
144 grade.place = None;
145 grade.border = Border::Black;
146 grade.crop = None;
147 grade.crops.clear();
148 grade.warp = None;
149 grade.hooks.insert(
150 0,
151 mapped_hook(mapped, warps, source, (out.width, out.height)),
152 );
153 if let Some(area) = out.safe_area {
154 grade.hooks.push(safe_area_hook(&area));
155 }
156}
157
158#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
161pub fn mapped_size(mapped: &[Mapped], (ow, oh): (u32, u32)) -> (u32, u32) {
162 let k = |c: usize| {
164 mapped
165 .iter()
166 .map(|m| m.m[0][c].hypot(m.m[1][c]))
167 .fold(1.0_f64, f64::max)
168 };
169 let side = |o: u32, k: f64| (f64::from(o) * k).ceil().clamp(1.0, 8192.0) as u32;
170 (side(ow, k(0)), side(oh, k(1)))
171}
172
173const MASK: [&str; 5] = ["mx0", "my0", "mx1", "my1", "feather"];
174
175#[allow(clippy::cast_possible_truncation)]
178pub fn mapped_hook(
179 mapped: &[Mapped],
180 warps: &[Mat3],
181 (sw, sh): (u32, u32),
182 (ow, oh): (u32, u32),
183) -> Hook {
184 let (iw, ih) = mapped_size(mapped, (ow, oh));
185 let to_out = [
186 [f64::from(ow) / f64::from(iw), 0.0, 0.0],
187 [0.0, f64::from(oh) / f64::from(ih), 0.0],
188 [0.0, 0.0, 1.0],
189 ];
190 let n = mapped.len().max(warps.len()).max(1);
191 let at = |i: usize| mapped.get(i).or(mapped.last());
192 let maps: Vec<Mat3> = (0..n)
193 .map(|i| at(i).map_or(to_out, |m| geom::mul(&m.m, &to_out)))
194 .collect();
195 let stab: Vec<Mat3> = (0..n)
196 .map(|i| *warps.get(i).or(warps.last()).unwrap_or(&geom::IDENTITY))
197 .collect();
198 let full = [0.0, 0.0, f64::from(sw), f64::from(sh), 0.0];
199 let masks: Vec<[f64; 5]> = (0..n)
200 .map(|i| {
201 at(i).map_or(full, |m| {
202 [
203 m.mask.x,
204 m.mask.y,
205 m.mask.x + m.mask.w,
206 m.mask.y + m.mask.h,
207 m.feather,
208 ]
209 })
210 })
211 .collect();
212 let mut s = String::with_capacity(4096);
213 let mut tracks = Vec::new();
214 for (prefix, mats) in [("a", &maps), ("m", &stab)] {
215 for (k, name) in M.iter().enumerate() {
216 let name = format!("{prefix}{}", &name[1..]);
217 let init = if k % 4 == 0 { "1.0" } else { "0.0" };
218 let _ = write!(s, "//!PARAM {name}\n//!TYPE DYNAMIC float\n{init}\n\n");
219 tracks.push(ParamTrack {
220 name,
221 values: mats.iter().map(|m| m[k / 3][k % 3] as f32).collect(),
222 });
223 }
224 }
225 for (k, name) in MASK.iter().enumerate() {
226 let _ = write!(s, "//!PARAM {name}\n//!TYPE DYNAMIC float\n{}\n\n", full[k]);
227 tracks.push(ParamTrack {
228 name: (*name).to_owned(),
229 values: masks.iter().map(|m| m[k] as f32).collect(),
230 });
231 }
232 let _ = write!(
233 s,
234 "//!HOOK MAIN
235//!BIND HOOKED
236//!WIDTH {iw}
237//!HEIGHT {ih}
238//!DESC ffcine crop + transform ({sw}x{sh} -> {iw}x{ih})
239#define WARP_TEX HOOKED_tex
240#define WARP_PT HOOKED_pt
241#define WARP_SIZE vec2({sw}.0, {sh}.0)
242{WARP_GLSL}
243vec4 hook() {{
244 vec3 d = vec3(HOOKED_pos * vec2({iw}.0, {ih}.0), 1.0);
245 vec3 h = vec3(dot(vec3(a00, a01, a02), d), dot(vec3(a10, a11, a12), d),
246 dot(vec3(a20, a21, a22), d));
247 vec2 q = h.xy / h.z;
248 vec2 e = min(q - vec2(mx0, my0), vec2(mx1, my1) - q);
249 float k = min(e.x, e.y);
250 float a = feather > 0.0 ? clamp(k / feather, 0.0, 1.0) : step(0.0, k);
251 vec4 c = ffcine_warp(vec3(m00, m01, m02), vec3(m10, m11, m12), vec3(m20, m21, m22), q);
252 return vec4(c.rgb * a, 1.0);
253}}
254"
255 );
256 Hook {
257 name: GEOMETRY_HOOK.to_owned(),
258 source: s,
259 params: Vec::new(),
260 tracks,
261 }
262}
263
264pub fn safe_area_hook(area: &SafeArea) -> Hook {
267 let SafeArea {
268 top,
269 right,
270 bottom,
271 left,
272 } = *area;
273 let source = format!(
274 "//!HOOK PREOUTPUT
275//!BIND HOOKED
276//!DESC ffcine safe area
277vec4 hook() {{
278 vec4 c = HOOKED_texOff(vec2(0.0));
279 vec2 s = HOOKED_size;
280 vec2 p = HOOKED_pos * s;
281 vec2 lo = vec2({left:.5}, {top:.5}) * s;
282 vec2 hi = vec2({:.5}, {:.5}) * s;
283 float lw = max(2.0, s.y / 540.0);
284 bool inside = all(greaterThanEqual(p, lo)) && all(lessThan(p, hi));
285 vec2 d = min(abs(p - lo), abs(p - hi));
286 bool edge = all(greaterThanEqual(p, lo - lw)) && all(lessThan(p, hi + lw)) && min(d.x, d.y) < lw;
287 if (!inside)
288 c.rgb *= 0.5;
289 if (edge)
290 c.rgb = vec3(1.0, 0.85, 0.0);
291 return c;
292}}
293",
294 1.0 - right,
295 1.0 - bottom,
296 );
297 Hook {
298 name: SAFE_AREA_HOOK.to_owned(),
299 source,
300 ..Hook::default()
301 }
302}