1use egui::{Color32, Mesh, Pos2, Rect, Vec2};
7use std::f32::consts::PI;
8
9#[derive(Clone, Debug)]
12pub struct ColorStop {
13 pub position: f32,
15 pub color: Color32,
17}
18
19impl ColorStop {
20 #[must_use]
22 pub const fn new(position: f32, color: Color32) -> Self {
23 Self { position, color }
24 }
25}
26
27pub struct Gradient {
39 stops: Vec<ColorStop>,
40}
41
42impl Gradient {
43 #[must_use]
45 pub const fn new(stops: Vec<ColorStop>) -> Self {
46 Self { stops }
47 }
48
49 #[must_use]
51 pub fn linear(from: Color32, to: Color32) -> Self {
52 Self {
53 stops: vec![ColorStop::new(0.0, from), ColorStop::new(1.0, to)],
54 }
55 }
56
57 #[must_use]
59 pub fn sample(&self, t: f32) -> Color32 {
60 let t = t.clamp(0.0, 1.0);
61
62 if self.stops.is_empty() {
63 return Color32::BLACK;
64 }
65
66 if self.stops.len() == 1 {
67 return self.stops[0].color;
68 }
69
70 let mut before = &self.stops[0];
72 let mut after = &self.stops[self.stops.len() - 1];
73
74 for i in 0..self.stops.len() - 1 {
75 if self.stops[i].position <= t && self.stops[i + 1].position >= t {
76 before = &self.stops[i];
77 after = &self.stops[i + 1];
78 break;
79 }
80 }
81
82 let range = after.position - before.position;
84 if range < 0.0001 {
85 return before.color;
86 }
87
88 let local_t = (t - before.position) / range;
89 lerp_color(before.color, after.color, local_t)
90 }
91
92 #[must_use]
96 pub fn radial_mesh(&self, center: Pos2, radius: f32, segments: usize) -> Mesh {
97 let mut mesh = Mesh::default();
98
99 let center_color = self.sample(0.0);
101 mesh.colored_vertex(center, center_color);
102
103 let num_rings = 10;
105 for ring in 1..=num_rings {
106 let t = ring as f32 / num_rings as f32;
107 let ring_radius = radius * t;
108 let ring_color = self.sample(t);
109
110 for segment in 0..segments {
111 let angle = (segment as f32 / segments as f32) * 2.0 * PI;
112 let pos = center + Vec2::new(angle.cos(), angle.sin()) * ring_radius;
113 mesh.colored_vertex(pos, ring_color);
114 }
115 }
116
117 for segment in 0..segments {
120 let next = (segment + 1) % segments;
121 mesh.add_triangle(0, 1 + segment as u32, 1 + next as u32);
122 }
123
124 for ring in 0..num_rings - 1 {
126 let base = 1 + ring * segments;
127 let next_base = 1 + (ring + 1) * segments;
128
129 for segment in 0..segments {
130 let next = (segment + 1) % segments;
131
132 let a = base + segment;
133 let b = base + next;
134 let c = next_base + segment;
135 let d = next_base + next;
136
137 mesh.add_triangle(a as u32, c as u32, b as u32);
138 mesh.add_triangle(b as u32, c as u32, d as u32);
139 }
140 }
141
142 mesh
143 }
144
145 #[must_use]
149 pub fn conic_mesh(
150 &self,
151 center: Pos2,
152 radius: f32,
153 angle_offset: f32,
154 segments: usize,
155 ) -> Mesh {
156 let mut mesh = Mesh::default();
157
158 let center_color = self.sample(0.5);
160 mesh.colored_vertex(center, center_color);
161
162 for segment in 0..segments {
164 let angle = angle_offset + (segment as f32 / segments as f32) * 2.0 * PI;
165 let t = (angle.rem_euclid(2.0 * PI)) / (2.0 * PI);
166 let color = self.sample(t);
167
168 let pos = center + Vec2::new(angle.cos(), angle.sin()) * radius;
169 mesh.colored_vertex(pos, color);
170 }
171
172 for segment in 0..segments {
174 let next = (segment + 1) % segments;
175 mesh.add_triangle(0, 1 + segment as u32, 1 + next as u32);
176 }
177
178 mesh
179 }
180
181 #[must_use]
183 pub fn rect_mesh(&self, rect: Rect, horizontal: bool) -> Mesh {
184 let mut mesh = Mesh::default();
185
186 let steps = 20;
187 for i in 0..=steps {
188 let t = i as f32 / steps as f32;
189 let color = self.sample(t);
190
191 if horizontal {
192 let x = rect.left() + t * rect.width();
193 let top = Pos2::new(x, rect.top());
194 let bottom = Pos2::new(x, rect.bottom());
195
196 mesh.colored_vertex(top, color);
197 mesh.colored_vertex(bottom, color);
198 } else {
199 let y = rect.top() + t * rect.height();
200 let left = Pos2::new(rect.left(), y);
201 let right = Pos2::new(rect.right(), y);
202
203 mesh.colored_vertex(left, color);
204 mesh.colored_vertex(right, color);
205 }
206 }
207
208 for i in 0..steps {
210 let base = i * 2;
211 mesh.add_triangle(base, base + 1, base + 2);
212 mesh.add_triangle(base + 1, base + 3, base + 2);
213 }
214
215 mesh
216 }
217}
218
219#[must_use]
221pub fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 {
222 let t = t.clamp(0.0, 1.0);
223 Color32::from_rgba_unmultiplied(
224 (f32::from(a.r()) + (f32::from(b.r()) - f32::from(a.r())) * t) as u8,
225 (f32::from(a.g()) + (f32::from(b.g()) - f32::from(a.g())) * t) as u8,
226 (f32::from(a.b()) + (f32::from(b.b()) - f32::from(a.b())) * t) as u8,
227 (f32::from(a.a()) + (f32::from(b.a()) - f32::from(a.a())) * t) as u8,
228 )
229}
230
231#[must_use]
233pub fn with_alpha(color: Color32, alpha: u8) -> Color32 {
234 Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), alpha)
235}
236
237#[must_use]
239#[allow(clippy::many_single_char_names)]
240pub fn blend(a: Color32, b: Color32, t: f32, mode: BlendMode) -> Color32 {
241 match mode {
242 BlendMode::Normal => lerp_color(a, b, t),
243 BlendMode::Multiply => {
244 let r = ((f32::from(a.r()) / 255.0) * (f32::from(b.r()) / 255.0) * 255.0) as u8;
245 let g = ((f32::from(a.g()) / 255.0) * (f32::from(b.g()) / 255.0) * 255.0) as u8;
246 let b_val = ((f32::from(a.b()) / 255.0) * (f32::from(b.b()) / 255.0) * 255.0) as u8;
247 Color32::from_rgb(r, g, b_val)
248 }
249 BlendMode::Screen => {
250 let r = (255.0 - (255.0 - f32::from(a.r())) * (255.0 - f32::from(b.r())) / 255.0) as u8;
251 let g = (255.0 - (255.0 - f32::from(a.g())) * (255.0 - f32::from(b.g())) / 255.0) as u8;
252 let b_val =
253 (255.0 - (255.0 - f32::from(a.b())) * (255.0 - f32::from(b.b())) / 255.0) as u8;
254 Color32::from_rgb(r, g, b_val)
255 }
256 BlendMode::Overlay => {
257 let overlay_channel = |base: u8, blend: u8| -> u8 {
258 let base_f = f32::from(base) / 255.0;
259 let blend_f = f32::from(blend) / 255.0;
260 let result = if base_f < 0.5 {
261 2.0 * base_f * blend_f
262 } else {
263 1.0 - 2.0 * (1.0 - base_f) * (1.0 - blend_f)
264 };
265 (result * 255.0) as u8
266 };
267 Color32::from_rgb(
268 overlay_channel(a.r(), b.r()),
269 overlay_channel(a.g(), b.g()),
270 overlay_channel(a.b(), b.b()),
271 )
272 }
273 }
274}
275
276#[derive(Debug, Clone, Copy)]
278pub enum BlendMode {
279 Normal,
281 Multiply,
283 Screen,
285 Overlay,
287}
288
289#[must_use]
291pub fn saturate(color: Color32, amount: f32) -> Color32 {
292 let r = f32::from(color.r()) / 255.0;
293 let g = f32::from(color.g()) / 255.0;
294 let b = f32::from(color.b()) / 255.0;
295
296 let gray = 0.299 * r + 0.587 * g + 0.114 * b;
297
298 let r = (gray + (r - gray) * amount).clamp(0.0, 1.0);
299 let g = (gray + (g - gray) * amount).clamp(0.0, 1.0);
300 let b = (gray + (b - gray) * amount).clamp(0.0, 1.0);
301
302 Color32::from_rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
303}
304
305pub struct NeonPalette;
307
308impl NeonPalette {
309 #[must_use]
311 pub fn cyberpunk() -> Vec<Color32> {
312 vec![
313 Color32::from_rgb(0, 255, 255), Color32::from_rgb(255, 0, 255), Color32::from_rgb(138, 43, 226), Color32::from_rgb(255, 20, 147), Color32::from_rgb(0, 191, 255), ]
319 }
320
321 #[must_use]
323 pub fn synthwave() -> Vec<Color32> {
324 vec![
325 Color32::from_rgb(251, 86, 7), Color32::from_rgb(255, 0, 110), Color32::from_rgb(131, 58, 180), Color32::from_rgb(253, 29, 29), Color32::from_rgb(252, 176, 69), ]
331 }
332
333 #[must_use]
335 pub fn aurora() -> Vec<Color32> {
336 vec![
337 Color32::from_rgb(0, 255, 127), Color32::from_rgb(0, 191, 255), Color32::from_rgb(138, 43, 226), Color32::from_rgb(64, 224, 208), Color32::from_rgb(123, 104, 238), ]
343 }
344
345 #[must_use]
347 pub fn rainbow() -> Vec<Color32> {
348 vec![
349 Color32::from_rgb(255, 0, 0), Color32::from_rgb(255, 127, 0), Color32::from_rgb(255, 255, 0), Color32::from_rgb(0, 255, 0), Color32::from_rgb(0, 0, 255), Color32::from_rgb(75, 0, 130), Color32::from_rgb(148, 0, 211), ]
357 }
358
359 #[must_use]
361 pub fn electric() -> Vec<Color32> {
362 vec![
363 Color32::from_rgb(59, 130, 246), Color32::from_rgb(96, 165, 250), Color32::from_rgb(147, 197, 253), Color32::from_rgb(191, 219, 254), ]
368 }
369
370 #[must_use]
372 pub fn hot() -> Vec<Color32> {
373 vec![
374 Color32::from_rgb(139, 0, 0), Color32::from_rgb(220, 20, 60), Color32::from_rgb(255, 69, 0), Color32::from_rgb(255, 140, 0), Color32::from_rgb(255, 215, 0), ]
380 }
381
382 #[must_use]
384 pub fn cool() -> Vec<Color32> {
385 vec![
386 Color32::from_rgb(0, 255, 255), Color32::from_rgb(0, 191, 255), Color32::from_rgb(65, 105, 225), Color32::from_rgb(138, 43, 226), ]
391 }
392
393 #[must_use]
395 pub fn gold() -> Vec<Color32> {
396 vec![
397 Color32::from_rgb(255, 215, 0), Color32::from_rgb(255, 223, 0), Color32::from_rgb(255, 193, 37), Color32::from_rgb(218, 165, 32), ]
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_color_lerp() {
411 let white = Color32::WHITE;
412 let black = Color32::BLACK;
413
414 let mid = lerp_color(black, white, 0.5);
415 assert_eq!(mid.r(), 127);
416 assert_eq!(mid.g(), 127);
417 assert_eq!(mid.b(), 127);
418 }
419
420 #[test]
421 fn test_gradient_sample() {
422 let gradient = Gradient::linear(Color32::BLACK, Color32::WHITE);
423 let mid = gradient.sample(0.5);
424 assert_eq!(mid.r(), 127);
425 }
426
427 #[test]
428 fn test_with_alpha() {
429 let color = Color32::from_rgb(255, 0, 0);
430 let transparent = with_alpha(color, 128);
431 assert_eq!(transparent.a(), 128);
432 }
433
434 #[test]
435 fn test_saturate() {
436 let gray = Color32::from_rgb(128, 128, 128);
437 let saturated = saturate(gray, 2.0);
438 assert_eq!(saturated.r(), saturated.g());
440 }
441}