1use std::fmt::Write as _;
6use std::ops::Range;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::time::Duration;
10
11use ffcine_core::anim::{Animatable, Clock, Env, Track};
12use ffcine_core::camera::{self, CameraLut, Profile, ProfileChoice};
13use ffcine_core::{Lut, LutType, Result};
14use ffcine_hooks::ShaderEffect;
15use ffcine_hooks::grade::{Luts, grade_for, look_params};
16use ffcine_render::extra::{Filter, filter_hook, strict_hooks};
17use ffcine_render::filter_node::{Clock as FClock, FILTER_TABLE_REVISION, Node, Prep, Reach};
18use ffcine_render::{
19 BlendMode, Crop, Deband, Grade, Hook, Image, Layer, LayerSource, LutStage, Overlay, ParamTrack,
20 Scene, Size, shaders,
21};
22use ffcine_text::{TextClip, TextLayer};
23use ffcine_timeline::{ClipGrade, SourceKind};
24use serde_json::{Value, json};
25use sha2::{Digest, Sha256};
26
27use crate::model::{
28 Comp, CompLayer, CropSpec, FILTER_HINT, FilterOpt, FilterSpec, Fit, LayerGrade, MaskShape,
29 Scale, Source, err, limits,
30};
31use crate::time;
32
33fn table_revision() -> u32 {
35 #[cfg(test)]
36 return TABLE.get();
37 #[cfg(not(test))]
38 FILTER_TABLE_REVISION
39}
40
41#[cfg(test)]
42thread_local! {
43 pub(crate) static TABLE: std::cell::Cell<u32> =
44 const { std::cell::Cell::new(FILTER_TABLE_REVISION) };
45}
46
47pub trait Media {
49 fn dims(&self, path: &Path) -> Result<(u32, u32)>;
51 fn identity(&self, path: &Path) -> Value;
53 fn profile(&self, _path: &Path) -> Result<Profile> {
55 Ok(Profile::Rec709)
56 }
57 fn video_rate(&self, path: &Path) -> Result<(f64, u64)> {
59 Err(err(
60 path.display().to_string(),
61 "the frame rate is unknown (needed for a time remap)",
62 ))
63 }
64 fn content_hash(&self, path: &Path) -> Value {
67 self.identity(path)
68 }
69 fn ffmpeg_build(&self) -> String {
71 String::from("unknown")
72 }
73 fn camera_lut(&self, profile: Profile) -> Result<Option<CameraLut>> {
75 profile.camera_lut(&camera::lut_dirs())
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub enum Base {
82 Black,
84 Video { input: PathBuf, start: Duration },
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub struct Piece {
91 pub from: i64,
92 pub frames: i64,
93 pub base: Base,
94 pub grade: Grade,
96 pub layers: Vec<Layer>,
98 pub shown: Vec<usize>,
100 pub overlays: Vec<Overlay>,
102 pub recipe: Value,
104}
105
106#[allow(clippy::cast_possible_truncation)]
108fn frame_at(t: f64, fps: f64) -> i64 {
109 (t * fps - 1e-6).ceil() as i64
110}
111
112fn map_digest(picks: &[i64]) -> Value {
114 let mut h = Sha256::new();
115 for k in picks {
116 h.update(k.to_le_bytes());
117 }
118 let d = h.finalize();
119 json!({
120 "first": picks.first(),
121 "frames": picks.len(),
122 "digest": d[..16].iter().fold(String::new(), |mut s, b| {
123 let _ = write!(s, "{b:02x}");
124 s
125 }),
126 })
127}
128
129fn digest(values: &[f32]) -> Value {
131 match values.first() {
132 None => Value::Null,
133 Some(v) if values.iter().all(|x| x.to_bits() == v.to_bits()) => json!(v),
134 Some(_) => {
135 let mut h = Sha256::new();
136 for v in values {
137 h.update(v.to_bits().to_le_bytes());
138 }
139 let d = h.finalize();
140 Value::String(d[..16].iter().fold(String::new(), |mut s, b| {
141 let _ = write!(s, "{b:02x}");
142 s
143 }))
144 }
145 }
146}
147
148fn param(h: &mut Hook, name: &str, values: Vec<f32>) {
150 match values.first() {
151 Some(v) if values.iter().all(|x| x.to_bits() == v.to_bits()) => {
152 h.params.push((name.into(), f64::from(*v)));
153 }
154 Some(_) => h.tracks.push(ParamTrack {
155 name: name.into(),
156 values,
157 }),
158 None => {}
159 }
160}
161
162fn hook(name: &str, source: &str) -> Hook {
163 Hook {
164 name: name.into(),
165 source: source.into(),
166 params: Vec::new(),
167 tracks: Vec::new(),
168 }
169}
170
171#[allow(clippy::cast_possible_truncation)]
172fn f32s(v: impl IntoIterator<Item = f64>) -> Vec<f32> {
173 v.into_iter().map(|x| x as f32).collect()
174}
175
176fn fade_hook(name: &str, fade: &[f64], colour: &[[f64; 4]]) -> Option<Hook> {
178 if fade.iter().all(|f| *f >= 1.0) {
179 return None;
180 }
181 let mut h = hook(name, shaders::finish());
182 param(&mut h, "fade", f32s(fade.iter().map(|f| f.clamp(0.0, 1.0))));
183 for (i, c) in ["fade_r", "fade_g", "fade_b"].into_iter().enumerate() {
184 param(&mut h, c, f32s(colour.iter().map(|v| v[i])));
185 }
186 Some(h)
187}
188
189fn grade_recipe(g: &Grade) -> Value {
190 let mut v = grade_json(g);
191 if !g.crops.is_empty() {
193 v["crops"] = digest(
194 &g.crops
195 .iter()
196 .flat_map(|c| [c.x, c.y, c.width, c.height])
197 .collect::<Vec<_>>(),
198 );
199 }
200 v
201}
202
203fn grade_json(g: &Grade) -> Value {
204 json!({
205 "size": g.size.map(|s| [s.width, s.height]),
206 "crop": g.crop.map(|c| [c.x, c.y, c.width, c.height]),
207 "hooks": g.hooks.iter().map(|h| json!({
208 "name": h.name,
209 "params": h.params,
210 "tracks": h.tracks.iter().map(|t| json!([t.name, digest(&t.values)])).collect::<Vec<_>>(),
211 })).collect::<Vec<_>>(),
212 })
213}
214
215fn sha(bytes: impl IntoIterator<Item = impl AsRef<[u8]>>) -> String {
217 let mut h = Sha256::new();
218 for b in bytes {
219 h.update(b.as_ref());
220 }
221 h.finalize()[..16].iter().fold(String::new(), |mut s, b| {
222 let _ = write!(s, "{b:02x}");
223 s
224 })
225}
226
227fn lut_digest(l: &Lut) -> String {
229 let head = [
230 u32::try_from(l.size)
231 .unwrap_or(u32::MAX)
232 .to_le_bytes()
233 .to_vec(),
234 l.domain_min
235 .iter()
236 .chain(&l.domain_max)
237 .flat_map(|v| v.to_bits().to_le_bytes())
238 .collect(),
239 ];
240 sha(head.into_iter().chain(
241 l.data
242 .iter()
243 .map(|p| p.iter().flat_map(|v| v.to_bits().to_le_bytes()).collect()),
244 ))
245}
246
247struct Graded {
250 grade: Grade,
251 hooks: Vec<Hook>,
252 recipe: Value,
253}
254
255fn solid_image() -> LayerSource {
257 LayerSource::Image(Arc::new(Image {
258 width: 1,
259 height: 1,
260 data: vec![255; 4],
261 }))
262}
263
264struct Framing {
266 crop: Option<Crop>,
267 crops: Vec<Crop>,
269 pic: [f64; 2],
271 cropped: [f64; 2],
273 feather: Vec<f64>,
275}
276
277fn place(r: [f64; 4], w: f64, h: f64, [px, py]: [f64; 2]) -> [f64; 4] {
279 [
280 r[0] + (r[2] - w) * px.clamp(0.0, 1.0),
281 r[1] + (r[3] - h) * py.clamp(0.0, 1.0),
282 w,
283 h,
284 ]
285}
286
287fn zoomed(r: [f64; 4], zoom: f64, pos: [f64; 2]) -> [f64; 4] {
290 let z = if zoom.is_finite() { zoom.max(1.0) } else { 1.0 };
291 place(r, r[2] / z, r[3] / z, pos)
292}
293
294struct Sampled {
296 rects: Vec<Crop>,
297 rotation: Vec<f64>,
298 opacity: Vec<f64>,
299}
300
301impl Comp {
302 pub fn frames(&self) -> i64 {
304 frame_at(self.canvas.duration, self.canvas.fps).max(1)
305 }
306
307 pub fn span(&self, layer: &CompLayer) -> Range<i64> {
309 let fps = self.canvas.fps;
310 let n = self.frames();
311 frame_at(layer.in_, fps).clamp(0, n)..frame_at(layer.out, fps).clamp(0, n)
312 }
313
314 pub fn pieces(&self) -> Vec<Range<i64>> {
317 let n = self.frames();
318 #[allow(clippy::cast_possible_truncation)]
319 let min = (self.canvas.fps.round() as i64).max(1);
320 let mut cuts: Vec<i64> = self
321 .layers
322 .iter()
323 .flat_map(|l| {
324 let s = self.span(l);
325 [s.start, s.end]
326 })
327 .filter(|c| *c > 0 && *c < n)
328 .collect();
329 cuts.sort_unstable();
330 cuts.dedup();
331 let mut out = Vec::new();
332 let mut start = 0;
333 for c in cuts {
334 if c - start >= min && n - c >= min {
335 out.push(start..c);
336 start = c;
337 }
338 }
339 out.push(start..n);
340 out
341 }
342
343 fn env<'a>(&self, l: &'a CompLayer, source: [f64; 2]) -> Env<'a> {
344 Env {
345 fps: self.canvas.fps,
346 canvas: self.canvas.size.map(f64::from),
347 size: l.size,
348 source,
349 in_: l.in_,
350 out: l.out,
351 total: self.canvas.duration,
352 source_in: l.source_in,
353 vars: Some(&l.vars),
354 }
355 }
356
357 #[allow(
359 clippy::cast_precision_loss,
360 clippy::cast_possible_wrap,
361 clippy::float_cmp,
362 clippy::many_single_char_names,
363 clippy::too_many_lines
364 )]
365 fn framing(
366 &self,
367 l: &CompLayer,
368 [sw, sh]: [f64; 2],
369 span: &Range<i64>,
370 vis: Range<i64>,
371 env: &Env,
372 path: &str,
373 ) -> Result<Framing> {
374 let at = |name: &str| format!("{path}.{name}");
375 let n = usize::try_from(vis.end - vis.start).unwrap_or(0);
376 let regions: Vec<[f64; 4]> = match &l.crop {
378 None => vec![[0.0, 0.0, sw, sh]; n],
379 Some(CropSpec {
380 left,
381 right,
382 top,
383 bottom,
384 ..
385 }) => {
386 let side = |t: &Track<f64>, name: &str| -> Result<Vec<f64>> {
387 let v = sample(t, &at(&format!("crop.{name}")), vis.clone(), env)?;
388 match v.iter().find(|f| !(0.0..1.0).contains(*f)) {
389 Some(f) => Err(err(
390 at(&format!("crop.{name}")),
391 format!("{f}: expected a fraction 0 to 1"),
392 )),
393 None => Ok(v),
394 }
395 };
396 let (l_, r) = (side(left, "left")?, side(right, "right")?);
397 let (t, b) = (side(top, "top")?, side(bottom, "bottom")?);
398 (0..n)
399 .map(|j| {
400 if l_[j] + r[j] >= 1.0 || t[j] + b[j] >= 1.0 {
401 return Err(err(
402 at("crop"),
403 "left + right and top + bottom must each be under 1",
404 ));
405 }
406 Ok([
407 l_[j] * sw,
408 t[j] * sh,
409 (1.0 - l_[j] - r[j]) * sw,
410 (1.0 - t[j] - b[j]) * sh,
411 ])
412 })
413 .collect::<Result<_>>()?
414 }
415 };
416 let first = regions.first().copied().unwrap_or([0.0, 0.0, sw, sh]);
417 let cropped = [first[2], first[3]];
418 let [bw, bh] = l.size;
419 let pic = match l.fit {
420 Fit::Cover | Fit::Fill => l.size,
421 Fit::Contain => {
422 let s = (bw / cropped[0]).min(bh / cropped[1]);
423 [cropped[0] * s, cropped[1] * s]
424 }
425 Fit::None => [cropped[0].min(bw), cropped[1].min(bh)],
426 };
427 let half = Track::Const(0.5);
428 let one = Track::Const(1.0);
429 let centre = Track::Const([0.5, 0.5]);
430 let (zoom, zc, cx, cy) = match &l.content {
431 Some(c) => (&c.zoom, &c.zoom_center, &c.crop_x, &c.crop_y),
432 None => (&one, ¢re, &half, &half),
433 };
434 let zoom = sample(zoom, &at("content.zoom"), vis.clone(), env)?;
435 if let Some(z) = zoom
436 .iter()
437 .find(|z| !(z.is_finite() && **z > 0.0 && **z <= limits::ZOOM))
438 {
439 return Err(err(
440 at("content.zoom"),
441 format!("{z}: expected a zoom > 0, at most {}", limits::ZOOM),
442 ));
443 }
444 let zc = sample(zc, &at("content.zoom_center"), vis.clone(), env)?;
445 let cx = sample(cx, &at("content.crop_x"), vis.clone(), env)?;
446 let cy = sample(cy, &at("content.crop_y"), vis.clone(), env)?;
447 finite(&at("content"), zc.iter().flatten().chain(&cx).chain(&cy))?;
448 let fps = self.canvas.fps;
449 let life = (span.end - span.start - 1).max(1) as f64;
450 let windows: Vec<[f64; 4]> = (0..n)
451 .map(|j| {
452 let r = regions[j];
453 let base = match l.fit {
454 Fit::Cover => {
455 let ar = bw / bh;
456 let (w, h) = if r[2] / r[3] > ar {
457 (r[3] * ar, r[3])
458 } else {
459 (r[2], r[2] / ar)
460 };
461 place(r, w, h, [cx[j], cy[j]])
462 }
463 Fit::Fill | Fit::Contain => r,
464 Fit::None => place(r, r[2].min(bw), r[3].min(bh), [cx[j], cy[j]]),
465 };
466 let i = (vis.start + j as i64 - span.start) as f64;
468 let punch = l
469 .content
470 .iter()
471 .flat_map(|c| c.punch.iter())
472 .rev()
473 .find(|p| {
474 let to = p.to.map_or(f64::INFINITY, |t| (t * fps).round());
475 ((p.at * fps).round()..to).contains(&i)
476 });
477 let (z, c) = punch.map_or((zoom[j], zc[j]), |p| {
478 (p.zoom * zoom[j], p.center.unwrap_or(zc[j]))
479 });
480 let w = zoomed(base, z, c);
481 match l.content.as_ref().and_then(|c| c.push.map(|p| (c, p))) {
482 Some((c, [z0, z1])) => {
483 let u = (i / life).clamp(0.0, 1.0);
484 let e = 0.5 * u + 0.5 * (0.5 - 0.5 * (std::f64::consts::PI * u).cos());
485 let centre = match c.pan {
486 Some([a, b]) => [a[0] + (b[0] - a[0]) * e, a[1] + (b[1] - a[1]) * e],
487 None => c.push_center.unwrap_or(zc[j]),
488 };
489 zoomed(w, z0 + (z1 - z0) * e, centre)
490 }
491 None => w,
492 }
493 })
494 .collect();
495 if windows
497 .iter()
498 .any(|w| !(w[2] >= sw / limits::ZOOM && w[3] >= sh / limits::ZOOM))
499 {
500 return Err(err(
501 at("content"),
502 format!(
503 "zoom and crop together go in over {}x (the source window would be under \
504 1/{} of the source a side)",
505 limits::ZOOM,
506 limits::ZOOM
507 ),
508 ));
509 }
510 #[allow(clippy::cast_possible_truncation)]
511 let to_crop = |w: &[f64; 4]| Crop {
512 x: w[0] as f32,
513 y: w[1] as f32,
514 width: w[2] as f32,
515 height: w[3] as f32,
516 };
517 let full = [0.0, 0.0, sw, sh];
518 let (crop, crops) = match windows.first() {
519 None => (None, Vec::new()),
520 Some(w) if windows.iter().all(|v| v == w) => {
521 ((*w != full).then(|| to_crop(w)), Vec::new())
522 }
523 Some(_) => (None, windows.iter().map(to_crop).collect()),
524 };
525 let feather = match &l.crop {
526 Some(c) => sample(&c.feather, &at("crop.feather"), vis, env)?
527 .into_iter()
528 .zip(&windows)
529 .map(|(f, w)| f * sw * pic[0] / w[2].max(1e-6))
531 .collect(),
532 None => Vec::new(),
533 };
534 Ok(Framing {
535 crop,
536 crops,
537 pic,
538 cropped,
539 feather,
540 })
541 }
542
543 fn canvas_env(&self) -> Env<'_> {
544 let [cw, ch] = self.canvas.size.map(f64::from);
545 Env {
546 fps: self.canvas.fps,
547 canvas: [cw, ch],
548 size: [cw, ch],
549 source: [cw, ch],
550 in_: 0.0,
551 out: self.canvas.duration,
552 total: self.canvas.duration,
553 source_in: 0.0,
554 vars: Some(&self.vars),
555 }
556 }
557
558 #[allow(
560 clippy::too_many_lines,
561 clippy::many_single_char_names,
562 clippy::cast_possible_truncation,
563 clippy::cast_sign_loss,
564 clippy::cast_precision_loss
565 )]
566 pub fn lower(&self, range: Range<i64>, out: Size, media: &dyn Media) -> Result<Piece> {
567 self.lower_in(range, out, media, true)
568 }
569
570 #[allow(
573 clippy::too_many_arguments,
574 clippy::cast_precision_loss,
575 clippy::cast_possible_truncation,
576 clippy::cast_sign_loss
577 )]
578 fn filter_prep(
579 &self,
580 l: &CompLayer,
581 p: &Path,
582 node: &Node,
583 reach: Reach,
584 span: &Range<i64>,
585 vis: &Range<i64>,
586 env: &Env<'_>,
587 path: &str,
588 media: &dyn Media,
589 ) -> Result<(Prep, Value)> {
590 let at = |k: &str| format!("{path}.{k}");
591 let (rate, n) = media.video_rate(p)?;
592 if !(rate.is_finite() && rate > 0.0 && n > 0) {
593 return Err(err(
594 at("filter"),
595 format!("{}: no frames or frame rate to filter", p.display()),
596 ));
597 }
598 let ext = if reach.whole_clip {
601 span.clone()
602 } else {
603 (vis.start - i64::from(reach.preroll)).max(span.start)
604 ..(vis.end + i64::from(reach.postroll)).min(span.end)
605 };
606 let fps = self.canvas.fps;
607 let picks: Vec<i64> = match &l.time {
608 Some(t) => time::source_times(t, env, ext.clone(), &at("time"))?
609 .into_iter()
610 .map(|(s, mid)| time::pick(s, mid, rate, n))
611 .collect(),
612 None => ext
613 .clone()
614 .map(|f| {
615 let s = l.source_in + f as f64 / fps - l.in_;
616 time::pick(s, s + 0.5 / fps, rate, n)
617 })
618 .collect(),
619 };
620 let lo = picks.iter().copied().min().unwrap_or(0);
621 let pts = match node.clock {
622 FClock::Layer => ext.start - span.start,
623 FClock::Comp => ext.start,
624 };
625 let shown = (vis.start - ext.start) as usize..(vis.end - ext.start) as usize;
626 let (store, show) = if reach.whole_clip {
627 (0..picks.len(), shown)
628 } else {
629 (shown.clone(), 0..shown.len())
630 };
631 let mut rec = json!({
632 "graph": node.graph,
633 "hw": node.hw.as_str(),
634 "clock": node.clock.as_str(),
635 "preroll": reach.preroll,
636 "postroll": reach.postroll,
637 "whole_clip": reach.whole_clip,
638 "timing": if node.free_timing { "free" } else { "keep" },
639 "fps": fps,
640 "files": node.files.iter().map(|f| media.content_hash(f)).collect::<Vec<_>>(),
641 "ffmpeg": media.ffmpeg_build(),
642 "video": media.identity(p),
643 "map": map_digest(&picks),
644 "pts": pts,
645 });
646 if reach.stateful() {
647 rec["table"] = json!(table_revision());
648 }
649 if node.clock == FClock::Comp {
650 rec["in"] = json!(l.in_);
651 }
652 let id = json!({"node": rec, "store": [store.start, store.end]});
653 let key = Sha256::digest(id.to_string().as_bytes())[..16].iter().fold(
654 String::new(),
655 |mut s, b| {
656 let _ = write!(s, "{b:02x}");
657 s
658 },
659 );
660 let prep = Prep {
661 graph: node.run.clone(),
662 labelled: node.labelled,
663 dims: media.dims(p)?,
664 hw: node.hw,
665 free_timing: node.free_timing,
666 input: p.to_owned(),
667 start: Duration::from_secs_f64(((lo as f64 - 0.5) / rate).max(0.0)),
669 map: picks.iter().map(|k| k - lo).collect(),
670 rate: fps,
671 pts,
672 store: store.clone(),
673 show: show.clone(),
674 key,
675 };
676 Ok((prep, json!({"filter": id, "show": [show.start, show.end]})))
677 }
678
679 #[allow(
683 clippy::too_many_lines,
684 clippy::many_single_char_names,
685 clippy::cast_possible_truncation
686 )]
687 fn layer_grade(
688 &self,
689 l: &CompLayer,
690 path: &str,
691 vis: &Range<i64>,
692 env: &Env,
693 media: &dyn Media,
694 ) -> Result<Option<Graded>> {
695 let cp = &*self.project;
696 let empty = LayerGrade::default();
697 let lg = match (&l.grade, &l.source) {
698 (_, Source::Solid(_) | Source::Text { .. }) => return Ok(None),
699 (Some(g), _) => g,
700 (None, _) if cp.given => &empty,
701 (None, _) => return Ok(None),
702 };
703 let gp = format!("{path}.grade");
704 let at = |k: &str| format!("{gp}.{k}");
705 let gerr = |e: &dyn std::fmt::Display| err(&gp, e.to_string());
706 let resolve = |c: &ClipGrade, kind| {
707 cp.project
708 .resolve_grade(Some(c), kind)
709 .map_err(|e| gerr(&e))
710 };
711 let merged = resolve(&lg.params, SourceKind::Display)?;
713 let choice = match merged.camera.as_deref() {
714 None => ProfileChoice::Auto,
715 Some(c) => c
716 .parse::<ProfileChoice>()
717 .map_err(|e| err(at("camera"), e.to_string()))?,
718 };
719 let profile = match (&l.source, choice) {
720 (Source::Video(_), ProfileChoice::Fixed(p)) => p,
721 (Source::Video(p) | Source::Still(p), ProfileChoice::Auto) => media.profile(p)?,
722 _ => Profile::Rec709,
724 };
725 let log = !matches!(profile, Profile::Rec709 | Profile::Hdr);
726 let kind = if log {
727 SourceKind::Log
728 } else {
729 SourceKind::Display
730 };
731 let mut params = resolve(&lg.params, kind)?;
732 params.denoise = None;
734 let (mut g, warn) =
735 grade_for(&cp.project, &cp.dir, ¶ms, &mut Luts::default()).map_err(|e| gerr(&e))?;
736 if let Some(w) = warn.first() {
737 return Err(err(&gp, w));
738 }
739 if matches!(l.source, Source::Video(_))
742 && log
743 && (matches!(choice, ProfileChoice::Fixed(_))
744 || cp.project.lut_path(params.lut.as_deref()).is_none())
745 && let Some(cam) = media.camera_lut(profile)?
746 {
747 if profile.as_str().starts_with("dlog") {
748 g.deband.get_or_insert_with(Deband::default);
749 }
750 g.input_color.primaries.get_or_insert(cam.primaries);
751 g.input_color.transfer.get_or_insert(cam.transfer);
752 g.input_lut = Some(LutStage {
753 lut: cam.lut,
754 kind: LutType::Normalized,
755 });
756 }
757 let mut hooks = std::mem::take(&mut g.hooks);
758
759 if !lg.tracks.is_empty() {
761 let mut cols: Vec<(&str, Vec<f64>)> = Vec::new();
762 for (k, t) in &lg.tracks {
763 let v = sample(t, &at(k), vis.clone(), env)?;
764 finite(&at(k), &v)?;
765 if *k == "gamma"
766 && let Some(x) = v.iter().find(|x| **x < 0.01)
767 {
768 return Err(err(at(k), format!("{x}: at least 0.01")));
769 }
770 cols.push((k, v));
771 }
772 let n = cols[0].1.len();
773 let mut frames: Vec<Vec<(String, f64)>> = Vec::with_capacity(n);
774 for j in 0..n {
775 let mut c = lg.params.clone();
776 for (k, v) in &cols {
777 let x = Some(v[j]);
778 let p = &mut c.params;
779 match *k {
780 "exposure" => p.exposure = x,
781 "warmth" => p.warmth = x,
782 "tint" => p.tint = x,
783 "contrast" => p.contrast = x,
784 "gamma" => p.gamma = x,
785 "lift" => p.lift = x,
786 "gain" => p.gain = x,
787 "saturation" => p.saturation = x,
788 _ => unreachable!("not a look param: {k}"),
789 }
790 }
791 frames.push(look_params(&resolve(&c, kind)?).params());
792 }
793 let mut h = hook("look", shaders::look());
794 for (i, (name, _)) in frames[0].iter().enumerate() {
795 param(&mut h, name, f32s(frames.iter().map(|f| f[i].1)));
796 }
797 match hooks.first_mut() {
798 Some(first) if first.name == "look" => *first = h,
799 _ => hooks.insert(0, h),
800 }
801 }
802
803 for (fi, f) in lg.filters.iter().enumerate() {
805 let fp = at(&format!("filters[{fi}]"));
806 match f {
807 FilterSpec::Chain(c) => {
808 hooks.extend(
809 strict_hooks(std::slice::from_ref(c))
810 .map_err(|e| err(&fp, format!("{e} {FILTER_HINT}")))?,
811 );
812 }
813 FilterSpec::Object { name, opts } => {
814 let fp = format!("{fp}.{name}");
815 let mut cols = Vec::new();
816 for (k, o) in opts {
817 cols.push(match o {
818 FilterOpt::Num(t) => {
819 let v = sample(t, &format!("{fp}.{k}"), vis.clone(), env)?;
820 finite(&format!("{fp}.{k}"), &v)?;
821 Err(v)
822 }
823 FilterOpt::Text(s) => Ok(s.clone()),
824 });
825 }
826 let moving = cols.iter().any(
827 |c| matches!(c, Err(v) if v.iter().any(|x| x.to_bits() != v[0].to_bits())),
828 );
829 let n = if moving { vis.end - vis.start } else { 1 };
830 let mut per = Vec::new();
831 for j in 0..usize::try_from(n).unwrap_or(0) {
832 let f = Filter {
833 name: name.clone(),
834 args: opts
835 .iter()
836 .zip(&cols)
837 .map(|((k, _), c)| {
838 (
839 Some(k.clone()),
840 match c {
841 Ok(s) => s.clone(),
842 Err(v) => format!("{}", v[j]),
843 },
844 )
845 })
846 .collect(),
847 };
848 per.push(
849 filter_hook(&f).map_err(|e| err(&fp, format!("{e} {FILTER_HINT}")))?,
850 );
851 }
852 let first = &per[0];
853 if per.iter().any(|h| {
854 h.source != first.source
855 || h.params.len() != first.params.len()
856 || h.params.iter().zip(&first.params).any(|(a, b)| a.0 != b.0)
857 }) {
858 return Err(err(
859 &fp,
860 "an animated option changes the filter's shader (only its numbers can \
861 move)",
862 ));
863 }
864 let mut h = hook(&first.name, &first.source);
865 for (i, (pn, _)) in first.params.iter().enumerate() {
866 param(&mut h, pn, f32s(per.iter().map(|x| x.params[i].1)));
867 }
868 hooks.push(h);
869 }
870 }
871 }
872
873 for (si, sh) in lg.shaders.iter().enumerate() {
875 let sp = at(&format!("shaders[{si}]"));
876 let (name, source) = ShaderEffect {
877 shader: sh.shader.clone(),
878 params: Vec::new(),
879 }
880 .load(&cp.project, &cp.dir)
881 .map_err(|e| err(&sp, e.to_string()))?;
882 let info = ffcine_hooks::parse(&source).map_err(|e| err(&sp, e.to_string()))?;
883 let mut h = hook(&name, &source);
884 for (k, t) in &sh.params {
885 let pp = format!("{sp}.params.{k}");
886 let p = info.param(k).ok_or_else(|| {
887 err(
888 &pp,
889 format!(
890 "not a //!PARAM of {} (has: {})",
891 sh.shader,
892 info.params
893 .iter()
894 .map(|p| p.name.as_str())
895 .collect::<Vec<_>>()
896 .join(", ")
897 ),
898 )
899 })?;
900 let v = sample(t, &pp, vis.clone(), env)?
901 .into_iter()
902 .map(|x| p.check(x))
903 .collect::<Result<Vec<_>>>()
904 .map_err(|e| err(&pp, e.to_string()))?;
905 param(&mut h, k, f32s(v));
906 }
907 hooks.push(h);
908 }
909
910 let recipe = json!({
911 "profile": profile.as_str(),
912 "input_lut": g.input_lut.as_ref().map(|s| lut_digest(&s.lut)),
913 "input_color": format!("{:?}", g.input_color),
914 "lut": g.lut.as_ref().map(|s| lut_digest(&s.lut)),
915 "rgb_gain": g.rgb_gain,
916 "sharpen": g.sharpen,
917 "deband": g.deband.is_some(),
918 "shaders": hooks.iter().map(|h| sha([&h.source])).collect::<Vec<_>>(),
919 });
920 Ok(Some(Graded {
921 grade: g,
922 hooks,
923 recipe,
924 }))
925 }
926
927 #[allow(
930 clippy::too_many_lines,
931 clippy::many_single_char_names,
932 clippy::cast_possible_truncation,
933 clippy::cast_sign_loss,
934 clippy::cast_precision_loss
935 )]
936 fn lower_in(
937 &self,
938 range: Range<i64>,
939 out: Size,
940 media: &dyn Media,
941 top: bool,
942 ) -> Result<Piece> {
943 let [cw, ch] = self.canvas.size.map(f64::from);
944 let k = [f64::from(out.width) / cw, f64::from(out.height) / ch];
945 let p0 = range.start;
946 let frames = range.end - range.start;
947 let cenv = self.canvas_env();
948 let canvas_fade = sample(&self.canvas.fade, "canvas.fade", range.clone(), &cenv)?;
949 let canvas_colour = sample(
950 &self.canvas.fade_colour,
951 "canvas.fade_colour",
952 range.clone(),
953 &cenv,
954 )?;
955 let mut base = Base::Black;
956 let mut base_grade = None;
957 let mut layers = Vec::new();
958 let mut shown = Vec::new();
959 let mut overlays = Vec::new();
960 let mut recipes = Vec::new();
961
962 for (i, l) in self.layers.iter().enumerate() {
963 let span = self.span(l);
964 let vis = span.start.max(range.start)..span.end.min(range.end);
965 if vis.is_empty() {
966 continue;
967 }
968 let path = format!("layers[{i}]");
969 let at = |name: &str| format!("{path}.{name}");
970 if let Source::Text { params, dir } = &l.source {
972 let clip = TextClip {
973 id: l.id.clone(),
974 name: l.id.clone().unwrap_or_default(),
975 text: params
976 .get("text")
977 .and_then(Value::as_str)
978 .unwrap_or_default()
979 .to_owned(),
980 start: l.in_,
981 end: l.out,
982 track: 0,
983 params: params.clone(),
984 };
985 let mut text =
986 TextLayer::new(vec![clip], Some([cw as f32, ch as f32]), dir.clone());
987 let under = layers.len();
988 let first = overlays.len();
989 let o = text
990 .overlays(
991 p0 as f64 / self.canvas.fps,
992 frames as u64,
993 self.canvas.fps,
994 out,
995 )
996 .map_err(|e| err(at("source.text"), e.to_string()))?;
997 overlays.extend(o.into_iter().map(|mut o| {
998 o.under = Some(under);
999 o
1000 }));
1001 recipes.push(json!({
1004 "text": params,
1005 "in": l.in_,
1006 "out": l.out,
1007 "under": under,
1008 "overlays": overlay_digest(&overlays[first..]),
1009 "fonts": font_files(params, dir).iter().map(|f| media.identity(f)).collect::<Vec<_>>(),
1010 }));
1011 continue;
1012 }
1013 let (src_dims, source) = match &l.source {
1014 Source::Video(p) => {
1015 let (w, h) = media.dims(p)?;
1016 (Some([f64::from(w), f64::from(h)]), None)
1017 }
1018 Source::Still(p) => {
1019 let (w, h) = media.dims(p)?;
1020 if !ffcine_render::still_fits(w, h) {
1021 return Err(err(
1022 at("source.still"),
1023 format!(
1024 "{}: {w}x{h} px (a still is at most {} px a side, {} in all)",
1025 p.display(),
1026 limits::STILL_SIDE,
1027 limits::STILL_PIXELS
1028 ),
1029 ));
1030 }
1031 (Some([f64::from(w), f64::from(h)]), None)
1032 }
1033 Source::Comp(c) => (Some(c.canvas.size.map(f64::from)), None),
1034 Source::Solid(c) => (None, Some(c)),
1035 Source::Text { .. } => unreachable!("text is lowered above"),
1036 };
1037 let framing = match src_dims {
1040 Some(src) if l.content.is_some() || l.crop.is_some() => {
1041 let env = self.env(l, src);
1042 Some(self.framing(l, src, &span, vis.clone(), &env, &path)?)
1043 }
1044 _ => None,
1045 };
1046 let (crop, crops, pic, cropped) = if let Some(f) = &framing {
1047 (f.crop, f.crops.clone(), f.pic, f.cropped)
1048 } else {
1049 let (c, p) = fit(l.fit, src_dims, l.size);
1050 (c, Vec::new(), p, src_dims.unwrap_or(l.size))
1051 };
1052 let env = self.env(l, cropped);
1053 let off = [(l.size[0] - pic[0]) / 2.0, (l.size[1] - pic[1]) / 2.0];
1054
1055 let position = sample(
1056 &l.transform.position,
1057 &at("transform.position"),
1058 vis.clone(),
1059 &env,
1060 )?;
1061 let anchor = sample(
1062 &l.transform.anchor,
1063 &at("transform.anchor"),
1064 vis.clone(),
1065 &env,
1066 )?;
1067 let scale: Vec<[f64; 2]> = match &l.transform.scale {
1068 Scale::Uniform(t) => sample(t, &at("transform.scale"), vis.clone(), &env)?
1069 .into_iter()
1070 .map(|s| [s.max(0.0); 2])
1071 .collect(),
1072 Scale::Xy(t) => sample(t, &at("transform.scale"), vis.clone(), &env)?
1073 .into_iter()
1074 .map(|s| s.map(|v| v.max(0.0)))
1075 .collect(),
1076 };
1077 let rotation = sample(
1078 &l.transform.rotation,
1079 &at("transform.rotation"),
1080 vis.clone(),
1081 &env,
1082 )?;
1083 let mut opacity: Vec<f64> = sample(
1084 &l.transform.opacity,
1085 &at("transform.opacity"),
1086 vis.clone(),
1087 &env,
1088 )?
1089 .into_iter()
1090 .map(|o| o.clamp(0.0, 1.0))
1091 .collect();
1092 let solid = match source {
1093 Some(c) => {
1094 let c = sample(c, &at("source.solid"), vis.clone(), &env)?;
1095 for (o, c) in opacity.iter_mut().zip(&c) {
1096 *o *= c[3].clamp(0.0, 1.0);
1097 }
1098 Some(c)
1099 }
1100 None => None,
1101 };
1102 let radius = sample(&l.radius, &at("radius"), vis.clone(), &env)?;
1103 finite(&at("radius"), &radius)?;
1104 let fade = sample(&l.fade, &at("fade"), vis.clone(), &env)?;
1105 let fade_colour = sample(&l.fade_colour, &at("fade_colour"), vis.clone(), &env)?;
1106
1107 let smax = scale
1109 .iter()
1110 .fold([0.0f64; 2], |m, s| [m[0].max(s[0]), m[1].max(s[1])]);
1111 let tw = (pic[0] * smax[0].max(1e-3) * k[0]).ceil().max(2.0);
1112 let th = (pic[1] * smax[1].max(1e-3) * k[1]).ceil().max(2.0);
1113 if !(tw.is_finite() && th.is_finite() && tw.max(th) <= limits::TEXTURE_SIDE) {
1114 return Err(err(
1115 at("transform.scale"),
1116 format!(
1117 "the layer's texture would be {tw}x{th} px at its largest scale (at most {} \
1118 px a side at this render size)",
1119 limits::TEXTURE_SIDE
1120 ),
1121 ));
1122 }
1123 finite(&at("transform.position"), position.iter().flatten())?;
1124 finite(&at("transform.anchor"), anchor.iter().flatten())?;
1125 if let Some(r) = rotation
1126 .iter()
1127 .find(|r| !r.is_finite() || r.abs() > limits::ROTATION)
1128 {
1129 return Err(err(
1130 at("transform.rotation"),
1131 format!("{r}: at most {} degrees either way", limits::ROTATION),
1132 ));
1133 }
1134 let s = Sampled {
1135 rects: (0..position.len())
1136 .map(|j| {
1137 let [sx, sy] = scale[j];
1138 let q = [
1141 l.size[0] * (0.5 - anchor[j][0]) * sx,
1142 l.size[1] * (0.5 - anchor[j][1]) * sy,
1143 ];
1144 let (sin, cos) = rotation[j].to_radians().sin_cos();
1145 let c = [
1146 position[j][0] + cos * q[0] + sin * q[1],
1147 position[j][1] - sin * q[0] + cos * q[1],
1148 ];
1149 let (w, h) = (pic[0] * sx, pic[1] * sy);
1150 Crop {
1151 x: ((c[0] - w / 2.0) * k[0]) as f32,
1152 y: ((c[1] - h / 2.0) * k[1]) as f32,
1153 width: (w * k[0]) as f32,
1154 height: (h * k[1]) as f32,
1155 }
1156 })
1157 .collect(),
1158 rotation,
1159 opacity,
1160 };
1161
1162 let graded = self.layer_grade(l, &path, &vis, &env, media)?;
1164 let (layer_grade, mut hooks, graded_recipe) = match graded {
1165 Some(g) => (g.grade, g.hooks, Some(g.recipe)),
1166 None => (Grade::default(), Vec::new(), None),
1167 };
1168 if let Some(c) = &solid {
1169 let mut h = hook("solid", shaders::solid());
1170 for (n, name) in ["solid_r", "solid_g", "solid_b"].into_iter().enumerate() {
1171 param(&mut h, name, f32s(c.iter().map(|v| v[n])));
1172 }
1173 hooks.push(h);
1174 }
1175 hooks.extend(fade_hook("fade", &fade, &fade_colour));
1176 let px = tw / pic[0].max(1e-6);
1179 let ky = (th / pic[1].max(1e-6)) / px;
1180 let edge = framing
1181 .as_ref()
1182 .map(|f| f.feather.clone())
1183 .filter(|f| f.iter().any(|v| *v > 0.0));
1184 let has_mask = l.mask.is_some() || edge.is_some();
1185 if has_mask || radius.iter().any(|r| *r > 0.0) || s.opacity.iter().any(|o| *o < 1.0) {
1186 let mut h = hook("alpha", shaders::layer_alpha());
1187 param(&mut h, "opacity", f32s(s.opacity.iter().copied()));
1188 if (ky - 1.0).abs() > 1e-9 {
1189 param(&mut h, "ky", vec![ky as f32]);
1190 }
1191 param(
1192 &mut h,
1193 "radius",
1194 f32s(radius.iter().map(|r| r.max(0.0) * px)),
1195 );
1196 let mask = match (&l.mask, &edge) {
1197 (Some(m), _) => {
1198 let rect = sample(&m.rect, &at("mask.rect"), vis.clone(), &env)?;
1199 let feather = sample(&m.feather, &at("mask.feather"), vis.clone(), &env)?;
1200 finite(&at("mask.rect"), rect.iter().flatten())?;
1201 finite(&at("mask.feather"), &feather)?;
1202 Some((m.shape, rect, feather, m.invert))
1203 }
1204 (None, Some(f)) => {
1206 finite(&at("crop.feather"), f)?;
1207 let rect = f
1208 .iter()
1209 .map(|f| {
1210 let f = f.max(0.0);
1211 [off[0] + f / 2.0, off[1] + f / 2.0, pic[0] - f, pic[1] - f]
1212 })
1213 .collect::<Vec<_>>();
1214 Some((MaskShape::Rect, rect, f.clone(), false))
1215 }
1216 (None, None) => None,
1217 };
1218 if let Some((shape, rect, feather, invert)) = &mask {
1219 param(
1220 &mut h,
1221 "shape",
1222 vec![if *shape == MaskShape::Rect { 1.0 } else { 2.0 }],
1223 );
1224 param(
1225 &mut h,
1226 "mask_x",
1227 f32s(rect.iter().map(|r| (r[0] - off[0]) / pic[0])),
1228 );
1229 param(
1230 &mut h,
1231 "mask_y",
1232 f32s(rect.iter().map(|r| (r[1] - off[1]) / pic[1])),
1233 );
1234 param(&mut h, "mask_w", f32s(rect.iter().map(|r| r[2] / pic[0])));
1235 param(&mut h, "mask_h", f32s(rect.iter().map(|r| r[3] / pic[1])));
1236 param(
1237 &mut h,
1238 "feather",
1239 f32s(feather.iter().map(|f| f.max(0.0) * px)),
1240 );
1241 param(&mut h, "invert", vec![if *invert { 1.0 } else { 0.0 }]);
1242 }
1243 hooks.push(h);
1244 }
1245
1246 let (mut crop, mut crops) = (crop, crops);
1247 let mut scene = None;
1248 let mut remap = None;
1249 let src = match (&l.source, &solid) {
1250 (Source::Video(p), _) if let Some((node, reach)) = &l.filter => {
1251 let (prep, r) =
1252 self.filter_prep(l, p, node, *reach, &span, &vis, &env, &path, media)?;
1253 remap = Some(r);
1254 LayerSource::Filtered(Arc::new(prep))
1255 }
1256 (Source::Video(p), _) if let Some(t) = &l.time => {
1257 let (rate, n) = media.video_rate(p)?;
1258 if !(rate.is_finite() && rate > 0.0 && n > 0) {
1259 return Err(err(
1260 at("time"),
1261 format!("{}: no frames or frame rate to remap", p.display()),
1262 ));
1263 }
1264 let picks: Vec<i64> = time::source_times(t, &env, vis.clone(), &at("time"))?
1265 .into_iter()
1266 .map(|(s, mid)| time::pick(s, mid, rate, n))
1267 .collect();
1268 let lo = picks.iter().copied().min().unwrap_or(0);
1269 remap = Some(json!({
1271 "video": media.identity(p),
1272 "map": map_digest(&picks),
1273 }));
1274 LayerSource::Video {
1275 input: p.clone(),
1276 start: Duration::from_secs_f64(((lo as f64 - 0.5) / rate).max(0.0)),
1278 map: Some(picks.iter().map(|k| k - lo).collect()),
1279 }
1280 }
1281 (Source::Video(p), _) => {
1282 let st = l.source_in + vis.start as f64 / self.canvas.fps - l.in_;
1283 LayerSource::Video {
1284 input: p.clone(),
1285 start: Duration::from_secs_f64(st.max(0.0)),
1286 map: None,
1287 }
1288 }
1289 (Source::Still(p), _) => LayerSource::Still(p.clone()),
1290 (Source::Comp(c), _) => {
1291 let [sw, sh] = c.canvas.size.map(f64::from);
1294 let wmin = |f: fn(&Crop) -> f32, full: f64| {
1295 crop.iter()
1296 .chain(&crops)
1297 .map(|c| f64::from(f(c)))
1298 .fold(full, f64::min)
1299 .max(1e-3)
1300 };
1301 let mut size = [
1302 sw * tw / wmin(|c| c.width, sw),
1303 sh * th / wmin(|c| c.height, sh),
1304 ];
1305 let cap = limits::TEXTURE_SIDE / size[0].max(size[1]);
1306 if cap < 1.0 {
1307 size = size.map(|v| v * cap);
1308 }
1309 let size = size.map(|v| v.ceil().max(2.0));
1310 let ratio = [size[0] / sw, size[1] / sh];
1311 let scale = |c: &Crop| Crop {
1312 x: (f64::from(c.x) * ratio[0]) as f32,
1313 y: (f64::from(c.y) * ratio[1]) as f32,
1314 width: (f64::from(c.width) * ratio[0]) as f32,
1315 height: (f64::from(c.height) * ratio[1]) as f32,
1316 };
1317 crop = crop.as_ref().map(scale);
1318 crops = crops.iter().map(scale).collect();
1319 let start =
1321 (l.source_in * self.canvas.fps).round() as i64 + (vis.start - span.start);
1322 let size = Size {
1323 width: size[0] as u32,
1324 height: size[1] as u32,
1325 };
1326 let sub =
1327 c.lower_in(start..start + (vis.end - vis.start), size, media, false)?;
1328 scene = Some(json!({"comp": sub.recipe, "start": start}));
1329 LayerSource::Scene(Arc::new(Scene {
1330 size,
1331 grade: sub.grade,
1332 layers: sub.layers,
1333 overlays: sub.overlays,
1334 }))
1335 }
1336 (Source::Solid(_), _) => solid_image(),
1337 (Source::Text { .. }, _) => unreachable!("text is lowered above"),
1338 };
1339
1340 let covers = |r: &Crop| {
1343 (f64::from(r.x)).abs() < 0.5
1344 && (f64::from(r.y)).abs() < 0.5
1345 && (f64::from(r.width) - f64::from(out.width)).abs() < 0.5
1346 && (f64::from(r.height) - f64::from(out.height)).abs() < 0.5
1347 };
1348 let promote = top
1349 && i == 0
1350 && matches!(l.source, Source::Video(_))
1351 && l.time.is_none()
1352 && l.filter.is_none()
1353 && l.blend == BlendMode::Normal
1354 && vis == range
1355 && !has_mask
1356 && radius.iter().all(|r| *r <= 0.0)
1357 && s.opacity.iter().all(|o| *o >= 1.0)
1358 && s.rotation.iter().all(|r| r % 360.0 == 0.0)
1359 && s.rects.iter().all(covers);
1360 if promote {
1361 let LayerSource::Video { input, start, .. } = src else {
1362 unreachable!()
1363 };
1364 let mut r = json!({
1365 "base": media.identity(&input),
1366 "start": start.as_secs_f64(),
1367 });
1368 if let Some(g) = &graded_recipe {
1369 r["layer_grade"] = g.clone();
1370 }
1371 recipes.push(r);
1372 base = Base::Video { input, start };
1373 base_grade = Some(Grade {
1374 size: Some(out),
1375 crop,
1376 crops,
1377 hooks,
1378 ..layer_grade
1379 });
1380 continue;
1381 }
1382
1383 let grade = Grade {
1384 size: Some(Size {
1385 width: tw as u32,
1386 height: th as u32,
1387 }),
1388 crop,
1389 crops,
1390 dither: None,
1391 peak_detect: false,
1392 hooks,
1393 ..layer_grade
1394 };
1395 let rotation = if s.rotation.iter().all(|r| *r == 0.0) {
1396 Vec::new()
1397 } else {
1398 f32s(s.rotation.iter().copied())
1399 };
1400 let layer = Layer {
1401 source: src,
1402 grade,
1403 from: vis.start - p0,
1404 to: vis.end - p0,
1405 rects: s.rects,
1406 rotation,
1407 blend: l.blend,
1408 };
1409 let mut r = json!({
1410 "source": match &layer.source {
1411 LayerSource::Video { input, start, .. } => remap.take().unwrap_or_else(|| json!({"video": media.identity(input), "start": start.as_secs_f64()})),
1412 LayerSource::Image(_) => json!("solid"),
1413 LayerSource::Still(p) => json!({"still": media.identity(p)}),
1414 LayerSource::Scene(_) => scene.take().unwrap_or_default(),
1415 LayerSource::Filtered(_) => remap.take().unwrap_or_default(),
1416 },
1417 "from": layer.from,
1418 "to": layer.to,
1419 "rects": digest(&layer.rects.iter().flat_map(|r| [r.x, r.y, r.width, r.height]).collect::<Vec<_>>()),
1420 "rotation": digest(&layer.rotation),
1421 "blend": layer.blend.as_str(),
1422 "grade": grade_recipe(&layer.grade),
1423 });
1424 if let Some(g) = graded_recipe {
1425 r["layer_grade"] = g;
1426 }
1427 recipes.push(r);
1428 layers.push(layer);
1429 shown.push(i);
1430 }
1431
1432 let grade = if let Some(g) = base_grade {
1433 g
1434 } else {
1435 let bg = sample(
1436 &self.canvas.background,
1437 "canvas.background",
1438 range.clone(),
1439 &cenv,
1440 )?;
1441 let mut h = hook("background", shaders::background());
1442 for (n, name) in ["bg_r", "bg_g", "bg_b"].into_iter().enumerate() {
1443 param(&mut h, name, f32s(bg.iter().map(|v| v[n])));
1444 }
1445 Grade {
1446 size: Some(out),
1447 hooks: vec![h],
1448 ..Grade::default()
1449 }
1450 };
1451 if canvas_fade.iter().any(|f| *f < 1.0) {
1454 let mut h = hook("solid", shaders::solid());
1455 for (n, name) in ["solid_r", "solid_g", "solid_b"].into_iter().enumerate() {
1456 param(&mut h, name, f32s(canvas_colour.iter().map(|v| v[n])));
1457 }
1458 let mut a = hook("alpha", shaders::layer_alpha());
1459 param(
1460 &mut a,
1461 "opacity",
1462 f32s(canvas_fade.iter().map(|f| 1.0 - f.clamp(0.0, 1.0))),
1463 );
1464 let layer = Layer {
1465 source: solid_image(),
1466 grade: Grade {
1467 size: Some(Size {
1468 width: 2,
1469 height: 2,
1470 }),
1471 hooks: vec![h, a],
1472 ..Grade::default()
1473 },
1474 from: 0,
1475 to: frames,
1476 rects: Vec::new(),
1477 rotation: Vec::new(),
1478 blend: BlendMode::Normal,
1479 };
1480 recipes.push(json!({
1481 "canvas_fade": grade_recipe(&layer.grade),
1482 }));
1483 layers.push(layer);
1484 }
1485 let recipe = json!({
1486 "comp": 1,
1487 "canvas": self.canvas.size,
1488 "out": [out.width, out.height],
1489 "grade": grade_recipe(&grade),
1490 "layers": recipes,
1491 });
1492 Ok(Piece {
1493 from: p0,
1494 frames,
1495 base,
1496 grade,
1497 layers,
1498 shown,
1499 overlays,
1500 recipe,
1501 })
1502 }
1503}
1504
1505fn overlay_digest(overlays: &[Overlay]) -> Value {
1507 let mut h = Sha256::new();
1508 let f32s = |h: &mut Sha256, v: &[f32]| {
1509 for x in v {
1510 h.update(x.to_bits().to_le_bytes());
1511 }
1512 };
1513 let parts = |h: &mut Sha256, parts: &[ffcine_render::OverlayPart]| {
1514 h.update((parts.len() as u64).to_le_bytes());
1515 for p in parts {
1516 f32s(h, &p.pos);
1517 f32s(h, &p.size.unwrap_or([f32::NAN; 2]));
1518 f32s(h, &p.src.unwrap_or([f32::NAN; 4]));
1519 f32s(h, &p.colour);
1520 }
1521 };
1522 for o in overlays {
1523 h.update(o.mask.width.to_le_bytes());
1524 h.update(o.mask.height.to_le_bytes());
1525 h.update(Sha256::digest(&o.mask.data));
1526 parts(&mut h, &o.parts);
1527 h.update(o.from.to_le_bytes());
1528 h.update(o.to.to_le_bytes());
1529 h.update(o.fade_in.to_bits().to_le_bytes());
1530 h.update(o.fade_out.to_bits().to_le_bytes());
1531 if let Some(m) = &o.motion {
1532 h.update(m.first.to_le_bytes());
1533 h.update((m.frames.len() as u64).to_le_bytes());
1534 for f in &m.frames {
1535 parts(&mut h, f);
1536 }
1537 }
1538 }
1539 let d = h.finalize();
1540 Value::String(d[..16].iter().fold(String::new(), |mut s, b| {
1541 let _ = write!(s, "{b:02x}");
1542 s
1543 }))
1544}
1545
1546fn font_files(params: &serde_json::Map<String, Value>, dir: &Path) -> Vec<PathBuf> {
1549 let mut out = Vec::new();
1550 let top = Value::Object(params.clone());
1551 let mut stack = vec![&top];
1552 while let Some(v) = stack.pop() {
1553 match v {
1554 Value::Object(m) => {
1555 if let Some(f) = m.get("font_file").and_then(Value::as_str) {
1556 let sub = m.get("fonts_dir").and_then(Value::as_str).unwrap_or("");
1557 out.push(dir.join(sub).join(f));
1558 }
1559 stack.extend(m.values());
1560 }
1561 Value::Array(a) => stack.extend(a),
1562 _ => {}
1563 }
1564 }
1565 out.sort();
1566 out.dedup();
1567 out
1568}
1569
1570fn finite<'a>(what: &str, values: impl IntoIterator<Item = &'a f64>) -> Result<()> {
1573 match values
1574 .into_iter()
1575 .find(|v| !v.is_finite() || v.abs() > limits::COORD)
1576 {
1577 Some(v) => Err(err(
1578 what,
1579 format!("{v}: out of range (at most {:e} either way)", limits::COORD),
1580 )),
1581 None => Ok(()),
1582 }
1583}
1584
1585fn sample<T: Animatable>(
1587 track: &Track<T>,
1588 what: &str,
1589 range: Range<i64>,
1590 env: &Env,
1591) -> Result<Vec<T>> {
1592 track
1593 .sample(Clock::Layer, range, env)
1594 .map_err(|e| err(what, e.to_string()))
1595}
1596
1597fn fit(fit: Fit, src: Option<[f64; 2]>, size: [f64; 2]) -> (Option<Crop>, [f64; 2]) {
1599 let Some([sw, sh]) = src.filter(|s| s[0] > 0.0 && s[1] > 0.0) else {
1600 return (None, size);
1601 };
1602 #[allow(clippy::cast_possible_truncation)]
1603 let crop = |w: f64, h: f64| {
1604 Some(Crop {
1605 x: ((sw - w) / 2.0) as f32,
1606 y: ((sh - h) / 2.0) as f32,
1607 width: w as f32,
1608 height: h as f32,
1609 })
1610 };
1611 let [bw, bh] = size;
1612 match fit {
1613 Fit::Fill => (None, size),
1614 Fit::Cover => {
1615 let s = (bw / sw).max(bh / sh);
1616 let (w, h) = ((bw / s).min(sw), (bh / s).min(sh));
1617 if (w - sw).abs() < 1e-6 && (h - sh).abs() < 1e-6 {
1618 (None, size)
1619 } else {
1620 (crop(w, h), size)
1621 }
1622 }
1623 Fit::Contain => {
1624 let s = (bw / sw).min(bh / sh);
1625 (None, [sw * s, sh * s])
1626 }
1627 Fit::None => {
1628 let (w, h) = (sw.min(bw), sh.min(bh));
1630 let c = if w < sw || h < sh { crop(w, h) } else { None };
1631 (c, [w, h])
1632 }
1633 }
1634}