use std::cmp::Ordering;
use crate::{
ColorRGB, Complex, FractalConfig, FractalDescriptor, FractalPreset, MAX_ITERATIONS,
MIN_ITERATIONS, Monitor, NEON_PALETTES, ProceduralEffect, ROTATION_STEPS, RandomExt, Viewport,
ViewportSpecs, color_distance_estimator, get_random_integer, get_rotation_phasors,
mandelbrot_escape,
};
use rayon::prelude::*;
pub struct MandelbrotGenerator {
pub preset: FractalPreset,
pub config: FractalConfig,
}
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,
},
config: FractalConfig {
scan_iterations: MIN_ITERATIONS,
color_palette: NEON_PALETTES[5],
zoom: 0.0025,
rotation: Complex::one(),
},
}
}
}
impl FractalDescriptor for MandelbrotGenerator {
#[inline(always)]
fn config(&self) -> &FractalConfig {
&self.config
}
#[inline(always)]
fn center(&self) -> Complex {
self.preset.center
}
#[inline(always)]
fn is_julia(&self) -> bool {
false
}
#[inline(always)]
fn render_pixel(&self, c: Complex, scale: f64, _max_radius: f64) -> (ColorRGB, f64, f64) {
let (i, z, dz) = mandelbrot_escape(c, self.config.scan_iterations);
color_distance_estimator(
i,
self.config.scan_iterations,
z,
dz,
scale,
self.config.color_palette,
)
}
fn info_text(&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,
self.preset.center.im,
self.config.scan_iterations,
self.config.zoom,
self.config.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, _, _) = mandelbrot_escape(sample_point, scan_iterations);
if k > 1 {
total_variation += (i as f64 - prev_i as f64).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, _, _) = mandelbrot_escape(test_point, 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 = 32;
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 c_init = viewport.map(x_f, y_f);
let (i, _, _) = mandelbrot_escape(c_init, 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: &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 selected_preset = presets.choose().copied().unwrap_or(presets[0]);
let color_palette = NEON_PALETTES.choose().copied().unwrap_or(NEON_PALETTES[0]);
let scan_iterations = MIN_ITERATIONS;
let rotations_count = ROTATION_STEPS;
let zooms_count = get_random_integer(30, 50);
let rotation_phasors: Vec<Complex> = get_rotation_phasors(rotations_count).collect();
let candidates = generate_zoom_candidates(zooms_count, rotations_count);
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,
config: FractalConfig {
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.config.zoom * 0.25;
let branch_phasor = Self::find_branch_phasor(
self.preset.center,
search_radius,
self.config.scan_iterations,
);
let aligned_center = Self::locked_interior_grid_alignment(
self.preset.center,
branch_phasor,
search_radius,
self.config.scan_iterations,
);
self.preset.center = aligned_center;
let best_entropy = Self::calculate_entropy(
self.preset.center,
self.config.zoom,
self.config.rotation,
self.config.scan_iterations,
width,
height,
);
let climb_radius = self.config.zoom * 0.05;
let search_directions: Vec<Complex> = std::iter::once(Complex::zero())
.chain(get_rotation_phasors(ROTATION_STEPS).map(|phasor| phasor * climb_radius))
.collect();
let (best_center, _) = search_directions
.par_iter()
.map(|&offset| {
let candidate_center = self.preset.center + offset;
let entropy = Self::calculate_entropy(
candidate_center,
self.config.zoom,
self.config.rotation,
self.config.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.config.zoom / (width.min(height) as f64);
let lod_iterations = (150.0 + 45.0 * (1.0 / scale).ln()) as u32;
self.config.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.config.zoom *= aspect_ratio.sqrt();
}
}
}
pub fn generate_zoom_candidates(zooms_count: usize, rotations_count: usize) -> Vec<(f64, usize)> {
if zooms_count == 0 || rotations_count == 0 {
return Vec::new();
}
let min_zoom = 2e-8;
let max_zoom = 9.0;
let log_ratio: f64 = max_zoom / min_zoom;
let mut candidates = Vec::with_capacity(zooms_count * rotations_count);
for z_idx in 0..zooms_count {
let t = if zooms_count > 1 {
(z_idx as f64) / ((zooms_count - 1) as f64)
} else {
0.0
};
let base_zoom = min_zoom * log_ratio.powf(t);
for r_idx in 0..rotations_count {
candidates.push((base_zoom, r_idx));
}
}
candidates
}
#[cfg(test)]
mod tests_mandelbrot {
use super::*;
#[test]
fn test_mandelbrot_generation_random() {
let monitor = Monitor::default();
let mandelbrot = MandelbrotGenerator::random(&monitor);
assert!(mandelbrot.config.zoom > 0.0);
assert_eq!(mandelbrot.preset.effect_name, ProceduralEffect::Mandelbrot);
}
#[test]
fn test_zoom_candidates_boundaries() {
let zooms_count = 50;
let rotations_count = 16;
let candidates = generate_zoom_candidates(zooms_count, rotations_count);
assert_eq!(candidates.len(), zooms_count * rotations_count);
let min_value = candidates[0].0;
assert!((min_value - 2e-8).abs() < 1e-12);
let last_idx = candidates.len() - 1;
let max_value = candidates[last_idx].0;
assert!((max_value - 9.0).abs() < 1e-12);
}
}