Skip to main content

armas_basic/ext/
painter.rs

1//! Painter extensions for advanced rendering effects
2//!
3//! Provides additional rendering capabilities beyond egui's built-in painter,
4//! including blur approximation, glow effects, and shadows.
5
6use egui::{Color32, CornerRadius, Painter, Pos2, Rect, Shape, Stroke, Vec2};
7
8/// Extension trait for egui's Painter with advanced effects
9pub trait PainterExt {
10    /// Draw a blurred rectangle using layered alpha
11    ///
12    /// Approximates blur by drawing multiple expanding rectangles with decreasing opacity
13    fn blur_rect(&self, rect: Rect, blur_radius: f32, color: Color32);
14
15    /// Draw a glow effect around a rectangle
16    ///
17    /// Creates a soft glow by drawing multiple expanding outlines
18    fn glow_rect(&self, rect: Rect, rounding: CornerRadius, color: Color32, intensity: f32);
19
20    /// Draw a shadow with blur
21    ///
22    /// Renders a blurred shadow offset from the original rectangle
23    fn shadow(
24        &self,
25        rect: Rect,
26        rounding: CornerRadius,
27        offset: Vec2,
28        blur_radius: f32,
29        color: Color32,
30    );
31
32    /// Draw a dashed line
33    ///
34    /// Creates a line with alternating dashes and gaps
35    fn dashed_line(&self, points: &[Pos2], stroke: Stroke, dash_length: f32, gap_length: f32);
36
37    /// Draw a dotted line
38    ///
39    /// Creates a line with dots at regular intervals
40    fn dotted_line(&self, points: &[Pos2], color: Color32, dot_radius: f32, spacing: f32);
41
42    /// Draw a gradient-filled rectangle
43    ///
44    /// Uses mesh rendering for smooth gradients
45    fn gradient_rect_horizontal(
46        &self,
47        rect: Rect,
48        rounding: CornerRadius,
49        from: Color32,
50        to: Color32,
51    );
52
53    /// Draw a radial glow at a point
54    ///
55    /// Useful for creating spotlight or highlight effects
56    fn radial_glow(&self, center: Pos2, radius: f32, color: Color32, falloff: f32);
57}
58
59impl PainterExt for Painter {
60    fn blur_rect(&self, rect: Rect, blur_radius: f32, color: Color32) {
61        let layers = 5;
62        let base_alpha = f32::from(color.a()) / layers as f32;
63
64        for i in 0..layers {
65            let expand = (i as f32 / layers as f32) * blur_radius;
66            let alpha = (base_alpha * (1.0 - i as f32 / layers as f32)) as u8;
67            let layer_color =
68                Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha);
69
70            let expanded_rect = rect.expand(expand);
71            self.rect_filled(expanded_rect, 0.0, layer_color);
72        }
73    }
74
75    fn glow_rect(&self, rect: Rect, rounding: CornerRadius, color: Color32, intensity: f32) {
76        let layers = 8;
77        let max_expansion = 12.0 * intensity;
78
79        for i in 0..layers {
80            let t = i as f32 / layers as f32;
81            let expansion = max_expansion * t;
82            let alpha = ((1.0 - t) * intensity * 255.0) as u8;
83
84            let glow_color = Color32::from_rgba_unmultiplied(
85                color.r(),
86                color.g(),
87                color.b(),
88                alpha.min(color.a()),
89            );
90
91            let expanded_rect = rect.expand(expansion);
92            self.rect_stroke(
93                expanded_rect,
94                rounding,
95                Stroke::new(1.5, glow_color),
96                egui::epaint::StrokeKind::Middle,
97            );
98        }
99    }
100
101    fn shadow(
102        &self,
103        rect: Rect,
104        rounding: CornerRadius,
105        offset: Vec2,
106        blur_radius: f32,
107        color: Color32,
108    ) {
109        let shadow_rect = rect.translate(offset);
110        let layers = 8;
111
112        for i in 0..layers {
113            let t = i as f32 / layers as f32;
114            let expansion = blur_radius * t;
115            let alpha = ((1.0 - t) * f32::from(color.a())) as u8;
116
117            let shadow_color =
118                Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha);
119
120            let expanded_rect = shadow_rect.expand(expansion);
121            self.rect_filled(
122                expanded_rect,
123                rounding.at_most(expansion as u8),
124                shadow_color,
125            );
126        }
127    }
128
129    fn dashed_line(&self, points: &[Pos2], stroke: Stroke, dash_length: f32, gap_length: f32) {
130        if points.len() < 2 {
131            return;
132        }
133
134        for i in 0..points.len() - 1 {
135            let start = points[i];
136            let end = points[i + 1];
137            let segment = end - start;
138            let length = segment.length();
139
140            if length < 0.001 {
141                continue;
142            }
143
144            let direction = segment / length;
145            let pattern_length = dash_length + gap_length;
146            let num_dashes = (length / pattern_length).ceil() as usize;
147
148            for i in 0..num_dashes {
149                let current_pos = i as f32 * pattern_length;
150                if current_pos >= length {
151                    break;
152                }
153                let dash_start = start + direction * current_pos;
154                let dash_end_pos = (current_pos + dash_length).min(length);
155                let dash_end = start + direction * dash_end_pos;
156
157                self.line_segment([dash_start, dash_end], stroke);
158            }
159        }
160    }
161
162    fn dotted_line(&self, points: &[Pos2], color: Color32, dot_radius: f32, spacing: f32) {
163        if points.len() < 2 {
164            return;
165        }
166
167        for i in 0..points.len() - 1 {
168            let start = points[i];
169            let end = points[i + 1];
170            let segment = end - start;
171            let length = segment.length();
172
173            if length < 0.001 {
174                continue;
175            }
176
177            let num_dots = (length / spacing).ceil() as usize;
178
179            for j in 0..=num_dots {
180                let t = (j as f32 * spacing / length).min(1.0);
181                let pos = start + segment * t;
182                self.circle_filled(pos, dot_radius, color);
183            }
184        }
185    }
186
187    fn gradient_rect_horizontal(
188        &self,
189        rect: Rect,
190        rounding: CornerRadius,
191        from: Color32,
192        to: Color32,
193    ) {
194        let mut mesh = egui::Mesh::default();
195
196        // Simple 4-vertex gradient
197        let tl = rect.left_top();
198        let tr = rect.right_top();
199        let bl = rect.left_bottom();
200        let br = rect.right_bottom();
201
202        // For rounding, we need to subdivide, but for now keep it simple
203        if rounding == CornerRadius::ZERO {
204            mesh.colored_vertex(tl, from);
205            mesh.colored_vertex(tr, to);
206            mesh.colored_vertex(bl, from);
207            mesh.colored_vertex(br, to);
208
209            mesh.add_triangle(0, 1, 2);
210            mesh.add_triangle(1, 3, 2);
211        } else {
212            // Rounded corners - need to subdivide
213            let steps = 10;
214            for i in 0..=steps {
215                let t = i as f32 / steps as f32;
216                let x = rect.left() + t * rect.width();
217                let color = lerp_color(from, to, t);
218
219                mesh.colored_vertex(Pos2::new(x, rect.top()), color);
220                mesh.colored_vertex(Pos2::new(x, rect.bottom()), color);
221            }
222
223            for i in 0..steps {
224                let base = i * 2;
225                mesh.add_triangle(base as u32, (base + 1) as u32, (base + 2) as u32);
226                mesh.add_triangle((base + 1) as u32, (base + 3) as u32, (base + 2) as u32);
227            }
228        }
229
230        self.add(Shape::Mesh(std::sync::Arc::new(mesh)));
231    }
232
233    fn radial_glow(&self, center: Pos2, radius: f32, color: Color32, falloff: f32) {
234        let layers = 12;
235
236        for i in 0..layers {
237            let t = i as f32 / layers as f32;
238            let layer_radius = radius * (1.0 - t.powf(falloff));
239            let alpha = ((1.0 - t) * f32::from(color.a())) as u8;
240
241            let glow_color =
242                Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha);
243
244            self.circle_filled(center, layer_radius, glow_color);
245        }
246    }
247}
248
249/// Helper function to interpolate between colors
250fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 {
251    let t = t.clamp(0.0, 1.0);
252    Color32::from_rgba_unmultiplied(
253        (f32::from(a.r()) + (f32::from(b.r()) - f32::from(a.r())) * t) as u8,
254        (f32::from(a.g()) + (f32::from(b.g()) - f32::from(a.g())) * t) as u8,
255        (f32::from(a.b()) + (f32::from(b.b()) - f32::from(a.b())) * t) as u8,
256        (f32::from(a.a()) + (f32::from(b.a()) - f32::from(a.a())) * t) as u8,
257    )
258}
259
260/// Draw a neon line with glow effect
261pub fn neon_line(
262    painter: &Painter,
263    points: &[Pos2],
264    color: Color32,
265    thickness: f32,
266    glow_intensity: f32,
267) {
268    if points.len() < 2 {
269        return;
270    }
271
272    // Draw glow layers
273    let glow_layers = 5;
274    for i in 0..glow_layers {
275        let t = i as f32 / glow_layers as f32;
276        let layer_thickness = thickness + (glow_intensity * 8.0 * t);
277        let alpha = ((1.0 - t) * glow_intensity * 255.0) as u8;
278
279        let glow_color =
280            Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha.min(80));
281
282        for j in 0..points.len() - 1 {
283            painter.line_segment(
284                [points[j], points[j + 1]],
285                Stroke::new(layer_thickness, glow_color),
286            );
287        }
288    }
289
290    // Draw core line
291    for j in 0..points.len() - 1 {
292        painter.line_segment([points[j], points[j + 1]], Stroke::new(thickness, color));
293    }
294}
295
296/// Draw a glowing circle
297pub fn neon_circle(
298    painter: &Painter,
299    center: Pos2,
300    radius: f32,
301    color: Color32,
302    glow_intensity: f32,
303) {
304    // Draw glow
305    let glow_layers = 8;
306    for i in 0..glow_layers {
307        let t = i as f32 / glow_layers as f32;
308        let layer_radius = radius + (glow_intensity * 10.0 * t);
309        let alpha = ((1.0 - t) * glow_intensity * 255.0) as u8;
310
311        let glow_color =
312            Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha.min(60));
313
314        painter.circle_stroke(center, layer_radius, Stroke::new(1.5, glow_color));
315    }
316
317    // Draw core circle
318    painter.circle_filled(center, radius, color);
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_lerp_color() {
327        let black = Color32::BLACK;
328        let white = Color32::WHITE;
329        let gray = lerp_color(black, white, 0.5);
330
331        assert_eq!(gray.r(), 127);
332        assert_eq!(gray.g(), 127);
333        assert_eq!(gray.b(), 127);
334    }
335}