1use std::f64::consts::FRAC_PI_2;
8
9use ffcine_reframe::Keyed;
10use ffcine_render::BlendMode;
11use serde_json::{Map, Value, json};
12
13fn f(m: &Map<String, Value>, k: &str) -> Option<f64> {
14 m.get(k).and_then(Value::as_f64)
15}
16
17pub fn colour(v: Option<&Value>, default: [f64; 3]) -> [f64; 3] {
19 match v {
20 Some(Value::String(s)) if s == "white" => [1.0; 3],
21 Some(Value::String(s)) if s == "black" => [0.0; 3],
22 Some(Value::Array(a)) if a.len() >= 3 => {
23 let c = |i: usize| a[i].as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
24 [c(0), c(1), c(2)]
25 }
26 _ => default,
27 }
28}
29
30#[derive(Debug, Clone, PartialEq)]
32pub struct Finish {
33 pub vignette: f64,
35 pub darken: f64,
37 pub fade_in: f64,
39 pub fade_out: f64,
40 pub fade_colour: [f64; 3],
41 pub flash: Option<(f64, Vec<f64>)>,
43}
44
45impl Default for Finish {
46 fn default() -> Self {
47 Self {
48 vignette: 0.0,
49 darken: 1.0,
50 fade_in: 0.0,
51 fade_out: 0.0,
52 fade_colour: [0.0; 3],
53 flash: None,
54 }
55 }
56}
57
58impl Finish {
59 pub fn add(&mut self, schema: (&str, u32), m: &Map<String, Value>) -> bool {
61 match schema {
62 ("ffcine.fx.vignette", 1) => self.vignette = f(m, "amount").unwrap_or(0.0),
63 ("ffcine.fx.darken", 1) => self.darken = f(m, "amount").unwrap_or(1.0),
64 ("ffcine.fx.fade", 1) => {
65 self.fade_in = f(m, "in").unwrap_or(0.0).max(0.0);
66 self.fade_out = f(m, "out").unwrap_or(0.0).max(0.0);
67 self.fade_colour = colour(m.get("colour"), [0.0; 3]);
68 }
69 ("ffcine.fx.flash", 1) => {
70 let levels = m.get("levels").and_then(Value::as_array).map_or_else(
71 || vec![0.5, 0.5, 0.2],
72 |a| a.iter().filter_map(Value::as_f64).collect(),
73 );
74 self.flash = Some((f(m, "at").unwrap_or(0.0), levels));
75 }
76 _ => return false,
77 }
78 true
79 }
80
81 pub fn is_identity(&self) -> bool {
82 *self == Self::default()
83 }
84
85 pub fn is_timed(&self) -> bool {
87 self.fade_in > 0.0 || self.fade_out > 0.0 || self.flash.is_some()
88 }
89
90 #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
93 pub fn tracks(&self, at: i64, n: i64, len: i64, rate: f64) -> (Vec<f32>, Vec<f32>) {
94 let flash_at = self
95 .flash
96 .as_ref()
97 .map(|(s, l)| ((s * rate).round() as i64, l));
98 (at..at + n)
99 .map(|i| {
100 let t = i as f64 / rate;
101 let mut k: f64 = 1.0;
102 if self.fade_in > 0.0 {
103 k = k.min(t / self.fade_in);
104 }
105 if self.fade_out > 0.0 {
106 let st = len as f64 / rate - self.fade_out;
107 k = k.min(1.0 - (t - st) / self.fade_out);
108 }
109 let lift = flash_at
110 .and_then(|(s, l)| usize::try_from(i - s).ok().and_then(|j| l.get(j)))
111 .copied()
112 .unwrap_or(0.0);
113 (k.clamp(0.0, 1.0) as f32, lift as f32)
114 })
115 .unzip()
116 }
117
118 pub fn vignette_angle(&self) -> f64 {
120 self.vignette * FRAC_PI_2
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
126pub enum TransitionKind {
127 Dissolve,
128 Dip([f64; 3]),
130 Flash,
132 Whip([f64; 2]),
134}
135
136impl TransitionKind {
137 pub fn from_meta(m: Option<&Map<String, Value>>) -> Self {
140 let Some(m) = m else { return Self::Dissolve };
141 match m.get("kind").and_then(Value::as_str) {
142 Some("dip") => Self::Dip(colour(m.get("colour"), [0.0; 3])),
143 Some("flash") => {
144 let c = colour(m.get("colour"), [1.0; 3]);
145 if c.iter().all(|x| (x - 1.0).abs() < 1e-9) {
146 Self::Flash
147 } else {
148 Self::Dip(c)
149 }
150 }
151 Some("whip") => Self::Whip(match m.get("direction").and_then(Value::as_str) {
152 Some("right") => [1.0, 0.0],
153 Some("up") => [0.0, -1.0],
154 Some("down") => [0.0, 1.0],
155 _ => [-1.0, 0.0],
156 }),
157 _ => Self::Dissolve,
158 }
159 }
160
161 pub fn recipe(&self) -> Value {
162 match self {
163 Self::Dissolve => json!("dissolve"),
164 Self::Dip(c) => json!({"dip": c}),
165 Self::Flash => json!("flash"),
166 Self::Whip(d) => json!({"whip": d}),
167 }
168 }
169}
170
171#[allow(clippy::cast_precision_loss)]
173pub fn progress(i: i64, n: i64) -> f64 {
174 ((i as f64 + 0.5) / n.max(1) as f64).clamp(0.0, 1.0)
175}
176
177pub fn smoothstep(p: f64) -> f64 {
179 let p = p.clamp(0.0, 1.0);
180 p * p * (3.0 - 2.0 * p)
181}
182
183pub use ffcine_comp::templates::layout::{Layout, Rect};
184
185pub fn cover(sw: f64, sh: f64, w: f64, h: f64, focus: [f64; 2], zoom: f64) -> Rect {
188 let (mut cw, mut ch) = if sw / sh > w / h {
189 (sh * w / h, sh)
190 } else {
191 (sw, sw * h / w)
192 };
193 let z = zoom.max(1e-3);
194 cw /= z;
195 ch /= z;
196 Rect {
197 x: (sw - cw) * focus[0].clamp(0.0, 1.0),
198 y: (sh - ch) * focus[1].clamp(0.0, 1.0),
199 w: cw,
200 h: ch,
201 }
202}
203
204#[derive(Debug, Clone)]
214pub struct Placement {
215 picture_units: bool,
216 tx: Keyed<f64>,
217 ty: Keyed<f64>,
218 sx: Keyed<f64>,
219 sy: Keyed<f64>,
220 rotate: Keyed<f64>,
221 anchor: Keyed<[f64; 2]>,
222 opacity: Keyed<f64>,
223 pub mode: BlendMode,
224 pub radius: f64,
225}
226
227impl Placement {
228 pub fn parse(
231 transform: &Map<String, Value>,
232 blend: &Map<String, Value>,
233 warn: &mut dyn FnMut(String),
234 ) -> Self {
235 let mut key = |m: &Map<String, Value>, k: &'static str, d: f64| match m.get(k) {
236 None => Keyed::Const(d),
237 Some(v) => Keyed::parse(v, k).unwrap_or_else(|e| {
238 warn(format!("{k}: {e}"));
239 Keyed::Const(d)
240 }),
241 };
242 let tx = key(transform, "translate_x", 0.0);
243 let ty = key(transform, "translate_y", 0.0);
244 let sx = key(transform, "scale_x", 1.0);
245 let sy = key(transform, "scale_y", 1.0);
246 let rotate = key(transform, "rotate_deg", 0.0);
247 let opacity = key(blend, "opacity", 1.0);
248 let anchor = match transform.get("anchor") {
249 None => Keyed::Const([0.0; 2]),
250 Some(v) => Keyed::parse(v, "anchor").unwrap_or_else(|e| {
251 warn(format!("anchor: {e}"));
252 Keyed::Const([0.0; 2])
253 }),
254 };
255 let mode = match blend.get("mode").and_then(Value::as_str) {
256 None => BlendMode::Normal,
257 Some(m) => m.parse().unwrap_or_else(|_| {
258 warn(format!("blend mode {m} rendered as normal"));
259 BlendMode::Normal
260 }),
261 };
262 Self {
263 picture_units: transform.get("units").and_then(Value::as_str) == Some("picture"),
264 tx,
265 ty,
266 sx,
267 sy,
268 rotate,
269 anchor,
270 opacity,
271 mode,
272 radius: f(blend, "radius").unwrap_or(0.0),
273 }
274 }
275
276 #[allow(clippy::many_single_char_names)]
279 pub fn rect(&self, t: f64, sw: f64, sh: f64, w: u32, h: u32) -> Rect {
280 let (fw, fh) = (f64::from(w), f64::from(h));
281 let fit = (fw / sw).min(fh / sh);
282 let ev = |k: &Keyed<f64>, d| k.eval(t).unwrap_or(d);
283 let (s0, s1) = (ev(&self.sx, 1.0), ev(&self.sy, 1.0));
284 let picture = [sw * fit, sh * fit];
285 let unit = if self.picture_units {
286 picture
287 } else {
288 [fw, fh]
289 };
290 let [ax, ay] = self.anchor.eval(t).unwrap_or([0.0; 2]);
291 let a = [ax * unit[0], ay * unit[1]];
292 let (sin, cos) = self.rotation(t).to_radians().sin_cos();
294 let z = [-a[0] * s0, -a[1] * s1];
295 let turned = [cos * z[0] + sin * z[1], -sin * z[0] + cos * z[1]];
296 let (pw, ph) = (picture[0] * s0, picture[1] * s1);
297 let (cx, cy) = (
298 fw / 2.0 + a[0] + turned[0] + ev(&self.tx, 0.0) * unit[0],
299 fh / 2.0 + a[1] + turned[1] + ev(&self.ty, 0.0) * unit[1],
300 );
301 Rect {
302 x: cx - pw / 2.0,
303 y: cy - ph / 2.0,
304 w: pw,
305 h: ph,
306 }
307 }
308
309 pub fn rotation(&self, t: f64) -> f64 {
311 self.rotate.eval(t).unwrap_or(0.0)
312 }
313
314 pub fn opacity(&self, t: f64) -> f64 {
315 self.opacity.eval(t).unwrap_or(1.0).clamp(0.0, 1.0)
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[allow(clippy::needless_pass_by_value)]
324 fn obj(v: Value) -> Map<String, Value> {
325 v.as_object().cloned().unwrap()
326 }
327
328 #[test]
329 fn fades_follow_ffmpeg() {
330 let mut fin = Finish::default();
331 assert!(fin.add(
332 ("ffcine.fx.fade", 1),
333 &obj(json!({"in": 1.0, "out": 0.5, "colour": "white"}))
334 ));
335 assert!(fin.is_timed());
336 assert_eq!(fin.fade_colour, [1.0; 3]);
337 let (k, lift) = fin.tracks(0, 48, 48, 24.0);
339 assert!(k[0].abs() < 1e-6);
340 assert!((k[12] - 0.5).abs() < 1e-6);
341 assert!((k[30] - 1.0).abs() < 1e-6);
342 assert!((k[47] - 1.0 / 12.0).abs() < 1e-5);
343 assert!(lift.iter().all(|l| *l == 0.0));
344 let (k2, _) = fin.tracks(40, 8, 48, 24.0);
346 assert_eq!(k2, k[40..]);
347 }
348
349 #[test]
350 fn flash_lifts_frames() {
351 let mut fin = Finish::default();
352 fin.add(("ffcine.fx.flash", 1), &obj(json!({"at": 0.5})));
353 let (_, lift) = fin.tracks(10, 6, 100, 24.0);
354 assert_eq!(lift, [0.0, 0.0, 0.5, 0.5, 0.2, 0.0]);
355 }
356
357 #[test]
358 fn transition_kinds() {
359 let k = |v: Value| TransitionKind::from_meta(Some(&obj(v)));
360 assert_eq!(k(json!({"kind": "dip"})), TransitionKind::Dip([0.0; 3]));
361 assert_eq!(
362 k(json!({"kind": "dip", "colour": "white"})),
363 TransitionKind::Dip([1.0; 3])
364 );
365 assert_eq!(k(json!({"kind": "flash"})), TransitionKind::Flash);
366 assert_eq!(
367 k(json!({"kind": "whip", "direction": "up"})),
368 TransitionKind::Whip([0.0, -1.0])
369 );
370 assert_eq!(k(json!({"kind": "wipe"})), TransitionKind::Dissolve);
371 assert_eq!(TransitionKind::from_meta(None), TransitionKind::Dissolve);
372 }
373
374 #[test]
375 fn cover_and_placement() {
376 let c = cover(3840.0, 2160.0, 960.0, 1080.0, [0.5, 0.5], 1.0);
377 assert!((c.w - 1920.0).abs() < 1e-9 && (c.x - 960.0).abs() < 1e-9);
378 let mut warns = Vec::new();
379 let p = Placement::parse(
380 &obj(json!({"translate_x": 0.25, "scale_x": 0.5, "scale_y": 0.5})),
381 &obj(json!({"opacity": [{"t": 0.0, "v": 0.0}, {"t": 1.0, "v": 1.0}], "radius": 0.02})),
382 &mut |w| warns.push(w),
383 );
384 assert_eq!(warns, Vec::<String>::new());
385 let r = p.rect(0.0, 1920.0, 1080.0, 1920, 1080);
386 assert_eq!((r.x, r.y, r.w, r.h), (960.0, 270.0, 960.0, 540.0));
387 assert!((p.opacity(0.5) - 0.5).abs() < 1e-9);
388 assert_eq!(p.mode, BlendMode::Normal);
389 assert!(p.rotation(0.0).abs() < 1e-12);
390 }
391
392 #[test]
393 fn rotation_pivots_on_the_anchor() {
394 let mut warns = Vec::new();
395 let p = Placement::parse(
397 &obj(
398 json!({"units": "picture", "anchor": [-0.5, -0.5], "scale_x": 0.5,
399 "scale_y": 0.5, "rotate_deg": 90.0}),
400 ),
401 &obj(json!({"mode": "multiply"})),
402 &mut |w| warns.push(w),
403 );
404 assert_eq!(warns, Vec::<String>::new());
405 assert_eq!(p.mode, BlendMode::Multiply);
406 assert!((p.rotation(0.0) - 90.0).abs() < 1e-12);
407 let r = p.rect(0.0, 1920.0, 1080.0, 1920, 1080);
408 let (cx, cy) = (r.x + r.w / 2.0, r.y + r.h / 2.0);
411 assert!((r.w - 960.0).abs() < 1e-9 && (r.h - 540.0).abs() < 1e-9);
412 assert!((cx - 270.0).abs() < 1e-9, "{cx}");
413 assert!((cy + 480.0).abs() < 1e-9, "{cy}");
414 let q = ffcine_import::resolve::Placement {
416 anchor: [-0.5, -0.5],
417 scale: [0.5, 0.5],
418 rotate_deg: 90.0,
419 ..Default::default()
420 }
421 .apply([1920.0, 1080.0], [0.0, 0.0]);
422 assert!((cx - 960.0 - q[0]).abs() < 1e-9 && (cy - 540.0 - q[1]).abs() < 1e-9);
423 let k = Placement::parse(
425 &obj(
426 json!({"rotate_deg": [{"t": 0.0, "v": 0.0}, {"t": 1.0, "v": 30.0}],
427 "anchor": [0.1, 0.0]}),
428 ),
429 &obj(json!({"mode": "resolve:27"})),
430 &mut |w| warns.push(w),
431 );
432 assert!((k.rotation(0.5) - 15.0).abs() < 1e-9);
433 assert_eq!(k.mode, BlendMode::Normal);
434 assert_eq!(warns.len(), 1);
435 let r0 = k.rect(0.0, 1920.0, 1080.0, 1920, 1080);
437 assert!(r0.x.abs() < 1e-9 && r0.y.abs() < 1e-9);
438 }
439}