Skip to main content

optic_color/
convert.rs

1use crate::{HSL, HSV, RGB, RGBA};
2
3/// Trait for types that can be converted to RGBA losslessly.
4///
5/// This is the primary conversion trait in the color system. Any type
6/// implementing `ToRgba` can be used as a color argument anywhere in
7/// the engine:
8///
9/// ```
10/// use optic_color::*;
11///
12/// fn set_color(c: impl ToRgba) {
13///     let rgba = c.to_rgba();
14///     // ...
15/// }
16///
17/// set_color(RED);
18/// set_color(HSV::new(200.0, 0.8, 0.9));
19/// ```
20///
21/// Implemented for [`RGBA`], [`RGB`], [`HSV`], [`HSL`].
22pub trait ToRgba: Copy {
23    /// Convert to RGBA.
24    fn to_rgba(self) -> RGBA;
25}
26
27/// Trait for types that can be constructed from RGBA.
28///
29/// Implemented for [`RGBA`], [`RGB`], [`HSV`], [`HSL`].
30pub trait FromRgba: Sized {
31    fn from_rgba(rgba: RGBA) -> Self;
32}
33
34impl ToRgba for RGBA {
35    fn to_rgba(self) -> RGBA { self }
36}
37
38impl FromRgba for RGBA {
39    fn from_rgba(rgba: RGBA) -> Self { rgba }
40}
41
42impl ToRgba for RGB {
43    fn to_rgba(self) -> RGBA { RGBA(self.0, self.1, self.2, 1.0) }
44}
45
46impl FromRgba for RGB {
47    fn from_rgba(rgba: RGBA) -> Self { RGB(rgba.0, rgba.1, rgba.2) }
48}
49
50impl ToRgba for HSV {
51    fn to_rgba(self) -> RGBA { hsv_to_rgba(self) }
52}
53
54impl FromRgba for HSV {
55    fn from_rgba(rgba: RGBA) -> Self { rgba_to_hsv(rgba) }
56}
57
58impl ToRgba for HSL {
59    fn to_rgba(self) -> RGBA { hsl_to_rgba(self) }
60}
61
62impl FromRgba for HSL {
63    fn from_rgba(rgba: RGBA) -> Self { rgba_to_hsl(rgba) }
64}
65
66impl From<RGB> for RGBA { fn from(rgb: RGB) -> Self { rgb.to_rgba() } }
67impl From<HSV> for RGBA { fn from(hsv: HSV) -> Self { hsv.to_rgba() } }
68impl From<HSL> for RGBA { fn from(hsl: HSL) -> Self { hsl.to_rgba() } }
69impl From<RGBA> for RGB { fn from(rgba: RGBA) -> Self { RGB::from_rgba(rgba) } }
70impl From<RGBA> for HSV { fn from(rgba: RGBA) -> Self { HSV::from_rgba(rgba) } }
71impl From<RGBA> for HSL { fn from(rgba: RGBA) -> Self { HSL::from_rgba(rgba) } }
72
73pub(crate) fn hsv_to_rgba(hsv: HSV) -> RGBA {
74    let h = hsv.h / 60.0;
75    let s = hsv.s;
76    let v = hsv.v;
77    let i = h.floor() as i32;
78    let f = h - h.floor();
79    let p = v * (1.0 - s);
80    let q = v * (1.0 - s * f);
81    let t = v * (1.0 - s * (1.0 - f));
82    let (r, g, b) = match i % 6 {
83        0 => (v, t, p),
84        1 => (q, v, p),
85        2 => (p, v, t),
86        3 => (p, q, v),
87        4 => (t, p, v),
88        5 => (v, p, q),
89        _ => (v, p, q),
90    };
91    RGBA(r, g, b, 1.0)
92}
93
94pub(crate) fn rgba_to_hsv(rgba: RGBA) -> HSV {
95    let r = rgba.0;
96    let g = rgba.1;
97    let b = rgba.2;
98    let mx = r.max(g).max(b);
99    let mn = r.min(g).min(b);
100    let d = mx - mn;
101    let h = if d == 0.0 {
102        0.0
103    } else if mx == r {
104        60.0 * (((g - b) / d) % 6.0)
105    } else if mx == g {
106        60.0 * (((b - r) / d) + 2.0)
107    } else {
108        60.0 * (((r - g) / d) + 4.0)
109    };
110    let h = if h < 0.0 { h + 360.0 } else { h };
111    let s = if mx == 0.0 { 0.0 } else { d / mx };
112    HSV { h: h.clamp(0.0, 360.0), s: s.clamp(0.0, 1.0), v: mx.clamp(0.0, 1.0) }
113}
114
115pub(crate) fn hsl_to_rgba(hsl: HSL) -> RGBA {
116    let h = hsl.h / 360.0;
117    let s = hsl.s;
118    let l = hsl.l;
119    if s == 0.0 {
120        return RGBA(l, l, l, 1.0);
121    }
122    fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
123        if t < 0.0 { t += 1.0; }
124        if t > 1.0 { t -= 1.0; }
125        if t < 1.0 / 6.0 { p + (q - p) * 6.0 * t }
126        else if t < 1.0 / 2.0 { q }
127        else if t < 2.0 / 3.0 { p + (q - p) * (2.0 / 3.0 - t) * 6.0 }
128        else { p }
129    }
130    let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
131    let p = 2.0 * l - q;
132    let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
133    let g = hue_to_rgb(p, q, h);
134    let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
135    RGBA(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0), 1.0)
136}
137
138pub(crate) fn rgba_to_hsl(rgba: RGBA) -> HSL {
139    let r = rgba.0;
140    let g = rgba.1;
141    let b = rgba.2;
142    let mx = r.max(g).max(b);
143    let mn = r.min(g).min(b);
144    let d = mx - mn;
145    let l = (mx + mn) / 2.0;
146    let s = if d == 0.0 { 0.0 } else { d / (1.0 - (2.0 * l - 1.0).abs()) };
147    let h = if d == 0.0 {
148        0.0
149    } else if mx == r {
150        60.0 * (((g - b) / d) % 6.0)
151    } else if mx == g {
152        60.0 * (((b - r) / d) + 2.0)
153    } else {
154        60.0 * (((r - g) / d) + 4.0)
155    };
156    let h = if h < 0.0 { h + 360.0 } else { h };
157    HSL { h: h.clamp(0.0, 360.0), s: s.clamp(0.0, 1.0), l: l.clamp(0.0, 1.0) }
158}
159
160/// Trait for computing luminance, contrast, and hex/byte serialization.
161///
162/// This trait has a blanket impl for all [`ToRgba`] types, so every color
163/// type gets these methods automatically:
164///
165/// ```
166/// use optic_color::*;
167///
168/// let c = RGB(0.5, 0.2, 0.8);
169/// let lum = c.luminance();
170/// let hex = c.to_hex();
171/// let (r, g, b, a) = c.to_bytes();
172/// ```
173pub trait ColorInfo: ToRgba {
174    /// Relative luminance per ITU-R BT.709.
175    ///
176    /// Uses the standard coefficients: 0.2126 R + 0.7152 G + 0.0722 B.
177    fn luminance(self) -> f32 {
178        let c = self.to_rgba();
179        0.2126 * c.0 + 0.7152 * c.1 + 0.0722 * c.2
180    }
181
182    /// Returns true if the luminance is greater than 0.5.
183    fn is_light(self) -> bool { self.luminance() > 0.5 }
184
185    /// Compute the WCAG contrast ratio against another color.
186    ///
187    /// The result is a value in 1..21. WCAG AA requires 4.5:1 for normal
188    /// text; WCAG AAA requires 7:1.
189    fn contrast_ratio(self, other: impl ToRgba) -> f32 {
190        let l1 = self.luminance();
191        let l2 = other.luminance();
192        let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
193        (lighter + 0.05) / (darker + 0.05)
194    }
195
196    /// Encode as a hex string: `#RRGGBBAA`.
197    fn to_hex(self) -> String {
198        let (r, g, b, a) = self.to_bytes();
199        format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
200    }
201
202    /// Convert to 8-bit byte channels: `(r, g, b, a)` in 0..255.
203    fn to_bytes(self) -> (u8, u8, u8, u8) {
204        let c = self.to_rgba();
205        let r = (c.0.clamp(0.0, 1.0) * 255.0).round() as u8;
206        let g = (c.1.clamp(0.0, 1.0) * 255.0).round() as u8;
207        let b = (c.2.clamp(0.0, 1.0) * 255.0).round() as u8;
208        let a = (c.3.clamp(0.0, 1.0) * 255.0).round() as u8;
209        (r, g, b, a)
210    }
211}
212
213impl<T: ToRgba> ColorInfo for T {}