wallswitch 0.60.5

randomly selects wallpapers for multiple monitors
Documentation
use crate::effects::partition_rows;
use crate::{NEON_PALETTES, NeonColor, get_random_integer};
use image::RgbImage;
use std::thread;

/// The minimum and maximum limits for the randomized star count.
pub const STAR_RANGE: [usize; 2] = [60, 120];

impl crate::effects::ImageEffect for StarfieldGenerator {
    fn apply(&self, rgb_img: &mut RgbImage) {
        self.apply_effect_in_memory(rgb_img);
    }

    fn info(&self) -> String {
        format!("overlay ({} stars)", self.stars.len())
    }
}

pub struct Star {
    pub x: f32,
    pub y: f32,
    pub radius: f32,
    pub intensity: f32,
    pub color_palette: NeonColor,
}

pub struct StarfieldGenerator {
    pub stars: Vec<Star>,
}

impl StarfieldGenerator {
    /// Generates a randomized field of stars based on the monitor's physical dimensions.
    pub fn random(monitor: &crate::Monitor) -> Self {
        let width = monitor.resolution.width as u32;
        let height = monitor.resolution.height as u32;

        let count = get_random_integer(STAR_RANGE[0], STAR_RANGE[1]);
        Self::new(count, width, height)
    }

    /// Generates a randomized field of stars using the centralized high-contrast neon colors.
    pub fn new(count: usize, width: u32, height: u32) -> Self {
        let mut stars = Vec::with_capacity(count);

        for _ in 0..count {
            let x: f32 = get_random_integer(0, width);
            let y: f32 = get_random_integer(0, height);
            let radius: f32 = get_random_integer(5, 45);
            let intensity = get_random_integer::<_, f32>(30, 95) / 100.0;

            // Pick a randomized high-visibility color palette for each star
            let p_idx: usize = get_random_integer(0, NEON_PALETTES.len() - 1);
            let color_palette = NEON_PALETTES[p_idx];

            stars.push(Star {
                x,
                y,
                radius,
                intensity,
                color_palette,
            });
        }

        Self { stars }
    }

    #[inline(always)]
    fn calculate_star_influence(
        star: &Star,
        x_f: f32,
        dy_sq: f32,
        star_radius_sq: f32,
        contrast_color: [f32; 3],
    ) -> Option<(f32, f32, f32, f32)> {
        let dx = star.x - 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;

            // Retrieve floating-point RGB channels from the selected NeonColor
            let star_rgb = star.color_palette.to_array();

            let r_star = (star_rgb[0] * 0.25 + contrast_color[0] * 0.75) * alpha;
            let g_star = (star_rgb[1] * 0.25 + contrast_color[1] * 0.75) * alpha;
            let b_star = (star_rgb[2] * 0.25 + contrast_color[2] * 0.75) * alpha;

            Some((r_star, g_star, b_star, alpha))
        } else {
            None
        }
    }

    /// Blends the generated starfield overlay onto the image buffer in parallel.
    pub fn apply_effect_in_memory(&self, rgb_img: &mut RgbImage) {
        let contrast_color = [0.64, 0.75, 0.85];
        let (mut rows, width_usize) = partition_rows(rgb_img);

        let cores = thread::available_parallelism()
            .map(|n| n.get())
            .unwrap_or(4);
        let chunk_size = (rows.len() / cores).max(1);

        thread::scope(|scope| {
            for chunk in rows.chunks_mut(chunk_size) {
                let stars = &self.stars;
                scope.spawn(move || {
                    for (y, row_data) in chunk.iter_mut() {
                        let y_f = *y as f32;

                        let mut active_stars = Vec::with_capacity(16);
                        for star in stars {
                            let dy = star.y - 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 in 0..width_usize {
                            let x_f = x as f32;

                            let mut r_contrib = 0.0;
                            let mut g_contrib = 0.0;
                            let mut b_contrib = 0.0;
                            let mut total_alpha = 0.0;

                            for &(star, dy_sq, star_radius_sq) in &active_stars {
                                if let Some((r, g, b, alpha)) = Self::calculate_star_influence(
                                    star,
                                    x_f,
                                    dy_sq,
                                    star_radius_sq,
                                    contrast_color,
                                ) {
                                    r_contrib += r;
                                    g_contrib += g;
                                    b_contrib += b;
                                    total_alpha += alpha;
                                }
                            }

                            if total_alpha > 0.001 {
                                let idx = x * 3;
                                let original_r = row_data[idx] as f32;
                                let original_g = row_data[idx + 1] as f32;
                                let original_b = row_data[idx + 2] as f32;

                                let alpha_clamp = total_alpha.min(0.95);

                                // Gamma-corrected blending for stars to prevent gray-fringe artifacts
                                let bg_r_linear = (original_r / 255.0).powi(2);
                                let bg_g_linear = (original_g / 255.0).powi(2);
                                let bg_b_linear = (original_b / 255.0).powi(2);

                                let fg_r = r_contrib / total_alpha;
                                let fg_g = g_contrib / total_alpha;
                                let fg_b = b_contrib / total_alpha;

                                let fg_r_linear = fg_r * fg_r;
                                let fg_g_linear = fg_g * fg_g;
                                let fg_b_linear = fg_b * fg_b;

                                let blended_r = (bg_r_linear * (1.0 - alpha_clamp)
                                    + fg_r_linear * alpha_clamp)
                                    .sqrt()
                                    * 255.0;
                                let blended_g = (bg_g_linear * (1.0 - alpha_clamp)
                                    + fg_g_linear * alpha_clamp)
                                    .sqrt()
                                    * 255.0;
                                let blended_b = (bg_b_linear * (1.0 - alpha_clamp)
                                    + fg_b_linear * alpha_clamp)
                                    .sqrt()
                                    * 255.0;

                                row_data[idx] = blended_r.clamp(0.0, 255.0) as u8;
                                row_data[idx + 1] = blended_g.clamp(0.0, 255.0) as u8;
                                row_data[idx + 2] = blended_b.clamp(0.0, 255.0) as u8;
                            }
                        }
                    }
                });
            }
        });
    }
}