use crate::Vec3;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use std::f32::consts::{PI, TAU};
pub struct SpawnContext {
pub index: u32,
pub count: u32,
pub bounds: f32,
rng: SmallRng,
}
impl SpawnContext {
pub(crate) fn new(index: u32, count: u32, bounds: f32) -> Self {
let seed = index as u64 ^ (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(42));
Self {
index,
count,
bounds,
rng: SmallRng::seed_from_u64(seed),
}
}
#[inline]
pub fn progress(&self) -> f32 {
self.index as f32 / self.count as f32
}
#[inline]
pub fn random(&mut self) -> f32 {
self.rng.gen()
}
#[inline]
pub fn random_range(&mut self, min: f32, max: f32) -> f32 {
self.rng.gen_range(min..max)
}
#[inline]
pub fn random_int(&mut self, min: i32, max: i32) -> i32 {
self.rng.gen_range(min..max)
}
#[inline]
pub fn random_uint(&mut self, min: u32, max: u32) -> u32 {
self.rng.gen_range(min..max)
}
pub fn random_in_sphere(&mut self, radius: f32) -> Vec3 {
let theta = self.rng.gen_range(0.0..TAU);
let phi = self.rng.gen_range(0.0..PI);
let r = radius * self.rng.gen::<f32>().cbrt();
Vec3::new(
r * phi.sin() * theta.cos(),
r * phi.sin() * theta.sin(),
r * phi.cos(),
)
}
pub fn random_on_sphere(&mut self, radius: f32) -> Vec3 {
let theta = self.rng.gen_range(0.0..TAU);
let phi = self.rng.gen_range(0.0..PI);
Vec3::new(
radius * phi.sin() * theta.cos(),
radius * phi.sin() * theta.sin(),
radius * phi.cos(),
)
}
pub fn random_in_cube(&mut self, half_size: f32) -> Vec3 {
Vec3::new(
self.rng.gen_range(-half_size..half_size),
self.rng.gen_range(-half_size..half_size),
self.rng.gen_range(-half_size..half_size),
)
}
pub fn random_in_bounds(&mut self) -> Vec3 {
self.random_in_cube(self.bounds)
}
pub fn random_in_cylinder(&mut self, radius: f32, half_height: f32) -> Vec3 {
let theta = self.rng.gen_range(0.0..TAU);
let r = radius * self.rng.gen::<f32>().sqrt();
Vec3::new(
r * theta.cos(),
self.rng.gen_range(-half_height..half_height),
r * theta.sin(),
)
}
pub fn random_in_disk(&mut self, radius: f32) -> Vec3 {
let theta = self.rng.gen_range(0.0..TAU);
let r = radius * self.rng.gen::<f32>().sqrt();
Vec3::new(r * theta.cos(), 0.0, r * theta.sin())
}
pub fn random_on_ring(&mut self, radius: f32) -> Vec3 {
let theta = self.rng.gen_range(0.0..TAU);
Vec3::new(radius * theta.cos(), 0.0, radius * theta.sin())
}
pub fn random_direction(&mut self) -> Vec3 {
self.random_on_sphere(1.0).normalize()
}
pub fn tangent_velocity(&self, position: Vec3, speed: f32) -> Vec3 {
let tangent = Vec3::new(-position.z, 0.0, position.x);
if tangent.length_squared() > 0.0001 {
tangent.normalize() * speed
} else {
Vec3::new(speed, 0.0, 0.0)
}
}
pub fn outward_velocity(&mut self, position: Vec3, speed: f32) -> Vec3 {
if position.length_squared() > 0.0001 {
position.normalize() * speed
} else {
self.random_direction() * speed
}
}
pub fn random_color(&mut self) -> Vec3 {
Vec3::new(self.rng.gen(), self.rng.gen(), self.rng.gen())
}
pub fn random_hue(&mut self, saturation: f32, value: f32) -> Vec3 {
let hue = self.rng.gen::<f32>();
hsv_to_rgb(hue, saturation, value)
}
pub fn hsv(&self, hue: f32, saturation: f32, value: f32) -> Vec3 {
hsv_to_rgb(hue, saturation, value)
}
pub fn rainbow(&mut self, saturation: f32, value: f32) -> Vec3 {
hsv_to_rgb(self.progress(), saturation, value)
}
pub fn grid_position(&self, cols: u32, rows: u32, layers: u32) -> Vec3 {
let total = cols * rows * layers;
let idx = self.index % total;
let x = idx % cols;
let y = (idx / cols) % rows;
let z = idx / (cols * rows);
let fx = (x as f32 / (cols - 1).max(1) as f32) * 2.0 - 1.0;
let fy = (y as f32 / (rows - 1).max(1) as f32) * 2.0 - 1.0;
let fz = (z as f32 / (layers - 1).max(1) as f32) * 2.0 - 1.0;
Vec3::new(fx * self.bounds, fy * self.bounds, fz * self.bounds)
}
pub fn grid_position_2d(&self, cols: u32, rows: u32) -> Vec3 {
let idx = self.index % (cols * rows);
let x = idx % cols;
let z = idx / cols;
let fx = (x as f32 / (cols - 1).max(1) as f32) * 2.0 - 1.0;
let fz = (z as f32 / (rows - 1).max(1) as f32) * 2.0 - 1.0;
Vec3::new(fx * self.bounds, 0.0, fz * self.bounds)
}
pub fn line_position(&self, start: Vec3, end: Vec3) -> Vec3 {
start + (end - start) * self.progress()
}
pub fn circle_position(&self, radius: f32) -> Vec3 {
let angle = self.progress() * TAU;
Vec3::new(radius * angle.cos(), 0.0, radius * angle.sin())
}
pub fn helix_position(&self, radius: f32, height: f32, turns: f32) -> Vec3 {
let t = self.progress();
let angle = t * TAU * turns;
Vec3::new(
radius * angle.cos(),
(t - 0.5) * height,
radius * angle.sin(),
)
}
}
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Vec3 {
let c = v * s;
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
let m = v - c;
let (r, g, b) = match (h * 6.0) as u32 % 6 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
Vec3::new(r + m, g + m, b + m)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spawn_context_progress() {
let ctx = SpawnContext::new(50, 100, 1.0);
assert!((ctx.progress() - 0.5).abs() < 0.001);
}
#[test]
fn test_random_in_sphere_bounds() {
let mut ctx = SpawnContext::new(0, 1, 1.0);
for _ in 0..100 {
let pos = ctx.random_in_sphere(0.5);
assert!(pos.length() <= 0.5 + 0.001);
}
}
#[test]
fn test_grid_position() {
let ctx = SpawnContext::new(0, 27, 1.0);
let pos = ctx.grid_position(3, 3, 3);
assert!((pos.x - (-1.0)).abs() < 0.001);
assert!((pos.y - (-1.0)).abs() < 0.001);
assert!((pos.z - (-1.0)).abs() < 0.001);
}
#[test]
fn test_hsv_to_rgb() {
let red = hsv_to_rgb(0.0, 1.0, 1.0);
assert!((red.x - 1.0).abs() < 0.001);
assert!(red.y < 0.001);
assert!(red.z < 0.001);
}
}