egui_snow/
lib.rs

1use egui::{Color32, Context, Id, LayerId, Order, Pos2, Rect, Vec2};
2use rand::Rng;
3use std::ops::RangeInclusive;
4
5#[derive(Default, Clone)]
6struct SnowState {
7    flakes: Vec<Snowflake>,
8}
9
10#[derive(Clone, Copy)]
11struct Snowflake {
12    /// Normalized coordinates (0.0..1.0)
13    normalized_pos: Pos2,
14    /// Fall speed (px/sec), unique for each particle
15    fall_speed: f32,
16    /// Random turbulence on X axis, unique for each particle
17    turbulence: f32,
18    /// Size of the snowflake
19    size: f32,
20    /// Oscillation phase
21    phase: f32,
22}
23
24/// A configurable snow effect for egui.
25///
26/// `Snow` allows you to add a falling snow animation to any egui layer or area.
27/// It maintains its state between frames using egui's temporary data storage.
28#[derive(Debug)]
29pub struct Snow {
30    id: Id,
31    color: Color32,
32    density: usize,
33    layer_order: Order,
34    custom_layer: Option<LayerId>,
35    custom_area: Option<Rect>,
36
37    size_range: RangeInclusive<f32>,
38    speed_range: RangeInclusive<f32>,
39    wind: Vec2,
40}
41
42impl Snow {
43    /// Creates a new snow effect with a unique ID.
44    pub fn new(id_source: impl std::hash::Hash) -> Self {
45        Self {
46            id: Id::new(id_source),
47            color: Color32::WHITE,
48            density: 50,
49            layer_order: Order::Foreground,
50            custom_layer: None,
51            custom_area: None,
52            // Default values
53            size_range: 0.3..=1.5,
54            speed_range: 40.0..=100.0,
55            wind: Vec2::ZERO,
56        }
57    }
58
59    /// Sets the color of the snowflakes.
60    pub fn color(mut self, color: Color32) -> Self {
61        self.color = color;
62        self
63    }
64
65    /// Sets the number of snowflakes.
66    pub fn density(mut self, density: usize) -> Self {
67        self.density = density;
68        self
69    }
70
71    /// Sets the falling speed range (min..=max) in pixels per second.
72    pub fn speed(mut self, range: RangeInclusive<f32>) -> Self {
73        self.speed_range = range;
74        self
75    }
76
77    /// Sets the snowflake size range (min..=max).
78    pub fn size(mut self, range: RangeInclusive<f32>) -> Self {
79        self.size_range = range;
80        self
81    }
82
83    /// Sets the wind vector.
84    pub fn wind(mut self, wind: impl Into<Vec2>) -> Self {
85        self.wind = wind.into();
86        self
87    }
88
89    /// Places the snow on the Debug layer (on top of everything).
90    pub fn on_top(mut self) -> Self {
91        self.layer_order = Order::Debug;
92        self
93    }
94
95    /// Places the snow on the Foreground layer.
96    pub fn on_foreground(mut self) -> Self {
97        self.layer_order = Order::Foreground;
98        self
99    }
100
101    /// Places the snow on the Background layer.
102    pub fn on_background(mut self) -> Self {
103        self.layer_order = Order::Background;
104        self
105    }
106
107    /// Provides full control over the rendering layer.
108    ///
109    /// This overrides `on_top`, `on_foreground`, and `on_background`.
110    pub fn layer(mut self, layer_id: LayerId) -> Self {
111        self.custom_layer = Some(layer_id);
112        self
113    }
114
115    /// Restricts the snow effect to a specific area.
116    ///
117    /// If not set, it defaults to `ctx.content_rect()`.
118    pub fn area(mut self, area: Rect) -> Self {
119        self.custom_area = Some(area);
120        self
121    }
122
123    /// Updates and renders the snow effect.
124    ///
125    /// This should be called every frame. It automatically requests a repaint.
126    pub fn show(self, ctx: &Context) {
127        let screen_rect = self.custom_area.unwrap_or_else(|| ctx.content_rect());
128        if screen_rect.width() <= 0.0 || screen_rect.height() <= 0.0 {
129            return;
130        }
131
132        let dt = ctx.input(|i| i.stable_dt).min(0.1);
133        let time = ctx.input(|i| i.time);
134
135        let mut snowflakes = ctx.data_mut(|data| {
136            let state = data.get_temp_mut_or_insert_with(self.id, SnowState::default);
137            std::mem::take(&mut state.flakes)
138        });
139
140        // Spawn logic
141        let mut rng = rand::rng();
142        if snowflakes.len() < self.density {
143            let needed = self.density - snowflakes.len();
144            for _ in 0..needed {
145                snowflakes.push(Snowflake::spawn(
146                    &mut rng,
147                    true,
148                    &self.size_range,
149                    &self.speed_range,
150                ));
151            }
152        } else if snowflakes.len() > self.density {
153            snowflakes.truncate(self.density);
154        }
155
156        let layer_id = self
157            .custom_layer
158            .unwrap_or_else(|| LayerId::new(self.layer_order, self.id));
159        let painter = ctx.layer_painter(layer_id);
160
161        // Cache values for alpha calculation
162        let min_size = *self.size_range.start();
163        let size_diff = (*self.size_range.end() - min_size).max(0.0001);
164
165        // Update & Render logic
166        for flake in &mut snowflakes {
167            // --- Update ---
168            // Y: Individual speed + vertical wind
169            let pixel_dy = (flake.fall_speed + self.wind.y) * dt;
170
171            // X: Individual turbulence + global wind + sine wave
172            let sway = ((time as f32 + flake.phase).sin()) * 5.0;
173            let pixel_dx = (flake.turbulence + self.wind.x + sway) * dt;
174
175            flake.normalized_pos.x += pixel_dx / screen_rect.width();
176            flake.normalized_pos.y += pixel_dy / screen_rect.height();
177
178            // Respawn (Wrap Y)
179            // If it flew down
180            if flake.normalized_pos.y > 1.0 {
181                *flake = Snowflake::spawn(&mut rng, false, &self.size_range, &self.speed_range);
182            }
183            // If it flew up (strong upward wind)
184            else if flake.normalized_pos.y < -0.05 {
185                flake.normalized_pos.y = 1.0;
186                flake.normalized_pos.x = rng.random_range(0.0..1.0);
187                flake.size = rng.random_range(self.size_range.clone());
188                flake.fall_speed = rng.random_range(self.speed_range.clone());
189            }
190
191            // Wrap X (seamless horizontal transition)
192            if flake.normalized_pos.x > 1.0 {
193                flake.normalized_pos.x -= 1.0;
194            } else if flake.normalized_pos.x < 0.0 {
195                flake.normalized_pos.x += 1.0;
196            }
197
198            // --- Render ---
199            let pixel_pos = Pos2::new(
200                screen_rect.min.x + flake.normalized_pos.x * screen_rect.width(),
201                screen_rect.min.y + flake.normalized_pos.y * screen_rect.height(),
202            );
203
204            // Calculate alpha based on size (simulating depth)
205            // Smaller particles are more transparent
206            let depth_factor = (flake.size - min_size) / size_diff;
207            let alpha_mult = 0.4 + (0.6 * depth_factor); // Min. transparency 40%
208            let final_color = self.color.gamma_multiply(alpha_mult);
209
210            painter.circle_filled(pixel_pos, flake.size, final_color);
211        }
212
213        // Return snowflakes to state
214        ctx.data_mut(|data| {
215            let state = data.get_temp_mut_or_insert_with(self.id, SnowState::default);
216            state.flakes = snowflakes;
217        });
218
219        ctx.request_repaint();
220    }
221}
222
223impl Snowflake {
224    fn spawn(
225        rng: &mut impl rand::Rng,
226        random_y: bool,
227        size_range: &RangeInclusive<f32>,
228        speed_range: &RangeInclusive<f32>,
229    ) -> Self {
230        Self {
231            normalized_pos: Pos2::new(
232                rng.random_range(0.0..1.0),
233                if random_y {
234                    rng.random_range(0.0..1.0)
235                } else {
236                    -0.05
237                },
238            ),
239            fall_speed: rng.random_range(speed_range.clone()),
240            turbulence: rng.random_range(-20.0..20.0),
241            size: rng.random_range(size_range.clone()),
242            phase: rng.random_range(0.0..std::f32::consts::PI * 2.0),
243        }
244    }
245}