use crate::{
ColorRGB, Complex, ImageEffect, Monitor, NEON_PALETTES, NeonColor, RandomExt, WallSwitchResult,
get_random_integer, process_rows_parallel_scoped,
};
use image::RgbImage;
pub struct AuroraParams {
pub density_u: f64,
pub density_w_coeff: f64,
pub density_w4_coeff: f64,
pub inv_w: f64,
}
impl AuroraParams {
#[inline(always)]
pub fn alpha(&self, x_f: f64, row: &AuroraRowState) -> f64 {
let u = x_f * self.inv_w;
let w1 = (x_f * self.density_u).sin();
let w3 = (x_f * self.density_w_coeff + row.v_density).sin();
let screen_coord = Complex::new(u, row.v);
let w4 = (screen_coord.abs() * self.density_w4_coeff).cos();
let val = (w1 + row.w2 + w3 + w4) * 0.25;
let wave = (val * std::f64::consts::PI).sin() * 0.5 + 0.5;
let intensity = wave.powi(2);
let center_offset = (screen_coord - Complex::new(0.5, 0.5)) * 2.0;
let edge_fade = (1.0 - center_offset.abs() * 0.45).clamp(0.0, 1.0);
intensity * edge_fade * 0.65
}
}
pub struct AuroraRowState {
pub v: f64,
pub w2: f64,
pub v_density: f64,
pub v_sq: f64,
}
pub struct AuroraGenerator {
pub color_palette: NeonColor,
pub density: f64,
pub aspect_ratio_density_multiplier: f64,
}
impl ImageEffect for AuroraGenerator {
fn apply(&self, rgb_img: &mut RgbImage) {
let (width, height) = rgb_img.dimensions();
let (w_f, h_f) = (width as f64, height as f64);
let inv_w = 1.0 / w_f;
let inv_h = 1.0 / h_f;
let adjusted_density = self.density * self.aspect_ratio_density_multiplier;
let params = AuroraParams {
density_u: adjusted_density * 1.5 * inv_w,
density_w_coeff: adjusted_density * inv_w,
density_w4_coeff: adjusted_density * 1.2,
inv_w,
};
let density_v_coeff = self.density * 2.0;
let density_val = self.density;
process_rows_parallel_scoped(rgb_img, |y, row_data| {
let y_f = y as f64;
let v = y_f * inv_h;
let row_state = AuroraRowState {
v,
w2: (v * density_v_coeff).cos(),
v_density: v * density_val,
v_sq: v * v,
};
for (x, pixel_slice) in row_data.chunks_exact_mut(3).enumerate() {
let alpha = params.alpha(x as f64, &row_state);
if alpha > 0.01 {
let bg = ColorRGB::from_slice(pixel_slice);
bg.blend(self.color_palette.color_rgb, alpha)
.write_to_slice(pixel_slice);
}
}
});
}
fn info(&self) -> String {
format!(
"overlay (density = {}), color: {}",
self.density, self.color_palette
)
}
}
impl AuroraGenerator {
pub fn new() -> WallSwitchResult<Self> {
let color_palette = NEON_PALETTES.get_random_sample()?;
let density = get_random_integer(4, 8);
Ok(Self {
color_palette,
density,
aspect_ratio_density_multiplier: 1.0,
})
}
pub fn random(monitor: &Monitor) -> WallSwitchResult<Self> {
let mut aurora = Self::new()?;
let width = monitor.resolution.width;
let height = monitor.resolution.height;
let aspect_ratio = width as f64 / height as f64;
if aspect_ratio > 1.0 {
aurora.aspect_ratio_density_multiplier = aspect_ratio.sqrt();
}
Ok(aurora)
}
}
#[cfg(test)]
mod tests_aurora {
use super::*;
use crate::core::Monitor;
use image::RgbImage;
#[test]
fn test_aurora_generator_new() -> WallSwitchResult<()> {
let aurora = AuroraGenerator::new()?;
assert!(
aurora.density >= 4.0 && aurora.density <= 8.0,
"density out of range: {}",
aurora.density
);
assert_eq!(aurora.aspect_ratio_density_multiplier, 1.0);
Ok(())
}
#[test]
fn test_aurora_generator_random_density_in_range() -> WallSwitchResult<()> {
let monitor = Monitor::default();
let aurora = AuroraGenerator::random(&monitor)?;
assert!(
aurora.density >= 4.0 && aurora.density <= 8.0,
"density out of range: {}",
aurora.density
);
Ok(())
}
#[test]
fn test_aurora_applies_without_panic() -> WallSwitchResult<()> {
let monitor = Monitor::default();
let aurora = AuroraGenerator::random(&monitor)?;
let mut img = RgbImage::new(32, 18);
aurora.apply(&mut img); Ok(())
}
#[test]
fn test_aurora_params_alpha_range() {
let params = AuroraParams {
density_u: 0.05,
density_w_coeff: 0.03,
density_w4_coeff: 1.2,
inv_w: 1.0 / 100.0,
};
let row = AuroraRowState {
v: 0.5,
w2: 0.0,
v_density: 2.0,
v_sq: 0.25,
};
for x in 0..100 {
let alpha = params.alpha(x as f64, &row);
assert!(
(0.0..=1.0).contains(&alpha),
"alpha out of range at x={x}: {alpha}"
);
}
}
#[test]
fn test_aurora_aspect_ratio_multiplier() -> WallSwitchResult<()> {
let wide_monitor = Monitor {
resolution: crate::Dimension {
width: 3840,
height: 1080,
},
..Monitor::default()
};
let aurora = AuroraGenerator::random(&wide_monitor)?;
assert!(
aurora.aspect_ratio_density_multiplier > 1.0,
"expected multiplier > 1 for wide monitor"
);
let square_monitor = Monitor {
resolution: crate::Dimension {
width: 1080,
height: 1080,
},
..Monitor::default()
};
let aurora_sq = AuroraGenerator::random(&square_monitor)?;
assert_eq!(aurora_sq.aspect_ratio_density_multiplier, 1.0);
Ok(())
}
}