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};
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(
row_data: &mut [u8],
idx: usize,
fractal_rgb: ColorRGB,
alpha: f32,
shadow_alpha: f32,
) {
let original_r = row_data[idx];
let original_g = row_data[idx + 1];
let original_b = row_data[idx + 2];
let (background_r, background_g, background_b) = if shadow_alpha > 0.005 {
let shadow_factor = 1.0 - shadow_alpha;
(
(original_r as f32 * shadow_factor).clamp(0.0, 255.0) as u8,
(original_g as f32 * shadow_factor).clamp(0.0, 255.0) as u8,
(original_b as f32 * shadow_factor).clamp(0.0, 255.0) as u8,
)
} else {
(original_r, original_g, original_b)
};
if alpha > 0.005 {
let blended_r = blend_channels_gamma(background_r, fractal_rgb.red * 255.0, alpha);
let blended_g = blend_channels_gamma(background_g, fractal_rgb.green * 255.0, alpha);
let blended_b = blend_channels_gamma(background_b, fractal_rgb.blue * 255.0, alpha);
row_data[idx] = blended_r;
row_data[idx + 1] = blended_g;
row_data[idx + 2] = blended_b;
} else if shadow_alpha > 0.005 {
row_data[idx] = background_r;
row_data[idx + 1] = background_g;
row_data[idx + 2] = background_b;
}
}
#[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,
)
}
#[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);
}
}
}