Skip to main content

egui_elements/widgets/particles/snow/
effect.rs

1//! Snow particle effect, ported from [`egui-snow`](https://github.com/LaVashikk/egui-snow)
2//! (v0.36.0, MIT License, Copyright (c) 2025 LaVashikk). The port keeps the upstream
3//! algorithm, defaults and behavior; randomness uses an internal PRNG instead of `rand`.
4
5use 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 coordinates (0.0..1.0)
29   normalized_pos: Pos2,
30   /// Fall speed (px/sec), unique for each particle
31   fall_speed: f32,
32   /// Random turbulence on X axis, unique for each particle
33   turbulence: f32,
34   /// Size of the snowflake
35   size: f32,
36   /// Oscillation phase
37   phase: f32,
38}
39
40/// A configurable snow effect for egui.
41///
42/// `Snow` allows you to add a falling snow animation to any egui layer or area.
43/// It maintains its state between frames using egui's temporary data storage.
44///
45/// Ported from [`egui-snow`](https://github.com/LaVashikk/egui-snow); the algorithm,
46/// defaults and builders match upstream.
47#[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   /// Creates a new snow effect with a unique ID.
63   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         // Default values
72         size_range: 0.3..=1.5,
73         speed_range: 40.0..=100.0,
74         wind: Vec2::ZERO,
75      }
76   }
77
78   /// Builds a snow effect from [`SnowSettings`].
79   ///
80   /// Inverted ranges are normalized (swapped) so a host can never hand the
81   /// spawner an empty range.
82   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   /// Sets the color of the snowflakes.
103   pub fn color(mut self, color: Color32) -> Self {
104      self.color = color;
105      self
106   }
107
108   /// Sets the number of snowflakes.
109   pub fn density(mut self, density: usize) -> Self {
110      self.density = density;
111      self
112   }
113
114   /// Sets the falling speed range (min..=max) in pixels per second.
115   pub fn speed(mut self, range: RangeInclusive<f32>) -> Self {
116      self.speed_range = range;
117      self
118   }
119
120   /// Sets the snowflake size range (min..=max).
121   pub fn size(mut self, range: RangeInclusive<f32>) -> Self {
122      self.size_range = range;
123      self
124   }
125
126   /// Sets the wind vector.
127   pub fn wind(mut self, wind: impl Into<Vec2>) -> Self {
128      self.wind = wind.into();
129      self
130   }
131
132   /// Places the snow on the Debug layer (on top of everything).
133   pub fn on_top(mut self) -> Self {
134      self.layer_order = Order::Debug;
135      self
136   }
137
138   /// Places the snow on the Foreground layer.
139   pub fn on_foreground(mut self) -> Self {
140      self.layer_order = Order::Foreground;
141      self
142   }
143
144   /// Places the snow on the Background layer.
145   pub fn on_background(mut self) -> Self {
146      self.layer_order = Order::Background;
147      self
148   }
149
150   /// Provides full control over the rendering layer.
151   ///
152   /// This overrides `on_top`, `on_foreground`, and `on_background`.
153   pub fn layer(mut self, layer_id: LayerId) -> Self {
154      self.custom_layer = Some(layer_id);
155      self
156   }
157
158   /// Restricts the snow effect to a specific area.
159   ///
160   /// If not set, it defaults to `ctx.content_rect()`.
161   pub fn area(mut self, area: Rect) -> Self {
162      self.custom_area = Some(area);
163      self
164   }
165
166   /// Updates and renders the snow effect.
167   ///
168   /// This should be called every frame. It automatically requests a repaint.
169   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      // Spawn logic
190      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      // Cache values for alpha calculation
207      let min_size = *self.size_range.start();
208      let size_diff = (*self.size_range.end() - min_size).max(0.0001);
209
210      // Update & Render logic
211      for flake in &mut snowflakes {
212         // --- Update ---
213         // Y: Individual speed + vertical wind
214         let pixel_dy = (flake.fall_speed + self.wind.y) * dt;
215
216         // X: Individual turbulence + global wind + sine wave
217         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         // Respawn (Wrap Y)
224         // If it flew down
225         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         // If it flew up (strong upward wind)
234         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         // Wrap X (seamless horizontal transition)
242         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         // --- Render ---
249         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         // Calculate alpha based on size (simulating depth)
255         // Smaller particles are more transparent
256         let depth_factor = (flake.size - min_size) / size_diff;
257         let alpha_mult = 0.4 + (0.6 * depth_factor); // Min. transparency 40%
258         let final_color = self.color.gamma_multiply(alpha_mult);
259
260         painter.circle_filled(pixel_pos, flake.size, final_color);
261      }
262
263      // Return snowflakes to state
264      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}