use crate::{WallSwitchError, WallSwitchResult, get_random_integer};
use clap::ValueEnum;
use image::RgbImage;
use serde::{Deserialize, Serialize};
use std::{io::Error, path::Path, thread};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
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,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ZoomMode {
Full,
Detail,
}
const SHARED_NEON_PALETTES: [[f32; 3]; 15] = [
[1.0, 0.0, 0.8], [0.0, 1.0, 1.0], [1.0, 0.6, 0.0], [0.0, 1.0, 0.2], [0.6, 0.0, 1.0], [1.0, 0.1, 0.1], [1.0, 1.0, 0.0], [0.0, 0.4, 1.0], [0.5, 1.0, 0.0], [1.0, 0.0, 0.4], [0.0, 1.0, 0.6], [1.0, 0.4, 0.4], [0.9, 0.9, 1.0], [1.0, 0.8, 0.0], [0.4, 0.0, 0.8], ];
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]
fn calculate_smooth_potential(i: u32, max_iterations: u32, z_re: f32, z_im: f32) -> f32 {
if i < max_iterations {
let mag2 = z_re * z_re + z_im * z_im;
if mag2 > 4.0 {
let log_zn = mag2.ln() * 0.5;
let nu = (log_zn * std::f32::consts::LOG2_E).ln() * std::f32::consts::LOG2_E;
let smooth_i = (i as f32 + 1.0 - nu).max(0.0);
(smooth_i / max_iterations as f32).clamp(0.0, 1.0)
} else {
i as f32 / max_iterations as f32
}
} else {
1.0
}
}
#[inline]
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]
fn blend_and_vignette_pixel(
row_data: &mut [u8],
idx: usize,
t: f32,
color_palette: [f32; 3],
dx_vignette: f32,
dy_vignette_sq: f32,
) {
let original_r = row_data[idx];
let original_g = row_data[idx + 1];
let original_b = row_data[idx + 2];
let shadow_factor = 1.0 - (t * 0.5);
let background_r = original_r as f32 * shadow_factor;
let background_g = original_g as f32 * shadow_factor;
let background_b = original_b as f32 * shadow_factor;
let r_fractal = color_palette[0] * t * 255.0;
let g_fractal = color_palette[1] * t * 255.0;
let b_fractal = color_palette[2] * t * 255.0;
let alpha = t.sqrt() * 0.8;
let blended_r = blend_channels_gamma(background_r as u8, r_fractal, alpha);
let blended_g = blend_channels_gamma(background_g as u8, g_fractal, alpha);
let blended_b = blend_channels_gamma(background_b as u8, b_fractal, alpha);
let dist = (dx_vignette * dx_vignette + dy_vignette_sq).sqrt();
let vignette = (1.0 - dist * 0.4).clamp(0.1, 1.0);
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;
}
#[inline(always)]
fn iterate_fractal(
fractal_type: ProceduralEffect,
z_re: &mut f32,
z_im: &mut f32,
c_re: f32,
c_im: f32,
) {
let re2 = *z_re * *z_re;
let im2 = *z_im * *z_im;
match fractal_type {
ProceduralEffect::JuliaSet | ProceduralEffect::Mandelbrot => {
let next_im = 2.0 * *z_re * *z_im + c_im;
*z_re = re2 - im2 + c_re;
*z_im = next_im;
}
_ => {}
}
}
pub struct FractalGenerator {
pub fractal_type: ProceduralEffect,
pub center_re: f32,
pub center_im: f32,
pub min_iterations: u32,
pub max_iterations: u32,
pub scan_iterations: u32,
pub color_palette: [f32; 3],
pub zoom: f32,
pub cos_angle: f32,
pub sin_angle: f32,
pub zoom_mode: ZoomMode,
}
impl Default for FractalGenerator {
fn default() -> Self {
let min_it = 250;
let max_it = 500;
Self {
fractal_type: ProceduralEffect::JuliaSet,
center_re: -0.7,
center_im: 0.27015,
min_iterations: min_it,
max_iterations: max_it,
scan_iterations: get_random_integer(min_it, max_it) as u32,
color_palette: [0.0, 1.0, 1.0], zoom: 3.0,
cos_angle: 1.0, sin_angle: 0.0,
zoom_mode: ZoomMode::Full,
}
}
}
impl FractalGenerator {
pub fn random(effect: ProceduralEffect) -> Self {
let fractal_type = match effect {
ProceduralEffect::Random => match get_random_integer(0, 1) {
0 => ProceduralEffect::JuliaSet,
_ => ProceduralEffect::Mandelbrot,
},
concrete => concrete,
};
let mut center_re = 0.0_f32;
let mut center_im = 0.0_f32;
let mut min_iterations = 250;
let mut max_iterations = 500;
let mut zoom = 3.0_f32;
let mut zoom_mode = ZoomMode::Detail;
match fractal_type {
ProceduralEffect::JuliaSet => {
let presets = [
(-0.7, 0.27015), (-0.4, 0.6), (-0.8, 0.156), (-0.7269, 0.1889), (-0.75, 0.11), (-0.1, 0.651), (-0.70176, -0.3842), (0.355, 0.355), (-0.4, -0.59), (-0.54, 0.54), (-0.74543, 0.11301), (-0.835, -0.2321), (-0.77269, 0.12428), (-0.51251, 0.5213), (0.4, 0.4), (-0.55, 0.55), (-0.624, 0.435), (-0.162, 1.04), (-0.12, 0.85), (-0.742, 0.1345), (-0.391, -0.587), (0.0, 0.8), (-0.73, 0.21), (-0.81, 0.2), (-0.68, 0.34), (-0.11, 0.83), (-0.76, 0.08), (-0.72, 0.22), ];
let c_idx = get_random_integer(0, (presets.len() - 1) as u64) as usize;
let (cre, cim) = presets[c_idx];
center_re = cre;
center_im = cim;
zoom = get_random_integer(250, 400) as f32 / 100.0;
zoom_mode = ZoomMode::Full;
}
ProceduralEffect::Mandelbrot => {
let presets = [
(-0.5623, 0.6421, 450.0, ZoomMode::Detail), (-0.7756838, 0.13646737, 850.0, ZoomMode::Detail), (-1.25066, 0.02012, 900.0, ZoomMode::Detail), (-0.748, 0.124, 350.0, ZoomMode::Detail), (-0.7436438, 0.1318259, 1200.0, ZoomMode::Detail), (-0.7431, 0.1319, 1000.0, ZoomMode::Detail), (-0.7432, 0.1320, 1000.0, ZoomMode::Detail), (-0.7433, 0.1321, 1000.0, ZoomMode::Detail), (-0.7434, 0.1322, 1000.0, ZoomMode::Detail), (-0.7434, 0.1323, 1000.0, ZoomMode::Detail), (-0.7435, 0.1324, 1000.0, ZoomMode::Detail), (-0.75, 0.10, 300.0, ZoomMode::Detail), (-0.088, 0.654, 650.0, ZoomMode::Detail), (0.275, 0.0, 300.0, ZoomMode::Detail), ];
let c_idx = get_random_integer(0, (presets.len() - 1) as u64) as usize;
let (cre, cim, base_zoom, mode) = presets[c_idx];
center_re = cre;
center_im = cim;
zoom_mode = mode;
if mode == ZoomMode::Full {
zoom = base_zoom;
} else {
zoom = base_zoom * (get_random_integer(85, 115) as f32 / 100.0);
}
min_iterations = 500;
max_iterations = 900;
}
_ => {}
}
let p_idx = get_random_integer(0, (SHARED_NEON_PALETTES.len() - 1) as u64) as usize;
let color_palette = SHARED_NEON_PALETTES[p_idx];
let scan_iterations = get_random_integer(min_iterations, max_iterations) as u32;
let angle_degrees = get_random_integer(0, 359) as f32;
let radians = angle_degrees.to_radians();
Self {
fractal_type,
center_re,
center_im,
min_iterations,
max_iterations,
scan_iterations,
color_palette,
zoom,
cos_angle: radians.cos(),
sin_angle: radians.sin(),
zoom_mode,
}
}
pub fn optimize_fit(&mut self, width: u32, height: u32) {
let w_f = width as f32;
let h_f = height as f32;
let min_dim = w_f.min(h_f);
let c_abs = (self.center_re * self.center_re + self.center_im * self.center_im).sqrt();
let r_bound = (1.0 + (1.0 + 4.0 * c_abs).sqrt()) / 2.0;
let search_limit = r_bound * 1.2;
let steps = 40; let inv_steps_minus_1 = 1.0 / (steps - 1) as f32;
let range = 2.0 * search_limit;
let scan_iterations = self.scan_iterations;
let mut active_points = Vec::with_capacity(steps * steps / 2);
for step_y in 0..steps {
let ry = -search_limit + (step_y as f32 * inv_steps_minus_1) * range;
for step_x in 0..steps {
let rx = -search_limit + (step_x as f32 * inv_steps_minus_1) * range;
let (mut z_re, mut z_im, c_re, c_im) = match self.fractal_type {
ProceduralEffect::JuliaSet => (rx, ry, self.center_re, self.center_im),
ProceduralEffect::Mandelbrot => (0.0, 0.0, rx, ry),
ProceduralEffect::None
| ProceduralEffect::Starfield
| ProceduralEffect::CosmicAurora
| ProceduralEffect::Fractal
| ProceduralEffect::Random => (0.0, 0.0, 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;
}
iterate_fractal(self.fractal_type, &mut z_re, &mut z_im, c_re, c_im);
i += 1;
}
if i > 3 && i < scan_iterations {
active_points.push((rx, ry));
}
}
}
if !active_points.is_empty() {
let mut best_zoom = f32::MAX;
let mut best_cos = self.cos_angle;
let mut best_sin = self.sin_angle;
for angle_step in 0..8 {
let angle_deg = (angle_step * 45) as f32;
let rad = angle_deg.to_radians();
let cos_t = rad.cos();
let sin_t = rad.sin();
let mut max_cx_abs = 0.0_f32;
let mut max_cy_abs = 0.0_f32;
for &(rx, ry) in &active_points {
let cx = rx * cos_t + ry * sin_t;
let cy = -rx * sin_t + ry * cos_t;
max_cx_abs = max_cx_abs.max(cx.abs());
max_cy_abs = max_cy_abs.max(cy.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 = cos_t;
best_sin = sin_t;
}
}
self.zoom = best_zoom * 1.10; self.cos_angle = best_cos;
self.sin_angle = best_sin;
} else {
self.zoom = 2.0 * r_bound * 1.10;
}
}
pub fn apply_effect_in_memory(&mut self, rgb_img: &mut RgbImage) {
let (width, height) = rgb_img.dimensions();
if self.zoom_mode == ZoomMode::Full {
self.optimize_fit(width, height);
}
let w_f = width as f32;
let h_f = height as f32;
let min_dim = w_f.min(h_f);
let scale = match self.zoom_mode {
ZoomMode::Full => self.zoom / min_dim,
ZoomMode::Detail => (3.0 / self.zoom) / min_dim,
};
let cos_angle = self.cos_angle;
let sin_angle = self.sin_angle;
let center_re = self.center_re;
let center_im = self.center_im;
let scan_iterations = self.scan_iterations;
let color_palette = self.color_palette;
let fractal_type = self.fractal_type;
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 cx_off = w_f / 2.0;
let cy_off = h_f / 2.0;
let dx_re = scale * cos_angle;
let dx_im = scale * sin_angle;
let dy_re = -scale * sin_angle;
let dy_im = scale * cos_angle;
let (v_center_re, v_center_im) = match self.zoom_mode {
ZoomMode::Full => (0.0, 0.0),
ZoomMode::Detail => (center_re, 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;
let inv_half_w = 2.0 / w_f;
let inv_half_h = 2.0 / h_f;
thread::scope(|scope| {
for chunk in rows.chunks_mut(chunk_size) {
scope.spawn(|| {
for (y, row_data) in chunk.iter_mut() {
let y_f = *y as f32;
let rx_row = start_re + y_f * dy_re;
let ry_row = start_im + y_f * dy_im;
let dy_vignette = y_f * inv_half_h - 1.0;
let dy_vignette_sq = dy_vignette * dy_vignette;
for x in 0..width_usize {
let x_f = x as f32;
let rx = rx_row + x_f * dx_re;
let ry = ry_row + x_f * dx_im;
let (mut z_re, mut z_im, c_re, c_im) = match fractal_type {
ProceduralEffect::JuliaSet => (rx, ry, center_re, center_im),
ProceduralEffect::Mandelbrot => (0.0, 0.0, rx, ry),
ProceduralEffect::None
| ProceduralEffect::Starfield
| ProceduralEffect::CosmicAurora
| ProceduralEffect::Fractal
| ProceduralEffect::Random => (0.0, 0.0, 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;
}
iterate_fractal(fractal_type, &mut z_re, &mut z_im, c_re, c_im);
i += 1;
}
let t = calculate_smooth_potential(i, scan_iterations, z_re, z_im);
let idx = x * 3;
let dx_vignette = x_f * inv_half_w - 1.0;
blend_and_vignette_pixel(
row_data,
idx,
t,
color_palette,
dx_vignette,
dy_vignette_sq,
);
}
}
});
}
});
}
pub fn apply_effect<P: AsRef<Path>>(
&mut 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(())
}
}
pub struct Star {
pub x: f32,
pub y: f32,
pub radius: f32,
pub color: [f32; 3],
pub intensity: f32,
}
pub struct StarfieldGenerator {
pub stars: Vec<Star>,
}
impl StarfieldGenerator {
pub fn new(count: usize, width: u32, height: u32) -> Self {
let mut stars = Vec::with_capacity(count);
let palettes = [
[1.0, 1.0, 1.0], [0.6, 0.8, 1.0], [1.0, 0.8, 0.4], [1.0, 0.4, 0.8], ];
for _ in 0..count {
let x = get_random_integer(0, width as u64) as f32;
let y = get_random_integer(0, height as u64) as f32;
let radius = get_random_integer(5, 45) as f32;
let intensity = get_random_integer(30, 95) as f32 / 100.0;
let p_idx = get_random_integer(0, (palettes.len() - 1) as u64) as usize;
let color = palettes[p_idx];
stars.push(Star {
x,
y,
radius,
color,
intensity,
});
}
Self { stars }
}
pub fn apply_effect_in_memory(&self, rgb_img: &mut RgbImage) {
let contrast_color = [0.64, 0.75, 0.85];
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);
thread::scope(|scope| {
for chunk in rows.chunks_mut(chunk_size) {
let stars = &self.stars;
scope.spawn(move || {
for (y, row_data) in chunk.iter_mut() {
let y_f = *y as f32;
let mut active_stars = Vec::with_capacity(16);
for star in stars {
let dy = star.y - y_f;
let limit = star.radius * 2.0;
if dy.abs() < limit {
let dy_sq = dy * dy;
let star_radius_sq = star.radius * star.radius;
active_stars.push((star, dy_sq, star_radius_sq));
}
}
for x in 0..width_usize {
let x_f = x as f32;
let mut r_contrib = 0.0;
let mut g_contrib = 0.0;
let mut b_contrib = 0.0;
let mut total_alpha = 0.0;
for &(star, dy_sq, star_radius_sq) in &active_stars {
let dx = star.x - x_f;
let dist_sq = dx * dx + dy_sq;
if dist_sq < star_radius_sq * 4.0 {
let factor = (-dist_sq / (2.0 * star_radius_sq)).exp();
let alpha = factor * star.intensity;
let r_star =
(star.color[0] * 0.25 + contrast_color[0] * 0.75) * alpha;
let g_star =
(star.color[1] * 0.25 + contrast_color[1] * 0.75) * alpha;
let b_star =
(star.color[2] * 0.25 + contrast_color[2] * 0.75) * alpha;
r_contrib += r_star;
g_contrib += g_star;
b_contrib += b_star;
total_alpha += alpha;
}
}
if total_alpha > 0.001 {
let idx = x * 3;
let original_r = row_data[idx] as f32;
let original_g = row_data[idx + 1] as f32;
let original_b = row_data[idx + 2] as f32;
let alpha_clamp = total_alpha.min(0.95);
let blended_r = (original_r * (1.0 - alpha_clamp))
+ (r_contrib * 255.0 / total_alpha * alpha_clamp);
let blended_g = (original_g * (1.0 - alpha_clamp))
+ (g_contrib * 255.0 / total_alpha * alpha_clamp);
let blended_b = (original_b * (1.0 - alpha_clamp))
+ (b_contrib * 255.0 / total_alpha * alpha_clamp);
row_data[idx] = blended_r.clamp(0.0, 255.0) as u8;
row_data[idx + 1] = blended_g.clamp(0.0, 255.0) as u8;
row_data[idx + 2] = blended_b.clamp(0.0, 255.0) as u8;
}
}
}
});
}
});
}
}
pub struct AuroraGenerator {
pub color_palette: [f32; 3],
pub density: f32,
}
impl AuroraGenerator {
pub fn random() -> Self {
let palettes = [
[0.2, 1.0, 0.5], [0.6, 0.0, 1.0], [0.0, 0.8, 1.0], [1.0, 0.0, 0.6], ];
let idx = get_random_integer(0, (palettes.len() - 1) as u64) as usize;
let density = get_random_integer(4, 8) as f32;
Self {
color_palette: palettes[idx],
density,
}
}
pub fn apply_effect_in_memory(&self, rgb_img: &mut RgbImage) {
let (width, height) = rgb_img.dimensions();
let w_f = width as f32;
let h_f = height as f32;
let contrast_color = self.color_palette;
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 inv_w = 1.0 / w_f;
let inv_h = 1.0 / h_f;
let density_u = self.density * 1.5 * inv_w;
let density_v_coeff = self.density * 2.0;
let density_w_coeff = self.density * inv_w;
let density_w4_coeff = self.density * 1.2;
thread::scope(|scope| {
for chunk in rows.chunks_mut(chunk_size) {
scope.spawn(move || {
for (y, row_data) in chunk.iter_mut() {
let y_f = *y as f32;
let v = y_f * inv_h;
let w2 = (v * density_v_coeff).cos();
let v_density = v * self.density;
let v_sq = v * v;
for x in 0..width_usize {
let x_f = x as f32;
let u = x_f * inv_w;
let w1 = (x_f * density_u).sin();
let w3 = (x_f * density_w_coeff + v_density).sin();
let w4 = ((u * u + v_sq).sqrt() * density_w4_coeff).cos();
let val = (w1 + w2 + w3 + w4) * 0.25;
let intensity = (val * std::f32::consts::PI).cos().abs().powf(3.0);
let dx = (u - 0.5) * 2.0;
let dy = (v - 0.5) * 2.0;
let edge_fade =
(1.0 - (dx * dx + dy * dy).sqrt() * 0.5).clamp(0.0, 1.0);
let alpha = intensity * edge_fade * 0.75;
if alpha > 0.01 {
let idx = x * 3;
let original_r = row_data[idx] as f32;
let original_g = row_data[idx + 1] as f32;
let original_b = row_data[idx + 2] as f32;
let r_aurora = contrast_color[0] * 255.0;
let g_aurora = contrast_color[1] * 255.0;
let b_aurora = contrast_color[2] * 255.0;
row_data[idx] =
((original_r * (1.0 - alpha)) + (r_aurora * alpha)) as u8;
row_data[idx + 1] =
((original_g * (1.0 - alpha)) + (g_aurora * alpha)) as u8;
row_data[idx + 2] =
((original_b * (1.0 - alpha)) + (b_aurora * alpha)) as u8;
}
}
}
});
}
});
}
}