use crate::{
ColorRGB, Complex, ImageEffect, NEON_PALETTES, NeonColor, RandomExt, WallSwitchResult,
get_random_integer, process_rows_parallel_scoped,
};
use image::RgbImage;
pub const STAR_RANGE: [usize; 2] = [60, 120];
pub struct Star {
pub position: Complex,
pub radius: f64,
pub intensity: f64,
pub color_palette: NeonColor,
}
pub struct StarfieldGenerator {
pub stars: Vec<Star>,
}
impl ImageEffect for StarfieldGenerator {
fn apply(&self, rgb_img: &mut RgbImage) {
let contrast_color = ColorRGB::new(0.64, 0.75, 0.85);
process_rows_parallel_scoped(rgb_img, |y, row_data| {
let y_f = y as f64;
let mut active_stars = Vec::with_capacity(16);
for star in &self.stars {
let dy = star.position.im - y_f;
let limit = star.radius * 2.0;
if dy.abs() < limit {
let dy_sq = dy * dy;
let star_radius_sq = star.radius * star.radius;
active_stars.push((star, dy_sq, star_radius_sq));
}
}
for (x, pixel_slice) in row_data.chunks_exact_mut(3).enumerate() {
let x_f = x as f64;
let mut color_acc = ColorRGB::default();
let mut total_alpha = 0.0;
for &(star, dy_sq, star_radius_sq) in &active_stars {
let dx = star.position.re - x_f;
let dist_sq = dx * dx + dy_sq;
if dist_sq < star_radius_sq * 4.0 {
let factor = (-dist_sq / (2.0 * star_radius_sq)).exp();
let alpha = factor * star.intensity;
let star_color = star.color_palette.color_rgb.lerp(contrast_color, 0.25);
color_acc = color_acc + star_color * alpha;
total_alpha += alpha;
}
}
if total_alpha > 0.001 {
let alpha_clamp = total_alpha.min(0.95);
let fg_color = color_acc * (1.0 / total_alpha);
let bg = ColorRGB::from_slice(pixel_slice);
let blended = bg.blend(fg_color, alpha_clamp);
blended.write_to_slice(pixel_slice);
}
}
});
}
fn info(&self) -> String {
format!("overlay ({} stars)", self.stars.len())
}
}
impl StarfieldGenerator {
pub fn random(monitor: &crate::Monitor) -> WallSwitchResult<Self> {
let width = monitor.resolution.width;
let height = monitor.resolution.height;
let count = get_random_integer(STAR_RANGE[0], STAR_RANGE[1]);
Self::new(count, width, height)
}
pub fn new(count: usize, width: u64, height: u64) -> WallSwitchResult<Self> {
let mut stars = Vec::with_capacity(count);
for _ in 0..count {
let x: f64 = get_random_integer(0, width);
let y: f64 = get_random_integer(0, height);
let radius: f64 = get_random_integer(5, 45);
let intensity = get_random_integer::<_, f64>(30, 95) / 100.0;
let color_palette = NEON_PALETTES.get_random_sample()?;
stars.push(Star {
position: Complex::new(x, y),
radius,
intensity,
color_palette,
});
}
Ok(Self { stars })
}
}
#[cfg(test)]
mod tests_star {
use super::*;
use crate::core::Monitor;
#[test]
fn test_star_generation() -> WallSwitchResult<()> {
let monitor = Monitor::default();
let starfield = StarfieldGenerator::random(&monitor)?;
assert!(!starfield.stars.is_empty());
Ok(())
}
}