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 = "random")]
Random,
}
impl ProceduralEffect {
pub fn resolve(self) -> Self {
match self {
Self::Random => match get_random_integer(0, 1) {
0 => Self::JuliaFractal,
_ => Self::Starfield,
},
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.285, 0.535), (-0.835, -0.2321), (-0.77269, 0.12428), (-0.08, 0.72), (-0.51251, 0.5213), (0.4, 0.4), (-0.55, 0.55), (0.26, 0.0), (-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.5, 0.56), (-0.76, 0.08), (-0.48, 0.53), (-0.72, 0.22), (-0.15, 0.75), ];
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 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 min_dim = w_f.min(h_f);
let scale = self.zoom / min_dim;
let width_usize = width as usize;
let row_stride = width_usize * 3;
let pixels_buffer = rgb_img.as_mut();
let mut rows: Vec<(usize, &mut [u8])> = pixels_buffer
.chunks_exact_mut(row_stride)
.enumerate()
.collect();
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) {
scope.spawn(move || {
for (y, row_data) in chunk.iter_mut() {
let y_f = *y as f32;
for x in 0..width_usize {
let x_f = x as f32;
let cx = (x_f - w_f / 2.0) * scale;
let cy = (y_f - h_f / 2.0) * scale;
let rx = cx * self.cos_angle - cy * self.sin_angle;
let ry = cx * self.sin_angle + cy * self.cos_angle;
let mut z_re = rx;
let mut z_im = ry;
let mut i = 0;
while i < self.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 + self.c_im;
z_re = re2 - im2 + self.c_re;
i += 1;
}
let t = if i < self.max_iterations {
let mag2 = z_re * z_re + z_im * z_im;
if mag2 > 4.0 {
let log_zn = mag2.ln() / 2.0;
let nu = (log_zn / 2.0_f32.ln()).ln() / 2.0_f32.ln();
let smooth_i = (i as f32 + 1.0 - nu).max(0.0);
(smooth_i / self.max_iterations as f32).clamp(0.0, 1.0)
} else {
i as f32 / self.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 = self.color_palette[0] * t * 255.0;
let g_fractal = self.color_palette[1] * t * 255.0;
let b_fractal = self.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 = (x_f - w_f / 2.0) / (w_f / 2.0);
let dy = (y_f - h_f / 2.0) / (h_f / 2.0);
let dist = (dx * dx + dy * dy).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>>(
&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 (width, _height) = rgb_img.dimensions();
let width_usize = width as usize;
let row_stride = width_usize * 3;
let pixels_buffer = rgb_img.as_mut();
let mut rows: Vec<(usize, &mut [u8])> = pixels_buffer
.chunks_exact_mut(row_stride)
.enumerate()
.collect();
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;
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 in stars {
let dx = star.x - x_f;
let dy = star.y - y_f;
let dist_sq = dx * dx + dy * dy;
let star_radius_sq = star.radius * star.radius;
if dist_sq < star_radius_sq * 4.0 {
let factor = (-dist_sq / (2.0 * star_radius_sq)).exp();
let alpha = factor * star.intensity;
r_contrib += star.color[0] * alpha;
g_contrib += star.color[1] * alpha;
b_contrib += star.color[2] * alpha;
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;
}
}
}
});
}
});
}
}