1use egui::{Color32, CornerRadius, Painter, Pos2, Rect, Shape, Stroke, Vec2};
7
8pub trait PainterExt {
10 fn blur_rect(&self, rect: Rect, blur_radius: f32, color: Color32);
14
15 fn glow_rect(&self, rect: Rect, rounding: CornerRadius, color: Color32, intensity: f32);
19
20 fn shadow(
24 &self,
25 rect: Rect,
26 rounding: CornerRadius,
27 offset: Vec2,
28 blur_radius: f32,
29 color: Color32,
30 );
31
32 fn dashed_line(&self, points: &[Pos2], stroke: Stroke, dash_length: f32, gap_length: f32);
36
37 fn dotted_line(&self, points: &[Pos2], color: Color32, dot_radius: f32, spacing: f32);
41
42 fn gradient_rect_horizontal(
46 &self,
47 rect: Rect,
48 rounding: CornerRadius,
49 from: Color32,
50 to: Color32,
51 );
52
53 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 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 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 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
249fn 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
260pub 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 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 for j in 0..points.len() - 1 {
292 painter.line_segment([points[j], points[j + 1]], Stroke::new(thickness, color));
293 }
294}
295
296pub fn neon_circle(
298 painter: &Painter,
299 center: Pos2,
300 radius: f32,
301 color: Color32,
302 glow_intensity: f32,
303) {
304 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 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}