use crate::effects::{
FractalPreset, MAX_ITERATIONS, MIN_ITERATIONS, ProceduralEffect, Viewport, ViewportSpecs,
blend_and_vignette_pixel, calculate_distance_estimator, calculate_smooth_potential,
compute_escape_iterations, get_rotation_steps, partition_rows,
};
use crate::{
Complex, ImageEffect, NEON_PALETTES, NeonColor, WallSwitchError, WallSwitchResult,
get_random_integer, smoothstep,
};
use image::RgbImage;
use std::{io::Error, path::Path, thread};
pub const TARGET_RANGE: [f64; 2] = [0.24, 0.26];
impl ImageEffect for MandelbrotGenerator {
fn apply(&self, rgb_img: &mut RgbImage) {
self.apply_effect_in_memory(rgb_img);
}
fn info(&self) -> String {
format!(
"fractal [{}]\n\
f(z) = z² + 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
)
}
}
pub struct MandelbrotGenerator {
pub preset: FractalPreset,
pub scan_iterations: u32,
pub color_palette: NeonColor,
pub zoom: f64,
pub cos_angle: f64,
pub sin_angle: f64,
}
impl Default for MandelbrotGenerator {
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 MandelbrotGenerator {
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
}
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)
}
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)
}
pub fn evaluate_rotation(&mut self, width: u32, height: u32) -> f64 {
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_angle_rad = 0.0;
let mut max_active_coverage = 0.0;
for (rad, cos_t, sin_t) in get_rotation_steps() {
self.cos_angle = cos_t;
self.sin_angle = sin_t;
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_angle_rad = rad;
}
}
self.cos_angle = original_cos;
self.sin_angle = original_sin;
best_angle_rad
}
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;
let best_rad = self.evaluate_rotation(width, height);
if best_rad > 0.0 {
self.cos_angle = best_rad.cos();
self.sin_angle = best_rad.sin();
} 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;
}
pub fn apply_effect_in_memory(&self, rgb_img: &mut RgbImage) {
let (width, height) = rgb_img.dimensions();
let w_f = width as f64;
let h_f = height as f64;
let specs = ViewportSpecs {
center: self.preset.center,
zoom: self.zoom,
cos_angle: self.cos_angle,
sin_angle: self.sin_angle,
is_julia: false,
};
let viewport = Viewport::new(w_f, h_f, &specs);
let scan_iterations = self.scan_iterations;
let center = self.preset.center;
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 min_dim = w_f.min(h_f);
let scale = self.zoom / min_dim;
thread::scope(|scope| {
let viewport_ref = &viewport;
let color_palette = self.color_palette.to_array();
for chunk in rows.chunks_mut(chunk_size) {
scope.spawn(move || {
for (y, row_data) in chunk.iter_mut() {
let y_f = *y as f64;
for x in 0..width_usize {
let x_f = x as f64;
let z_init = viewport_ref.map(x_f, y_f);
let (i, z, dz) = compute_escape_iterations(
ProceduralEffect::Mandelbrot,
z_init,
center,
scan_iterations,
);
let t = calculate_smooth_potential(i, scan_iterations, z);
let (fractal_rgb, alpha, s_alpha) = if t > 0.005 && i < scan_iterations
{
let dist_complex =
calculate_distance_estimator(i, scan_iterations, z, dz);
let dist_pixels = (dist_complex / scale) as f32;
let thickness = 2.5_f32;
let max_radius = thickness * 2.0;
let shadow_radius = max_radius * 1.5;
if dist_pixels < shadow_radius {
let norm_dist = dist_pixels / max_radius;
let core = if dist_pixels < 1.2 {
1.0 - (dist_pixels / 1.2)
} else {
0.0
};
let ripple_freq = 12.0_f32;
let ripple_wave =
(t * std::f32::consts::PI * ripple_freq).sin().abs();
let nested_detail = (1.0
- smoothstep(0.0, 0.4, 1.0 - ripple_wave))
* (1.0 - norm_dist);
let glow = if dist_pixels < max_radius {
(1.0 - norm_dist * norm_dist).powi(6) * 0.45
} else {
0.0
};
let profile = core * 0.65 + nested_detail * 0.20 + glow * 0.15;
let norm_shadow = dist_pixels / shadow_radius;
let shadow_profile =
(1.0 - norm_shadow * norm_shadow).powi(2) * 0.35;
let angle = z.im.atan2(z.re) as f32;
let light = 0.65_f32 + 0.35_f32 * (angle * 4.0).cos().abs();
let t_cycled = (t * 2.0) % 1.0;
let secondary_color =
[color_palette[1], color_palette[2], color_palette[0]];
let r_grad = if t_cycled < 0.5 {
let factor = t_cycled * 2.0;
color_palette[0] * (1.0 - factor)
+ secondary_color[0] * factor
} else {
let factor = (t_cycled - 0.5) * 2.0;
secondary_color[0] * (1.0 - factor)
+ color_palette[0] * factor
};
let g_grad = if t_cycled < 0.5 {
let factor = t_cycled * 2.0;
color_palette[1] * (1.0 - factor)
+ secondary_color[1] * factor
} else {
let factor = (t_cycled - 0.5) * 2.0;
secondary_color[1] * (1.0 - factor)
+ color_palette[1] * factor
};
let b_grad = if t_cycled < 0.5 {
let factor = t_cycled * 2.0;
color_palette[2] * (1.0 - factor)
+ secondary_color[2] * factor
} else {
let factor = (t_cycled - 0.5) * 2.0;
secondary_color[2] * (1.0 - factor)
+ color_palette[2] * factor
};
let core_color = [r_grad, g_grad, b_grad];
let mut border_color =
[1.0 - r_grad, 1.0 - g_grad, 1.0 - b_grad];
let max_val =
border_color[0].max(border_color[1]).max(border_color[2]);
if max_val > 0.0 {
border_color[0] /= max_val;
border_color[1] /= max_val;
border_color[2] /= max_val;
}
let color_blend = norm_dist.powi(2);
let r_blended = core_color[0] * (1.0 - color_blend)
+ border_color[0] * color_blend;
let g_blended = core_color[1] * (1.0 - color_blend)
+ border_color[1] * color_blend;
let b_blended = core_color[2] * (1.0 - color_blend)
+ border_color[2] * color_blend;
let brightness_boost = 1.20_f32;
let rgb = [
(r_blended * light * brightness_boost).clamp(0.0, 1.0),
(g_blended * light * brightness_boost).clamp(0.0, 1.0),
(b_blended * light * brightness_boost).clamp(0.0, 1.0),
];
let iteration_fade =
if i < 16 { (i as f32 - 3.0) / 13.0 } else { 1.0 };
(
rgb,
profile * 0.95 * iteration_fade,
shadow_profile * iteration_fade,
)
} else {
([0.0, 0.0, 0.0], 0.0, 0.0)
}
} else {
([0.0, 0.0, 0.0], 0.0, 0.0)
};
let idx = x * 3;
blend_and_vignette_pixel(row_data, idx, fractal_rgb, alpha, s_alpha);
}
}
});
}
});
}
pub fn apply_effect<P: AsRef<Path>>(
&self,
input_path: P,
output_path: P,
) -> WallSwitchResult<()> {
let img = image::open(&input_path)
.map_err(|e| WallSwitchError::UnableToFind(format!("Failed to open image: {e}")))?;
let mut rgb_img = img.to_rgb8();
self.apply_effect_in_memory(&mut rgb_img);
rgb_img
.save(&output_path)
.map_err(|e| WallSwitchError::Io(Error::other(e)))?;
Ok(())
}
}