use super::barcode;
use super::canvas::Rect;
use super::timecode::text_width;
use crate::{Error, Result};
pub const TIMECODE_TEMPLATE: &str = "00:00:00:00";
pub const FRAME_NUMBER_TEMPLATE: &str = "F 00000000";
pub const STACK_BELOW_ASPECT: f64 = 0.7;
const TIMECODE_MAX: f64 = 0.105;
const TIMECODE_MIN_PX: f64 = 20.0;
const FRAME_NUMBER: f64 = 0.7;
const SPACING: f64 = 0.33;
const PROGRESS: f64 = 0.35;
const INFO_SPACING: f64 = 0.25;
const INFO: f64 = 0.22;
const STACK: f64 =
1.0 + SPACING + FRAME_NUMBER + SPACING + PROGRESS + INFO_SPACING + INFO + SPACING;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TextBox {
pub x: i32,
pub y: i32,
pub height: i32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Layout {
pub width: usize,
pub height: usize,
pub border: i32,
pub timecode: TextBox,
pub frame_number: TextBox,
pub progress: Rect,
pub info: Rect,
pub stage: Rect,
pub qr: Rect,
pub link: Rect,
}
impl Layout {
pub fn new(width: usize, height: usize) -> Result<Self> {
if width < 320 || height < 180 || !width.is_multiple_of(2) || !height.is_multiple_of(2) {
return Err(Error::InvalidOption(format!(
"frame size {width}x{height} must be even and at least 320x180"
)));
}
let (w, h) = (width as f64, height as f64);
let border = ((w.min(h) / 180.0).round() as i32).max(2);
let margin = 0.03 * w;
let top = (barcode::STRIP_BOTTOM + 0.045) * h;
let bottom = 0.95 * h;
let (content_w, content_h) = (w - 2.0 * margin, bottom - top);
let timecode_per_px = f64::from(text_width(TIMECODE_TEMPLATE, 1000)) / 1000.0;
let (stack, stage_top, stage_w, qr) = if w / h >= STACK_BELOW_ASPECT {
let side = content_h.min(0.42 * w);
let qr_x = w - margin - side;
let column = qr_x - 2.0 * margin;
let t = (column / timecode_per_px).min((TIMECODE_MAX * h).max(TIMECODE_MIN_PX));
let stack = Stack::new(margin, top, column, t);
let qr_y = top + (content_h - side) / 2.0;
(stack, stack.bottom, column, (qr_x, qr_y, side))
} else {
let side_goal = 0.5 * content_w;
let t = (content_w / timecode_per_px)
.min((TIMECODE_MAX * h).max(TIMECODE_MIN_PX))
.min((content_h - side_goal) / STACK);
let stack = Stack::new(margin, top, content_w, t);
let below = bottom - stack.bottom;
let side = below.min(side_goal);
let qr_x = w - margin - side;
let qr_y = stack.bottom + (below - side) / 2.0;
(stack, stack.bottom, qr_x - 2.0 * margin, (qr_x, qr_y, side))
};
let (qr_x, qr_y, side) = qr;
let stage = Rect {
x0: margin as i32,
y0: stage_top.ceil() as i32,
x1: (margin + stage_w) as i32,
y1: bottom as i32,
};
let gap = (border / 2).max(1);
let link = Rect {
x0: stage.x0,
y0: stage.y1 + gap,
x1: stage.x1,
y1: height as i32 - border - gap,
};
Ok(Layout {
width,
height,
border,
timecode: stack.timecode,
frame_number: stack.frame_number,
progress: stack.progress,
info: stack.info,
stage,
qr: Rect::new(qr_x as i32, qr_y as i32, side as i32, side as i32),
link,
})
}
}
#[derive(Clone, Copy)]
struct Stack {
timecode: TextBox,
frame_number: TextBox,
progress: Rect,
info: Rect,
bottom: f64,
}
impl Stack {
fn new(x: f64, top: f64, width: f64, t: f64) -> Self {
let tc_h = t as i32;
let fn_h = (FRAME_NUMBER * t) as i32;
let (x, w) = (x as i32, width as i32);
let timecode = TextBox {
x,
y: top as i32,
height: tc_h,
};
let frame_number = TextBox {
x,
y: timecode.y + tc_h + (SPACING * t) as i32,
height: fn_h,
};
let progress_y = frame_number.y + fn_h + (SPACING * t) as i32;
let progress = Rect::new(x, progress_y, w, ((PROGRESS * t) as i32).max(2));
let info_y = progress.y1 + (INFO_SPACING * t) as i32;
let info = Rect::new(x, info_y, w, ((INFO * t) as i32).max(super::font::ROWS));
Stack {
timecode,
frame_number,
progress,
info,
bottom: f64::from(info.y1) + SPACING * t,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn overlaps(a: Rect, b: Rect) -> bool {
a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1
}
fn text_rect(t: TextBox, template: &str) -> Rect {
Rect::new(t.x, t.y, text_width(template, t.height), t.height)
}
const SIZES: [(usize, usize); 14] = [
(320, 180),
(320, 240),
(640, 480),
(1280, 720),
(1920, 1080),
(3840, 2160),
(3840, 1080),
(320, 320),
(1080, 1080),
(1080, 1350),
(1080, 1440),
(320, 568),
(720, 1280),
(1080, 1920),
];
#[test]
fn elements_fit_and_do_not_overlap() {
for (w, h) in SIZES {
let l = Layout::new(w, h).unwrap();
let strip_bottom = (barcode::STRIP_BOTTOM * h as f64) as i32;
let frame = Rect::new(0, 0, w as i32, h as i32).inset(l.border);
let elements = [
("timecode", text_rect(l.timecode, TIMECODE_TEMPLATE)),
(
"frame number",
text_rect(l.frame_number, FRAME_NUMBER_TEMPLATE),
),
("progress", l.progress),
("info", l.info),
("stage", l.stage),
("qr", l.qr),
("link", l.link),
];
for (i, &(name, r)) in elements.iter().enumerate() {
assert!(r.width() > 0 && r.height() > 0, "{w}x{h}: {name} is empty");
if name == "link" {
assert!(r.y0 >= l.stage.y1 && r.y0 >= l.qr.y1, "{w}x{h}: link {r:?}");
}
assert!(r.y0 > strip_bottom, "{w}x{h}: {name} overlaps the barcode");
assert!(
r.x0 >= frame.x0 && r.y0 >= frame.y0 && r.x1 <= frame.x1 && r.y1 <= frame.y1,
"{w}x{h}: {name} {r:?} is outside the frame"
);
for &(other, s) in &elements[i + 1..] {
assert!(
!overlaps(r, s),
"{w}x{h}: {name} {r:?} overlaps {other} {s:?}"
);
}
}
assert!(l.timecode.height >= 20, "{w}x{h}: timecode too small");
let short = w.min(h) as i32;
assert!(l.qr.width() * 3 >= short, "{w}x{h}: QR code too small");
assert!(l.stage.height() * 6 >= short, "{w}x{h}: stage too small");
}
assert!(Layout::new(321, 180).is_err());
assert!(Layout::new(300, 180).is_err());
}
#[test]
fn tall_frames_stack_the_text_above_the_qr_code() {
let wide = Layout::new(1280, 720).unwrap();
assert!(wide.qr.y0 < wide.progress.y1, "beside the text");
let square = Layout::new(1080, 1080).unwrap();
assert!(square.qr.x0 > square.progress.x1);
let tall = Layout::new(1080, 1920).unwrap();
assert!(tall.qr.y0 > tall.info.y1, "below the text");
assert!(
tall.progress.width() > tall.qr.width(),
"text spans the frame"
);
assert!(
tall.stage.y0 > tall.info.y1 && tall.stage.x1 <= tall.qr.x0,
"stage beside the QR code"
);
}
}