wallswitch 0.60.8

randomly selects wallpapers for multiple monitors
Documentation
//! Mandelbrot Set fractal generator overlay.
//!
//! This module provides the implementation of the Mandelbrot Set fractal renderer,
//! using precalculated zoom presets. It generates mathematical overlays dynamically
//! fitted to display proportions and blends them using high-definition neon curves,
//! chromatic dual-tone borders, and 3D parallax shadow depths.

use crate::effects::{
    FractalPreset, MAX_ITERATIONS, MIN_ITERATIONS, ProceduralEffect, color_distance_estimator,
    compute_escape_iterations, get_rotation_phasors, render_fractal_parallel,
};
use crate::{
    Complex, ImageEffect, NEON_PALETTES, NeonColor, Viewport, ViewportSpecs, get_random_integer,
};
use image::RgbImage;

/// Target coverage ratio range for the Mandelbrot fractal viewport.
///
/// Used by the automated fitting algorithm to ensure the rendered fractal
/// balances nicely with background wallpapers.
pub const TARGET_RANGE: [f64; 2] = [0.24, 0.26];

/// A procedural generator for rendering Mandelbrot Set fractals onto desktop backgrounds.
///
/// Groups coordinate presets, escape iterations, colors, and camera viewpoints into
/// a structured pipeline to execute parallelized CPU rendering.
pub struct MandelbrotGenerator {
    /// The active coordinate preset, enclosing the center points and metadata.
    pub preset: FractalPreset,
    /// The maximum iteration limit for escape-time calculations.
    pub scan_iterations: u32,
    /// The base color palette selected for the neon glow.
    pub color_palette: NeonColor,
    /// The viewport zoom level.
    pub zoom: f64,
    /// The cosine of the rotation angle.
    pub cos_angle: f64,
    /// The sine of the rotation angle.
    pub sin_angle: f64,
}

impl Default for MandelbrotGenerator {
    /// Returns the default fallback instance of the Mandelbrot Set generator.
    fn default() -> Self {
        Self {
            preset: FractalPreset {
                center: Complex::new(-0.7436438, 0.1318259),
                fractal_name: "Deep double spirals in Seahorse Valley",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            scan_iterations: get_random_integer(MIN_ITERATIONS, MAX_ITERATIONS),
            color_palette: NEON_PALETTES[5],
            zoom: 3.0,
            cos_angle: 1.0,
            sin_angle: 0.0,
        }
    }
}

impl ImageEffect for MandelbrotGenerator {
    /// Applies the Mandelbrot Set procedural overlay in-place directly to a mutable image buffer.
    ///
    /// Delegates core iteration mapping to the shared parallel rendering pipeline.
    fn apply(&self, rgb_img: &mut RgbImage) {
        let center = self.preset.center;
        let scan_iterations = self.scan_iterations;
        let color_palette = self.color_palette;

        render_fractal_parallel(
            rgb_img,
            self.zoom,
            self.cos_angle,
            self.sin_angle,
            center,
            false, // is_julia = false
            |z_init, scale| {
                let (i, z, dz) = compute_escape_iterations(
                    ProceduralEffect::Mandelbrot,
                    z_init,
                    center,
                    scan_iterations,
                );

                color_distance_estimator(i, scan_iterations, z, dz, scale, color_palette)
            },
        );
    }

