#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BurnInStyle {
Classic,
Modern,
Minimal,
}
impl BurnInStyle {
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn font_scale(&self) -> f32 {
match self {
BurnInStyle::Classic => 2.0,
BurnInStyle::Modern => 1.5,
BurnInStyle::Minimal => 1.0,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BurnInPosition {
TopLeft,
TopCenter,
TopRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl BurnInPosition {
#[must_use]
pub fn is_top(&self) -> bool {
matches!(
self,
BurnInPosition::TopLeft | BurnInPosition::TopCenter | BurnInPosition::TopRight
)
}
#[must_use]
pub fn is_right(&self) -> bool {
matches!(self, BurnInPosition::TopRight | BurnInPosition::BottomRight)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TimecodeOverlay {
pub style: BurnInStyle,
pub position: BurnInPosition,
pub background_alpha: f32,
pub color: [u8; 3],
}
impl TimecodeOverlay {
#[must_use]
pub fn default_broadcast() -> Self {
Self {
style: BurnInStyle::Classic,
position: BurnInPosition::BottomCenter,
background_alpha: 0.5,
color: [255, 255, 255],
}
}
#[must_use]
pub fn default_dailies() -> Self {
Self {
style: BurnInStyle::Modern,
position: BurnInPosition::TopLeft,
background_alpha: 0.0,
color: [255, 255, 0],
}
}
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn render_timecode_text(
tc: &str,
frame_width: u32,
frame_height: u32,
overlay: &TimecodeOverlay,
) -> (u32, u32) {
let char_w = (12.0 * overlay.style.font_scale()) as u32;
let char_h = (20.0 * overlay.style.font_scale()) as u32;
let text_w = char_w * tc.len() as u32;
let margin = 16u32;
let x = match overlay.position {
BurnInPosition::TopLeft | BurnInPosition::BottomLeft => margin,
BurnInPosition::TopCenter | BurnInPosition::BottomCenter => {
frame_width.saturating_sub(text_w) / 2
}
BurnInPosition::TopRight | BurnInPosition::BottomRight => {
frame_width.saturating_sub(text_w + margin)
}
};
let y = if overlay.position.is_top() {
margin
} else {
frame_height.saturating_sub(char_h + margin)
};
(x, y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_burn_in_style_classic_scale() {
assert!((BurnInStyle::Classic.font_scale() - 2.0).abs() < f32::EPSILON);
}
#[test]
fn test_burn_in_style_modern_scale() {
assert!((BurnInStyle::Modern.font_scale() - 1.5).abs() < f32::EPSILON);
}
#[test]
fn test_burn_in_style_minimal_scale() {
assert!((BurnInStyle::Minimal.font_scale() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_position_is_top_true() {
assert!(BurnInPosition::TopLeft.is_top());
assert!(BurnInPosition::TopCenter.is_top());
assert!(BurnInPosition::TopRight.is_top());
}
#[test]
fn test_position_is_top_false() {
assert!(!BurnInPosition::BottomLeft.is_top());
assert!(!BurnInPosition::BottomCenter.is_top());
assert!(!BurnInPosition::BottomRight.is_top());
}
#[test]
fn test_position_is_right_true() {
assert!(BurnInPosition::TopRight.is_right());
assert!(BurnInPosition::BottomRight.is_right());
}
#[test]
fn test_position_is_right_false() {
assert!(!BurnInPosition::TopLeft.is_right());
assert!(!BurnInPosition::BottomCenter.is_right());
}
#[test]
fn test_default_broadcast_style() {
let o = TimecodeOverlay::default_broadcast();
assert_eq!(o.style, BurnInStyle::Classic);
assert_eq!(o.position, BurnInPosition::BottomCenter);
assert!(!o.position.is_top());
}
#[test]
fn test_default_dailies_style() {
let o = TimecodeOverlay::default_dailies();
assert_eq!(o.style, BurnInStyle::Modern);
assert_eq!(o.position, BurnInPosition::TopLeft);
assert!(o.position.is_top());
}
#[test]
fn test_render_timecode_text_bottom_center() {
let overlay = TimecodeOverlay::default_broadcast();
let (x, y) = render_timecode_text("01:02:03:04", 1920, 1080, &overlay);
assert!(x < 1920);
assert!(y > 1080 / 2);
}
#[test]
fn test_render_timecode_text_top_left() {
let overlay = TimecodeOverlay::default_dailies();
let (x, y) = render_timecode_text("01:02:03:04", 1920, 1080, &overlay);
assert!(x < 100);
assert!(y < 100);
}
#[test]
fn test_render_timecode_text_top_right() {
let overlay = TimecodeOverlay {
style: BurnInStyle::Minimal,
position: BurnInPosition::TopRight,
background_alpha: 0.0,
color: [255, 255, 255],
};
let (x, _y) = render_timecode_text("01:02:03:04", 1920, 1080, &overlay);
assert!(x > 1920 / 2);
}
}