use super::{clamp_u8, sample_bilinear, validate_buffer, PixelFormat, VideoResult};
#[derive(Debug, Clone)]
pub struct ChromaticAberrationConfig {
pub red_offset: f32,
pub green_offset: f32,
pub blue_offset: f32,
pub center_x: f32,
pub center_y: f32,
}
impl Default for ChromaticAberrationConfig {
fn default() -> Self {
Self {
red_offset: 0.005,
green_offset: 0.0,
blue_offset: -0.005,
center_x: 0.5,
center_y: 0.5,
}
}
}
impl ChromaticAberrationConfig {
#[must_use]
pub fn subtle() -> Self {
Self {
red_offset: 0.002,
blue_offset: -0.002,
..Default::default()
}
}
#[must_use]
pub fn strong() -> Self {
Self {
red_offset: 0.015,
blue_offset: -0.015,
..Default::default()
}
}
}
pub struct ChromaticAberration {
config: ChromaticAberrationConfig,
}
impl ChromaticAberration {
#[must_use]
pub fn new(config: ChromaticAberrationConfig) -> 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 diag = ((width * width + height * height) as f32).sqrt();
let source = data.to_vec();
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 (ux, uy) = if dist < 0.5 {
(0.0, 0.0)
} else {
(dx / dist, dy / dist)
};
let idx = (py * width + px) * bpp;
let roffset_px = cfg.red_offset * diag;
let rx = px as f32 + ux * roffset_px;
let ry = py as f32 + uy * roffset_px;
let r_sample = sample_bilinear(&source, width, height, bpp, rx, ry);
let goffset_px = cfg.green_offset * diag;
let gx = px as f32 + ux * goffset_px;
let gy = py as f32 + uy * goffset_px;
let g_sample = sample_bilinear(&source, width, height, bpp, gx, gy);
let boffset_px = cfg.blue_offset * diag;
let bx = px as f32 + ux * boffset_px;
let by = py as f32 + uy * boffset_px;
let b_sample = sample_bilinear(&source, width, height, bpp, bx, by);
data[idx] = clamp_u8(r_sample[0]);
data[idx + 1] = clamp_u8(g_sample[1]);
data[idx + 2] = clamp_u8(b_sample[2]);
if bpp == 4 {
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gradient_buf(w: usize, h: usize) -> Vec<u8> {
let mut buf = vec![0u8; w * h * 3];
for py in 0..h {
for px in 0..w {
let idx = (py * w + px) * 3;
buf[idx] = (px * 255 / w) as u8;
buf[idx + 1] = (py * 255 / h) as u8;
buf[idx + 2] = 128;
}
}
buf
}
#[test]
fn test_chromatic_default_applies() {
let mut buf = gradient_buf(64, 64);
let ca = ChromaticAberration::new(ChromaticAberrationConfig::default());
assert!(ca.apply(&mut buf, 64, 64, PixelFormat::Rgb).is_ok());
}
#[test]
fn test_chromatic_zero_offset_unchanged() {
let orig = gradient_buf(32, 32);
let mut buf = orig.clone();
let cfg = ChromaticAberrationConfig {
red_offset: 0.0,
green_offset: 0.0,
blue_offset: 0.0,
..Default::default()
};
let ca = ChromaticAberration::new(cfg);
ca.apply(&mut buf, 32, 32, PixelFormat::Rgb)
.expect("apply should succeed");
assert_eq!(buf, orig, "Zero offsets should leave image unchanged");
}
#[test]
fn test_chromatic_strong_differs_from_subtle() {
let orig = gradient_buf(64, 64);
let mut buf_subtle = orig.clone();
ChromaticAberration::new(ChromaticAberrationConfig::subtle())
.apply(&mut buf_subtle, 64, 64, PixelFormat::Rgb)
.expect("test expectation failed");
let mut buf_strong = orig.clone();
ChromaticAberration::new(ChromaticAberrationConfig::strong())
.apply(&mut buf_strong, 64, 64, PixelFormat::Rgb)
.expect("test expectation failed");
let diff: u32 = buf_subtle
.iter()
.zip(buf_strong.iter())
.map(|(&a, &b)| (a as i32 - b as i32).unsigned_abs())
.sum();
assert!(diff > 0, "Strong should differ from subtle");
}
#[test]
fn test_chromatic_rgba() {
let mut buf = vec![128u8; 32 * 32 * 4];
let ca = ChromaticAberration::new(ChromaticAberrationConfig::default());
assert!(ca.apply(&mut buf, 32, 32, PixelFormat::Rgba).is_ok());
}
#[test]
fn test_chromatic_wrong_size_err() {
let mut buf = vec![0u8; 10];
let ca = ChromaticAberration::new(ChromaticAberrationConfig::default());
assert!(ca.apply(&mut buf, 32, 32, PixelFormat::Rgb).is_err());
}
}