pub(crate) fn letterbox_size(container_w: f32, container_h: f32, aspect: f32) -> (f32, f32) {
if !aspect.is_finite() || aspect <= 0.0 {
return (container_w, container_h);
}
if container_h == 0.0 {
return (container_w, 0.0);
}
let container_aspect = container_w / container_h;
if container_aspect > aspect {
(container_h * aspect, container_h)
} else {
(container_w, container_w / aspect)
}
}
pub(crate) fn letterbox_rect(
origin: [f32; 2],
container_size: [f32; 2],
aspect: f32,
) -> RectLayout {
let (draw_w, draw_h) = letterbox_size(container_size[0], container_size[1], aspect);
let rect_min = [
origin[0] + (container_size[0] - draw_w) * 0.5,
origin[1] + (container_size[1] - draw_h) * 0.5,
];
let rect_max = [rect_min[0] + draw_w, rect_min[1] + draw_h];
RectLayout { rect_min, rect_max }
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct TextPanelLayout {
pub rect_min: [f32; 2],
pub rect_max: [f32; 2],
pub text_pos: [f32; 2],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct RectLayout {
pub rect_min: [f32; 2],
pub rect_max: [f32; 2],
}
impl RectLayout {
pub(crate) fn size(self) -> [f32; 2] {
[
self.rect_max[0] - self.rect_min[0],
self.rect_max[1] - self.rect_min[1],
]
}
}
pub(crate) fn top_left_text_panel(
origin: [f32; 2],
text_size: [f32; 2],
margin: [f32; 2],
padding: [f32; 2],
) -> TextPanelLayout {
let text_pos = [origin[0] + margin[0], origin[1] + margin[1]];
let rect_min = [text_pos[0] - padding[0], text_pos[1] - padding[1]];
let rect_max = [
text_pos[0] + text_size[0] + padding[0],
text_pos[1] + text_size[1] + padding[1],
];
TextPanelLayout {
rect_min,
rect_max,
text_pos,
}
}
pub(crate) fn top_right_text_panel(
origin: [f32; 2],
width: f32,
text_size: [f32; 2],
margin: [f32; 2],
padding: [f32; 2],
) -> TextPanelLayout {
let rect_w = text_size[0] + padding[0] * 2.0;
let rect_h = text_size[1] + padding[1] * 2.0;
let rect_min = [
origin[0] + width - rect_w - margin[0],
origin[1] + margin[1],
];
let rect_max = [rect_min[0] + rect_w, rect_min[1] + rect_h];
let text_pos = [rect_min[0] + padding[0], rect_min[1] + padding[1]];
TextPanelLayout {
rect_min,
rect_max,
text_pos,
}
}
pub(crate) fn bottom_center_text_panel(
origin: [f32; 2],
size: [f32; 2],
text_size: [f32; 2],
stack_index: usize,
bottom_margin: f32,
spacing: f32,
padding: [f32; 2],
) -> TextPanelLayout {
let rect_w = text_size[0] + padding[0] * 2.0;
let rect_h = text_size[1] + padding[1] * 2.0;
let rect_x = origin[0] + (size[0] - rect_w) * 0.5;
let rect_max_y = origin[1] + size[1] - bottom_margin - stack_index as f32 * (rect_h + spacing);
let rect_min = [rect_x, rect_max_y - rect_h];
let rect_max = [rect_x + rect_w, rect_max_y];
let text_pos = [rect_min[0] + padding[0], rect_min[1] + padding[1]];
TextPanelLayout {
rect_min,
rect_max,
text_pos,
}
}
pub(crate) fn crosshair_marker_rects(
origin: [f32; 2],
size: [f32; 2],
cropped_size: [u32; 2],
cropped_index: [f32; 2],
) -> [RectLayout; 8] {
let pixel_w = size[0] / cropped_size[0].max(1) as f32;
let pixel_h = size[1] / cropped_size[1].max(1) as f32;
let center_x = origin[0] + (cropped_index[0] + 0.5) * pixel_w;
let center_y = origin[1] + (cropped_index[1] + 0.5) * pixel_h;
let pattern: [(i32, i32); 8] = [
(0, -2),
(0, -1),
(-2, 0),
(-1, 0),
(1, 0),
(2, 0),
(0, 1),
(0, 2),
];
pattern.map(|(dx, dy)| {
let cx = center_x + dx as f32 * pixel_w;
let cy = center_y + dy as f32 * pixel_h;
RectLayout {
rect_min: [cx - pixel_w * 0.5, cy - pixel_h * 0.5],
rect_max: [cx + pixel_w * 0.5, cy + pixel_h * 0.5],
}
})
}
#[cfg(test)]
mod tests {
use super::*;
const NTSC_ASPECT: f32 = 8.0 / 7.0 * 16.0 / 15.0;
fn assert_close(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < 0.01,
"expected {expected}, got {actual}"
);
}
#[test]
fn letterbox_size_wide_container_preserves_aspect() {
let container_w = 1920.0;
let container_h = 1080.0;
let (w, h) = letterbox_size(container_w, container_h, NTSC_ASPECT);
assert_close(w, 1316.5714);
assert_close(h, 1080.0);
}
#[test]
fn letterbox_size_tall_container_preserves_aspect() {
let container_w = 800.0;
let container_h = 1200.0;
let (w, h) = letterbox_size(container_w, container_h, NTSC_ASPECT);
assert_close(w, 800.0);
assert_close(h, 656.25);
}
#[test]
fn letterbox_size_zero_height_preserves_width() {
let container_w = 800.0;
let container_h = 0.0;
let (w, h) = letterbox_size(container_w, container_h, NTSC_ASPECT);
assert_close(w, 800.0);
assert_close(h, 0.0);
}
#[test]
fn letterbox_size_invalid_aspect_falls_back_to_container() {
for aspect in [0.0, -1.0, f32::NAN, f32::INFINITY] {
let container_w = 800.0;
let container_h = 600.0;
let (w, h) = letterbox_size(container_w, container_h, aspect);
assert_close(w, container_w);
assert_close(h, container_h);
}
}
#[test]
fn letterbox_rect_centers_preserved_aspect_size() {
let origin = [10.0, 20.0];
let container_size = [1920.0, 1080.0];
let rect = letterbox_rect(origin, container_size, NTSC_ASPECT);
assert_close(rect.rect_min[0], 311.7143);
assert_close(rect.rect_min[1], 20.0);
assert_close(rect.rect_max[0], 1628.2856);
assert_close(rect.rect_max[1], 1100.0);
}
#[test]
fn rect_layout_size_returns_width_and_height() {
let rect = RectLayout {
rect_min: [12.0, 34.0],
rect_max: [112.0, 94.0],
};
let size = rect.size();
assert_eq!(size, [100.0, 60.0]);
}
#[test]
fn top_left_text_panel_offsets_text_and_padding_from_origin() {
let origin = [100.0, 50.0];
let text_size = [80.0, 20.0];
let margin = [8.0, 8.0];
let padding = [6.0, 4.0];
let layout = top_left_text_panel(origin, text_size, margin, padding);
assert_eq!(layout.text_pos, [108.0, 58.0]);
assert_eq!(layout.rect_min, [102.0, 54.0]);
assert_eq!(layout.rect_max, [194.0, 82.0]);
}
#[test]
fn top_right_text_panel_aligns_to_right_edge() {
let origin = [100.0, 50.0];
let width = 800.0;
let text_size = [60.0, 20.0];
let margin = [8.0, 8.0];
let padding = [6.0, 4.0];
let layout = top_right_text_panel(origin, width, text_size, margin, padding);
assert_eq!(layout.rect_min, [820.0, 58.0]);
assert_eq!(layout.rect_max, [892.0, 86.0]);
assert_eq!(layout.text_pos, [826.0, 62.0]);
}
#[test]
fn bottom_center_text_panel_stacks_up_from_bottom_edge() {
let origin = [100.0, 50.0];
let size = [800.0, 600.0];
let text_size = [120.0, 24.0];
let bottom_margin = 12.0;
let spacing = 8.0;
let padding = [8.0, 6.0];
let bottom_layout =
bottom_center_text_panel(origin, size, text_size, 0, bottom_margin, spacing, padding);
let third_layout =
bottom_center_text_panel(origin, size, text_size, 2, bottom_margin, spacing, padding);
assert_eq!(bottom_layout.rect_min, [432.0, 602.0]);
assert_eq!(bottom_layout.rect_max, [568.0, 638.0]);
assert_eq!(bottom_layout.text_pos, [440.0, 608.0]);
assert_eq!(third_layout.rect_min, [432.0, 514.0]);
assert_eq!(third_layout.rect_max, [568.0, 550.0]);
assert_eq!(third_layout.text_pos, [440.0, 520.0]);
}
#[test]
fn crosshair_marker_rects_center_around_projected_pixel() {
let origin = [10.0, 20.0];
let size = [256.0, 240.0];
let cropped_size = [256, 240];
let cropped_index = [10.0, 20.0];
let rects = crosshair_marker_rects(origin, size, cropped_size, cropped_index);
assert_eq!(
rects,
[
RectLayout {
rect_min: [20.0, 38.0],
rect_max: [21.0, 39.0],
},
RectLayout {
rect_min: [20.0, 39.0],
rect_max: [21.0, 40.0],
},
RectLayout {
rect_min: [18.0, 40.0],
rect_max: [19.0, 41.0],
},
RectLayout {
rect_min: [19.0, 40.0],
rect_max: [20.0, 41.0],
},
RectLayout {
rect_min: [21.0, 40.0],
rect_max: [22.0, 41.0],
},
RectLayout {
rect_min: [22.0, 40.0],
rect_max: [23.0, 41.0],
},
RectLayout {
rect_min: [20.0, 41.0],
rect_max: [21.0, 42.0],
},
RectLayout {
rect_min: [20.0, 42.0],
rect_max: [21.0, 43.0],
},
]
);
}
}