#![allow(dead_code)]
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct DustOverlayConfig {
pub particle_count: usize,
pub radius_min: f32,
pub radius_max: f32,
pub drift_x: f32,
pub drift_y: f32,
pub opacity: f32,
pub tint: [f32; 3],
pub enabled: bool,
}
impl Default for DustOverlayConfig {
fn default() -> Self {
Self {
particle_count: 80,
radius_min: 0.001,
radius_max: 0.006,
drift_x: 0.0005,
drift_y: -0.0002,
opacity: 0.4,
tint: [0.9, 0.85, 0.75],
enabled: true,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DustParticle {
pub pos: [f32; 2],
pub radius: f32,
pub alpha: f32,
}
fn pcg(state: &mut u64) -> f32 {
*state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let x = ((*state >> 18) ^ *state) >> 27;
let rot = (*state >> 59) as u32;
let v = (x as u32).rotate_right(rot);
(v >> 8) as f32 / 16_777_216.0
}
#[allow(dead_code)]
pub fn generate_dust_particles(seed: u64, cfg: &DustOverlayConfig) -> Vec<DustParticle> {
let mut rng = seed ^ 0xDA57_CAFE_BABE_0001;
let r_range = cfg.radius_max - cfg.radius_min;
(0..cfg.particle_count)
.map(|_| {
let x = pcg(&mut rng);
let y = pcg(&mut rng);
let radius = cfg.radius_min + pcg(&mut rng) * r_range;
let alpha = (pcg(&mut rng) * 0.6 + 0.2) * cfg.opacity;
DustParticle {
pos: [x, y],
radius,
alpha,
}
})
.collect()
}
#[allow(dead_code)]
pub fn new_dust_overlay_config() -> DustOverlayConfig {
DustOverlayConfig::default()
}
#[allow(dead_code)]
pub fn do_set_opacity(cfg: &mut DustOverlayConfig, v: f32) {
cfg.opacity = v.clamp(0.0, 1.0);
}
#[allow(dead_code)]
pub fn do_set_tint(cfg: &mut DustOverlayConfig, r: f32, g: f32, b: f32) {
cfg.tint = [r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)];
}
#[allow(dead_code)]
pub fn do_step(particles: &mut [DustParticle], cfg: &DustOverlayConfig) {
for p in particles.iter_mut() {
p.pos[0] = (p.pos[0] + cfg.drift_x).rem_euclid(1.0);
p.pos[1] = (p.pos[1] + cfg.drift_y).rem_euclid(1.0);
}
}
#[allow(dead_code)]
pub fn do_average_alpha(particles: &[DustParticle]) -> f32 {
if particles.is_empty() {
return 0.0;
}
particles.iter().map(|p| p.alpha).sum::<f32>() / particles.len() as f32
}
#[allow(dead_code)]
pub fn do_to_json(cfg: &DustOverlayConfig) -> String {
format!(
"{{\"particle_count\":{},\"opacity\":{:.3},\"enabled\":{}}}",
cfg.particle_count, cfg.opacity, cfg.enabled
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_correct_count() {
let cfg = new_dust_overlay_config();
let p = generate_dust_particles(1, &cfg);
assert_eq!(p.len(), cfg.particle_count);
}
#[test]
fn deterministic() {
let cfg = new_dust_overlay_config();
let a = generate_dust_particles(9, &cfg);
let b = generate_dust_particles(9, &cfg);
assert_eq!(a[0].pos[0].to_bits(), b[0].pos[0].to_bits());
}
#[test]
fn positions_in_range() {
let cfg = new_dust_overlay_config();
let p = generate_dust_particles(2, &cfg);
for dp in &p {
assert!((0.0..=1.0).contains(&dp.pos[0]));
assert!((0.0..=1.0).contains(&dp.pos[1]));
}
}
#[test]
fn radius_in_range() {
let cfg = new_dust_overlay_config();
let p = generate_dust_particles(3, &cfg);
for dp in &p {
assert!(dp.radius >= cfg.radius_min && dp.radius <= cfg.radius_max + 1e-5);
}
}
#[test]
fn step_moves_particles() {
let cfg = new_dust_overlay_config();
let mut p = generate_dust_particles(4, &cfg);
let x0 = p[0].pos[0];
do_step(&mut p, &cfg);
let x1 = p[0].pos[0];
let _ = x0;
let _ = x1;
}
#[test]
fn step_wraps() {
let cfg = new_dust_overlay_config();
let mut p = vec![DustParticle {
pos: [0.9999, 0.5],
radius: 0.001,
alpha: 0.5,
}];
do_step(&mut p, &cfg);
assert!((0.0..=1.0).contains(&p[0].pos[0]));
}
#[test]
fn opacity_clamps() {
let mut cfg = new_dust_overlay_config();
do_set_opacity(&mut cfg, -1.0);
assert!(cfg.opacity < 1e-6);
}
#[test]
fn tint_clamps() {
let mut cfg = new_dust_overlay_config();
do_set_tint(&mut cfg, 2.0, -1.0, 0.5);
assert!((cfg.tint[0] - 1.0).abs() < 1e-6);
assert!(cfg.tint[1] < 1e-6);
}
#[test]
fn empty_average_alpha_zero() {
assert!(do_average_alpha(&[]) < 1e-8);
}
#[test]
fn json_has_keys() {
let j = do_to_json(&new_dust_overlay_config());
assert!(j.contains("particle_count") && j.contains("enabled"));
}
}