use crate::{
AuroraGenerator, Complex, JuliaGenerator, MandelbrotGenerator, Monitor, NewtonGenerator,
NovaGenerator, StarfieldGenerator, get_random_integer,
};
use clap::ValueEnum;
use image::RgbImage;
use serde::{Deserialize, Serialize};
use std::f32::consts::LOG2_E;
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;
}
#[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_mag2 = z.norm_sq();
let dz_mag2 = dz.norm_sq();
if z_mag2 > 0.0 && dz_mag2 > 0.0 {
let z_mag = z_mag2.sqrt();
let dz_mag = dz_mag2.sqrt();
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: [f32; 3],
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[0] * 255.0, alpha);
let blended_g = blend_channels_gamma(background_g, fractal_rgb[1] * 255.0, alpha);
let blended_b = blend_channels_gamma(background_b, fractal_rgb[2] * 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 get_rotation_steps() -> impl Iterator<Item = (f64, f64, f64)> {
(0..ROTATION_STEPS).map(|step| {
let angle_deg = (step * 360 / ROTATION_STEPS) as f64;
let rad = angle_deg.to_radians();
(rad, rad.cos(), rad.sin())
})
}
#[inline(always)]
pub fn rotate_point(x: f64, y: f64, cos_t: f64, sin_t: f64) -> (f64, f64) {
let rx = x * cos_t + y * sin_t;
let ry = -x * sin_t + y * cos_t;
(rx, ry)
}
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_re: f64,
pub start_im: f64,
pub dx_re: f64,
pub dx_im: f64,
pub dy_re: f64,
pub dy_im: f64,
}
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_re = scale * specs.cos_angle;
let dx_im = scale * specs.sin_angle;
let dy_re = -scale * specs.sin_angle;
let dy_im = scale * specs.cos_angle;
let v_center = if specs.is_julia {
Complex::new(0.0, 0.0)
} else {
specs.center
};
let start_re = v_center.re - cx_off * dx_re - cy_off * dy_re;
let start_im = v_center.im - cx_off * dx_im - cy_off * dy_im;
Self {
start_re,
start_im,
dx_re,
dx_im,
dy_re,
dy_im,
}
}
#[inline(always)]
pub fn map(&self, x: f64, y: f64) -> Complex {
let rx_row = self.start_re + y * self.dy_re;
let ry_row = self.start_im + y * self.dy_im;
let rx = rx_row + x * self.dx_re;
let ry = ry_row + x * self.dx_im;
Complex::new(rx, ry)
}
}