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 = "fractal")]
JuliaFractal,
#[value(name = "star")]
Starfield,
#[value(name = "aurora")]
CosmicAurora,
#[value(name = "random")]
Random,
}
impl ProceduralEffect {
pub fn resolve(self) -> Self {
match self {
Self::Random => match get_random_integer(0, 2) {
0 => Self::JuliaFractal,
1 => Self::Starfield,
_ => Self::CosmicAurora,
},
concrete => concrete,
}
}
}
pub struct FractalGenerator {
pub c_re: f32,
pub c_im: f32,
pub max_iterations: u32,
pub color_palette: [f32; 3],
pub zoom: f32,
pub cos_angle: f32,
pub sin_angle: f32,
}
impl Default for FractalGenerator {
fn default() -> Self {
Self {
c_re: -0.7,
c_im: 0.27015,
max_iterations: 255,
color_palette: [0.0, 1.0, 1.0], zoom: 3.0,
cos_angle: 1.0, sin_angle: 0.0,
}
}
}
impl FractalGenerator {
pub fn new(
c_re: f32,
c_im: f32,
max_iterations: u32,
color_palette: [f32; 3],
zoom: f32,
angle_degrees: f32,
) -> Self {
let radians = angle_degrees.to_radians();
Self {
c_re,
c_im,
max_iterations,
color_palette,
zoom,
cos_angle: radians.cos(),
sin_angle: radians.sin(),
}
}
pub fn random() -> Self {
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 palettes = [
[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], ];
let c_idx = get_random_integer(0, (presets.len() - 1) as u64) as usize;
let p_idx = get_random_integer(0, (palettes.len() - 1) as u64) as usize;
let zoom = get_random_integer(250, 400) as f32 / 100.0;
let angle_degrees = get_random_integer(0, 359) as f32;
let radians = angle_degrees.to_radians();
let (c_re, c_im) = presets[c_idx];
let color_palette = palettes[p_idx];
Self {
c_re,
c_im,
max_iterations: 255,
color_palette,
zoom,
cos_angle: radians.cos(),
sin_angle: radians.sin(),
}
}
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.c_re * self.c_re + self.c_im * self.c_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.max_iterations.min(60);
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 = rx;
let mut z_im = ry;
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;
}
z_im = 2.0 * z_re * z_im + self.c_im;
z_re = re2 - im2 + self.c_re;
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();
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 = self.zoom / min_dim;
let cos_angle = self.cos_angle;
let sin_angle = self.sin_angle;
let c_re = self.c_re;
let c_im = self.c_im;
let max_iterations = self.max_iterations;
let color_palette = 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 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 start_re = -cx_off * dx_re - cy_off * dy_re;
let start_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;
let inv_ln_2 = std::f32::consts::LOG2_E;
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 = rx;
let mut z_im = ry;
let mut i = 0;
while i < max_iterations {
let re2 = z_re * z_re;
let im2 = z_im * z_im;
if re2 + im2 > 4.0 {
break;
}
z_im = 2.0 * z_re * z_im + c_im;
z_re = re2 - im2 + c_re;
i += 1;
}
let t = 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 * inv_ln_2).ln() * inv_ln_2;
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
};
let idx = x * 3;
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 = (background_r * (1.0 - alpha)) + (r_fractal * alpha);
let blended_g = (background_g * (1.0 - alpha)) + (g_fractal * alpha);
let blended_b = (background_b * (1.0 - alpha)) + (b_fractal * alpha);
let dx_vignette = x_f * inv_half_w - 1.0;
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 * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 1] = (blended_g * vignette).clamp(0.0, 255.0) as u8;
row_data[idx + 2] = (blended_b * vignette).clamp(0.0, 255.0) as u8;
}
}
});
}
});
}
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(())
}
}
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)
}
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;
}
}
}
});
}
});
}
}