use egui::{Color32, Context, Id, LayerId, Order, Pos2, Rect, Vec2};
use std::ops::RangeInclusive;
use super::super::shared::{ParticleLayer, ParticleRng, ordered, resolve_target};
use super::settings::SnowSettings;
#[derive(Clone)]
struct SnowState {
flakes: Vec<Snowflake>,
rng: ParticleRng,
}
impl SnowState {
fn new(seed: u64) -> Self {
Self {
flakes: Vec::new(),
rng: ParticleRng::new(seed),
}
}
}
#[derive(Clone, Copy)]
struct Snowflake {
normalized_pos: Pos2,
fall_speed: f32,
turbulence: f32,
size: f32,
phase: f32,
}
#[derive(Debug)]
pub struct Snow {
id: Id,
color: Color32,
density: usize,
layer_order: Order,
custom_layer: Option<LayerId>,
custom_area: Option<Rect>,
size_range: RangeInclusive<f32>,
speed_range: RangeInclusive<f32>,
wind: Vec2,
}
impl Snow {
pub fn new(id_source: impl std::hash::Hash + std::fmt::Debug) -> Self {
Self {
id: Id::unique(id_source),
color: Color32::WHITE,
density: 50,
layer_order: Order::Foreground,
custom_layer: None,
custom_area: None,
size_range: 0.3..=1.5,
speed_range: 40.0..=100.0,
wind: Vec2::ZERO,
}
}
pub fn from_settings(
id_source: impl std::hash::Hash + std::fmt::Debug,
settings: &SnowSettings,
) -> Self {
let (size_min, size_max) = ordered(settings.size_min, settings.size_max);
let (speed_min, speed_max) = ordered(settings.speed_min, settings.speed_max);
let snow = Self::new(id_source)
.color(settings.color)
.density(settings.density)
.size(size_min..=size_max)
.speed(speed_min..=speed_max)
.wind(settings.wind);
match settings.layer {
ParticleLayer::Background => snow.on_background(),
ParticleLayer::Foreground => snow.on_foreground(),
ParticleLayer::Top => snow.on_top(),
}
}
pub fn color(mut self, color: Color32) -> Self {
self.color = color;
self
}
pub fn density(mut self, density: usize) -> Self {
self.density = density;
self
}
pub fn speed(mut self, range: RangeInclusive<f32>) -> Self {
self.speed_range = range;
self
}
pub fn size(mut self, range: RangeInclusive<f32>) -> Self {
self.size_range = range;
self
}
pub fn wind(mut self, wind: impl Into<Vec2>) -> Self {
self.wind = wind.into();
self
}
pub fn on_top(mut self) -> Self {
self.layer_order = Order::Debug;
self
}
pub fn on_foreground(mut self) -> Self {
self.layer_order = Order::Foreground;
self
}
pub fn on_background(mut self) -> Self {
self.layer_order = Order::Background;
self
}
pub fn layer(mut self, layer_id: LayerId) -> Self {
self.custom_layer = Some(layer_id);
self
}
pub fn area(mut self, area: Rect) -> Self {
self.custom_area = Some(area);
self
}
pub fn show(self, ctx: &Context) {
let Some((layer_id, screen_rect)) = resolve_target(
ctx,
self.id,
self.layer_order,
self.custom_layer,
self.custom_area,
) else {
return;
};
let dt = ctx.input(|i| i.stable_dt).min(0.1);
let time = ctx.input(|i| i.time);
let seed = self.id.value() ^ time.to_bits();
let (mut snowflakes, mut rng) = ctx.data_mut(|data| {
let state = data.get_temp_mut_or_insert_with(self.id, || SnowState::new(seed));
(std::mem::take(&mut state.flakes), state.rng)
});
if snowflakes.len() < self.density {
let needed = self.density - snowflakes.len();
for _ in 0..needed {
snowflakes.push(Snowflake::spawn(
&mut rng,
true,
&self.size_range,
&self.speed_range,
));
}
} else if snowflakes.len() > self.density {
snowflakes.truncate(self.density);
}
let painter = ctx.layer_painter(layer_id);
let min_size = *self.size_range.start();
let size_diff = (*self.size_range.end() - min_size).max(0.0001);
for flake in &mut snowflakes {
let pixel_dy = (flake.fall_speed + self.wind.y) * dt;
let sway = ((time as f32 + flake.phase).sin()) * 5.0;
let pixel_dx = (flake.turbulence + self.wind.x + sway) * dt;
flake.normalized_pos.x += pixel_dx / screen_rect.width();
flake.normalized_pos.y += pixel_dy / screen_rect.height();
if flake.normalized_pos.y > 1.0 {
*flake = Snowflake::spawn(
&mut rng,
false,
&self.size_range,
&self.speed_range,
);
}
else if flake.normalized_pos.y < -0.05 {
flake.normalized_pos.y = 1.0;
flake.normalized_pos.x = rng.range(0.0, 1.0);
flake.size = rng.range(*self.size_range.start(), *self.size_range.end());
flake.fall_speed = rng.range(*self.speed_range.start(), *self.speed_range.end());
}
if flake.normalized_pos.x > 1.0 {
flake.normalized_pos.x -= 1.0;
} else if flake.normalized_pos.x < 0.0 {
flake.normalized_pos.x += 1.0;
}
let pixel_pos = Pos2::new(
screen_rect.min.x + flake.normalized_pos.x * screen_rect.width(),
screen_rect.min.y + flake.normalized_pos.y * screen_rect.height(),
);
let depth_factor = (flake.size - min_size) / size_diff;
let alpha_mult = 0.4 + (0.6 * depth_factor); let final_color = self.color.gamma_multiply(alpha_mult);
painter.circle_filled(pixel_pos, flake.size, final_color);
}
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_insert_with(self.id, || SnowState::new(seed));
state.flakes = snowflakes;
state.rng = rng;
});
ctx.request_repaint();
}
}
impl Snowflake {
fn spawn(
rng: &mut ParticleRng,
random_y: bool,
size_range: &RangeInclusive<f32>,
speed_range: &RangeInclusive<f32>,
) -> Self {
Self {
normalized_pos: Pos2::new(
rng.range(0.0, 1.0),
if random_y { rng.range(0.0, 1.0) } else { -0.05 },
),
fall_speed: rng.range(*speed_range.start(), *speed_range.end()),
turbulence: rng.range(-20.0, 20.0),
size: rng.range(*size_range.start(), *size_range.end()),
phase: rng.range(0.0, std::f32::consts::PI * 2.0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::__run_test_ctx;
use egui::{Pos2, Rect, vec2};
#[test]
fn from_settings_swaps_inverted_ranges() {
let settings = SnowSettings {
size_min: 2.0,
size_max: 0.5,
speed_min: 100.0,
speed_max: 10.0,
..Default::default()
};
let snow = Snow::from_settings("inverted", &settings);
assert_eq!(*snow.size_range.start(), 0.5);
assert_eq!(*snow.size_range.end(), 2.0);
assert_eq!(*snow.speed_range.start(), 10.0);
assert_eq!(*snow.speed_range.end(), 100.0);
}
#[test]
fn from_settings_maps_layer() {
let settings = SnowSettings {
layer: ParticleLayer::Top,
color: Color32::from_rgb(1, 2, 3),
density: 12,
..Default::default()
};
let snow = Snow::from_settings("top", &settings);
assert_eq!(snow.layer_order, Order::Debug);
assert_eq!(snow.color, Color32::from_rgb(1, 2, 3));
assert_eq!(snow.density, 12);
let background = Snow::from_settings(
"background",
&SnowSettings {
layer: ParticleLayer::Background,
..Default::default()
},
);
assert_eq!(background.layer_order, Order::Background);
}
#[test]
fn show_spawns_flakes() {
__run_test_ctx(|ctx| {
let area = Rect::from_min_size(Pos2::ZERO, vec2(64.0, 64.0));
Snow::new("test_snow").density(7).area(area).show(ctx);
let state = ctx
.data(|d| d.get_temp::<SnowState>(Id::unique("test_snow")))
.expect("SnowState should exist after show");
assert_eq!(state.flakes.len(), 7);
});
}
}