use super::{clamp_u8, validate_buffer, PixelFormat, VideoResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VignetteFalloff {
Cosine,
Power(f32),
Linear,
}
#[derive(Debug, Clone)]
pub struct VignetteConfig {
pub strength: f32,
pub inner_radius: f32,
pub outer_radius: f32,
pub falloff: VignetteFalloff,
pub center_x: f32,
pub center_y: f32,
}
impl Default for VignetteConfig {
fn default() -> Self {
Self {
strength: 0.6,
inner_radius: 0.5,
outer_radius: 1.4,
falloff: VignetteFalloff::Cosine,
center_x: 0.5,
center_y: 0.5,
}
}
}
impl VignetteConfig {
#[must_use]
pub fn subtle() -> Self {
Self {
strength: 0.3,
inner_radius: 0.7,
outer_radius: 1.6,
..Default::default()
}
}
#[must_use]
pub fn heavy() -> Self {
Self {
strength: 0.9,
inner_radius: 0.3,
outer_radius: 1.1,
falloff: VignetteFalloff::Power(3.0),
..Default::default()
}
}
}
pub struct Vignette {
config: VignetteConfig,
}
impl Vignette {
#[must_use]
pub fn new(config: VignetteConfig) -> Self {
Self { config }
}
#[allow(clippy::cast_precision_loss)]
pub fn apply(
&self,
data: &mut [u8],
width: usize,
height: usize,
format: PixelFormat,
) -> VideoResult<()> {
validate_buffer(data, width, height, format)?;
let bpp = format.bytes_per_pixel();
let cfg = &self.config;
let cx = cfg.center_x * width as f32;
let cy = cfg.center_y * height as f32;
let ref_radius = (width.min(height) as f32) * 0.5;
let inner = cfg.inner_radius * ref_radius;
let outer = cfg.outer_radius * ref_radius;
let range = (outer - inner).max(1.0);
for py in 0..height {
for px in 0..width {
let dx = px as f32 - cx;
let dy = py as f32 - cy;
let dist = (dx * dx + dy * dy).sqrt();
let t = ((dist - inner) / range).clamp(0.0, 1.0);
let dark = match cfg.falloff {
VignetteFalloff::Cosine => {
(1.0 - (std::f32::consts::PI * t).cos()) * 0.5
}
VignetteFalloff::Power(p) => t.powf(p),
VignetteFalloff::Linear => t,
};
let mul = 1.0 - cfg.strength * dark;
let idx = (py * width + px) * bpp;
data[idx] = clamp_u8(f32::from(data[idx]) * mul);
data[idx + 1] = clamp_u8(f32::from(data[idx + 1]) * mul);
data[idx + 2] = clamp_u8(f32::from(data[idx + 2]) * mul);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn white_buf(w: usize, h: usize) -> Vec<u8> {
vec![255u8; w * h * 3]
}
#[test]
fn test_vignette_default_applies() {
let mut buf = white_buf(64, 64);
let v = Vignette::new(VignetteConfig::default());
assert!(v.apply(&mut buf, 64, 64, PixelFormat::Rgb).is_ok());
}
#[test]
fn test_vignette_center_brighter_than_corner() {
let mut buf = white_buf(128, 128);
let v = Vignette::new(VignetteConfig::default());
v.apply(&mut buf, 128, 128, PixelFormat::Rgb).unwrap();
let center_idx = (64 * 128 + 64) * 3;
let corner_idx = 0;
assert!(
buf[center_idx] >= buf[corner_idx],
"Center should be at least as bright as corner"
);
}
#[test]
fn test_vignette_zero_strength_unchanged() {
let orig = white_buf(32, 32);
let mut buf = orig.clone();
let cfg = VignetteConfig {
strength: 0.0,
..Default::default()
};
let v = Vignette::new(cfg);
v.apply(&mut buf, 32, 32, PixelFormat::Rgb).unwrap();
assert_eq!(buf, orig, "Zero strength should not change image");
}
#[test]
fn test_vignette_subtle_vs_heavy() {
let orig = white_buf(64, 64);
let mut subtle = orig.clone();
Vignette::new(VignetteConfig::subtle())
.apply(&mut subtle, 64, 64, PixelFormat::Rgb)
.unwrap();
let mut heavy = orig.clone();
Vignette::new(VignetteConfig::heavy())
.apply(&mut heavy, 64, 64, PixelFormat::Rgb)
.unwrap();
let corner = 0usize;
assert!(
heavy[corner] <= subtle[corner],
"Heavy should be darker at corner"
);
}
#[test]
fn test_vignette_power_falloff() {
let mut buf = white_buf(32, 32);
let cfg = VignetteConfig {
falloff: VignetteFalloff::Power(2.0),
..Default::default()
};
let v = Vignette::new(cfg);
assert!(v.apply(&mut buf, 32, 32, PixelFormat::Rgb).is_ok());
}
#[test]
fn test_vignette_linear_falloff() {
let mut buf = white_buf(32, 32);
let cfg = VignetteConfig {
falloff: VignetteFalloff::Linear,
..Default::default()
};
let v = Vignette::new(cfg);
assert!(v.apply(&mut buf, 32, 32, PixelFormat::Rgb).is_ok());
}
#[test]
fn test_vignette_rgba() {
let mut buf = vec![200u8; 32 * 32 * 4];
let v = Vignette::new(VignetteConfig::default());
assert!(v.apply(&mut buf, 32, 32, PixelFormat::Rgba).is_ok());
}
#[test]
fn test_vignette_wrong_size_err() {
let mut buf = vec![0u8; 5];
let v = Vignette::new(VignetteConfig::default());
assert!(v.apply(&mut buf, 32, 32, PixelFormat::Rgb).is_err());
}
}