use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Mask {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
impl Mask {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
data: vec![0; width as usize * height as usize],
}
}
pub fn solid() -> Self {
Self {
width: 1,
height: 1,
data: vec![255],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct OverlayPart {
pub pos: [f32; 2],
pub size: Option<[f32; 2]>,
pub src: Option<[f32; 4]>,
pub colour: [f32; 4],
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Overlay {
pub mask: Arc<Mask>,
pub parts: Vec<OverlayPart>,
pub from: i64,
pub to: i64,
pub fade_in: f64,
pub fade_out: f64,
pub motion: Option<Arc<Motion>>,
pub under: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Motion {
pub first: i64,
pub frames: Vec<Vec<OverlayPart>>,
}
impl OverlayPart {
pub fn rects(&self, mask: &Mask) -> ([f32; 4], [f32; 4]) {
#[allow(clippy::cast_precision_loss)]
let src = self
.src
.unwrap_or([0.0, 0.0, mask.width as f32, mask.height as f32]);
let [w, h] = self.size.unwrap_or([src[2] - src[0], src[3] - src[1]]);
let [x, y] = self.pos;
(src, [x, y, x + w, y + h])
}
}
impl Overlay {
pub fn parts_at(&self, frame: i64) -> &[OverlayPart] {
match &self.motion {
Some(m) if !m.frames.is_empty() => {
let last = m.frames.len() - 1;
let i = usize::try_from(frame - m.first).map_or(0, |i| i.min(last));
&m.frames[i]
}
_ => &self.parts,
}
}
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
pub fn opacity(&self, frame: i64) -> f32 {
if frame < self.from || frame >= self.to {
return 0.0;
}
let t = (frame - self.from) as f64 + 0.5;
let left = (self.to - frame) as f64 - 0.5;
let a = if self.fade_in > 0.0 {
(t / self.fade_in).min(1.0)
} else {
1.0
};
let b = if self.fade_out > 0.0 {
(left / self.fade_out).min(1.0)
} else {
1.0
};
a.min(b).clamp(0.0, 1.0) as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opacity_fades_and_range() {
let o = Overlay {
mask: Arc::new(Mask::solid()),
parts: Vec::new(),
from: 10,
to: 20,
fade_in: 2.0,
fade_out: 4.0,
motion: None,
under: None,
};
assert!(o.opacity(9).abs() < f32::EPSILON);
assert!((o.opacity(10) - 0.25).abs() < 1e-6);
assert!((o.opacity(12) - 1.0).abs() < 1e-6);
assert!((o.opacity(19) - 0.125).abs() < 1e-6);
assert!(o.opacity(20).abs() < f32::EPSILON);
}
#[test]
fn motion_parts_and_crops() {
let part = |x: f32| OverlayPart {
pos: [x, 0.0],
..OverlayPart::default()
};
let o = Overlay {
mask: Arc::new(Mask::new(8, 4)),
parts: vec![part(-1.0)],
from: 0,
to: 10,
motion: Some(Arc::new(Motion {
first: 2,
frames: vec![vec![part(2.0)], vec![part(3.0)]],
})),
..Overlay::default()
};
assert_eq!(o.parts_at(0)[0].pos[0], 2.0);
assert_eq!(o.parts_at(3)[0].pos[0], 3.0);
assert_eq!(o.parts_at(9)[0].pos[0], 3.0);
let p = OverlayPart {
pos: [10.0, 20.0],
src: Some([2.0, 0.0, 6.0, 4.0]),
..OverlayPart::default()
};
assert_eq!(
p.rects(&o.mask),
([2.0, 0.0, 6.0, 4.0], [10.0, 20.0, 14.0, 24.0])
);
}
}