use crate::{
AuroraGenerator, JuliaGenerator, MandelbrotGenerator, Monitor, 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 = 400;
pub const MAX_ITERATIONS: u32 = 990;
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 = "star")]
Starfield,
#[value(name = "aurora")]
CosmicAurora,
#[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::Starfield => "Starfield",
Self::CosmicAurora => "Cosmic Aurora",
Self::Fractal => "Fractal",
Self::Random => "Random",
}
}
pub fn resolve(self) -> Self {
match self {
Self::Random => match get_random_integer(0, 3) {
0 => Self::JuliaSet,
1 => Self::Starfield,
2 => Self::CosmicAurora,
_ => Self::Mandelbrot,
},
Self::Fractal => match get_random_integer(0, 1) {
0 => Self::JuliaSet,
_ => Self::Mandelbrot,
},
concrete => concrete,
}
}
pub fn get_renderer(self, monitor: &Monitor) -> Option<Box<dyn ImageEffect>> {
match self.resolve() {
Self::JuliaSet => Some(Box::new(JuliaGenerator::random(monitor))),
Self::Mandelbrot => Some(Box::new(MandelbrotGenerator::random(monitor))),
Self::Starfield => Some(Box::new(StarfieldGenerator::random(monitor))),
Self::CosmicAurora => Some(Box::new(AuroraGenerator::random(monitor))),
Self::None | Self::Fractal | Self::Random => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct FractalPreset {
pub center_re: f64,
pub center_im: f64,
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_re: f64, z_im: f64) -> f32 {
if i < max_iterations {
let mag2 = z_re * z_re + z_im * z_im;
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_re: f64,
z_im: f64,
dz_re: f64,
dz_im: f64,
) -> f64 {
if i < max_iterations {
let z_mag2 = z_re * z_re + z_im * z_im;
let dz_mag2 = dz_re * dz_re + dz_im * dz_im;
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,
dx_vignette: f32,
dy_vignette_sq: f32,
) {
let dist = (dx_vignette * dx_vignette + dy_vignette_sq).sqrt();
let vignette = (1.0 - dist * 0.4).clamp(0.1, 1.0);
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 as f32 * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 1] = (blended_g as f32 * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 2] = (blended_b as f32 * vignette).clamp(0.0, 255.0) as u8;
} else if shadow_alpha > 0.005 {
row_data[idx] = (background_r as f32 * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 1] = (background_g as f32 * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 2] = (background_b as f32 * vignette).clamp(0.0, 255.0) as u8;
} else {
row_data[idx] = (original_r as f32 * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 1] = (original_g as f32 * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 2] = (original_b as f32 * vignette).clamp(0.0, 255.0) as u8;
}
}
#[inline(always)]
pub fn compute_escape_iterations(
fractal_type: ProceduralEffect,
rx: f64,
ry: f64,
c_re: f64,
c_im: f64,
scan_iterations: u32,
) -> (u32, f64, f64, f64, f64) {
let (mut z_re, mut z_im, param_re, param_im) = if fractal_type == ProceduralEffect::JuliaSet {
(rx, ry, c_re, c_im)
} else {
let q = (rx - 0.25) * (rx - 0.25) + ry * ry;
if q * (q + (rx - 0.25)) < 0.25 * ry * ry {
return (scan_iterations, 0.0, 0.0, 0.0, 0.0);
}
if (rx + 1.0) * (rx + 1.0) + ry * ry < 0.0625 {
return (scan_iterations, 0.0, 0.0, 0.0, 0.0);
}
(0.0, 0.0, rx, ry)
};
let (mut dz_re, mut dz_im) = if fractal_type == ProceduralEffect::JuliaSet {
(1.0, 0.0)
} else {
(0.0, 0.0)
};
let mut i = 0;
while i < scan_iterations {
let re2 = z_re * z_re;
let im2 = z_im * z_im;
if re2 + im2 > 4.0 {
break;
}
let next_dz_re = 2.0 * (z_re * dz_re - z_im * dz_im)
+ if fractal_type == ProceduralEffect::JuliaSet {
0.0
} else {
1.0
};
let next_dz_im = 2.0 * (z_re * dz_im + z_im * dz_re);
dz_re = next_dz_re;
dz_im = next_dz_im;
z_im = 2.0 * z_re * z_im + param_im;
z_re = re2 - im2 + param_re;
i += 1;
}
(i, z_re, z_im, dz_re, dz_im)
}
#[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_re: f64,
pub center_im: f64,
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_re, v_center_im) = if specs.is_julia {
(0.0, 0.0)
} else {
(specs.center_re, specs.center_im)
};
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) -> (f64, f64) {
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;
(rx, ry)
}
}