use crate::effects::{
MAX_ITERATIONS, MIN_ITERATIONS, Viewport, ViewportSpecs, blend_and_vignette_pixel,
get_rotation_steps, partition_rows, rotate_point,
};
use crate::{
Complex, ImageEffect, NEON_PALETTES, NeonColor, WallSwitchError, WallSwitchResult,
get_random_integer,
};
use image::RgbImage;
use std::{io::Error, path::Path, thread};
const ZOOM_RANGE: [f64; 2] = [1.2, 3.2];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NovaPreset {
pub power: u32,
pub r: Complex,
pub c: Complex,
pub name: &'static str,
}
impl ImageEffect for NovaGenerator {
fn apply(&self, rgb_img: &mut RgbImage) {
self.apply_effect_in_memory(rgb_img);
}
fn info(&self) -> String {
format!(
"fractal [{}]\n\
f(z) = z^{} - 1 = 0, where c = {:6.3} {} {:5.3}i (iter = {:4}, zoom = {:.2}), color: {}",
self.preset.name,
self.preset.power,
self.preset.c.re,
if self.preset.c.im >= 0.0 { "+" } else { "-" },
self.preset.c.im.abs(),
self.scan_iterations,
self.zoom,
self.color_palette
)
}
}
pub struct NovaGenerator {
pub preset: NovaPreset,
pub scan_iterations: u32,
pub color_palette: NeonColor,
pub zoom: f64,
pub cos_angle: f64,
pub sin_angle: f64,
}
impl Default for NovaGenerator {
fn default() -> Self {
Self {
preset: NovaPreset {
power: 3,
r: Complex::new(1.0, 0.0),
c: Complex::new(0.1, 0.15),
name: "Liquid Mercury Flow",
},
scan_iterations: get_random_integer::<_, u32>(MIN_ITERATIONS / 12, MAX_ITERATIONS / 12)
.max(50),
color_palette: NEON_PALETTES[0],
zoom: 1.8,
cos_angle: 1.0,
sin_angle: 0.0,
}
}
}
impl NovaGenerator {
pub fn random(monitor: &crate::Monitor) -> Self {
let width = monitor.resolution.width as u32;
let height = monitor.resolution.height as u32;
let presets = [
NovaPreset {
power: 3,
r: Complex::new(1.0, 0.0),
c: Complex::new(0.10, 0.15),
name: "Liquid Mercury Flow",
},
NovaPreset {
power: 3,
r: Complex::new(1.0, 0.0),
c: Complex::new(-0.20, 0.45),
name: "Cosmic Plasma Flare",
},
NovaPreset {
power: 4,
r: Complex::new(1.0, 0.0),
c: Complex::new(0.22, 0.10),
name: "Ornate Coral Filigree",
},
NovaPreset {
power: 3,
r: Complex::new(0.9, 0.0),
c: Complex::new(-0.35, 0.25),
name: "Nebulous Dust Whispers",
},
NovaPreset {
power: 4,
r: Complex::new(1.0, 0.0),
c: Complex::new(-0.10, 0.35),
name: "Gilded Lace Tapestry",
},
NovaPreset {
power: 5,
r: Complex::new(1.0, 0.0),
c: Complex::new(-0.05, 0.55),
name: "Glacial Frost Lattice",
},
NovaPreset {
power: 3,
r: Complex::new(1.15, 0.0),
c: Complex::new(0.0, 0.12),
name: "Spiritual Mandala Ripple",
},
NovaPreset {
power: 4,
r: Complex::new(0.8, 0.0),
c: Complex::new(0.30, -0.20),
name: "Bioluminescent Spore Nest",
},
NovaPreset {
power: 3,
r: Complex::new(1.0, 0.0),
c: Complex::new(0.18, -0.40),
name: "Abyssal Trench Vines",
},
NovaPreset {
power: 6,
r: Complex::new(1.0, 0.0),
c: Complex::new(-0.15, 0.15),
name: "Hyperdimensional Loom",
},
NovaPreset {
power: 3,
r: Complex::new(1.0, 0.15),
c: Complex::new(-0.15, 0.35),
name: "Gothic Cathedral Rose",
},
NovaPreset {
power: 5,
r: Complex::new(0.85, 0.25),
c: Complex::new(0.25, 0.05),
name: "Quantum Foam Fluctuation",
},
NovaPreset {
power: 4,
r: Complex::new(1.2, -0.10),
c: Complex::new(-0.28, -0.28),
name: "Stellar Nucleosynthesis",
},
NovaPreset {
power: 6,
r: Complex::new(0.95, 0.05),
c: Complex::new(0.05, 0.42),
name: "Emerald Moss Labyrinth",
},
NovaPreset {
power: 7,
r: Complex::new(1.0, 0.0),
c: Complex::new(-0.08, 0.38),
name: "Bismuth Crystal Citadel",
},
NovaPreset {
power: 3,
r: Complex::new(0.75, -0.30),
c: Complex::new(0.32, 0.18),
name: "Astral Jellyfish Canopy",
},
NovaPreset {
power: 4,
r: Complex::new(1.1, 0.15),
c: Complex::new(-0.45, 0.10),
name: "Solar Prominence Loops",
},
NovaPreset {
power: 5,
r: Complex::new(0.9, -0.20),
c: Complex::new(-0.12, -0.32),
name: "Aetheric Ley Line Matrix",
},
NovaPreset {
power: 8,
r: Complex::new(1.05, 0.10),
c: Complex::new(0.15, 0.15),
name: "Phytoplankton Radiance",
},
NovaPreset {
power: 6,
r: Complex::new(0.8, 0.40),
c: Complex::new(-0.22, 0.22),
name: "Chronos Vortex Gear",
},
NovaPreset {
power: 5,
r: Complex::new(1.0, 0.3),
c: Complex::new(-0.18, 0.12),
name: "Aeon Temple Portico",
},
NovaPreset {
power: 8,
r: Complex::new(0.9, -0.15),
c: Complex::new(0.20, 0.35),
name: "Hyperborean Crown",
},
NovaPreset {
power: 3,
r: Complex::new(1.1, 0.45),
c: Complex::new(-0.33, -0.05),
name: "Abyssal Nautilus Shell",
},
NovaPreset {
power: 4,
r: Complex::new(0.7, 0.5),
c: Complex::new(0.15, -0.55),
name: "Spectral Dragon Spine",
},
NovaPreset {
power: 3,
r: Complex::new(1.3, -0.2),
c: Complex::new(0.25, 0.25),
name: "Opalescent Silk Ribbons",
},
NovaPreset {
power: 5,
r: Complex::new(1.0, -0.4),
c: Complex::new(-0.42, 0.18),
name: "Phoenix Heart Nebula",
},
NovaPreset {
power: 7,
r: Complex::new(0.85, 0.1),
c: Complex::new(0.30, -0.30),
name: "Crystalline Geode Valley",
},
NovaPreset {
power: 6,
r: Complex::new(1.15, -0.3),
c: Complex::new(-0.02, 0.48),
name: "Eldritch Eye Lattice",
},
NovaPreset {
power: 4,
r: Complex::new(0.9, 0.35),
c: Complex::new(-0.25, 0.30),
name: "Prismatic Quantum Lattice",
},
NovaPreset {
power: 9,
r: Complex::new(1.0, 0.25),
c: Complex::new(0.08, -0.28),
name: "Void Weaver Spindle",
},
];
let p_idx: usize = get_random_integer(0, NEON_PALETTES.len() - 1);
let color_palette = NEON_PALETTES[p_idx];
let angle_degrees: f64 = get_random_integer(0, 359);
let radians = angle_degrees.to_radians();
let preset_idx: usize = get_random_integer(0, presets.len() - 1);
let selected_preset = presets[preset_idx];
let mut nova = Self {
preset: selected_preset,
scan_iterations: get_random_integer(40, 80),
color_palette,
zoom: 1.8,
cos_angle: radians.cos(),
sin_angle: radians.sin(),
};
nova.optimize_fit(width, height);
nova
}
pub fn optimize_fit(&mut self, width: u32, height: u32) {
let w_f = width as f64;
let h_f = height as f64;
let min_dim = w_f.min(h_f);
let search_limit = 1.6_f64;
let steps = 64;
let inv_steps_minus_1 = 1.0 / (steps - 1) as f64;
let range = 2.0 * search_limit;
let scan_iterations = self.scan_iterations;
let mut active_points = Vec::with_capacity(steps * steps);
for step_y in 0..steps {
let ry = -search_limit + (step_y as f64 * inv_steps_minus_1) * range;
for step_x in 0..steps {
let rx = -search_limit + (step_x as f64 * inv_steps_minus_1) * range;
let (i, _, _) = compute_nova_escape(
rx,
ry,
self.preset.power,
self.preset.r,
self.preset.c,
scan_iterations,
);
if i > 6 && i < scan_iterations - 2 {
active_points.push((rx, ry));
}
}
}
if !active_points.is_empty() {
let mut best_zoom = f64::MAX;
let mut best_cos = self.cos_angle;
let mut best_sin = self.sin_angle;
for (_rad, cos_t, sin_t) in get_rotation_steps() {
let mut max_cx_abs = 0.0_f64;
let mut max_cy_abs = 0.0_f64;
for &(rx, ry) in &active_points {
let (cx, cy) = rotate_point(rx, ry, cos_t, sin_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;
}
}
let rand_factor = get_random_integer::<_, f64>(90, 135) / 100.0;
self.zoom = (best_zoom * rand_factor).clamp(ZOOM_RANGE[0], ZOOM_RANGE[1]);
self.cos_angle = best_cos;
self.sin_angle = best_sin;
} else {
let flat_rand = get_random_integer::<_, f64>(130, 280) / 100.0;
self.zoom = flat_rand.clamp(ZOOM_RANGE[0], ZOOM_RANGE[1]);
}
}
pub fn apply_effect_in_memory(&self, rgb_img: &mut RgbImage) {
let (width, height) = rgb_img.dimensions();
let w_f = width as f64;
let h_f = height as f64;
let specs = ViewportSpecs {
center: Complex::new(0.0, 0.0),
zoom: self.zoom,
cos_angle: self.cos_angle,
sin_angle: self.sin_angle,
is_julia: true,
};
let viewport = Viewport::new(w_f, h_f, &specs);
let scan_iterations = self.scan_iterations;
let power = self.preset.power;
let r = self.preset.r;
let c = self.preset.c;
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| {
let viewport_ref = &viewport;
let color_palette = self.color_palette.to_array();
for chunk in rows.chunks_mut(chunk_size) {
scope.spawn(move || {
for (y, row_data) in chunk.iter_mut() {
let y_f = *y as f64;
for x in 0..width_usize {
let x_f = x as f64;
let z_init = viewport_ref.map(x_f, y_f);
let (i, diff_norm, z_final) = compute_nova_escape(
z_init.re,
z_init.im,
power,
r,
c,
scan_iterations,
);
let smooth_i = if i < scan_iterations {
i as f32
+ (diff_norm.ln() as f32 / (1e-5_f64).ln() as f32)
.clamp(0.0, 1.0)
} else {
scan_iterations as f32
};
let (fractal_rgb, alpha, s_alpha) = if i < scan_iterations {
let rad_distance =
(z_init.re * z_init.re + z_init.im * z_init.im).sqrt() as f32;
let edge_fade =
(1.0 - (rad_distance / 1.8)).clamp(0.0, 1.0).powf(1.2);
let ripple_frequency = 0.50_f32;
let raw_wave = (smooth_i * ripple_frequency * std::f32::consts::PI)
.sin()
.abs();
let norm_dist = raw_wave.powf(2.5);
let core = if norm_dist > 0.92 {
(norm_dist - 0.92) / 0.08
} else {
0.0
};
let glow = norm_dist.powi(6) * 0.52;
let profile = core * 0.78 + glow * 0.22;
let shadow_profile = (1.0 - norm_dist).powi(3) * 0.48;
let angle = z_final.im.atan2(z_final.re) as f32;
let light = 0.75_f32 + 0.25_f32 * (angle * 4.0).cos().abs();
let t_cycled = (smooth_i * 0.08) % 1.0;
let secondary_color =
[color_palette[1], color_palette[2], color_palette[0]];
let t_cos = (t_cycled * std::f32::consts::PI).cos() * 0.5 + 0.5;
let r_grad =
color_palette[0] * t_cos + secondary_color[0] * (1.0 - t_cos);
let g_grad =
color_palette[1] * t_cos + secondary_color[1] * (1.0 - t_cos);
let b_grad =
color_palette[2] * t_cos + secondary_color[2] * (1.0 - t_cos);
let core_color = [r_grad, g_grad, b_grad];
let mut border_color = [1.0 - r_grad, 1.0 - g_grad, 1.0 - b_grad];
let max_val =
border_color[0].max(border_color[1]).max(border_color[2]);
if max_val > 0.0 {
border_color[0] /= max_val;
border_color[1] /= max_val;
border_color[2] /= max_val;
}
let color_blend = norm_dist.powf(3.0);
let r_blended = core_color[0] * color_blend
+ border_color[0] * (1.0 - color_blend);
let g_blended = core_color[1] * color_blend
+ border_color[1] * (1.0 - color_blend);
let b_blended = core_color[2] * color_blend
+ border_color[2] * (1.0 - color_blend);
let brightness_boost = 1.45_f32;
let rgb = [
(r_blended * light * brightness_boost).clamp(0.0, 1.0),
(g_blended * light * brightness_boost).clamp(0.0, 1.0),
(b_blended * light * brightness_boost).clamp(0.0, 1.0),
];
let iteration_fade = if i < 6 { i as f32 / 6.0 } else { 1.0 };
(
rgb,
profile * 0.95 * iteration_fade * edge_fade,
shadow_profile * iteration_fade * edge_fade,
)
} else {
([0.0, 0.0, 0.0], 0.0, 0.0)
};
let idx = x * 3;
blend_and_vignette_pixel(row_data, idx, fractal_rgb, alpha, s_alpha);
}
}
});
}
});
}
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(())
}
}
#[inline(always)]
fn compute_nova_escape(
rx: f64,
ry: f64,
power: u32,
r: Complex,
c: Complex,
scan_iterations: u32,
) -> (u32, f64, Complex) {
let mut z = Complex::new(rx, ry);
let p_f = power as f64;
let mut i = 0;
let mut diff_norm = 1.0;
while i < scan_iterations {
let z_norm_sq = z.norm_sq();
if z_norm_sq < 1e-6 {
break;
}
if z_norm_sq > 100.0 {
break;
}
let z_prev_p_minus_1 = z.pow(power - 1);
let z_p = z_prev_p_minus_1 * z;
let f_z = z_p - Complex::new(1.0, 0.0);
let f_prime_z = z_prev_p_minus_1 * p_f;
let step = r * (f_z / f_prime_z);
let z_next = z - step + c;
let diff = z_next - z;
diff_norm = diff.norm_sq();
if diff_norm < 1e-5 {
z = z_next;
break;
}
z = z_next;
i += 1;
}
(i, diff_norm, z)
}