Skip to main content

chroma_forge/
lib.rs

1//! # Chroma Forge
2//!
3//! A comprehensive color conversion library for Rust that provides seamless
4//! conversion between various color formats including RGB, Hex, HSL, HSV, and CMYK.
5//!
6//! ## Examples
7//!
8//! ```rust
9//! use chroma_forge::Color;
10//!
11//! let color = Color::from_hex("#FF5733").unwrap();
12//! println!("RGB: {}", color.to_rgb());
13//! println!("HSL: {}", color.to_hsl());
14//! ```
15
16use std::fmt;
17
18/// Represents a color with various conversion capabilities
19#[derive(Debug, Clone, PartialEq)]
20pub struct Color {
21    r: u8,
22    g: u8,
23    b: u8,
24    a: f32, // Alpha channel (0.0 to 1.0)
25}
26
27/// RGB color representation
28#[derive(Debug, Clone, PartialEq)]
29pub struct Rgb {
30    pub r: u8,
31    pub g: u8,
32    pub b: u8,
33}
34
35/// HSL (Hue, Saturation, Lightness) color representation
36#[derive(Debug, Clone, PartialEq)]
37pub struct Hsl {
38    pub h: f32, // 0.0 to 360.0
39    pub s: f32, // 0.0 to 100.0
40    pub l: f32, // 0.0 to 100.0
41}
42
43/// HSV (Hue, Saturation, Value) color representation
44#[derive(Debug, Clone, PartialEq)]
45pub struct Hsv {
46    pub h: f32, // 0.0 to 360.0
47    pub s: f32, // 0.0 to 100.0
48    pub v: f32, // 0.0 to 100.0
49}
50
51/// CMYK (Cyan, Magenta, Yellow, Key/Black) color representation
52#[derive(Debug, Clone, PartialEq)]
53pub struct Cmyk {
54    pub c: f32, // 0.0 to 100.0
55    pub m: f32, // 0.0 to 100.0
56    pub y: f32, // 0.0 to 100.0
57    pub k: f32, // 0.0 to 100.0
58}
59
60/// Custom error type for color conversion operations
61#[derive(Debug, PartialEq)]
62pub enum ColorError {
63    InvalidHexFormat,
64    InvalidHexLength,
65    InvalidRgbValue,
66    InvalidHslValue,
67    InvalidHsvValue,
68    InvalidCmykValue,
69    InvalidMinecraftCode,
70}
71
72impl fmt::Display for ColorError {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            ColorError::InvalidHexFormat => write!(f, "Invalid hex color format"),
76            ColorError::InvalidHexLength => write!(f, "Invalid hex color length"),
77            ColorError::InvalidRgbValue => write!(f, "RGB values must be between 0 and 255"),
78            ColorError::InvalidHslValue => write!(f, "Invalid HSL values"),
79            ColorError::InvalidHsvValue => write!(f, "Invalid HSV values"),
80            ColorError::InvalidCmykValue => write!(f, "CMYK values must be between 0 and 100"),
81            ColorError::InvalidMinecraftCode => write!(f, "Invalid Minecraft color code"),
82        }
83    }
84}
85
86impl std::error::Error for ColorError {}
87
88impl Color {
89    /// Create a new Color from RGB values
90    pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
91        Color { r, g, b, a: 1.0 }
92    }
93
94    /// Create a new Color from RGBA values
95    pub fn from_rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
96        Color {
97            r,
98            g,
99            b,
100            a: a.clamp(0.0, 1.0),
101        }
102    }
103
104    /// Create a Color from a hex string (e.g., "#FF5733" or "FF5733")
105    pub fn from_hex(hex: &str) -> Result<Self, ColorError> {
106        let hex = hex.trim_start_matches('#');
107
108        match hex.len() {
109            3 => {
110                // Short form: #RGB -> #RRGGBB
111                let chars: Vec<char> = hex.chars().collect();
112                let r = u8::from_str_radix(&format!("{}{}", chars[0], chars[0]), 16)
113                    .map_err(|_| ColorError::InvalidHexFormat)?;
114                let g = u8::from_str_radix(&format!("{}{}", chars[1], chars[1]), 16)
115                    .map_err(|_| ColorError::InvalidHexFormat)?;
116                let b = u8::from_str_radix(&format!("{}{}", chars[2], chars[2]), 16)
117                    .map_err(|_| ColorError::InvalidHexFormat)?;
118                Ok(Color::from_rgb(r, g, b))
119            }
120            6 => {
121                // Full form: #RRGGBB
122                let r =
123                    u8::from_str_radix(&hex[0..2], 16).map_err(|_| ColorError::InvalidHexFormat)?;
124                let g =
125                    u8::from_str_radix(&hex[2..4], 16).map_err(|_| ColorError::InvalidHexFormat)?;
126                let b =
127                    u8::from_str_radix(&hex[4..6], 16).map_err(|_| ColorError::InvalidHexFormat)?;
128                Ok(Color::from_rgb(r, g, b))
129            }
130            8 => {
131                // With alpha: #RRGGBBAA
132                let r =
133                    u8::from_str_radix(&hex[0..2], 16).map_err(|_| ColorError::InvalidHexFormat)?;
134                let g =
135                    u8::from_str_radix(&hex[2..4], 16).map_err(|_| ColorError::InvalidHexFormat)?;
136                let b =
137                    u8::from_str_radix(&hex[4..6], 16).map_err(|_| ColorError::InvalidHexFormat)?;
138                let a = u8::from_str_radix(&hex[6..8], 16)
139                    .map_err(|_| ColorError::InvalidHexFormat)? as f32
140                    / 255.0;
141                Ok(Color::from_rgba(r, g, b, a))
142            }
143            _ => Err(ColorError::InvalidHexLength),
144        }
145    }
146
147    /// Create a Color from HSL values
148    pub fn from_hsl(h: f32, s: f32, l: f32) -> Result<Self, ColorError> {
149        if s < 0.0 || s > 100.0 || l < 0.0 || l > 100.0 {
150            return Err(ColorError::InvalidHslValue);
151        }
152
153        let h = h % 360.0;
154        let s = s / 100.0;
155        let l = l / 100.0;
156
157        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
158        let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
159        let m = l - c / 2.0;
160
161        let (r_prime, g_prime, b_prime) = match h {
162            h if h < 60.0 => (c, x, 0.0),
163            h if h < 120.0 => (x, c, 0.0),
164            h if h < 180.0 => (0.0, c, x),
165            h if h < 240.0 => (0.0, x, c),
166            h if h < 300.0 => (x, 0.0, c),
167            _ => (c, 0.0, x),
168        };
169
170        let r = ((r_prime + m) * 255.0).round() as u8;
171        let g = ((g_prime + m) * 255.0).round() as u8;
172        let b = ((b_prime + m) * 255.0).round() as u8;
173
174        Ok(Color::from_rgb(r, g, b))
175    }
176
177    /// Create a Color from Minecraft color code (e.g., "§c", "&4", "&#FF5733")
178    pub fn from_minecraft_code(code: &str) -> Result<Self, ColorError> {
179        if code.is_empty() {
180            return Err(ColorError::InvalidMinecraftCode);
181        }
182
183        // Handle modern hex format: &#RRGGBB or &x&R&R&G&G&B&B
184        if code.starts_with("&#") && code.len() == 8 {
185            return Color::from_hex(&code[2..]);
186        }
187
188        // Handle alternate hex format: &x&R&R&G&G&B&B
189        if code.starts_with("&x") && code.len() == 14 {
190            let hex_chars: String = code
191                .chars()
192                .skip(2)
193                .enumerate()
194                .filter_map(|(i, c)| if i % 2 == 1 { Some(c) } else { None })
195                .collect();
196            return Color::from_hex(&hex_chars);
197        }
198
199        // Handle legacy color codes
200        let color_char = if code.starts_with('§') || code.starts_with('&') {
201            code.chars()
202                .nth(1)
203                .ok_or(ColorError::InvalidMinecraftCode)?
204        } else if code.len() == 1 {
205            code.chars().next().unwrap()
206        } else {
207            return Err(ColorError::InvalidMinecraftCode);
208        };
209
210        match color_char {
211            '0' => Ok(Color::from_rgb(0, 0, 0)),             // Black
212            '1' => Ok(Color::from_rgb(0, 0, 170)),           // Dark Blue
213            '2' => Ok(Color::from_rgb(0, 170, 0)),           // Dark Green
214            '3' => Ok(Color::from_rgb(0, 170, 170)),         // Dark Aqua
215            '4' => Ok(Color::from_rgb(170, 0, 0)),           // Dark Red
216            '5' => Ok(Color::from_rgb(170, 0, 170)),         // Dark Purple
217            '6' => Ok(Color::from_rgb(255, 170, 0)),         // Gold
218            '7' => Ok(Color::from_rgb(170, 170, 170)),       // Gray
219            '8' => Ok(Color::from_rgb(85, 85, 85)),          // Dark Gray
220            '9' => Ok(Color::from_rgb(85, 85, 255)),         // Blue
221            'a' | 'A' => Ok(Color::from_rgb(85, 255, 85)),   // Green
222            'b' | 'B' => Ok(Color::from_rgb(85, 255, 255)),  // Aqua
223            'c' | 'C' => Ok(Color::from_rgb(255, 85, 85)),   // Red
224            'd' | 'D' => Ok(Color::from_rgb(255, 85, 255)),  // Light Purple
225            'e' | 'E' => Ok(Color::from_rgb(255, 255, 85)),  // Yellow
226            'f' | 'F' => Ok(Color::from_rgb(255, 255, 255)), // White
227            _ => Err(ColorError::InvalidMinecraftCode),
228        }
229    }
230
231    /// Create a Color from HSV values
232    pub fn from_hsv(h: f32, s: f32, v: f32) -> Result<Self, ColorError> {
233        if s < 0.0 || s > 100.0 || v < 0.0 || v > 100.0 {
234            return Err(ColorError::InvalidHsvValue);
235        }
236
237        let h = h % 360.0;
238        let s = s / 100.0;
239        let v = v / 100.0;
240
241        let c = v * s;
242        let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
243        let m = v - c;
244
245        let (r_prime, g_prime, b_prime) = match h {
246            h if h < 60.0 => (c, x, 0.0),
247            h if h < 120.0 => (x, c, 0.0),
248            h if h < 180.0 => (0.0, c, x),
249            h if h < 240.0 => (0.0, x, c),
250            h if h < 300.0 => (x, 0.0, c),
251            _ => (c, 0.0, x),
252        };
253
254        let r = ((r_prime + m) * 255.0).round() as u8;
255        let g = ((g_prime + m) * 255.0).round() as u8;
256        let b = ((b_prime + m) * 255.0).round() as u8;
257
258        Ok(Color::from_rgb(r, g, b))
259    }
260
261    /// Create a Color from CMYK values
262    pub fn from_cmyk(c: f32, m: f32, y: f32, k: f32) -> Result<Self, ColorError> {
263        if c < 0.0
264            || c > 100.0
265            || m < 0.0
266            || m > 100.0
267            || y < 0.0
268            || y > 100.0
269            || k < 0.0
270            || k > 100.0
271        {
272            return Err(ColorError::InvalidCmykValue);
273        }
274
275        let c = c / 100.0;
276        let m = m / 100.0;
277        let y = y / 100.0;
278        let k = k / 100.0;
279
280        let r = (255.0 * (1.0 - c) * (1.0 - k)).round() as u8;
281        let g = (255.0 * (1.0 - m) * (1.0 - k)).round() as u8;
282        let b = (255.0 * (1.0 - y) * (1.0 - k)).round() as u8;
283
284        Ok(Color::from_rgb(r, g, b))
285    }
286
287    /// Convert to RGB
288    pub fn to_rgb(&self) -> Rgb {
289        Rgb {
290            r: self.r,
291            g: self.g,
292            b: self.b,
293        }
294    }
295
296    /// Convert to hex string
297    pub fn to_hex(&self) -> String {
298        format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
299    }
300
301    /// Convert to hex string with alpha
302    pub fn to_hex_alpha(&self) -> String {
303        let alpha = (self.a * 255.0).round() as u8;
304        format!("#{:02X}{:02X}{:02X}{:02X}", self.r, self.g, self.b, alpha)
305    }
306
307    /// Convert to HSL
308    pub fn to_hsl(&self) -> Hsl {
309        let r = self.r as f32 / 255.0;
310        let g = self.g as f32 / 255.0;
311        let b = self.b as f32 / 255.0;
312
313        let max = r.max(g.max(b));
314        let min = r.min(g.min(b));
315        let delta = max - min;
316
317        let l = (max + min) / 2.0;
318
319        let (h, s) = if delta == 0.0 {
320            (0.0, 0.0)
321        } else {
322            let s = if l < 0.5 {
323                delta / (max + min)
324            } else {
325                delta / (2.0 - max - min)
326            };
327
328            let h = match max {
329                x if x == r => ((g - b) / delta + if g < b { 6.0 } else { 0.0 }) * 60.0,
330                x if x == g => ((b - r) / delta + 2.0) * 60.0,
331                _ => ((r - g) / delta + 4.0) * 60.0,
332            };
333
334            (h, s * 100.0)
335        };
336
337        Hsl { h, s, l: l * 100.0 }
338    }
339
340    /// Convert to HSV
341    pub fn to_hsv(&self) -> Hsv {
342        let r = self.r as f32 / 255.0;
343        let g = self.g as f32 / 255.0;
344        let b = self.b as f32 / 255.0;
345
346        let max = r.max(g.max(b));
347        let min = r.min(g.min(b));
348        let delta = max - min;
349
350        let v = max;
351
352        let (h, s) = if delta == 0.0 {
353            (0.0, 0.0)
354        } else {
355            let s = delta / max;
356
357            let h = match max {
358                x if x == r => ((g - b) / delta + if g < b { 6.0 } else { 0.0 }) * 60.0,
359                x if x == g => ((b - r) / delta + 2.0) * 60.0,
360                _ => ((r - g) / delta + 4.0) * 60.0,
361            };
362
363            (h, s * 100.0)
364        };
365
366        Hsv { h, s, v: v * 100.0 }
367    }
368
369    /// Convert to CMYK
370    pub fn to_cmyk(&self) -> Cmyk {
371        let r = self.r as f32 / 255.0;
372        let g = self.g as f32 / 255.0;
373        let b = self.b as f32 / 255.0;
374
375        let k = 1.0 - r.max(g.max(b));
376
377        if k == 1.0 {
378            Cmyk {
379                c: 0.0,
380                m: 0.0,
381                y: 0.0,
382                k: 100.0,
383            }
384        } else {
385            let c = (1.0 - r - k) / (1.0 - k) * 100.0;
386            let m = (1.0 - g - k) / (1.0 - k) * 100.0;
387            let y = (1.0 - b - k) / (1.0 - k) * 100.0;
388
389            Cmyk {
390                c,
391                m,
392                y,
393                k: k * 100.0,
394            }
395        }
396    }
397
398    /// Get luminance (brightness) of the color
399    pub fn luminance(&self) -> f32 {
400        let r = self.r as f32 / 255.0;
401        let g = self.g as f32 / 255.0;
402        let b = self.b as f32 / 255.0;
403
404        0.299 * r + 0.587 * g + 0.114 * b
405    }
406
407    /// Check if the color is considered "dark" (luminance < 0.5)
408    pub fn is_dark(&self) -> bool {
409        self.luminance() < 0.5
410    }
411
412    /// Check if the color is considered "light" (luminance >= 0.5)
413    pub fn is_light(&self) -> bool {
414        !self.is_dark()
415    }
416
417    /// Get a contrasting color (black or white) for text overlay
418    pub fn contrasting_text_color(&self) -> Color {
419        if self.is_dark() {
420            Color::from_rgb(255, 255, 255) // White
421        } else {
422            Color::from_rgb(0, 0, 0) // Black
423        }
424    }
425
426    /// Blend this color with another color
427    pub fn blend(&self, other: &Color, ratio: f32) -> Color {
428        let ratio = ratio.clamp(0.0, 1.0);
429        let inv_ratio = 1.0 - ratio;
430
431        let r = (self.r as f32 * inv_ratio + other.r as f32 * ratio).round() as u8;
432        let g = (self.g as f32 * inv_ratio + other.g as f32 * ratio).round() as u8;
433        let b = (self.b as f32 * inv_ratio + other.b as f32 * ratio).round() as u8;
434        let a = self.a * inv_ratio + other.a * ratio;
435
436        Color::from_rgba(r, g, b, a)
437    }
438
439    /// Convert to Minecraft legacy color code (closest match)
440    pub fn to_minecraft_code(&self) -> String {
441        let distances: Vec<(f32, char)> = vec![
442            (self.color_distance(&Color::from_rgb(0, 0, 0)), '0'), // Black
443            (self.color_distance(&Color::from_rgb(0, 0, 170)), '1'), // Dark Blue
444            (self.color_distance(&Color::from_rgb(0, 170, 0)), '2'), // Dark Green
445            (self.color_distance(&Color::from_rgb(0, 170, 170)), '3'), // Dark Aqua
446            (self.color_distance(&Color::from_rgb(170, 0, 0)), '4'), // Dark Red
447            (self.color_distance(&Color::from_rgb(170, 0, 170)), '5'), // Dark Purple
448            (self.color_distance(&Color::from_rgb(255, 170, 0)), '6'), // Gold
449            (self.color_distance(&Color::from_rgb(170, 170, 170)), '7'), // Gray
450            (self.color_distance(&Color::from_rgb(85, 85, 85)), '8'), // Dark Gray
451            (self.color_distance(&Color::from_rgb(85, 85, 255)), '9'), // Blue
452            (self.color_distance(&Color::from_rgb(85, 255, 85)), 'a'), // Green
453            (self.color_distance(&Color::from_rgb(85, 255, 255)), 'b'), // Aqua
454            (self.color_distance(&Color::from_rgb(255, 85, 85)), 'c'), // Red
455            (self.color_distance(&Color::from_rgb(255, 85, 255)), 'd'), // Light Purple
456            (self.color_distance(&Color::from_rgb(255, 255, 85)), 'e'), // Yellow
457            (self.color_distance(&Color::from_rgb(255, 255, 255)), 'f'), // White
458        ];
459
460        let closest = distances
461            .iter()
462            .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
463            .unwrap();
464        format!("§{}", closest.1)
465    }
466
467    /// Convert to Minecraft modern hex color code format
468    pub fn to_minecraft_hex(&self) -> String {
469        format!("&#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
470    }
471
472    /// Convert to Minecraft alternate hex format (&x&R&R&G&G&B&B)
473    pub fn to_minecraft_hex_alt(&self) -> String {
474        let hex = format!("{:02X}{:02X}{:02X}", self.r, self.g, self.b);
475        let mut result = String::from("&x");
476        for ch in hex.chars() {
477            result.push('&');
478            result.push(ch);
479        }
480        result
481    }
482
483    /// Helper function to calculate color distance for closest match
484    fn color_distance(&self, other: &Color) -> f32 {
485        let dr = self.r as f32 - other.r as f32;
486        let dg = self.g as f32 - other.g as f32;
487        let db = self.b as f32 - other.b as f32;
488        (dr * dr + dg * dg + db * db).sqrt()
489    }
490
491    /// Darken the color by a percentage
492    pub fn darken(&self, percentage: f32) -> Color {
493        let factor = 1.0 - (percentage / 100.0).clamp(0.0, 1.0);
494        let r = (self.r as f32 * factor).round() as u8;
495        let g = (self.g as f32 * factor).round() as u8;
496        let b = (self.b as f32 * factor).round() as u8;
497
498        Color::from_rgba(r, g, b, self.a)
499    }
500
501    /// Lighten the color by a percentage
502    pub fn lighten(&self, percentage: f32) -> Color {
503        let factor = (percentage / 100.0).clamp(0.0, 1.0);
504        let r = (self.r as f32 + (255.0 - self.r as f32) * factor).round() as u8;
505        let g = (self.g as f32 + (255.0 - self.g as f32) * factor).round() as u8;
506        let b = (self.b as f32 + (255.0 - self.b as f32) * factor).round() as u8;
507
508        Color::from_rgba(r, g, b, self.a)
509    }
510}
511
512// Display implementations for easy printing
513impl fmt::Display for Rgb {
514    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515        write!(f, "rgb({}, {}, {})", self.r, self.g, self.b)
516    }
517}
518
519impl fmt::Display for Hsl {
520    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521        write!(f, "hsl({:.1}°, {:.1}%, {:.1}%)", self.h, self.s, self.l)
522    }
523}
524
525impl fmt::Display for Hsv {
526    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527        write!(f, "hsv({:.1}°, {:.1}%, {:.1}%)", self.h, self.s, self.v)
528    }
529}
530
531impl fmt::Display for Cmyk {
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        write!(
534            f,
535            "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)",
536            self.c, self.m, self.y, self.k
537        )
538    }
539}
540
541impl fmt::Display for Color {
542    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543        write!(f, "{}", self.to_hex())
544    }
545}
546
547// Common color constants
548impl Color {
549    pub const BLACK: Color = Color {
550        r: 0,
551        g: 0,
552        b: 0,
553        a: 1.0,
554    };
555    pub const WHITE: Color = Color {
556        r: 255,
557        g: 255,
558        b: 255,
559        a: 1.0,
560    };
561    pub const RED: Color = Color {
562        r: 255,
563        g: 0,
564        b: 0,
565        a: 1.0,
566    };
567    pub const GREEN: Color = Color {
568        r: 0,
569        g: 255,
570        b: 0,
571        a: 1.0,
572    };
573    pub const BLUE: Color = Color {
574        r: 0,
575        g: 0,
576        b: 255,
577        a: 1.0,
578    };
579    pub const YELLOW: Color = Color {
580        r: 255,
581        g: 255,
582        b: 0,
583        a: 1.0,
584    };
585    pub const CYAN: Color = Color {
586        r: 0,
587        g: 255,
588        b: 255,
589        a: 1.0,
590    };
591    pub const MAGENTA: Color = Color {
592        r: 255,
593        g: 0,
594        b: 255,
595        a: 1.0,
596    };
597
598    // Minecraft color constants
599    pub const MC_BLACK: Color = Color {
600        r: 0,
601        g: 0,
602        b: 0,
603        a: 1.0,
604    };
605    pub const MC_DARK_BLUE: Color = Color {
606        r: 0,
607        g: 0,
608        b: 170,
609        a: 1.0,
610    };
611    pub const MC_DARK_GREEN: Color = Color {
612        r: 0,
613        g: 170,
614        b: 0,
615        a: 1.0,
616    };
617    pub const MC_DARK_AQUA: Color = Color {
618        r: 0,
619        g: 170,
620        b: 170,
621        a: 1.0,
622    };
623    pub const MC_DARK_RED: Color = Color {
624        r: 170,
625        g: 0,
626        b: 0,
627        a: 1.0,
628    };
629    pub const MC_DARK_PURPLE: Color = Color {
630        r: 170,
631        g: 0,
632        b: 170,
633        a: 1.0,
634    };
635    pub const MC_GOLD: Color = Color {
636        r: 255,
637        g: 170,
638        b: 0,
639        a: 1.0,
640    };
641    pub const MC_GRAY: Color = Color {
642        r: 170,
643        g: 170,
644        b: 170,
645        a: 1.0,
646    };
647    pub const MC_DARK_GRAY: Color = Color {
648        r: 85,
649        g: 85,
650        b: 85,
651        a: 1.0,
652    };
653    pub const MC_BLUE: Color = Color {
654        r: 85,
655        g: 85,
656        b: 255,
657        a: 1.0,
658    };
659    pub const MC_GREEN: Color = Color {
660        r: 85,
661        g: 255,
662        b: 85,
663        a: 1.0,
664    };
665    pub const MC_AQUA: Color = Color {
666        r: 85,
667        g: 255,
668        b: 255,
669        a: 1.0,
670    };
671    pub const MC_RED: Color = Color {
672        r: 255,
673        g: 85,
674        b: 85,
675        a: 1.0,
676    };
677    pub const MC_LIGHT_PURPLE: Color = Color {
678        r: 255,
679        g: 85,
680        b: 255,
681        a: 1.0,
682    };
683    pub const MC_YELLOW: Color = Color {
684        r: 255,
685        g: 255,
686        b: 85,
687        a: 1.0,
688    };
689    pub const MC_WHITE: Color = Color {
690        r: 255,
691        g: 255,
692        b: 255,
693        a: 1.0,
694    };
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700
701    #[test]
702    fn test_hex_to_rgb() {
703        let color = Color::from_hex("#FF5733").unwrap();
704        assert_eq!(color.r, 255);
705        assert_eq!(color.g, 87);
706        assert_eq!(color.b, 51);
707    }
708
709    #[test]
710    fn test_rgb_to_hex() {
711        let color = Color::from_rgb(255, 87, 51);
712        assert_eq!(color.to_hex(), "#FF5733");
713    }
714
715    #[test]
716    fn test_short_hex() {
717        let color = Color::from_hex("#F53").unwrap();
718        assert_eq!(color.r, 255);
719        assert_eq!(color.g, 85);
720        assert_eq!(color.b, 51);
721    }
722
723    #[test]
724    fn test_hsl_conversion() {
725        let color = Color::from_hsl(10.0, 100.0, 60.0).unwrap();
726        let hsl = color.to_hsl();
727        assert!((hsl.h - 10.0).abs() < 1.0);
728        assert!((hsl.s - 100.0).abs() < 1.0);
729        assert!((hsl.l - 60.0).abs() < 1.0);
730    }
731
732    #[test]
733    fn test_color_blending() {
734        let red = Color::RED;
735        let blue = Color::BLUE;
736        let purple = red.blend(&blue, 0.5);
737
738        assert_eq!(purple.r, 127);
739        assert_eq!(purple.g, 0);
740        assert_eq!(purple.b, 127);
741    }
742
743    #[test]
744    fn test_luminance() {
745        assert!(Color::WHITE.is_light());
746        assert!(Color::BLACK.is_dark());
747    }
748
749    #[test]
750    fn test_darken_lighten() {
751        let color = Color::from_rgb(100, 100, 100);
752        let darker = color.darken(50.0);
753        let lighter = color.lighten(50.0);
754
755        assert!(darker.r < color.r);
756        assert!(lighter.r > color.r);
757    }
758
759    #[test]
760    fn test_cmyk_conversion() {
761        let color = Color::from_cmyk(0.0, 100.0, 100.0, 0.0).unwrap(); // Should be red
762        assert_eq!(color.r, 255);
763        assert_eq!(color.g, 0);
764        assert_eq!(color.b, 0);
765    }
766
767    #[test]
768    fn test_minecraft_legacy_codes() {
769        // Test legacy color codes
770        let red = Color::from_minecraft_code("§c").unwrap();
771        assert_eq!(red.r, 255);
772        assert_eq!(red.g, 85);
773        assert_eq!(red.b, 85);
774
775        let blue = Color::from_minecraft_code("&1").unwrap();
776        assert_eq!(blue.r, 0);
777        assert_eq!(blue.g, 0);
778        assert_eq!(blue.b, 170);
779
780        let yellow = Color::from_minecraft_code("e").unwrap();
781        assert_eq!(yellow.r, 255);
782        assert_eq!(yellow.g, 255);
783        assert_eq!(yellow.b, 85);
784    }
785
786    #[test]
787    fn test_minecraft_hex_codes() {
788        // Test modern hex format
789        let color = Color::from_minecraft_code("&#FF5733").unwrap();
790        assert_eq!(color.r, 255);
791        assert_eq!(color.g, 87);
792        assert_eq!(color.b, 51);
793
794        // Test alternate hex format
795        let color2 = Color::from_minecraft_code("&x&F&F&5&7&3&3").unwrap();
796        assert_eq!(color2.r, 255);
797        assert_eq!(color2.g, 87);
798        assert_eq!(color2.b, 51);
799    }
800
801    #[test]
802    fn test_minecraft_code_conversion() {
803        let color = Color::from_rgb(255, 85, 85);
804        let mc_code = color.to_minecraft_code();
805        assert_eq!(mc_code, "§c");
806
807        let hex_code = color.to_minecraft_hex();
808        assert_eq!(hex_code, "&#FF5555");
809
810        let alt_hex = color.to_minecraft_hex_alt();
811        assert_eq!(alt_hex, "&x&F&F&5&5&5&5");
812    }
813
814    #[test]
815    fn test_minecraft_constants() {
816        assert_eq!(Color::MC_RED.r, 255);
817        assert_eq!(Color::MC_RED.g, 85);
818        assert_eq!(Color::MC_RED.b, 85);
819
820        assert_eq!(Color::MC_GOLD.r, 255);
821        assert_eq!(Color::MC_GOLD.g, 170);
822        assert_eq!(Color::MC_GOLD.b, 0);
823    }
824}