use crate::{
AuroraGenerator, ColorRGB, Complex, JuliaGenerator, MandelbrotGenerator, Monitor, NeonColor,
NewtonGenerator, NovaGenerator, StarfieldGenerator, WallSwitchError, WallSwitchResult,
get_random_integer,
};
use clap::ValueEnum;
use image::RgbImage;
use serde::{Deserialize, Serialize};
use std::{f32::consts::LOG2_E, io::Error, path::Path, thread};
pub const MIN_ITERATIONS: u32 = 500;
pub const MAX_ITERATIONS: u32 = 1200;
pub const ROTATION_STEPS: u32 = 16;
pub trait ImageEffect {
fn apply(&self, rgb_img: &mut RgbImage);
fn info(&self) -> String;
fn apply_effect(&self, input_path: &Path, output_path: &Path) -> 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(&mut rgb_img);
rgb_img
.save(output_path)
.map_err(|e| WallSwitchError::Io(Error::other(e)))?;
Ok(())
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum ProceduralEffect {
#[value(name = "none")]
#[default]
None,
#[value(name = "julia")]
JuliaSet,
#[value(name = "mandelbrot")]
Mandelbrot,
#[value(name = "newton")]
NewtonBasins,
#[value(name = "nova")]
NovaJulia,
#[value(name = "aurora")]
CosmicAurora,
#[value(name = "star")]
Starfield,
#[value(name = "fractal")]
Fractal,
#[value(name = "random")]
Random,
}
impl ProceduralEffect {
pub fn get_name(self) -> &'static str {
match self {
Self::None => "None",
Self::JuliaSet => "Julia Sets",
Self::Mandelbrot => "Mandelbrot",
Self::NewtonBasins => "Newton Basins",
Self::NovaJulia => "Nova Julia",
Self::CosmicAurora => "Cosmic Aurora",
Self::Starfield => "Starfield",
Self::Fractal => "Fractal",
Self::Random => "Random",
}
}
pub fn resolve(self) -> Self {
match self {
Self::Random => match get_random_integer(0, 5) {
0 => Self::JuliaSet,
1 => Self::Mandelbrot,
2 => Self::NewtonBasins,
3 => Self::NovaJulia,
4 => Self::CosmicAurora,
_ => Self::Starfield,
},
Self::Fractal => match get_random_integer(0, 3) {
0 => Self::JuliaSet,
1 => Self::Mandelbrot,
2 => Self::NewtonBasins,
_ => Self::NovaJulia,
},
concrete => concrete,
}
}
pub fn get_renderer(self, monitor: &Monitor) -> Option<Box<dyn ImageEffect>> {
match self {
Self::JuliaSet => Some(Box::new(JuliaGenerator::random(monitor))),
Self::Mandelbrot => Some(Box::new(MandelbrotGenerator::random(monitor))),
Self::NewtonBasins => Some(Box::new(NewtonGenerator::random(monitor))),
Self::NovaJulia => Some(Box::new(NovaGenerator::random(monitor))),
Self::Starfield => Some(Box::new(StarfieldGenerator::random(monitor))),
Self::CosmicAurora => Some(Box::new(AuroraGenerator::random(monitor))),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct FractalPreset {
pub center: Complex,
pub fractal_name: &'static str,
pub effect_name: ProceduralEffect,
}
impl std::fmt::Display for FractalPreset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ({:+.5} {:+.5}i) under {:?}",
self.fractal_name, self.center.re, self.center.im, self.effect_name
)
}
}
pub fn partition_rows(rgb_img: &mut RgbImage) -> (Vec<(usize, &mut [u8])>, usize) {
let (width, _) = rgb_img.dimensions();
let width_usize = width as usize;
let row_stride = width_usize * 3;
let pixels_buffer = rgb_img.as_mut();
let rows: Vec<(usize, &mut [u8])> = pixels_buffer
.chunks_exact_mut(row_stride)
.enumerate()
.collect();
(rows, width_usize)
}
#[inline(always)]
pub fn stretch_potential(raw_t: f32) -> f32 {
raw_t.clamp(0.0, 1.0).powf(0.35)
}
#[inline]
pub fn calculate_smooth_potential(i: u32, max_iterations: u32, z: Complex) -> f32 {
if i < max_iterations {
let mag2 = z.norm_sq();
let smooth_i = if mag2 > 4.0 {
let log_zn = (mag2.ln() * 0.5) as f32; let nu = log_zn.ln() * LOG2_E;
(i as f32 + 1.0 - nu).max(0.0)
} else {
i as f32
};
let min_render_iter = 32.0_f32;
if smooth_i < min_render_iter {
return 0.0;
}
let normalized = (smooth_i - min_render_iter) / (max_iterations as f32 - min_render_iter);
stretch_potential(normalized)
} else {
1.0 }
}
#[inline]
pub fn calculate_distance_estimator(i: u32, max_iterations: u32, z: Complex, dz: Complex) -> f64 {
if i < max_iterations {
let z_mag = z.norm();
let dz_mag = dz.norm();
if z_mag > 0.0 && dz_mag > 0.0 {
return 2.0 * z_mag * z_mag.ln() / dz_mag;
}
}
0.0
}
#[inline]
pub fn blend_channels_gamma(bg: u8, fg: f32, alpha: f32) -> u8 {
let bg_f = bg as f32 / 255.0;
let bg_linear = bg_f * bg_f;
let fg_f = fg / 255.0;
let fg_linear = fg_f * fg_f;
let blended_linear = bg_linear * (1.0 - alpha) + fg_linear * alpha;
(blended_linear.sqrt() * 255.0).clamp(0.0, 255.0) as u8 }
#[inline]
pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
#[inline]
pub fn blend_and_vignette(
pixel: &mut ColorRGB,
fractal_rgb: ColorRGB,
alpha: f32,
shadow_alpha: f32,
) {
if shadow_alpha > 0.005 {
*pixel = pixel.scale(1.0 - shadow_alpha);
}
if alpha > 0.005 {
let bg_linear = pixel.squared();
let fg_linear = fractal_rgb.squared();
let blended_linear = fg_linear.lerp(&bg_linear, alpha);
*pixel = blended_linear.sqrt();
}
}
#[inline(always)]
pub fn compute_escape_iterations(
fractal_type: ProceduralEffect,
init: Complex,
c: Complex,
scan_iterations: u32,
) -> (u32, Complex, Complex) {
let (mut z, param) = if fractal_type == ProceduralEffect::JuliaSet {
(init, c)
} else {
let q = (init.re - 0.25) * (init.re - 0.25) + init.im * init.im;
if q * (q + (init.re - 0.25)) < 0.25 * init.im * init.im {
return (
scan_iterations,
Complex::new(0.0, 0.0),
Complex::new(0.0, 0.0),
);
}
if (init.re + 1.0) * (init.re + 1.0) + init.im * init.im < 0.0625 {
return (
scan_iterations,
Complex::new(0.0, 0.0),
Complex::new(0.0, 0.0),
);
}
(Complex::new(0.0, 0.0), init)
};
let mut dz = if fractal_type == ProceduralEffect::JuliaSet {
Complex::new(1.0, 0.0)
} else {
Complex::new(0.0, 0.0)
};
let mut i = 0;
while i < scan_iterations {
if z.norm_sq() > 4.0 {
break;
}
let add_factor = if fractal_type == ProceduralEffect::JuliaSet {
Complex::new(0.0, 0.0)
} else {
Complex::new(1.0, 0.0)
};
dz = 2.0 * z * dz + add_factor;
z = z * z + param;
i += 1;
}
(i, z, dz)
}
#[inline]
pub fn calculate_circular_fade(z: Complex, max_radius: f32, flat_ratio: f32) -> f32 {
let dist = z.norm() as f32;
let r = dist / max_radius;
if r < flat_ratio {
1.0
} else if r < 1.0 {
let t = (r - flat_ratio) / (1.0 - flat_ratio);
1.0 - t * t * (3.0 - 2.0 * t)
} else {
0.0
}
}
#[inline]
pub fn get_rotation_phasors() -> impl Iterator<Item = Complex> {
(0..ROTATION_STEPS).map(|step| {
let angle_deg = (step * 360 / ROTATION_STEPS) as f64;
let rad = angle_deg.to_radians();
Complex::new(rad.cos(), rad.sin())
})
}
pub struct ViewportSpecs {
pub center: Complex,
pub zoom: f64,
pub cos_angle: f64,
pub sin_angle: f64,
pub is_julia: bool,
}
pub struct Viewport {
pub start: Complex,
pub dx: Complex,
pub dy: Complex,
}
impl Viewport {
pub fn new(width: f64, height: f64, specs: &ViewportSpecs) -> Self {
let min_dim = width.min(height);
let scale = specs.zoom / min_dim;
let cx_off = width / 2.0;
let cy_off = height / 2.0;
let dx = scale * Complex::new(specs.cos_angle, specs.sin_angle);
let dy = dx * Complex::new(0.0, 1.0);
let v_center = if specs.is_julia {
Complex::new(0.0, 0.0)
} else {
specs.center
};
let start = v_center - dx * cx_off - dy * cy_off;
Self { start, dx, dy }
}
#[inline(always)]
pub fn map(&self, x: f64, y: f64) -> Complex {
self.start + self.dx * x + self.dy * y
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RelaxedEscape {
pub iterations: u32,
pub max_iterations: u32,
pub diff_norm: f64,
pub z_final: Complex,
}
impl RelaxedEscape {
pub fn new(iterations: u32, max_iterations: u32, diff_norm: f64, z_final: Complex) -> Self {
Self {
iterations,
max_iterations,
diff_norm,
z_final,
}
}
}
#[inline(always)]
pub fn color_distance_estimator(
i: u32,
scan_iterations: u32,
z: Complex,
dz: Complex,
scale: f64,
color_palette: NeonColor,
) -> (ColorRGB, f32, f32) {
let t = calculate_smooth_potential(i, scan_iterations, z);
if t <= 0.005 || i >= scan_iterations {
return (ColorRGB::default(), 0.0, 0.0);
}
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 {
return (ColorRGB::default(), 0.0, 0.0);
}
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.arg() 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_palette.rotated();
let core_color = if t_cycled < 0.5 {
let factor = t_cycled * 2.0;
color_palette.color_rgb.lerp(&secondary, 1.0 - factor)
} else {
let factor = (t_cycled - 0.5) * 2.0;
secondary.lerp(&color_palette.color_rgb, 1.0 - factor)
};
let border_color = core_color.complementary().saturate_components();
let color_blend = norm_dist.powi(2);
let blended = border_color.lerp(&core_color, color_blend);
let brightness_boost = 1.20_f32;
let rgb = blended.scale(light * brightness_boost).clamp_bounds();
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,
)
}
#[inline(always)]
pub fn color_relaxed_newton_fractal(
escape: &RelaxedEscape,
color_palette: NeonColor,
edge_fade: f32,
ln_epsilon: f32,
is_nova: bool,
) -> (ColorRGB, f32, f32) {
if escape.iterations >= escape.max_iterations {
return (ColorRGB::default(), 0.0, 0.0);
}
let smooth_i =
escape.iterations as f32 + (escape.diff_norm.ln() as f32 / ln_epsilon).clamp(0.0, 1.0);
let ripple_frequency = 0.50_f32;
let raw_wave = (smooth_i * ripple_frequency * std::f32::consts::PI)
.sin()
.abs();
let norm_dist = if is_nova {
raw_wave.powf(2.5)
} else {
raw_wave
};
let core = if is_nova {
if norm_dist > 0.92 {
(norm_dist - 0.92) / 0.08
} else {
0.0
}
} else {
if norm_dist > 0.95 {
(norm_dist - 0.95) / 0.05
} else {
0.0
}
};
let glow = if is_nova {
norm_dist.powi(6) * 0.52
} else {
norm_dist.powi(5) * 0.40
};
let profile = if is_nova {
core * 0.78 + glow * 0.22
} else {
core * 0.70 + glow * 0.30
};
let shadow_profile = if is_nova {
(1.0 - norm_dist).powi(3) * 0.48
} else {
(1.0 - norm_dist).powi(2) * 0.35
};
let angle = escape.z_final.arg() as f32;
let light = if is_nova {
0.75_f32 + 0.25_f32 * (angle * 4.0).cos().abs()
} else {
0.70_f32 + 0.30_f32 * (angle * 3.0).cos().abs()
};
let t_cycled = (smooth_i * 0.08) % 1.0;
let secondary = color_palette.rotated();
let core_color = if is_nova {
let t_cos = (t_cycled * std::f32::consts::PI).cos() * 0.5 + 0.5;
color_palette.color_rgb.lerp(&secondary, t_cos)
} else {
color_palette.color_rgb.lerp(&secondary, 1.0 - t_cycled)
};
let border_color = core_color.complementary().saturate_components();
let blended = if is_nova {
let color_blend = norm_dist.powf(3.0);
border_color.lerp(&core_color, color_blend)
} else {
border_color.lerp(&core_color, norm_dist)
};
let brightness_boost = if is_nova { 1.45_f32 } else { 1.25_f32 };
let rgb = blended.scale(light * brightness_boost).clamp_bounds();
let limit_fade_iter = if is_nova { 6 } else { 8 };
let iteration_fade = if escape.iterations < limit_fade_iter {
escape.iterations as f32 / limit_fade_iter as f32
} else {
1.0
};
(
rgb,
profile * 0.95 * iteration_fade * edge_fade,
shadow_profile * iteration_fade * edge_fade,
)
}
pub fn render_fractal_parallel<F>(
rgb_img: &mut RgbImage,
zoom: f64,
cos_angle: f64,
sin_angle: f64,
center: Complex,
is_julia: bool,
pixel_fn: F,
) where
F: Fn(Complex, f64) -> (ColorRGB, f32, f32) + Send + Sync,
{
let (width, height) = rgb_img.dimensions();
let w_f = width as f64;
let h_f = height as f64;
let specs = ViewportSpecs {
center,
zoom,
cos_angle,
sin_angle,
is_julia,
};
let viewport = Viewport::new(w_f, h_f, &specs);
let (mut rows, _) = 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 = zoom / min_dim;
thread::scope(|scope| {
let viewport_ref = &viewport;
let pixel_fn_ref = &pixel_fn;
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, pixel_slice) in row_data.chunks_exact_mut(3).enumerate() {
let x_f = x as f64;
let z_init = viewport_ref.map(x_f, y_f);
let (fractal_rgb, alpha, s_alpha) = pixel_fn_ref(z_init, scale);
let mut pixel_color = ColorRGB::from_slice(pixel_slice);
blend_and_vignette(&mut pixel_color, fractal_rgb, alpha, s_alpha);
pixel_color.write_to_slice(pixel_slice);
}
}
});
}
});
}
pub fn find_optimal_framing(
active_points: &[Complex],
width: u32,
height: u32,
default_cos: f64,
default_sin: f64,
) -> (f64, f64, f64) {
if active_points.is_empty() {
return (f64::MAX, default_cos, default_sin);
}
let w_f = width as f64;
let h_f = height as f64;
let min_dim = w_f.min(h_f);
let mut best_zoom = f64::MAX;
let mut best_cos = default_cos;
let mut best_sin = default_sin;
for phasor in get_rotation_phasors() {
let mut max_cx_abs = 0.0_f64;
let mut max_cy_abs = 0.0_f64;
let inverse_phasor = phasor.conj();
for &point in active_points {
let rotated = point * inverse_phasor;
max_cx_abs = max_cx_abs.max(rotated.re.abs());
max_cy_abs = max_cy_abs.max(rotated.im.abs());
}
let zoom_x = 2.0 * max_cx_abs * min_dim / w_f;
let zoom_y = 2.0 * max_cy_abs * min_dim / h_f;
let required_zoom = zoom_x.max(zoom_y);
if required_zoom < best_zoom {
best_zoom = required_zoom;
best_cos = phasor.re;
best_sin = phasor.im;
}
}
(best_zoom, best_cos, best_sin)
}
#[cfg(test)]
mod tests_common {
use super::*;
#[test]
fn test_procedural_effect_resolution() {
let effect_rand = ProceduralEffect::Random;
let resolved_rand = effect_rand.resolve();
assert_ne!(resolved_rand, ProceduralEffect::Random);
let effect_fractal = ProceduralEffect::Fractal;
let resolved_fractal = effect_fractal.resolve();
assert_ne!(resolved_fractal, ProceduralEffect::Fractal);
let resolved_julia = ProceduralEffect::JuliaSet.resolve();
assert_eq!(resolved_julia, ProceduralEffect::JuliaSet);
}
#[test]
fn test_smooth_potential_clamping() {
let z = Complex::new(5.0, 5.0);
let t = calculate_smooth_potential(50, 100, z);
assert!((0.0..=1.0).contains(&t));
}
#[test]
fn test_viewport_complex_operations() {
let specs = ViewportSpecs {
center: Complex::new(0.5, -0.5),
zoom: 2.0,
cos_angle: 1.0,
sin_angle: 0.0,
is_julia: false,
};
let viewport = Viewport::new(100.0, 100.0, &specs);
let mapped = viewport.map(50.0, 50.0);
assert!((mapped.re - 0.5).abs() < 1e-9);
}
#[test]
fn test_complex_phasors() {
let phasors: Vec<Complex> = get_rotation_phasors().collect();
assert_eq!(phasors.len() as u32, ROTATION_STEPS);
for phasor in phasors {
let magnitude = phasor.norm();
assert!((magnitude - 1.0).abs() < 1e-9);
}
}
#[test]
fn test_render_fractal_parallel() {
let mut img = RgbImage::new(10, 10);
let center = Complex::new(0.0, 0.0);
render_fractal_parallel(&mut img, 2.0, 1.0, 0.0, center, true, |_z_init, _scale| {
(ColorRGB::new(1.0, 0.0, 0.0), 1.0, 0.0)
});
let pixel = img.get_pixel(5, 5);
assert!(pixel[0] > 0);
}
#[test]
fn test_find_optimal_framing_empty() {
let (zoom, cos, sin) = find_optimal_framing(&[], 100, 100, 1.0, 0.0);
assert_eq!(zoom, f64::MAX);
assert_eq!(cos, 1.0);
assert_eq!(sin, 0.0);
}
#[test]
fn test_find_optimal_framing_with_points() {
let points = vec![Complex::new(1.0, 1.0), Complex::new(-1.0, -1.0)];
let (zoom, cos, sin) = find_optimal_framing(&points, 100, 100, 1.0, 0.0);
assert!(zoom > 0.0);
assert!(zoom < f64::MAX);
assert!((cos * cos + sin * sin - 1.0).abs() < 1e-9);
}
}