Skip to main content

armas_basic/
color.rs

1//! Color utilities for advanced effects
2//!
3//! Provides gradient generation, color manipulation, and neon palette presets
4//! for creating aceternity-style visual effects.
5
6use egui::{Color32, Mesh, Pos2, Rect, Vec2};
7use std::f32::consts::PI;
8
9/// Color stop for gradients (position 0.0-1.0, color)
10/// A color stop in a gradient, defining a color at a specific position
11#[derive(Clone, Debug)]
12pub struct ColorStop {
13    /// Position in the gradient (0.0 to 1.0)
14    pub position: f32,
15    /// Color at this position
16    pub color: Color32,
17}
18
19impl ColorStop {
20    /// Create a new color stop
21    #[must_use]
22    pub const fn new(position: f32, color: Color32) -> Self {
23        Self { position, color }
24    }
25}
26
27/// Gradient builder for creating various gradient types
28///
29/// # Example
30///
31/// ```rust
32/// use egui::Color32;
33/// use armas_basic::color::{Gradient, ColorStop};
34///
35/// let gradient = Gradient::linear(Color32::RED, Color32::BLUE);
36/// let mid_color = gradient.sample(0.5);
37/// ```
38pub struct Gradient {
39    stops: Vec<ColorStop>,
40}
41
42impl Gradient {
43    /// Create a new gradient with stops
44    #[must_use]
45    pub const fn new(stops: Vec<ColorStop>) -> Self {
46        Self { stops }
47    }
48
49    /// Create a simple two-color gradient
50    #[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    /// Sample color at position t (0.0-1.0)
58    #[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        // Find the two stops to interpolate between
71        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        // Interpolate
83        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    /// Generate a radial gradient mesh
93    ///
94    /// Creates a circular gradient emanating from a center point
95    #[must_use]
96    pub fn radial_mesh(&self, center: Pos2, radius: f32, segments: usize) -> Mesh {
97        let mut mesh = Mesh::default();
98
99        // Center vertex
100        let center_color = self.sample(0.0);
101        mesh.colored_vertex(center, center_color);
102
103        // Create rings
104        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        // Generate triangles
118        // Center to first ring
119        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        // Ring to ring
125        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    /// Generate a conic (angular) gradient mesh
146    ///
147    /// Creates a gradient that rotates around a center point
148    #[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        // Center vertex (average color)
159        let center_color = self.sample(0.5);
160        mesh.colored_vertex(center, center_color);
161
162        // Create outer ring with varying colors based on angle
163        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        // Generate triangles from center to perimeter
173        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    /// Generate a rectangular gradient mesh (corner-to-corner)
182    #[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        // Generate triangle strips
209        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/// Interpolate between two colors
220#[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/// Add alpha to a color
232#[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/// Blend two colors using different modes
238#[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/// Blend modes for color composition
277#[derive(Debug, Clone, Copy)]
278pub enum BlendMode {
279    /// Normal blend mode (no blending)
280    Normal,
281    /// Multiply blend mode (darkens)
282    Multiply,
283    /// Screen blend mode (lightens)
284    Screen,
285    /// Overlay blend mode (combines multiply and screen)
286    Overlay,
287}
288
289/// Saturate/desaturate a color
290#[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
305/// Neon color palette presets for aceternity-style effects
306pub struct NeonPalette;
307
308impl NeonPalette {
309    /// Cyberpunk neon palette (blues, purples, pinks)
310    #[must_use]
311    pub fn cyberpunk() -> Vec<Color32> {
312        vec![
313            Color32::from_rgb(0, 255, 255),  // Cyan
314            Color32::from_rgb(255, 0, 255),  // Magenta
315            Color32::from_rgb(138, 43, 226), // Blue Violet
316            Color32::from_rgb(255, 20, 147), // Deep Pink
317            Color32::from_rgb(0, 191, 255),  // Deep Sky Blue
318        ]
319    }
320
321    /// Synthwave palette (purples, pinks, oranges)
322    #[must_use]
323    pub fn synthwave() -> Vec<Color32> {
324        vec![
325            Color32::from_rgb(251, 86, 7),   // Orange
326            Color32::from_rgb(255, 0, 110),  // Pink
327            Color32::from_rgb(131, 58, 180), // Purple
328            Color32::from_rgb(253, 29, 29),  // Red
329            Color32::from_rgb(252, 176, 69), // Yellow
330        ]
331    }
332
333    /// Aurora palette (blues, greens, purples)
334    #[must_use]
335    pub fn aurora() -> Vec<Color32> {
336        vec![
337            Color32::from_rgb(0, 255, 127),   // Spring Green
338            Color32::from_rgb(0, 191, 255),   // Deep Sky Blue
339            Color32::from_rgb(138, 43, 226),  // Blue Violet
340            Color32::from_rgb(64, 224, 208),  // Turquoise
341            Color32::from_rgb(123, 104, 238), // Medium Slate Blue
342        ]
343    }
344
345    /// Neon rainbow (full spectrum, saturated)
346    #[must_use]
347    pub fn rainbow() -> Vec<Color32> {
348        vec![
349            Color32::from_rgb(255, 0, 0),   // Red
350            Color32::from_rgb(255, 127, 0), // Orange
351            Color32::from_rgb(255, 255, 0), // Yellow
352            Color32::from_rgb(0, 255, 0),   // Green
353            Color32::from_rgb(0, 0, 255),   // Blue
354            Color32::from_rgb(75, 0, 130),  // Indigo
355            Color32::from_rgb(148, 0, 211), // Violet
356        ]
357    }
358
359    /// Electric blue palette
360    #[must_use]
361    pub fn electric() -> Vec<Color32> {
362        vec![
363            Color32::from_rgb(59, 130, 246),  // Blue 500
364            Color32::from_rgb(96, 165, 250),  // Blue 400
365            Color32::from_rgb(147, 197, 253), // Blue 300
366            Color32::from_rgb(191, 219, 254), // Blue 200
367        ]
368    }
369
370    /// Hot gradient (red to yellow)
371    #[must_use]
372    pub fn hot() -> Vec<Color32> {
373        vec![
374            Color32::from_rgb(139, 0, 0),   // Dark Red
375            Color32::from_rgb(220, 20, 60), // Crimson
376            Color32::from_rgb(255, 69, 0),  // Red Orange
377            Color32::from_rgb(255, 140, 0), // Dark Orange
378            Color32::from_rgb(255, 215, 0), // Gold
379        ]
380    }
381
382    /// Cool gradient (cyan to blue to purple)
383    #[must_use]
384    pub fn cool() -> Vec<Color32> {
385        vec![
386            Color32::from_rgb(0, 255, 255),  // Cyan
387            Color32::from_rgb(0, 191, 255),  // Deep Sky Blue
388            Color32::from_rgb(65, 105, 225), // Royal Blue
389            Color32::from_rgb(138, 43, 226), // Blue Violet
390        ]
391    }
392
393    /// Premium gold gradient
394    #[must_use]
395    pub fn gold() -> Vec<Color32> {
396        vec![
397            Color32::from_rgb(255, 215, 0),  // Gold
398            Color32::from_rgb(255, 223, 0),  // Golden Yellow
399            Color32::from_rgb(255, 193, 37), // Amber
400            Color32::from_rgb(218, 165, 32), // Goldenrod
401        ]
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        // Should remain gray since there's no color to saturate
439        assert_eq!(saturated.r(), saturated.g());
440    }
441}