use super::{clamp_u8, validate_buffer, PixelFormat, VideoResult};
#[derive(Debug, Clone, Copy)]
pub struct LensFlareColor {
pub r: f32,
pub g: f32,
pub b: f32,
}
impl LensFlareColor {
#[must_use]
pub const fn new(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b }
}
#[must_use]
pub const fn white() -> Self {
Self::new(1.0, 1.0, 1.0)
}
#[must_use]
pub const fn sunlight() -> Self {
Self::new(1.0, 0.95, 0.7)
}
#[must_use]
pub const fn anamorphic() -> Self {
Self::new(0.4, 0.6, 1.0)
}
}
impl Default for LensFlareColor {
fn default() -> Self {
Self::sunlight()
}
}
#[derive(Debug, Clone)]
pub struct LensFlareConfig {
pub source_x: f32,
pub source_y: f32,
pub intensity: f32,
pub color: LensFlareColor,
pub glow_radius: f32,
pub halo_radius: f32,
pub halo_width: f32,
pub streak_count: usize,
pub streak_length: f32,
pub ghost_count: usize,
}
impl Default for LensFlareConfig {
fn default() -> Self {
Self {
source_x: 0.3,
source_y: 0.25,
intensity: 0.7,
color: LensFlareColor::default(),
glow_radius: 0.08,
halo_radius: 0.20,
halo_width: 0.015,
streak_count: 4,
streak_length: 0.45,
ghost_count: 3,
}
}
}
pub struct LensFlare {
config: LensFlareConfig,
}
impl LensFlare {
#[must_use]
pub fn new(config: LensFlareConfig) -> Self {
Self { config }
}
#[allow(clippy::cast_precision_loss, clippy::similar_names)]
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 src_x = cfg.source_x * width as f32;
let src_y = cfg.source_y * height as f32;
let diag = ((width * width + height * height) as f32).sqrt();
let glow_r = cfg.glow_radius * diag;
let halo_r = cfg.halo_radius * diag;
let halo_w = cfg.halo_width * diag;
let streak_len = cfg.streak_length * diag;
for py in 0..height {
for px in 0..width {
let dx = px as f32 - src_x;
let dy = py as f32 - src_y;
let dist = (dx * dx + dy * dy).sqrt();
let mut add_r = 0.0f32;
let mut add_g = 0.0f32;
let mut add_b = 0.0f32;
if glow_r > 0.0 {
let sigma = glow_r * 0.5;
let glow = (-0.5 * (dist / sigma).powi(2)).exp();
add_r += cfg.color.r * glow * cfg.intensity * 255.0;
add_g += cfg.color.g * glow * cfg.intensity * 255.0;
add_b += cfg.color.b * glow * cfg.intensity * 255.0;
}
if halo_r > 0.0 {
let ring_dist = (dist - halo_r).abs();
if ring_dist < halo_w * 2.0 {
let t = 1.0 - ring_dist / (halo_w * 2.0);
let halo = t * t * cfg.intensity * 0.5;
add_r += cfg.color.r * halo * 255.0;
add_g += cfg.color.g * halo * 255.0;
add_b += cfg.color.b * halo * 255.0;
}
}
if cfg.streak_count > 0 && streak_len > 0.0 {
let angle = dy.atan2(dx);
for i in 0..cfg.streak_count {
let streak_angle =
std::f32::consts::PI * i as f32 / cfg.streak_count as f32;
let angle_diff = (angle - streak_angle).abs();
let angular_width = 0.015_f32; let ang_factor = if angle_diff < angular_width
|| (std::f32::consts::PI - angle_diff).abs() < angular_width
{
let closest = angle_diff.min((std::f32::consts::PI - angle_diff).abs());
(1.0 - closest / angular_width).max(0.0)
} else {
0.0
};
let dist_factor = (1.0 - dist / streak_len).max(0.0);
let streak = ang_factor * dist_factor * cfg.intensity * 0.4;
add_r += LensFlareColor::anamorphic().r * streak * 255.0;
add_g += LensFlareColor::anamorphic().g * streak * 255.0;
add_b += LensFlareColor::anamorphic().b * streak * 255.0;
}
}
if cfg.ghost_count > 0 {
let img_cx = width as f32 * 0.5;
let img_cy = height as f32 * 0.5;
for g in 1..=cfg.ghost_count {
let t = g as f32 / (cfg.ghost_count + 1) as f32;
let ghost_x = src_x + (img_cx - src_x) * (1.0 + t * 0.5);
let ghost_y = src_y + (img_cy - src_y) * (1.0 + t * 0.5);
let gdx = px as f32 - ghost_x;
let gdy = py as f32 - ghost_y;
let gdist = (gdx * gdx + gdy * gdy).sqrt();
let ghost_sigma = glow_r * 0.3 * (1.0 - t * 0.5);
let ghost_intensity = cfg.intensity * 0.15 * (1.0 - t * 0.5);
let glow = (-0.5 * (gdist / ghost_sigma.max(1.0)).powi(2)).exp();
add_r += cfg.color.r * glow * ghost_intensity * 255.0;
add_g += cfg.color.g * glow * ghost_intensity * 255.0;
add_b += cfg.color.b * glow * ghost_intensity * 255.0;
}
}
let idx = (py * width + px) * bpp;
data[idx] = clamp_u8(f32::from(data[idx]) + add_r);
data[idx + 1] = clamp_u8(f32::from(data[idx + 1]) + add_g);
data[idx + 2] = clamp_u8(f32::from(data[idx + 2]) + add_b);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_buf(w: usize, h: usize) -> Vec<u8> {
vec![50u8; w * h * 3]
}
#[test]
fn test_lens_flare_default_applies() {
let mut buf = make_buf(64, 64);
let lf = LensFlare::new(LensFlareConfig::default());
assert!(lf.apply(&mut buf, 64, 64, PixelFormat::Rgb).is_ok());
}
#[test]
fn test_lens_flare_brightens_source_region() {
let mut buf = vec![0u8; 128 * 128 * 3];
let cfg = LensFlareConfig {
source_x: 0.5,
source_y: 0.5,
intensity: 1.0,
glow_radius: 0.2,
..Default::default()
};
let lf = LensFlare::new(cfg);
lf.apply(&mut buf, 128, 128, PixelFormat::Rgb).unwrap();
let cx = 64usize;
let cy = 64usize;
let idx = (cy * 128 + cx) * 3;
assert!(buf[idx] > 100, "Center should be bright");
}
#[test]
fn test_lens_flare_rgba() {
let mut buf = vec![128u8; 32 * 32 * 4];
let lf = LensFlare::new(LensFlareConfig::default());
assert!(lf.apply(&mut buf, 32, 32, PixelFormat::Rgba).is_ok());
}
#[test]
fn test_lens_flare_wrong_size_err() {
let mut buf = vec![0u8; 10];
let lf = LensFlare::new(LensFlareConfig::default());
assert!(lf.apply(&mut buf, 32, 32, PixelFormat::Rgb).is_err());
}
#[test]
fn test_lens_flare_color_white() {
let c = LensFlareColor::white();
assert_eq!(c.r, 1.0);
assert_eq!(c.g, 1.0);
assert_eq!(c.b, 1.0);
}
#[test]
fn test_lens_flare_zero_intensity() {
let buf_orig = vec![100u8; 32 * 32 * 3];
let mut buf = buf_orig.clone();
let cfg = LensFlareConfig {
intensity: 0.0,
ghost_count: 0,
streak_count: 0,
..Default::default()
};
let lf = LensFlare::new(cfg);
lf.apply(&mut buf, 32, 32, PixelFormat::Rgb).unwrap();
assert_eq!(buf, buf_orig, "Zero intensity should not change pixels");
}
}