1#![allow(clippy::many_single_char_names)]
7
8use ffcine_core::{Error, Result};
9use serde_json::{Map, Value};
10
11use crate::keyframe::{Key, Keyed, ease, is_keys};
12use crate::profile::Profile;
13use crate::window::Rect;
14
15pub const SCHEMA: &str = "ffcine.geo.reframe";
16pub const MAJOR: u32 = 1;
17
18#[derive(Debug, Clone, PartialEq)]
21pub struct Punch {
22 pub at: f64,
23 pub to: Option<f64>,
24 pub zoom: f64,
25 pub center: Option<[f64; 2]>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum Fit {
31 #[default]
33 Cover,
34 Contain,
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum Slide {
42 Keyed(Keyed<f64>),
44 ClipKeys(Keyed<f64>),
46 Pan(f64, f64),
48}
49
50impl Default for Slide {
51 fn default() -> Self {
52 Self::Keyed(Keyed::Const(0.5))
53 }
54}
55
56impl Slide {
57 fn parse(v: &Value, what: &'static str) -> Result<Self> {
58 let invalid = || Error::Invalid {
59 what,
60 value: v.to_string(),
61 };
62 if v.is_null() {
63 return Ok(Self::default());
64 }
65 if is_keys(v) || v.is_number() || v.is_string() || v.get("expr").is_some() {
66 return Keyed::parse(v, what).map(Self::Keyed);
67 }
68 let a = v.as_array().ok_or_else(invalid)?;
69 if a.first().is_some_and(Value::is_array) {
70 let keys = a
71 .iter()
72 .map(|k| match k.as_array().map(Vec::as_slice) {
73 Some([t, x]) => Some(Key {
74 t: t.as_f64()?,
75 v: x.as_f64()?,
76 ease: crate::keyframe::Interp::Ease,
77 }),
78 _ => None,
79 })
80 .collect::<Option<Vec<_>>>()
81 .ok_or_else(invalid)?;
82 return Ok(Self::ClipKeys(Keyed::from_keys(keys)));
83 }
84 match a.as_slice() {
85 [x, y] => Ok(Self::Pan(
86 x.as_f64().ok_or_else(invalid)?,
87 y.as_f64().ok_or_else(invalid)?,
88 )),
89 _ => Err(invalid()),
90 }
91 }
92
93 fn eval(&self, t: f64, start: f64, duration: f64) -> f64 {
95 match self {
96 Self::Keyed(k) => k.eval(start + t).unwrap_or(0.5),
97 Self::ClipKeys(k) => k.eval(t).unwrap_or(0.5),
98 Self::Pan(a, b) => a + (b - a) * ease(t / duration.max(1e-3)),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct Shot {
106 pub width: u32,
108 pub height: u32,
109 pub frames: u64,
111 pub rate: f64,
113 pub start: f64,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq)]
120pub struct Frame {
121 pub crop: Rect,
123 pub place: Option<Rect>,
125 pub blur: Option<f64>,
128}
129
130#[derive(Debug, Clone, PartialEq, Default)]
131pub struct Reframe {
132 pub zoom: Option<Keyed<f64>>,
134 pub zoom_center: Option<Keyed<[f64; 2]>>,
135 pub punch: Vec<Punch>,
136 pub push: Option<(f64, f64)>,
138 pub push_center: Option<[f64; 2]>,
140 pub pan: Option<([f64; 2], [f64; 2])>,
142 pub crop_x: Slide,
144 pub crop_y: Slide,
146 pub fit: Fit,
147 pub fit_y: f64,
149 pub fill_blur: f64,
152 pub focus: Option<[f64; 2]>,
155}
156
157pub const FILL_BLUR: f64 = 0.04;
159
160fn pair(v: &Value) -> Option<[f64; 2]> {
161 match v.as_array()?.as_slice() {
162 [a, b] => Some([a.as_f64()?, b.as_f64()?]),
163 _ => None,
164 }
165}
166
167impl Reframe {
168 pub fn parse(params: &Map<String, Value>, profile: Option<&str>) -> Result<Self> {
172 let mut p = params.clone();
173 if let Some(over) = profile
174 .and_then(|n| params.get("profiles")?.get(n))
175 .and_then(Value::as_object)
176 {
177 for (k, v) in over {
178 p.insert(k.clone(), v.clone());
179 }
180 }
181 let invalid = |what: &'static str, v: &Value| Error::Invalid {
182 what,
183 value: v.to_string(),
184 };
185 let mut r = Self {
186 fit_y: 0.42,
187 fill_blur: FILL_BLUR,
188 ..Self::default()
189 };
190 for (k, v) in &p {
191 if v.is_null() {
192 continue;
193 }
194 match k.as_str() {
195 "zoom" => r.zoom = Some(Keyed::parse(v, "reframe zoom")?),
196 "zoom_center" => r.zoom_center = Some(Keyed::parse(v, "reframe zoom_center")?),
197 "punch" => {
198 for pu in v.as_array().ok_or_else(|| invalid("reframe punch", v))? {
199 let num = |k: &str| pu.get(k).and_then(Value::as_f64);
200 r.punch.push(Punch {
201 at: num("at").ok_or_else(|| invalid("punch at", pu))?,
202 to: num("to"),
203 zoom: num("zoom").unwrap_or(1.25),
204 center: pu.get("center").and_then(pair),
205 });
206 }
207 }
208 "push" => {
209 let [a, b] = pair(v).ok_or_else(|| invalid("reframe push", v))?;
210 r.push = Some((a, b));
211 }
212 "push_center" => {
213 r.push_center = Some(pair(v).ok_or_else(|| invalid("push_center", v))?);
214 }
215 "pan" => {
216 let a = v.as_array().ok_or_else(|| invalid("reframe pan", v))?;
217 match a.as_slice() {
218 [x, y] => {
219 r.pan = Some((
220 pair(x).ok_or_else(|| invalid("reframe pan", v))?,
221 pair(y).ok_or_else(|| invalid("reframe pan", v))?,
222 ));
223 }
224 _ => return Err(invalid("reframe pan", v)),
225 }
226 }
227 "crop_x" => r.crop_x = Slide::parse(v, "reframe crop_x")?,
228 "crop_y" => r.crop_y = Slide::parse(v, "reframe crop_y")?,
229 "fit" => {
230 r.fit = match v.as_str() {
231 Some("contain") => Fit::Contain,
232 Some("cover" | "fill") => Fit::Cover,
233 _ => return Err(invalid("reframe fit", v)),
234 }
235 }
236 "mode" => {
238 r.fit = match v.as_str() {
239 Some("fit") => Fit::Contain,
240 _ => Fit::Cover,
241 }
242 }
243 "fit_y" => r.fit_y = v.as_f64().ok_or_else(|| invalid("reframe fit_y", v))?,
244 "fill_blur" => {
245 r.fill_blur = v
246 .as_f64()
247 .filter(|b| *b >= 0.0)
248 .ok_or_else(|| invalid("reframe fill_blur", v))?;
249 }
250 "focus" => r.focus = Some(pair(v).ok_or_else(|| invalid("reframe focus", v))?),
251 _ => {}
252 }
253 }
254 Ok(r)
255 }
256
257 pub fn from_effects(
259 effects: &[ffcine_timeline::schema::Effect],
260 profile: Option<&str>,
261 ) -> Result<Option<Self>> {
262 for e in effects {
263 let ffcine_timeline::schema::Effect::Effect(g) = e else {
264 continue;
265 };
266 if let Some(params) = g.ffcine_params::<Map<String, Value>>(SCHEMA, MAJOR) {
267 return Self::parse(¶ms?, profile).map(Some);
268 }
269 }
270 Ok(None)
271 }
272
273 #[must_use]
276 pub fn focused(mut self) -> Self {
277 if let Some([x, y]) = self.focus {
278 if self.crop_x == Slide::default() {
279 self.crop_x = Slide::Keyed(Keyed::Const(x));
280 }
281 if self.crop_y == Slide::default() {
282 self.crop_y = Slide::Keyed(Keyed::Const(y));
283 }
284 self.zoom_center.get_or_insert(Keyed::Const([x, y]));
285 }
286 self
287 }
288
289 pub fn frame(&self, i: u64, shot: &Shot, out: &Profile) -> Frame {
291 let full = Rect::frame(f64::from(shot.width), f64::from(shot.height));
292 self.frame_in(i, shot, out, full)
293 }
294
295 #[allow(clippy::cast_precision_loss)]
298 pub fn frame_in(&self, i: u64, shot: &Shot, out: &Profile, full: Rect) -> Frame {
299 let rate = if shot.rate > 0.0 { shot.rate } else { 24.0 };
300 let t = i as f64 / rate;
301 let duration = shot.frames as f64 / rate;
302 let (ow, oh) = (f64::from(out.width), f64::from(out.height));
303
304 let src_t = shot.start + t;
305 let base_zoom = self
306 .zoom
307 .as_ref()
308 .and_then(|z| z.eval(src_t))
309 .unwrap_or(1.0);
310 let base_center = self
311 .zoom_center
312 .as_ref()
313 .and_then(|c| c.eval(src_t))
314 .unwrap_or([0.5, 0.5]);
315 let frame_of = |s: f64| (s * rate).round();
317 let punch = self.punch.iter().rev().find(|p| {
318 let (a, b) = (frame_of(p.at), p.to.map_or(f64::INFINITY, frame_of));
319 (a..b).contains(&(i as f64))
320 });
321 let (zoom, center) = punch.map_or((base_zoom, base_center), |p| {
322 (
323 p.zoom.min(out.max_punch) * base_zoom,
324 p.center.unwrap_or(base_center),
325 )
326 });
327
328 let (window, place) = if self.fit == Fit::Contain {
329 let w = full.zoom(base_zoom, base_center);
330 let ph = ow / w.aspect();
331 let y = (oh * self.fit_y - ph / 2.0).round();
332 (
333 w,
334 Some(Rect {
335 x: 0.0,
336 y,
337 w: ow,
338 h: ph.round(),
339 }),
340 )
341 } else {
342 let ar = ow / oh;
343 let aspect = if (full.aspect() / ar - 1.0).abs() > 0.01 {
344 full.fit_aspect(
345 ar,
346 [
347 self.crop_x.eval(t, shot.start, duration),
348 self.crop_y.eval(t, shot.start, duration),
349 ],
350 )
351 } else {
352 full
353 };
354 (aspect.zoom(zoom, center), None)
355 };
356
357 let crop = match self.push {
358 Some((z0, z1)) => {
359 let n = shot.frames.saturating_sub(1).max(1) as f64;
360 let u = (i as f64 / n).min(1.0);
361 let e = 0.5 * u + 0.5 * ease(u);
362 let c = match self.pan {
363 Some((a, b)) => [a[0] + (b[0] - a[0]) * e, a[1] + (b[1] - a[1]) * e],
364 None => self.push_center.unwrap_or(base_center),
365 };
366 window.zoom(z0 + (z1 - z0) * e, c)
367 }
368 None => window,
369 };
370 let blur = place
371 .filter(|_| self.fill_blur > 0.0)
372 .map(|p| self.fill_blur * p.h);
373 Frame { crop, place, blur }
374 }
375
376 pub fn frames(&self, shot: &Shot, out: &Profile) -> Vec<Frame> {
378 (0..shot.frames.max(1))
379 .map(|i| self.frame(i, shot, out))
380 .collect()
381 }
382
383 pub fn is_animated(&self) -> bool {
385 self.zoom.as_ref().is_some_and(Keyed::is_animated)
386 || self.zoom_center.as_ref().is_some_and(Keyed::is_animated)
387 || !self.punch.is_empty()
388 || self.push.is_some()
389 || [&self.crop_x, &self.crop_y].iter().any(|s| match s {
390 Slide::Pan(..) => true,
391 Slide::Keyed(k) | Slide::ClipKeys(k) => k.is_animated(),
392 })
393 }
394}