use std::cmp::Ordering;
use crate::{
Complex, FractalPreset, ImageEffect, MAX_ITERATIONS, MIN_ITERATIONS, NEON_PALETTES, NeonColor,
ProceduralEffect, ROTATION_STEPS, Viewport, ViewportSpecs, color_distance_estimator,
compute_escape_iterations, get_random_integer, get_rotation_phasors, render_fractal_parallel,
};
use image::RgbImage;
use rayon::prelude::*;
pub struct MandelbrotGenerator {
pub preset: FractalPreset,
pub scan_iterations: u32,
pub color_palette: NeonColor,
pub zoom: f64,
pub rotation: Complex,
}
impl Default for MandelbrotGenerator {
fn default() -> Self {
Self {
preset: FractalPreset {
center: Complex::new(-0.56226, 0.64273),
fractal_name: "Feathered Filament Cascades",
effect_name: ProceduralEffect::Mandelbrot,
},
scan_iterations: get_random_integer(MIN_ITERATIONS, MAX_ITERATIONS),
color_palette: NEON_PALETTES[5],
zoom: 0.0025,
rotation: Complex::one(),
}
}
}
impl ImageEffect for MandelbrotGenerator {
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.rotation,
center,
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)
},
);
}
fn info(&self) -> String {
format!(
"fractal [{}]\n\
f(z) = z^2 + c, where c = {:8.5} {} {:7.5}i (iter = {:4}, zoom = {:.5}), 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 {
fn find_branch_phasor(center: Complex, search_radius: f64, scan_iterations: u32) -> Complex {
let mut best_phasor = Complex::one();
let mut max_boundary_score = -1.0;
for phasor in get_rotation_phasors(ROTATION_STEPS) {
let mut total_variation = 0.0;
let mut prev_i = 0;
for k in 1..=4 {
let sample_point = center + phasor * (search_radius * (k as f64) * 0.25);
let (i, _, _) = compute_escape_iterations(
ProceduralEffect::Mandelbrot,
sample_point,
center,
scan_iterations,
);
if k > 1 {
total_variation += (i as f32 - prev_i as f32).abs();
}
prev_i = i;
}
if total_variation > max_boundary_score {
max_boundary_score = total_variation;
best_phasor = phasor;
}
}
best_phasor
}
fn locked_interior_grid_alignment(
center: Complex,
phasor: Complex,
search_radius: f64,
scan_iterations: u32,
) -> Complex {
let steps = 64;
let mut interior_segments = Vec::new();
let mut in_interior = false;
let mut segment_start = 0;
for step in 0..steps {
let t = -search_radius + (step as f64 / (steps - 1) as f64) * (2.0 * search_radius);
let test_point = center + phasor * t;
let (i, _, _) = compute_escape_iterations(
ProceduralEffect::Mandelbrot,
test_point,
center,
scan_iterations,
);
let is_interior = i >= scan_iterations;
if is_interior && !in_interior {
in_interior = true;
segment_start = step;
} else if !is_interior && in_interior {
in_interior = false;
interior_segments.push((segment_start, step - 1));
}
}
if in_interior {
interior_segments.push((segment_start, steps - 1));
}
let target_segment = if interior_segments.len() >= 4 {
Some(interior_segments[3])
} else {
interior_segments.last().cloned()
};
if let Some((start_idx, end_idx)) = target_segment {
let mid_step = (start_idx + end_idx) as f64 / 2.0;
let t_mid = -search_radius + (mid_step / (steps - 1) as f64) * (2.0 * search_radius);
center + phasor * t_mid
} else {
center
}
}
fn calculate_entropy(
center: Complex,
zoom: f64,
rotation: Complex,
scan_iterations: u32,
width: u32,
height: u32,
) -> f64 {
let grid_size = 64; let mut histogram = vec![0; scan_iterations as usize + 1];
let specs = ViewportSpecs {
center,
zoom,
rotation,
is_julia: false,
};
let viewport = Viewport::new(width as f64, height as f64, &specs);
let step_x = (width as f64) / (grid_size as f64);
let step_y = (height as f64) / (grid_size as f64);
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,
center,
scan_iterations,
);
if (i as usize) < histogram.len() {
histogram[i as usize] += 1;
}
}
}
let total_samples = (grid_size * grid_size) as f64;
let mut entropy: f64 = 0.0;
for &count in &histogram {
if count > 0 {
let p = (count as f64) / total_samples;
entropy -= p * p.ln();
}
}
entropy
}
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.8115, 0.2014),
fractal_name: "Tendril Valley Filaments",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-0.156, 1.033),
fractal_name: "Dreadlock Valley Basin",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-0.38, 0.66),
fractal_name: "Starburst Star Valley",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-0.56226, 0.64273),
fractal_name: "Feathered Filament Cascades",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-0.77568377, 0.13646737),
fractal_name: "Deep Seahorse Tail Spiral",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-1.45, 0.0),
fractal_name: "West Needle Crown Filaments",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-1.25, 0.05),
fractal_name: "Gothic Archway Scepters",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-0.55, 0.62),
fractal_name: "Pentagonal Star Valley",
effect_name: ProceduralEffect::Mandelbrot,
},
FractalPreset {
center: Complex::new(-1.625, 0.0),
fractal_name: "Bi-Directional Filament",
effect_name: ProceduralEffect::Mandelbrot,
},
];
let preset_idx: usize = get_random_integer(0, presets.len() - 1);
let selected_preset = presets[preset_idx];
let p_idx: usize = get_random_integer(0, NEON_PALETTES.len() - 1);
let color_palette = NEON_PALETTES[p_idx];
let scan_iterations = get_random_integer(MIN_ITERATIONS, MAX_ITERATIONS);
let rotations_count = ROTATION_STEPS;
let zooms_count = get_random_integer(30, 80);
let precision: usize = get_random_integer(2, 8);
let rotation_phasors: Vec<Complex> = get_rotation_phasors(rotations_count).collect();
let mut candidates = Vec::with_capacity(zooms_count * rotations_count);
for z_idx in 0..zooms_count {
let group = z_idx / 10;
let offset = z_idx % 10;
let exponent = precision.saturating_sub(group).max(1) as i32;
let multiplier = (offset + 1 + (group != 0) as usize) as f64;
let base_zoom = multiplier * 10.0_f64.powi(-exponent) + 1e-8;
candidates.extend((0..rotations_count).map(|r_idx| (base_zoom, r_idx)));
}
let (best_base_zoom, best_rotation, _best_entropy) = candidates
.par_iter()
.map(|&(base_zoom, r_idx)| {
let aspect_ratio = (width as f64) / (height as f64);
let adjusted_zoom = if aspect_ratio > 1.0 {
base_zoom * aspect_ratio.sqrt()
} else {
base_zoom
};
let rotation = rotation_phasors[r_idx];
let entropy = Self::calculate_entropy(
selected_preset.center,
adjusted_zoom,
rotation,
scan_iterations,
width,
height,
);
(base_zoom, rotation, entropy)
})
.max_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(Ordering::Equal))
.unwrap_or((0.0002, Complex::one(), 0.0));
let mut mandelbrot = Self {
preset: selected_preset,
scan_iterations,
color_palette,
zoom: best_base_zoom,
rotation: best_rotation,
};
mandelbrot.optimize_fit(width, height);
mandelbrot.dynamic_autofocus(width, height);
mandelbrot
}
pub fn dynamic_autofocus(&mut self, width: u32, height: u32) {
let search_radius = self.zoom * 0.25;
let branch_phasor =
Self::find_branch_phasor(self.preset.center, search_radius, self.scan_iterations);
let aligned_center = Self::locked_interior_grid_alignment(
self.preset.center,
branch_phasor,
search_radius,
self.scan_iterations,
);
self.preset.center = aligned_center;
let best_entropy = Self::calculate_entropy(
self.preset.center,
self.zoom,
self.rotation,
self.scan_iterations,
width,
height,
);
let climb_radius = self.zoom * 0.05;
let rotations = ROTATION_STEPS;
let search_directions: Vec<Complex> = std::iter::once(Complex::zero())
.chain(get_rotation_phasors(rotations).map(|phasor| phasor * climb_radius))
.collect();
let (best_center, _max_entropy) = search_directions
.par_iter()
.map(|&offset| {
let candidate_center = self.preset.center + offset;
let entropy = Self::calculate_entropy(
candidate_center,
self.zoom,
self.rotation,
self.scan_iterations,
width,
height,
);
(candidate_center, entropy)
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal))
.unwrap_or((self.preset.center, best_entropy));
self.preset.center = best_center;
let scale = self.zoom / (width.min(height) as f64);
let lod_iterations = (150.0 + 45.0 * (1.0 / scale).ln()) as u32;
self.scan_iterations = lod_iterations.clamp(MIN_ITERATIONS, MAX_ITERATIONS);
}
pub fn optimize_fit(&mut self, width: u32, height: u32) {
let aspect_ratio = (width as f64) / (height as f64);
if aspect_ratio > 1.0 {
self.zoom *= aspect_ratio.sqrt();
}
}
}
#[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);
}
}