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,
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub enum Slide {
41 Keyed(Keyed<f64>),
43 ClipKeys(Keyed<f64>),
45 Pan(f64, f64),
47}
48
49impl Default for Slide {
50 fn default() -> Self {
51 Self::Keyed(Keyed::Const(0.5))
52 }
53}
54
55impl Slide {
56 fn parse(v: &Value, what: &'static str) -> Result<Self> {
57 let invalid = || Error::Invalid {
58 what,
59 value: v.to_string(),
60 };
61 if v.is_null() {
62 return Ok(Self::default());
63 }
64 if is_keys(v) || v.is_number() {
65 return Keyed::parse(v, what).map(Self::Keyed);
66 }
67 let a = v.as_array().ok_or_else(invalid)?;
68 if a.first().is_some_and(Value::is_array) {
69 let keys = a
70 .iter()
71 .map(|k| match k.as_array().map(Vec::as_slice) {
72 Some([t, x]) => Some(Key {
73 t: t.as_f64()?,
74 v: x.as_f64()?,
75 interp: crate::keyframe::Interp::Ease,
76 }),
77 _ => None,
78 })
79 .collect::<Option<Vec<_>>>()
80 .ok_or_else(invalid)?;
81 return Ok(Self::ClipKeys(Keyed::from_keys(keys)));
82 }
83 match a.as_slice() {
84 [x, y] => Ok(Self::Pan(
85 x.as_f64().ok_or_else(invalid)?,
86 y.as_f64().ok_or_else(invalid)?,
87 )),
88 _ => Err(invalid()),
89 }
90 }
91
92 fn eval(&self, t: f64, start: f64, duration: f64) -> f64 {
94 match self {
95 Self::Keyed(k) => k.eval(start + t).unwrap_or(0.5),
96 Self::ClipKeys(k) => k.eval(t).unwrap_or(0.5),
97 Self::Pan(a, b) => a + (b - a) * ease(t / duration.max(1e-3)),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct Shot {
105 pub width: u32,
107 pub height: u32,
108 pub frames: u64,
110 pub rate: f64,
112 pub start: f64,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq)]
119pub struct Frame {
120 pub crop: Rect,
122 pub place: Option<Rect>,
124}
125
126#[derive(Debug, Clone, PartialEq, Default)]
127pub struct Reframe {
128 pub zoom: Option<Keyed<f64>>,
130 pub zoom_center: Option<Keyed<[f64; 2]>>,
131 pub punch: Vec<Punch>,
132 pub push: Option<(f64, f64)>,
134 pub push_center: Option<[f64; 2]>,
136 pub pan: Option<([f64; 2], [f64; 2])>,
138 pub crop_x: Slide,
140 pub crop_y: Slide,
142 pub fit: Fit,
143 pub fit_y: f64,
145}
146
147fn pair(v: &Value) -> Option<[f64; 2]> {
148 match v.as_array()?.as_slice() {
149 [a, b] => Some([a.as_f64()?, b.as_f64()?]),
150 _ => None,
151 }
152}
153
154impl Reframe {
155 pub fn parse(params: &Map<String, Value>, profile: Option<&str>) -> Result<Self> {
159 let mut p = params.clone();
160 if let Some(over) = profile
161 .and_then(|n| params.get("profiles")?.get(n))
162 .and_then(Value::as_object)
163 {
164 for (k, v) in over {
165 p.insert(k.clone(), v.clone());
166 }
167 }
168 let invalid = |what: &'static str, v: &Value| Error::Invalid {
169 what,
170 value: v.to_string(),
171 };
172 let mut r = Self {
173 fit_y: 0.42,
174 ..Self::default()
175 };
176 for (k, v) in &p {
177 if v.is_null() {
178 continue;
179 }
180 match k.as_str() {
181 "zoom" => r.zoom = Some(Keyed::parse(v, "reframe zoom")?),
182 "zoom_center" => r.zoom_center = Some(Keyed::parse(v, "reframe zoom_center")?),
183 "punch" => {
184 for pu in v.as_array().ok_or_else(|| invalid("reframe punch", v))? {
185 let num = |k: &str| pu.get(k).and_then(Value::as_f64);
186 r.punch.push(Punch {
187 at: num("at").ok_or_else(|| invalid("punch at", pu))?,
188 to: num("to"),
189 zoom: num("zoom").unwrap_or(1.25),
190 center: pu.get("center").and_then(pair),
191 });
192 }
193 }
194 "push" => {
195 let [a, b] = pair(v).ok_or_else(|| invalid("reframe push", v))?;
196 r.push = Some((a, b));
197 }
198 "push_center" => {
199 r.push_center = Some(pair(v).ok_or_else(|| invalid("push_center", v))?);
200 }
201 "pan" => {
202 let a = v.as_array().ok_or_else(|| invalid("reframe pan", v))?;
203 match a.as_slice() {
204 [x, y] => {
205 r.pan = Some((
206 pair(x).ok_or_else(|| invalid("reframe pan", v))?,
207 pair(y).ok_or_else(|| invalid("reframe pan", v))?,
208 ));
209 }
210 _ => return Err(invalid("reframe pan", v)),
211 }
212 }
213 "crop_x" => r.crop_x = Slide::parse(v, "reframe crop_x")?,
214 "crop_y" => r.crop_y = Slide::parse(v, "reframe crop_y")?,
215 "fit" => {
216 r.fit = match v.as_str() {
217 Some("contain") => Fit::Contain,
218 Some("cover" | "fill") => Fit::Cover,
219 _ => return Err(invalid("reframe fit", v)),
220 }
221 }
222 "mode" => {
224 r.fit = match v.as_str() {
225 Some("fit") => Fit::Contain,
226 _ => Fit::Cover,
227 }
228 }
229 "fit_y" => r.fit_y = v.as_f64().ok_or_else(|| invalid("reframe fit_y", v))?,
230 _ => {}
231 }
232 }
233 Ok(r)
234 }
235
236 pub fn from_effects(
238 effects: &[ffcine_timeline::schema::Effect],
239 profile: Option<&str>,
240 ) -> Result<Option<Self>> {
241 for e in effects {
242 let ffcine_timeline::schema::Effect::Effect(g) = e else {
243 continue;
244 };
245 if let Some(params) = g.ffcine_params::<Map<String, Value>>(SCHEMA, MAJOR) {
246 return Self::parse(¶ms?, profile).map(Some);
247 }
248 }
249 Ok(None)
250 }
251
252 #[allow(clippy::cast_precision_loss)]
254 pub fn frame(&self, i: u64, shot: &Shot, out: &Profile) -> Frame {
255 let rate = if shot.rate > 0.0 { shot.rate } else { 24.0 };
256 let t = i as f64 / rate;
257 let duration = shot.frames as f64 / rate;
258 let (sw, sh) = (f64::from(shot.width), f64::from(shot.height));
259 let full = Rect::frame(sw, sh);
260 let (ow, oh) = (f64::from(out.width), f64::from(out.height));
261
262 let src_t = shot.start + t;
263 let base_zoom = self
264 .zoom
265 .as_ref()
266 .and_then(|z| z.eval(src_t))
267 .unwrap_or(1.0);
268 let base_center = self
269 .zoom_center
270 .as_ref()
271 .and_then(|c| c.eval(src_t))
272 .unwrap_or([0.5, 0.5]);
273 let frame_of = |s: f64| (s * rate).round();
275 let punch = self.punch.iter().rev().find(|p| {
276 let (a, b) = (frame_of(p.at), p.to.map_or(f64::INFINITY, frame_of));
277 (a..b).contains(&(i as f64))
278 });
279 let (zoom, center) = punch.map_or((base_zoom, base_center), |p| {
280 (
281 p.zoom.min(out.max_punch) * base_zoom,
282 p.center.unwrap_or(base_center),
283 )
284 });
285
286 let (window, place) = if self.fit == Fit::Contain {
287 let w = full.zoom(base_zoom, base_center);
288 let ph = ow / w.aspect();
289 let y = (oh * self.fit_y - ph / 2.0).round();
290 (
291 w,
292 Some(Rect {
293 x: 0.0,
294 y,
295 w: ow,
296 h: ph.round(),
297 }),
298 )
299 } else {
300 let ar = ow / oh;
301 let aspect = if (full.aspect() / ar - 1.0).abs() > 0.01 {
302 full.fit_aspect(
303 ar,
304 [
305 self.crop_x.eval(t, shot.start, duration),
306 self.crop_y.eval(t, shot.start, duration),
307 ],
308 )
309 } else {
310 full
311 };
312 (aspect.zoom(zoom, center), None)
313 };
314
315 let crop = match self.push {
316 Some((z0, z1)) => {
317 let n = shot.frames.saturating_sub(1).max(1) as f64;
318 let u = (i as f64 / n).min(1.0);
319 let e = 0.5 * u + 0.5 * ease(u);
320 let c = match self.pan {
321 Some((a, b)) => [a[0] + (b[0] - a[0]) * e, a[1] + (b[1] - a[1]) * e],
322 None => self.push_center.unwrap_or(base_center),
323 };
324 window.zoom(z0 + (z1 - z0) * e, c)
325 }
326 None => window,
327 };
328 Frame { crop, place }
329 }
330
331 pub fn frames(&self, shot: &Shot, out: &Profile) -> Vec<Frame> {
333 (0..shot.frames.max(1))
334 .map(|i| self.frame(i, shot, out))
335 .collect()
336 }
337
338 pub fn is_animated(&self) -> bool {
340 self.zoom.as_ref().is_some_and(Keyed::is_animated)
341 || self.zoom_center.as_ref().is_some_and(Keyed::is_animated)
342 || !self.punch.is_empty()
343 || self.push.is_some()
344 || [&self.crop_x, &self.crop_y].iter().any(|s| match s {
345 Slide::Pan(..) => true,
346 Slide::Keyed(k) | Slide::ClipKeys(k) => k.is_animated(),
347 })
348 }
349}