1use glam::Vec2;
10
11#[must_use]
23pub fn aspect_fit_scale(surface_w: u32, surface_h: u32, video_w: u32, video_h: u32) -> Vec2 {
24 if surface_w == 0 || surface_h == 0 || video_w == 0 || video_h == 0 {
25 return Vec2::splat(1.0);
26 }
27 #[allow(
28 clippy::cast_precision_loss,
29 reason = "surface and video dimensions fit comfortably in f32"
30 )]
31 let surface_aspect = surface_w as f32 / surface_h as f32;
32 #[allow(
33 clippy::cast_precision_loss,
34 reason = "surface and video dimensions fit comfortably in f32"
35 )]
36 let video_aspect = video_w as f32 / video_h as f32;
37 if video_aspect > surface_aspect {
38 Vec2::new(2.0, 2.0 * surface_aspect / video_aspect)
39 } else {
40 Vec2::new(2.0 * video_aspect / surface_aspect, 2.0)
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::aspect_fit_scale;
47 use glam::Vec2;
48
49 fn approx(a: Vec2, b: Vec2) {
51 assert!(
52 (a.x - b.x).abs() < 1e-4 && (a.y - b.y).abs() < 1e-4,
53 "{a:?} != {b:?}"
54 );
55 }
56
57 #[test]
58 fn matching_aspect_fills_full_ndc() {
59 approx(aspect_fit_scale(1920, 1080, 1920, 1080), Vec2::splat(2.0));
60 approx(aspect_fit_scale(1000, 1000, 512, 512), Vec2::splat(2.0));
61 }
62
63 #[test]
64 fn wider_video_letterboxes_top_and_bottom() {
65 let scale = aspect_fit_scale(1000, 1000, 1920, 1080);
67 approx(scale, Vec2::new(2.0, 2.0 * (1.0_f32 / (16.0 / 9.0))));
68 assert!(scale.y < scale.x, "letterbox: y < x");
69 }
70
71 #[test]
72 fn taller_video_pillarboxes_left_and_right() {
73 let scale = aspect_fit_scale(1000, 1000, 1080, 1920);
75 approx(scale, Vec2::new(2.0 * (9.0 / 16.0), 2.0));
76 assert!(scale.x < scale.y, "pillarbox: x < y");
77 }
78
79 #[test]
80 fn zero_dimensions_return_identity() {
81 approx(aspect_fit_scale(0, 1080, 1920, 1080), Vec2::splat(1.0));
82 approx(aspect_fit_scale(1920, 0, 1920, 1080), Vec2::splat(1.0));
83 approx(aspect_fit_scale(1920, 1080, 0, 1080), Vec2::splat(1.0));
84 approx(aspect_fit_scale(1920, 1080, 1920, 0), Vec2::splat(1.0));
85 }
86}