1use ffcine_core::geom::Mat3;
5use ffcine_render::Crop;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct Rect {
10 pub x: f64,
11 pub y: f64,
12 pub w: f64,
13 pub h: f64,
14}
15
16impl Rect {
17 pub fn frame(w: f64, h: f64) -> Self {
19 Self {
20 x: 0.0,
21 y: 0.0,
22 w,
23 h,
24 }
25 }
26
27 pub fn aspect(&self) -> f64 {
28 self.w / self.h
29 }
30
31 #[must_use]
35 pub fn fit_aspect(&self, ar: f64, pos: [f64; 2]) -> Self {
36 let (w, h) = if self.aspect() > ar {
37 (self.h * ar, self.h)
38 } else {
39 (self.w, self.w / ar)
40 };
41 self.place(w, h, pos)
42 }
43
44 #[must_use]
48 pub fn zoom(&self, zoom: f64, pos: [f64; 2]) -> Self {
49 let z = if zoom.is_finite() { zoom.max(1.0) } else { 1.0 };
50 self.place(self.w / z, self.h / z, pos)
51 }
52
53 #[must_use]
54 fn place(&self, w: f64, h: f64, [px, py]: [f64; 2]) -> Self {
55 Self {
56 x: self.x + (self.w - w) * px.clamp(0.0, 1.0),
57 y: self.y + (self.h - h) * py.clamp(0.0, 1.0),
58 w,
59 h,
60 }
61 }
62
63 pub fn to_mat3(&self, out_w: f64, out_h: f64) -> Mat3 {
66 let (sx, sy) = (self.w / out_w, self.h / out_h);
67 [[sx, 0.0, self.x], [0.0, sy, self.y], [0.0, 0.0, 1.0]]
68 }
69
70 #[allow(clippy::cast_possible_truncation)]
71 pub fn to_crop(&self) -> Crop {
72 Crop {
73 x: self.x as f32,
74 y: self.y as f32,
75 width: self.w as f32,
76 height: self.h as f32,
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use ffcine_core::geom;
85
86 #[test]
87 fn vertical_windows_from_uhd() {
88 let src = Rect::frame(3840.0, 2160.0);
89 let reel = src.fit_aspect(9.0 / 16.0, [0.5, 0.5]);
90 assert!((reel.w - 1215.0).abs() < 1e-9 && (reel.h - 2160.0).abs() < 1e-9);
91 assert!((reel.x - (3840.0 - 1215.0) / 2.0).abs() < 1e-9 && reel.y == 0.0);
92 let feed = src.fit_aspect(4.0 / 5.0, [0.0, 0.5]);
93 assert!((feed.w - 1728.0).abs() < 1e-9 && feed.x == 0.0);
94 let right = src.fit_aspect(4.0 / 5.0, [1.0, 0.5]);
95 assert!((right.x + right.w - 3840.0).abs() < 1e-9);
96 let portrait = Rect::frame(2160.0, 3840.0).fit_aspect(16.0 / 9.0, [0.5, 0.25]);
98 assert!((portrait.h - 1215.0).abs() < 1e-9 && portrait.x == 0.0);
99 assert!((portrait.y - (3840.0 - 1215.0) * 0.25).abs() < 1e-9);
100 }
101
102 #[test]
103 fn zoom_matches_prototype_crop() {
104 let src = Rect::frame(3840.0, 2160.0);
105 let z = src.zoom(1.25, [0.3, 0.7]);
106 assert!((z.w - 3072.0).abs() < 1e-9 && (z.h - 1728.0).abs() < 1e-9);
107 assert!((z.x - 768.0 * 0.3).abs() < 1e-9 && (z.y - 432.0 * 0.7).abs() < 1e-9);
108 assert_eq!(src.zoom(0.8, [0.5, 0.5]), src);
110 assert!((z.aspect() - src.aspect()).abs() < 1e-12);
111 }
112
113 #[test]
114 fn matrix_maps_corners() {
115 let r = Rect {
116 x: 100.25,
117 y: 50.5,
118 w: 1215.0,
119 h: 2160.0,
120 };
121 let m = r.to_mat3(1080.0, 1920.0);
122 let [x, y] = geom::apply(&m, [0.0, 0.0]);
123 assert!((x - 100.25).abs() < 1e-9 && (y - 50.5).abs() < 1e-9);
124 let [x, y] = geom::apply(&m, [1080.0, 1920.0]);
125 assert!((x - 1315.25).abs() < 1e-9 && (y - 2210.5).abs() < 1e-9);
126 }
127}