use crate::effects::{ImageEffect, partition_rows};
use crate::{ColorRGB, NEON_PALETTES, NeonColor, get_random_integer};
use image::RgbImage;
use std::thread;
pub const STAR_RANGE: [usize; 2] = [60, 120];
impl ImageEffect for StarfieldGenerator {
fn apply(&self, rgb_img: &mut RgbImage) {
let contrast_color = [0.64, 0.75, 0.85];
let (mut rows, _) = 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, pixel_slice) in row_data.chunks_exact_mut(3).enumerate() {
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 {
if let Some((r, g, b, alpha)) = Self::calculate_star_influence(
star,
x_f,
dy_sq,
star_radius_sq,
contrast_color,
) {
r_contrib += r;
g_contrib += g;
b_contrib += b;
total_alpha += alpha;
}
}
if total_alpha > 0.001 {
let mut pixel_color = ColorRGB::from_slice(pixel_slice);
let alpha_clamp = total_alpha.min(0.95);
let fg_r = r_contrib / total_alpha;
let fg_g = g_contrib / total_alpha;
let fg_b = b_contrib / total_alpha;
let bg_linear = pixel_color.squared();
let fg_linear = ColorRGB::new(fg_r, fg_g, fg_b).squared();
let blended_linear = fg_linear.lerp(&bg_linear, alpha_clamp);
pixel_color = blended_linear.sqrt();
pixel_color.write_to_slice(pixel_slice);
}
}
}
});
}
});
}
fn info(&self) -> String {
format!("overlay ({} stars)", self.stars.len())
}
}
pub struct Star {
pub x: f32,
pub y: f32,
pub radius: f32,
pub intensity: f32,
pub color_palette: NeonColor,
}
pub struct StarfieldGenerator {
pub stars: Vec<Star>,
}
impl StarfieldGenerator {
pub fn random(monitor: &crate::Monitor) -> Self {
let width = monitor.resolution.width as u32;
let height = monitor.resolution.height as u32;
let count = get_random_integer(STAR_RANGE[0], STAR_RANGE[1]);
Self::new(count, width, height)
}
pub fn new(count: usize, width: u32, height: u32) -> Self {
let mut stars = Vec::with_capacity(count);
for _ in 0..count {
let x: f32 = get_random_integer(0, width);
let y: f32 = get_random_integer(0, height);
let radius: f32 = get_random_integer(5, 45);
let intensity = get_random_integer::<_, f32>(30, 95) / 100.0;
let p_idx: usize = get_random_integer(0, NEON_PALETTES.len() - 1);
let color_palette = NEON_PALETTES[p_idx];
stars.push(Star {
x,
y,
radius,
intensity,
color_palette,
});
}
Self { stars }
}
#[inline(always)]
fn calculate_star_influence(
star: &Star,
x_f: f32,
dy_sq: f32,
star_radius_sq: f32,
contrast_color: [f32; 3],
) -> Option<(f32, f32, f32, f32)> {
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 star_rgb = star.color_palette.to_array();
let r_star = (star_rgb[0] * 0.25 + contrast_color[0] * 0.75) * alpha;
let g_star = (star_rgb[1] * 0.25 + contrast_color[1] * 0.75) * alpha;
let b_star = (star_rgb[2] * 0.25 + contrast_color[2] * 0.75) * alpha;
Some((r_star, g_star, b_star, alpha))
} else {
None
}
}
}
#[cfg(test)]
mod tests_star {
use super::*;
use crate::core::Monitor;
#[test]
fn test_star_generation() {
let monitor = Monitor::default();
let starfield = StarfieldGenerator::random(&monitor);
assert!(!starfield.stars.is_empty());
}
}