egui_elements/widgets/particles/snow/
effect.rs1use egui::{Color32, Context, Id, LayerId, Order, Pos2, Rect, Vec2};
6use std::ops::RangeInclusive;
7
8use super::super::shared::{ParticleLayer, ParticleRng, ordered, resolve_target};
9use super::settings::SnowSettings;
10
11#[derive(Clone)]
12struct SnowState {
13 flakes: Vec<Snowflake>,
14 rng: ParticleRng,
15}
16
17impl SnowState {
18 fn new(seed: u64) -> Self {
19 Self {
20 flakes: Vec::new(),
21 rng: ParticleRng::new(seed),
22 }
23 }
24}
25
26#[derive(Clone, Copy)]
27struct Snowflake {
28 normalized_pos: Pos2,
30 fall_speed: f32,
32 turbulence: f32,
34 size: f32,
36 phase: f32,
38}
39
40#[derive(Debug)]
48pub struct Snow {
49 id: Id,
50 color: Color32,
51 density: usize,
52 layer_order: Order,
53 custom_layer: Option<LayerId>,
54 custom_area: Option<Rect>,
55
56 size_range: RangeInclusive<f32>,
57 speed_range: RangeInclusive<f32>,
58 wind: Vec2,
59}
60
61impl Snow {
62 pub fn new(id_source: impl std::hash::Hash + std::fmt::Debug) -> Self {
64 Self {
65 id: Id::new(id_source),
66 color: Color32::WHITE,
67 density: 50,
68 layer_order: Order::Foreground,
69 custom_layer: None,
70 custom_area: None,
71 size_range: 0.3..=1.5,
73 speed_range: 40.0..=100.0,
74 wind: Vec2::ZERO,
75 }
76 }
77
78 pub fn from_settings(
83 id_source: impl std::hash::Hash + std::fmt::Debug,
84 settings: &SnowSettings,
85 ) -> Self {
86 let (size_min, size_max) = ordered(settings.size_min, settings.size_max);
87 let (speed_min, speed_max) = ordered(settings.speed_min, settings.speed_max);
88 let snow = Self::new(id_source)
89 .color(settings.color)
90 .density(settings.density)
91 .size(size_min..=size_max)
92 .speed(speed_min..=speed_max)
93 .wind(settings.wind);
94
95 match settings.layer {
96 ParticleLayer::Background => snow.on_background(),
97 ParticleLayer::Foreground => snow.on_foreground(),
98 ParticleLayer::Top => snow.on_top(),
99 }
100 }
101
102 pub fn color(mut self, color: Color32) -> Self {
104 self.color = color;
105 self
106 }
107
108 pub fn density(mut self, density: usize) -> Self {
110 self.density = density;
111 self
112 }
113
114 pub fn speed(mut self, range: RangeInclusive<f32>) -> Self {
116 self.speed_range = range;
117 self
118 }
119
120 pub fn size(mut self, range: RangeInclusive<f32>) -> Self {
122 self.size_range = range;
123 self
124 }
125
126 pub fn wind(mut self, wind: impl Into<Vec2>) -> Self {
128 self.wind = wind.into();
129 self
130 }
131
132 pub fn on_top(mut self) -> Self {
134 self.layer_order = Order::Debug;
135 self
136 }
137
138 pub fn on_foreground(mut self) -> Self {
140 self.layer_order = Order::Foreground;
141 self
142 }
143
144 pub fn on_background(mut self) -> Self {
146 self.layer_order = Order::Background;
147 self
148 }
149
150 pub fn layer(mut self, layer_id: LayerId) -> Self {
154 self.custom_layer = Some(layer_id);
155 self
156 }
157
158 pub fn area(mut self, area: Rect) -> Self {
162 self.custom_area = Some(area);
163 self
164 }
165
166 pub fn show(self, ctx: &Context) {
170 let Some((layer_id, screen_rect)) = resolve_target(
171 ctx,
172 self.id,
173 self.layer_order,
174 self.custom_layer,
175 self.custom_area,
176 ) else {
177 return;
178 };
179
180 let dt = ctx.input(|i| i.stable_dt).min(0.1);
181 let time = ctx.input(|i| i.time);
182 let seed = self.id.value() ^ time.to_bits();
183
184 let (mut snowflakes, mut rng) = ctx.data_mut(|data| {
185 let state = data.get_temp_mut_or_insert_with(self.id, || SnowState::new(seed));
186 (std::mem::take(&mut state.flakes), state.rng)
187 });
188
189 if snowflakes.len() < self.density {
191 let needed = self.density - snowflakes.len();
192 for _ in 0..needed {
193 snowflakes.push(Snowflake::spawn(
194 &mut rng,
195 true,
196 &self.size_range,
197 &self.speed_range,
198 ));
199 }
200 } else if snowflakes.len() > self.density {
201 snowflakes.truncate(self.density);
202 }
203
204 let painter = ctx.layer_painter(layer_id);
205
206 let min_size = *self.size_range.start();
208 let size_diff = (*self.size_range.end() - min_size).max(0.0001);
209
210 for flake in &mut snowflakes {
212 let pixel_dy = (flake.fall_speed + self.wind.y) * dt;
215
216 let sway = ((time as f32 + flake.phase).sin()) * 5.0;
218 let pixel_dx = (flake.turbulence + self.wind.x + sway) * dt;
219
220 flake.normalized_pos.x += pixel_dx / screen_rect.width();
221 flake.normalized_pos.y += pixel_dy / screen_rect.height();
222
223 if flake.normalized_pos.y > 1.0 {
226 *flake = Snowflake::spawn(
227 &mut rng,
228 false,
229 &self.size_range,
230 &self.speed_range,
231 );
232 }
233 else if flake.normalized_pos.y < -0.05 {
235 flake.normalized_pos.y = 1.0;
236 flake.normalized_pos.x = rng.range(0.0, 1.0);
237 flake.size = rng.range(*self.size_range.start(), *self.size_range.end());
238 flake.fall_speed = rng.range(*self.speed_range.start(), *self.speed_range.end());
239 }
240
241 if flake.normalized_pos.x > 1.0 {
243 flake.normalized_pos.x -= 1.0;
244 } else if flake.normalized_pos.x < 0.0 {
245 flake.normalized_pos.x += 1.0;
246 }
247
248 let pixel_pos = Pos2::new(
250 screen_rect.min.x + flake.normalized_pos.x * screen_rect.width(),
251 screen_rect.min.y + flake.normalized_pos.y * screen_rect.height(),
252 );
253
254 let depth_factor = (flake.size - min_size) / size_diff;
257 let alpha_mult = 0.4 + (0.6 * depth_factor); let final_color = self.color.gamma_multiply(alpha_mult);
259
260 painter.circle_filled(pixel_pos, flake.size, final_color);
261 }
262
263 ctx.data_mut(|data| {
265 let state = data.get_temp_mut_or_insert_with(self.id, || SnowState::new(seed));
266 state.flakes = snowflakes;
267 state.rng = rng;
268 });
269
270 ctx.request_repaint();
271 }
272}
273
274impl Snowflake {
275 fn spawn(
276 rng: &mut ParticleRng,
277 random_y: bool,
278 size_range: &RangeInclusive<f32>,
279 speed_range: &RangeInclusive<f32>,
280 ) -> Self {
281 Self {
282 normalized_pos: Pos2::new(
283 rng.range(0.0, 1.0),
284 if random_y { rng.range(0.0, 1.0) } else { -0.05 },
285 ),
286 fall_speed: rng.range(*speed_range.start(), *speed_range.end()),
287 turbulence: rng.range(-20.0, 20.0),
288 size: rng.range(*size_range.start(), *size_range.end()),
289 phase: rng.range(0.0, std::f32::consts::PI * 2.0),
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use egui::__run_test_ctx;
298 use egui::{Pos2, Rect, vec2};
299
300 #[test]
301 fn from_settings_swaps_inverted_ranges() {
302 let settings = SnowSettings {
303 size_min: 2.0,
304 size_max: 0.5,
305 speed_min: 100.0,
306 speed_max: 10.0,
307 ..Default::default()
308 };
309
310 let snow = Snow::from_settings("inverted", &settings);
311 assert_eq!(*snow.size_range.start(), 0.5);
312 assert_eq!(*snow.size_range.end(), 2.0);
313 assert_eq!(*snow.speed_range.start(), 10.0);
314 assert_eq!(*snow.speed_range.end(), 100.0);
315 }
316
317 #[test]
318 fn from_settings_maps_layer() {
319 let settings = SnowSettings {
320 layer: ParticleLayer::Top,
321 color: Color32::from_rgb(1, 2, 3),
322 density: 12,
323 ..Default::default()
324 };
325
326 let snow = Snow::from_settings("top", &settings);
327 assert_eq!(snow.layer_order, Order::Debug);
328 assert_eq!(snow.color, Color32::from_rgb(1, 2, 3));
329 assert_eq!(snow.density, 12);
330
331 let background = Snow::from_settings(
332 "background",
333 &SnowSettings {
334 layer: ParticleLayer::Background,
335 ..Default::default()
336 },
337 );
338 assert_eq!(background.layer_order, Order::Background);
339 }
340
341 #[test]
342 fn show_spawns_flakes() {
343 __run_test_ctx(|ctx| {
344 let area = Rect::from_min_size(Pos2::ZERO, vec2(64.0, 64.0));
345 Snow::new("test_snow").density(7).area(area).show(ctx);
346
347 let state = ctx
348 .data(|d| d.get_temp::<SnowState>(Id::new("test_snow")))
349 .expect("SnowState should exist after show");
350 assert_eq!(state.flakes.len(), 7);
351 });
352 }
353}