    /// Returns formatting diagnostic information about the active generator.
    fn info(&self) -> String {
        format!(
            "fractal [{}]\n\
            f(z) = z^2 + c, where c = {:8.5} {} {:7.5}i (iter = {:4}, zoom = {:.2}), color: {}",
            self.preset.fractal_name,
            self.preset.center.re,
            if self.preset.center.im >= 0.0 {
                "+"
            } else {
                "-"
            },
            self.preset.center.im.abs(),
            self.scan_iterations,
            self.zoom,
            self.color_palette
        )
    }
}

impl MandelbrotGenerator {
    /// Generates a randomized Mandelbrot Set configuration fitted to the target aspect ratio.
    ///
    /// Pulls coordinates from custom high-definition locations, sets neon colors,
    /// applies random viewport rotations, and adjusts margins.
    pub fn random(monitor: &crate::Monitor) -> Self {
        let width = monitor.resolution.width as u32;
        let height = monitor.resolution.height as u32;

        let presets = [
            FractalPreset {
                center: Complex::new(-0.5623, 0.6421),
                fractal_name: "Filament branch patterns",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-0.7756838, 0.13646737),
                fractal_name: "Seahorse tail section",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-1.45, 0.0),
                fractal_name: "Needle Mini Mandelbrot",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-1.75, 0.0),
                fractal_name: "Satellite Mini Mandelbrot",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-0.1607, 1.0376),
                fractal_name: "Antenna Branching Plumes",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-0.1011, 0.9563),
                fractal_name: "Tendril Valley Spirals",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-0.8115, 0.2014),
                fractal_name: "Tenth-period Star Valley",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-1.3996669964593604, 0.0005429083913),
                fractal_name: "Aokoroko V1",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-0.6216751361351949, -0.4629018082582385),
                fractal_name: "Aokoroko V2",
                effect_name: ProceduralEffect::Mandelbrot,
            },
            FractalPreset {
                center: Complex::new(-0.6437677776968205, -0.4541461552468023),
                fractal_name: "Aokoroko V3",
                effect_name: ProceduralEffect::Mandelbrot,
            },
        ];

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

        let angle_degrees: f64 = get_random_integer(0, 359);
        let radians = angle_degrees.to_radians();

        let preset_idx: usize = get_random_integer(0, presets.len() - 1);
        let selected_preset = presets[preset_idx];

        let mut mandelbrot = Self {
            preset: selected_preset,
            scan_iterations: get_random_integer(MIN_ITERATIONS, MAX_ITERATIONS),
            color_palette,
            zoom: 3.0,
            cos_angle: radians.cos(),
            sin_angle: radians.sin(),
        };

        mandelbrot.optimize_fit(width, height);
        mandelbrot
    }

    /// Calculates active coverage ratios to dynamically scale the viewport.
    fn evaluate_ratio(&self, zoom: f64, width: u32, height: u32, min_escape_iter: u32) -> f64 {
        self.evaluate_ratio_parametrized(zoom, width, height, min_escape_iter, 128, None)
    }

    /// Computes the ratio of active escape-time coordinates inside a grid, supporting optimization sweeps.
    fn evaluate_ratio_parametrized(
        &self,
        zoom: f64,
        width: u32,
        height: u32,
        min_escape_iter: u32,
        grid_size: usize,
        scan_iter_override: Option<u32>,
    ) -> f64 {
        let w_f = width as f64;
        let h_f = height as f64;

        let specs = ViewportSpecs {
            center: self.preset.center,
            zoom,
            cos_angle: self.cos_angle,
            sin_angle: self.sin_angle,
            is_julia: false,
        };
        let viewport = Viewport::new(w_f, h_f, &specs);

        let mut active_count = 0;
        let step_x = w_f / (grid_size as f64);
        let step_y = h_f / (grid_size as f64);
        let scan_iterations = scan_iter_override.unwrap_or(self.scan_iterations);

        for gy in 0..grid_size {
            let y_f = (gy as f64) * step_y;
            for gx in 0..grid_size {
                let x_f = (gx as f64) * step_x;
                let z_init = viewport.map(x_f, y_f);

                let (i, _, _) = compute_escape_iterations(
                    ProceduralEffect::Mandelbrot,
                    z_init,
                    self.preset.center,
                    scan_iterations,
                );

                if i > min_escape_iter && i < scan_iterations {
                    active_count += 1;
                }
            }
        }

        (active_count as f64) / ((grid_size * grid_size) as f64)
    }

    /// Evaluates optimal viewport rotations by sweeping multiple angular increments.
    pub fn evaluate_rotation(&mut self, width: u32, height: u32) -> Option<Complex> {
        let original_cos = self.cos_angle;
        let original_sin = self.sin_angle;

        let mut working_zoom = 0.1;
        let mut best_diff = f64::MAX;

        for step in 0..8 {
            let t = step as f64 / 7.0;
            let log_z = 0.0001_f64.ln() + t * (3.0_f64.ln() - 0.0001_f64.ln());
            let current_zoom = log_z.exp();

            let ratio =
                self.evaluate_ratio_parametrized(current_zoom, width, height, 16, 32, Some(100));
            let diff = (ratio - 0.18).abs();
            if diff < best_diff {
                best_diff = diff;
                working_zoom = current_zoom;
            }
        }

        let mut best_phasor = None;
        let mut max_active_coverage = 0.0;

        for phasor in get_rotation_phasors() {
            self.cos_angle = phasor.re;
            self.sin_angle = phasor.im;

            let ratio = self.evaluate_ratio_parametrized(working_zoom, width, height, 24, 64, None);

            if ratio > max_active_coverage && ratio <= 0.40 {
                max_active_coverage = ratio;
                best_phasor = Some(phasor);
            }
        }

        self.cos_angle = original_cos;
        self.sin_angle = original_sin;
        best_phasor
    }

    /// Optimizes viewport parameters using progressive ratio sweeps.
    pub fn optimize_fit(&mut self, width: u32, height: u32) {
        let target_min = TARGET_RANGE[0];
        let target_max = TARGET_RANGE[1];
        let target_mid = (target_min + target_max) * 0.5;

        let original_cos = self.cos_angle;
        let original_sin = self.sin_angle;

        if let Some(best_phasor) = self.evaluate_rotation(width, height) {
            self.cos_angle = best_phasor.re;
            self.sin_angle = best_phasor.im;
        } else {
            self.cos_angle = original_cos;
            self.sin_angle = original_sin;
        }

        let mut log_min = 0.00001_f64.ln();
        let mut log_max = 3.5_f64.ln();
        let mut best_zoom = 0.02;
        let mut best_difference = f64::MAX;

        let max_iter = 32;
        let min_escape_iter = 32;

        for _ in 0..max_iter {
            let current_log = (log_min + log_max) * 0.5;
            let current_zoom = current_log.exp();
            let ratio = self.evaluate_ratio(current_zoom, width, height, min_escape_iter);

            let diff = if ratio >= target_min && ratio <= target_max {
                0.0
            } else if ratio < target_min {
                target_min - ratio
            } else {
                ratio - target_max
            };

            if diff < best_difference {
                best_difference = diff;
                best_zoom = current_zoom;
            }

            if ratio > target_mid {
                log_min = current_log;
            } else {
                log_max = current_log;
            }
        }

        self.zoom = best_zoom;
    }
}

#[cfg(test)]
mod tests_mandelbrot {
    use super::*;
    use crate::core::Monitor;

    #[test]
    fn test_mandelbrot_generation_random() {
        let monitor = Monitor::default();
        let mandelbrot = MandelbrotGenerator::random(&monitor);
        assert!(mandelbrot.zoom > 0.0);
        assert_eq!(mandelbrot.preset.effect_name, ProceduralEffect::Mandelbrot);
    }
}