wallswitch 0.60.1

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;

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

    fn info(&self) -> String {
        format!(
            "overlay (density = {}), color: {}",
            self.density, self.color_palette
        )
    }
}

pub struct AuroraParams {
    pub density_u: f32,
    pub density_w_coeff: f32,
    pub density_w4_coeff: f32,
    pub inv_w: f32,
}

pub struct AuroraRowState {
    pub v: f32,
    pub w2: f32,
    pub v_density: f32,
    pub v_sq: f32,
}

pub struct AuroraGenerator {
    pub color_palette: NeonColor,
    pub density: f32,
    pub aspect_ratio_density_multiplier: f32,
}

impl AuroraGenerator {
    /// Generates a randomized Aurora configuration using the target monitor's dimensions.
    pub fn random(monitor: &crate::Monitor) -> Self {
        let width = monitor.resolution.width as u32;
        let height = monitor.resolution.height as u32;

        let p_idx: usize = get_random_integer(0, NEON_PALETTES.len() - 1);
        let color_palette = NEON_PALETTES[p_idx];

        // Base density scalar
        let density: f32 = get_random_integer(4, 8);

        // Adjust horizontal density to prevent stretching on wider/narrower displays
        let aspect_ratio = width as f32 / height as f32;
        let aspect_ratio_density_multiplier = if aspect_ratio > 1.0 {
            aspect_ratio.sqrt()
        } else {
            1.0
        };

        Self {
            color_palette,
            density,
            aspect_ratio_density_multiplier,
        }
    }

    #[inline(always)]
    fn calculate_aurora_alpha(x_f: f32, params: &AuroraParams, row: &AuroraRowState) -> f32 {
        let u = x_f * params.inv_w;

        let w1 = (x_f * params.density_u).sin();
        let w3 = (x_f * params.density_w_coeff + row.v_density).sin();
        let w4 = ((u * u + row.v_sq).sqrt() * params.density_w4_coeff).cos();

        let val = (w1 + row.w2 + w3 + w4) * 0.25;

        // Smooth, anti-aliasing sine wave mapping instead of narrow, noisy power spikes
        let wave = (val * std::f32::consts::PI).sin() * 0.5 + 0.5;
        let intensity = wave.powf(2.0); // Perfect organic ambient curtain light falloff

        let dx = (u - 0.5) * 2.0;
        let dy = (row.v - 0.5) * 2.0;
        let edge_fade = (1.0 - (dx * dx + dy * dy).sqrt() * 0.45).clamp(0.0, 1.0);

        intensity * edge_fade * 0.65
    }

    pub fn apply_effect_in_memory(&self, rgb_img: &mut RgbImage) {
        let (width, height) = rgb_img.dimensions();
        let w_f = width as f32;
        let h_f = height as f32;

        let contrast_color = self.color_palette.to_array();
        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);

        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;

        thread::scope(|scope| {
            for chunk in rows.chunks_mut(chunk_size) {
                let params_ref = &params;
                scope.spawn(move || {
                    for (y, row_data) in chunk.iter_mut() {
                        let y_f = *y as f32;
                        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 in 0..width_usize {
                            let x_f = x as f32;
                            let alpha = Self::calculate_aurora_alpha(x_f, params_ref, &row_state);

                            if alpha > 0.01 {
                                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 r_aurora = contrast_color[0] * 255.0;
                                let g_aurora = contrast_color[1] * 255.0;
                                let b_aurora = contrast_color[2] * 255.0;

                                // Gamma-corrected, smooth linear-space blending
                                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 aurora_r_linear = (r_aurora / 255.0).powi(2);
                                let aurora_g_linear = (g_aurora / 255.0).powi(2);
                                let aurora_b_linear = (b_aurora / 255.0).powi(2);

                                let blended_r =
                                    (bg_r_linear * (1.0 - alpha) + aurora_r_linear * alpha).sqrt()
                                        * 255.0;
                                let blended_g =
                                    (bg_g_linear * (1.0 - alpha) + aurora_g_linear * alpha).sqrt()
                                        * 255.0;
                                let blended_b =
                                    (bg_b_linear * (1.0 - alpha) + aurora_b_linear * alpha).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;
                            }
                        }
                    }
                });
            }
        });
    }
}