1use serde_json::{Map, Value, json};
7
8use super::{item_layer, jnum, num};
9use crate::model::err;
10use ffcine_core::Result;
11
12fn f(m: &Map<String, Value>, k: &str) -> Option<f64> {
13 m.get(k).and_then(Value::as_f64)
14}
15
16pub const BACKDROP: &str = "#161412";
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Rect {
23 pub x: f64,
24 pub y: f64,
25 pub w: f64,
26 pub h: f64,
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub struct Layout {
32 pub kind: String,
34 pub gap: f64,
35 pub margin: f64,
36 pub radius: f64,
37 pub weights: Vec<f64>,
38 pub stagger: f64,
39 pub slide: f64,
40 pub enter: bool,
41 pub first_static: bool,
42}
43
44impl Layout {
45 pub fn parse(m: &Map<String, Value>) -> Self {
46 Self {
47 kind: m
48 .get("layout")
49 .and_then(Value::as_str)
50 .unwrap_or("split")
51 .to_owned(),
52 gap: f(m, "gap").unwrap_or(0.008),
53 margin: f(m, "margin").unwrap_or(0.0),
54 radius: f(m, "radius").unwrap_or(0.012),
55 weights: m
56 .get("weights")
57 .and_then(Value::as_array)
58 .map(|a| a.iter().filter_map(Value::as_f64).collect())
59 .unwrap_or_default(),
60 stagger: f(m, "stagger").unwrap_or(0.22),
61 slide: f(m, "slide").unwrap_or(0.42),
62 enter: m.get("enter").and_then(Value::as_str) != Some("none"),
63 first_static: m
64 .get("first_static")
65 .and_then(Value::as_bool)
66 .unwrap_or(true),
67 }
68 }
69
70 pub fn radius_px(&self, h: u32) -> f64 {
72 (self.radius * f64::from(h)).round()
73 }
74
75 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
77 #[allow(clippy::many_single_char_names)]
78 pub fn rects(&self, k: usize, w: u32, h: u32) -> Vec<Rect> {
79 let (fw, fh) = (f64::from(w), f64::from(h));
80 let gap = (self.gap * fh).round();
81 let mar = (self.margin * fh).round();
82 let even = |x: f64| (x / 2.0).floor() * 2.0;
83 let split = |avail: f64, k: usize, weights: &[f64]| -> Vec<f64> {
85 let wts: Vec<f64> = if weights.len() == k {
86 weights.to_vec()
87 } else {
88 vec![1.0; k]
89 };
90 let sum: f64 = wts.iter().sum();
91 let mut v: Vec<f64> = wts
92 .iter()
93 .map(|x| even((avail * x / sum).floor()))
94 .collect();
95 if v.pop().is_some() {
96 let rest: f64 = v.iter().sum();
97 v.push(even(avail - rest));
98 }
99 v
100 };
101 let row = |x0: f64, y0: f64, avail_w: f64, ph: f64, k: usize, wts: &[f64]| {
102 let mut x = x0;
103 split(avail_w, k, wts)
104 .into_iter()
105 .map(|pw| {
106 let r = Rect {
107 x,
108 y: y0,
109 w: pw,
110 h: ph,
111 };
112 x += pw + gap;
113 r
114 })
115 .collect::<Vec<_>>()
116 };
117 let col = |x0: f64, y0: f64, pw: f64, avail_h: f64, k: usize, wts: &[f64]| {
118 let mut y = y0;
119 split(avail_h, k, wts)
120 .into_iter()
121 .map(|ph| {
122 let r = Rect {
123 x: x0,
124 y,
125 w: pw,
126 h: ph,
127 };
128 y += ph + gap;
129 r
130 })
131 .collect::<Vec<_>>()
132 };
133 let kf = k.max(1) as f64;
134 match self.kind.as_str() {
135 "column" => col(
136 mar,
137 mar,
138 even(fw - 2.0 * mar),
139 fh - 2.0 * mar - gap * (kf - 1.0),
140 k,
141 &self.weights,
142 ),
143 "1+2" if k == 3 => {
144 let wts = if self.weights.len() == 2 {
145 self.weights.clone()
146 } else {
147 vec![1.0, 1.0]
148 };
149 let cols = row(
150 mar,
151 mar,
152 fw - 2.0 * mar - gap,
153 even(fh - 2.0 * mar),
154 2,
155 &wts,
156 );
157 let mut v = vec![cols[0]];
158 v.extend(col(cols[1].x, mar, cols[1].w, fh - 2.0 * mar - gap, 2, &[]));
159 v
160 }
161 _ => row(
162 mar,
163 mar,
164 fw - 2.0 * mar - gap * (kf - 1.0),
165 even(fh - 2.0 * mar),
166 k,
167 &self.weights,
168 ),
169 }
170 }
171
172 #[allow(clippy::many_single_char_names)]
176 pub fn enter_offset(&self, i: usize, t: f64, w: u32, h: u32) -> [f64; 2] {
177 if !self.enter || (i == 0 && self.first_static) {
178 return [0.0; 2];
179 }
180 #[allow(clippy::cast_precision_loss)]
181 let t0 = i as f64 * self.stagger - if self.first_static { 0.0 } else { 0.16 };
182 let u = ((t - t0) / self.slide.max(1e-6)).clamp(0.0, 1.0);
183 let e = 1.0 - (1.0 - u).powi(3);
184 let d = if i.is_multiple_of(2) { 1.0 } else { -1.0 };
185 if self.kind == "column" {
186 [d * f64::from(w) * (1.0 - e), 0.0]
187 } else {
188 [0.0, d * f64::from(h) * (1.0 - e)]
189 }
190 }
191}
192
193fn plus(a: &str, b: f64) -> String {
195 if b < 0.0 {
196 format!("{a} - {}", num(-b))
197 } else {
198 format!("{a} + {}", num(b))
199 }
200}
201
202#[allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
209pub fn expand(t: &Map<String, Value>) -> Result<Value> {
210 const P: &str = "template";
211 for k in t.keys() {
212 if !["layout", "panels", "size", "fps", "duration"].contains(&k.as_str()) {
213 return Err(err(format!("{P}.{k}"), "unknown key"));
214 }
215 }
216 let empty = Map::new();
217 let l = match t.get("layout") {
218 Some(Value::Object(m)) => Layout::parse(m),
219 None => Layout::parse(&empty),
220 Some(v) => {
221 return Err(err(
222 format!("{P}.layout"),
223 format!("expected an object, got {v}"),
224 ));
225 }
226 };
227 let size = super::size(t.get("size"), &format!("{P}.size"))?;
228 let panels = t
229 .get("panels")
230 .and_then(Value::as_array)
231 .filter(|a| !a.is_empty())
232 .ok_or_else(|| err(format!("{P}.panels"), "expected a list of panels"))?;
233 let (w, h) = (size[0], size[1]);
234 let rects = l.rects(panels.len(), w, h);
235 let mut layers = Vec::new();
236 let mut ids = super::Ids::default();
237 for (i, (p, r)) in panels.iter().zip(&rects).enumerate() {
238 let path = format!("{P}.panels[{i}]");
239 let mut m = item_layer(p, &path)?;
240 let id = ids.take(m.get("id").and_then(Value::as_str), &format!("p{i}"));
241 m.insert("id".into(), id.into());
242 m.insert("size".into(), json!([jnum(r.w), jnum(r.h)]));
243 let radius = l.radius_px(h);
244 if radius > 0.0 {
245 m.insert("radius".into(), jnum(radius));
246 }
247 let at = l.enter_offset(i, -1e9, w, h);
249 let pos = |base: f64, amp: f64| -> Value {
250 if amp == 0.0 {
251 return jnum(base);
252 }
253 let t0 = i as f64 * l.stagger - if l.first_static { 0.0 } else { 0.16 };
254 let u = format!("({}) / {}", plus("t", -t0), num(l.slide.max(1e-6)));
255 let e = format!("{} * (1 - ease_out({u}))", num(amp));
256 if base == 0.0 {
257 e.into()
258 } else {
259 format!("{} + {e}", num(base)).into()
260 }
261 };
262 m.insert(
263 "transform".into(),
264 json!({"anchor": [0, 0], "position": [pos(r.x, at[0]), pos(r.y, at[1])]}),
265 );
266 layers.push(Value::Object(m));
267 }
268 let mut canvas = Map::new();
269 canvas.insert("size".into(), json!([w, h]));
270 for k in ["fps", "duration"] {
271 if let Some(v) = t.get(k) {
272 canvas.insert(k.into(), v.clone());
273 }
274 }
275 canvas.insert("background".into(), BACKDROP.into());
276 Ok(json!({"canvas": canvas, "layers": layers}))
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[allow(clippy::needless_pass_by_value)]
284 fn obj(v: Value) -> Map<String, Value> {
285 v.as_object().cloned().unwrap()
286 }
287
288 #[test]
289 fn layout_rects_match_render_py() {
290 let l = Layout::parse(&obj(json!({"layout": "split"})));
291 let r = l.rects(2, 1920, 1080);
293 assert_eq!((r[0].x, r[0].w, r[0].h), (0.0, 954.0, 1080.0));
294 assert_eq!((r[1].x, r[1].w), (963.0, 956.0));
295 let r3 = l.rects(3, 1920, 1080);
296 assert_eq!(r3.len(), 3);
297 assert!(r3[2].x + r3[2].w <= 1920.0);
298 let one_two =
299 Layout::parse(&obj(json!({"layout": "1+2", "gap": 0.01}))).rects(3, 1920, 1080);
300 assert_eq!(one_two[0].h, 1080.0);
301 assert_eq!(one_two[1].x, one_two[2].x);
302 assert!(one_two[2].y > one_two[1].y);
303 let col = Layout::parse(&obj(json!({"layout": "column"}))).rects(2, 1080, 1920);
304 assert_eq!(col[0].w, 1080.0);
305 assert!(col[1].y > col[0].y);
306 }
307
308 #[test]
309 fn panels_slide_in() {
310 let l = Layout::parse(&obj(json!({})));
311 assert_eq!(l.enter_offset(0, 0.0, 1920, 1080), [0.0; 2]);
312 let start = l.enter_offset(1, 0.0, 1920, 1080);
313 assert_eq!(start, [0.0, -1080.0]);
314 assert_eq!(l.enter_offset(1, 0.22 + 0.42, 1920, 1080), [0.0, 0.0]);
315 let mid = l.enter_offset(2, 0.44 + 0.21, 1920, 1080)[1];
316 assert!(mid > 0.0 && mid < 1080.0 * 0.2);
317 }
318
319 #[test]
320 fn template_places_panels_at_the_rects() {
321 let v = expand(&obj(json!({
322 "layout": {"layout": "split"}, "size": [1920, 1080], "fps": 24, "duration": 2,
323 "panels": [{"video": "a.mp4"}, {"still": "b.png"}]
324 })))
325 .expect("expand");
326 assert_eq!(v["canvas"]["background"], BACKDROP);
327 let l = &v["layers"];
328 assert_eq!(l[0]["source"], json!({"video": "a.mp4"}));
329 assert_eq!(l[0]["size"], json!([954, 1080]));
330 assert_eq!(l[0]["radius"], json!(13));
331 assert_eq!(l[0]["transform"]["position"], json!([0, 0]));
332 assert_eq!(l[1]["id"], "p1");
333 assert_eq!(
334 l[1]["transform"]["position"],
335 json!([963, "-1080 * (1 - ease_out((t - 0.22) / 0.42))"])
336 );
337 }
338}