Skip to main content

iced_core/
color.rs

1//! Manage colors in different color spaces.
2use crate::animation::Interpolable;
3
4/// A color in the `sRGB` color space.
5///
6/// # String Representation
7///
8/// A color can be represented in either of the following valid formats: `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`.
9/// Where `rgba` represent hexadecimal digits. Both uppercase and lowercase letters are supported.
10///
11/// If `a` (transparency) is not specified, `1.0` (completely opaque) would be used by default.
12///
13/// If you have a static color string, using the [`color!`] macro should be preferred
14/// since it leverages hexadecimal literal notation and arithmetic directly.
15///
16/// [`color!`]: crate::color!
17#[derive(Debug, Clone, Copy, PartialEq, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[must_use]
20pub struct Color {
21    /// Red component, 0.0 - 1.0
22    pub r: f32,
23    /// Green component, 0.0 - 1.0
24    pub g: f32,
25    /// Blue component, 0.0 - 1.0
26    pub b: f32,
27    /// Transparency, 0.0 - 1.0
28    pub a: f32,
29}
30
31impl Color {
32    /// The black color.
33    pub const BLACK: Color = Color {
34        r: 0.0,
35        g: 0.0,
36        b: 0.0,
37        a: 1.0,
38    };
39
40    /// The white color.
41    pub const WHITE: Color = Color {
42        r: 1.0,
43        g: 1.0,
44        b: 1.0,
45        a: 1.0,
46    };
47
48    /// A color with no opacity.
49    pub const TRANSPARENT: Color = Color {
50        r: 0.0,
51        g: 0.0,
52        b: 0.0,
53        a: 0.0,
54    };
55
56    /// Creates a new [`Color`].
57    ///
58    /// In debug mode, it will panic if the values are not in the correct
59    /// range: 0.0 - 1.0
60    const fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
61        debug_assert!(
62            r >= 0.0 && r <= 1.0,
63            "Red component must be in [0, 1] range."
64        );
65        debug_assert!(
66            g >= 0.0 && g <= 1.0,
67            "Green component must be in [0, 1] range."
68        );
69        debug_assert!(
70            b >= 0.0 && b <= 1.0,
71            "Blue component must be in [0, 1] range."
72        );
73
74        Self { r, g, b, a }
75    }
76
77    /// Creates a [`Color`] from its RGB components.
78    pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
79        Self::from_rgba(r, g, b, 1.0f32)
80    }
81
82    /// Creates a [`Color`] from its RGBA components.
83    pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
84        Self::new(r, g, b, a)
85    }
86
87    /// Creates a [`Color`] from its RGB8 components.
88    pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
89        Self::from_rgba8(r, g, b, 1.0)
90    }
91
92    /// Creates a [`Color`] from its RGB8 components and an alpha value.
93    pub const fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Self {
94        Self::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a)
95    }
96
97    /// Creates a [`Color`] from its RGB8 components packed in the lower bits of a `u32`.
98    pub const fn from_packed_rgb8(rgb: u32) -> Self {
99        Self::from_packed_rgba8(rgb, 1.0)
100    }
101
102    /// Creates a [`Color`] from its RGB8 components packed in the lower bits of a `u32`
103    /// and an alpha value.
104    pub const fn from_packed_rgba8(rgb: u32, a: f32) -> Self {
105        let r = (rgb & 0xff0000) >> 16;
106        let g = (rgb & 0xff00) >> 8;
107        let b = rgb & 0xff;
108
109        Self::from_rgba8(r as u8, g as u8, b as u8, a)
110    }
111
112    /// Creates a [`Color`] from its linear RGBA components.
113    pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
114        // As described in:
115        // https://en.wikipedia.org/wiki/SRGB
116        fn gamma_component(u: f32) -> f32 {
117            if u < 0.0031308 {
118                12.92 * u
119            } else {
120                1.055 * u.powf(1.0 / 2.4) - 0.055
121            }
122        }
123
124        Self::new(
125            gamma_component(r),
126            gamma_component(g),
127            gamma_component(b),
128            a,
129        )
130    }
131
132    /// Creates the most approximate [`Color`] from its [`Oklch`] representation.
133    pub fn from_oklch(oklch: Oklch) -> Color {
134        // https://en.wikipedia.org/wiki/Oklab_color_space#Conversions_between_color_spaces
135        let Oklch { l, c, h, a: alpha } = oklch;
136
137        let a = c * h.cos();
138        let b = c * h.sin();
139
140        // Oklab → LMS (nonlinear)
141        let l_ = l + 0.39633778 * a + 0.21580376 * b;
142        let m_ = l - 0.105561346 * a - 0.06385417 * b;
143        let s_ = l - 0.08948418 * a - 1.2914855 * b;
144
145        // Cubing back
146        let l = l_ * l_ * l_;
147        let m = m_ * m_ * m_;
148        let s = s_ * s_ * s_;
149
150        let r = 4.0767417 * l - 3.3077116 * m + 0.23096994 * s;
151        let g = -1.268438 * l + 2.6097574 * m - 0.34131938 * s;
152        let b = -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s;
153
154        Color::from_linear_rgba(
155            r.clamp(0.0, 1.0),
156            g.clamp(0.0, 1.0),
157            b.clamp(0.0, 1.0),
158            alpha,
159        )
160    }
161
162    /// Inverts the [`Color`] in-place.
163    pub const fn invert(&mut self) {
164        self.r = 1.0f32 - self.r;
165        self.b = 1.0f32 - self.g;
166        self.g = 1.0f32 - self.b;
167    }
168
169    /// Returns the inverted [`Color`].
170    pub const fn inverse(self) -> Self {
171        Self::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
172    }
173
174    /// Scales the alpha channel of the [`Color`] by the given factor.
175    pub const fn scale_alpha(self, factor: f32) -> Self {
176        Self {
177            a: self.a * factor,
178            ..self
179        }
180    }
181
182    /// Mixes the current [`Color`] with another one by the given factor.
183    pub fn mix(self, b: Color, factor: f32) -> Color {
184        let b_amount = factor.clamp(0.0, 1.0);
185        let a_amount = 1.0 - b_amount;
186
187        let a_linear = self.into_linear().map(|c| c * a_amount);
188        let b_linear = b.into_linear().map(|c| c * b_amount);
189
190        Color::from_linear_rgba(
191            a_linear[0] + b_linear[0],
192            a_linear[1] + b_linear[1],
193            a_linear[2] + b_linear[2],
194            a_linear[3] + b_linear[3],
195        )
196    }
197
198    /// Returns the relative luminance of the [`Color`].
199    /// <https://www.w3.org/TR/WCAG21/#dfn-relative-luminance>
200    #[must_use]
201    pub fn relative_luminance(self) -> f32 {
202        let linear = self.into_linear();
203        0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
204    }
205
206    /// Returns the [relative contrast ratio] of the [`Color`] against another one.
207    ///
208    /// [relative contrast ratio]: https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
209    #[must_use]
210    pub fn relative_contrast(self, b: Self) -> f32 {
211        let lum_a = self.relative_luminance();
212        let lum_b = b.relative_luminance();
213
214        (lum_a.max(lum_b) + 0.05) / (lum_a.min(lum_b) + 0.05)
215    }
216
217    /// Returns true if the current [`Color`] is readable on top
218    /// of the given background [`Color`].
219    #[must_use]
220    pub fn is_readable_on(self, background: Self) -> bool {
221        background.relative_contrast(self) >= 6.0
222    }
223
224    /// Converts the [`Color`] into its RGBA8 equivalent.
225    #[must_use]
226    pub const fn into_rgba8(self) -> [u8; 4] {
227        [
228            (self.r * 255.0).round() as u8,
229            (self.g * 255.0).round() as u8,
230            (self.b * 255.0).round() as u8,
231            (self.a * 255.0).round() as u8,
232        ]
233    }
234
235    /// Converts the [`Color`] into its linear values.
236    #[must_use]
237    pub fn into_linear(self) -> [f32; 4] {
238        // As described in:
239        // https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
240        fn linear_component(u: f32) -> f32 {
241            if u < 0.04045 {
242                u / 12.92
243            } else {
244                ((u + 0.055) / 1.055).powf(2.4)
245            }
246        }
247
248        [
249            linear_component(self.r),
250            linear_component(self.g),
251            linear_component(self.b),
252            self.a,
253        ]
254    }
255
256    /// Converts the [`Color`] into its [`Oklch`] representation.
257    pub fn into_oklch(self) -> Oklch {
258        // https://en.wikipedia.org/wiki/Oklab_color_space#Conversions_between_color_spaces
259        let [r, g, b, alpha] = self.into_linear();
260
261        // linear RGB → LMS
262        let l = 0.41222146 * r + 0.53633255 * g + 0.051445995 * b;
263        let m = 0.2119035 * r + 0.6806995 * g + 0.10739696 * b;
264        let s = 0.08830246 * r + 0.28171885 * g + 0.6299787 * b;
265
266        // Nonlinear transform (cube root)
267        let l_ = l.cbrt();
268        let m_ = m.cbrt();
269        let s_ = s.cbrt();
270
271        // LMS → Oklab
272        let l = 0.21045426 * l_ + 0.7936178 * m_ - 0.004072047 * s_;
273        let a = 1.9779985 * l_ - 2.4285922 * m_ + 0.4505937 * s_;
274        let b = 0.025904037 * l_ + 0.78277177 * m_ - 0.80867577 * s_;
275
276        // Oklab → Oklch
277        let c = (a * a + b * b).sqrt();
278        let h = b.atan2(a); // radians
279
280        Oklch { l, c, h, a: alpha }
281    }
282}
283
284impl From<[f32; 3]> for Color {
285    fn from([r, g, b]: [f32; 3]) -> Self {
286        Color::new(r, g, b, 1.0)
287    }
288}
289
290impl From<[f32; 4]> for Color {
291    fn from([r, g, b, a]: [f32; 4]) -> Self {
292        Color::new(r, g, b, a)
293    }
294}
295
296impl From<Oklch> for Color {
297    fn from(oklch: Oklch) -> Self {
298        Self::from_oklch(oklch)
299    }
300}
301
302impl From<Color> for Oklch {
303    fn from(color: Color) -> Self {
304        color.into_oklch()
305    }
306}
307
308/// An error which can be returned when parsing color from an RGB hexadecimal string.
309///
310/// See [`Color`] for specifications for the string.
311#[derive(Debug, thiserror::Error)]
312pub enum ParseError {
313    /// The string could not be parsed to valid integers.
314    #[error(transparent)]
315    ParseIntError(#[from] std::num::ParseIntError),
316    /// The string is of invalid length.
317    #[error("expected hex string of length 3, 4, 6 or 8 excluding optional prefix '#', found {0}")]
318    InvalidLength(usize),
319}
320
321impl std::str::FromStr for Color {
322    type Err = ParseError;
323
324    fn from_str(s: &str) -> Result<Self, Self::Err> {
325        let hex = s.strip_prefix('#').unwrap_or(s);
326
327        let parse_channel = |from: usize, to: usize| -> Result<f32, std::num::ParseIntError> {
328            let num = usize::from_str_radix(&hex[from..=to], 16)? as f32 / 255.0;
329
330            // If we only got half a byte (one letter), expand it into a full byte (two letters)
331            Ok(if from == to { num + num * 16.0 } else { num })
332        };
333
334        let val = match hex.len() {
335            3 => Color::from_rgb(
336                parse_channel(0, 0)?,
337                parse_channel(1, 1)?,
338                parse_channel(2, 2)?,
339            ),
340            4 => Color::from_rgba(
341                parse_channel(0, 0)?,
342                parse_channel(1, 1)?,
343                parse_channel(2, 2)?,
344                parse_channel(3, 3)?,
345            ),
346            6 => Color::from_rgb(
347                parse_channel(0, 1)?,
348                parse_channel(2, 3)?,
349                parse_channel(4, 5)?,
350            ),
351            8 => Color::from_rgba(
352                parse_channel(0, 1)?,
353                parse_channel(2, 3)?,
354                parse_channel(4, 5)?,
355                parse_channel(6, 7)?,
356            ),
357            _ => return Err(ParseError::InvalidLength(hex.len())),
358        };
359
360        Ok(val)
361    }
362}
363
364impl std::fmt::Display for Color {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        let [r, g, b, a] = self.into_rgba8();
367
368        if self.a == 1.0 {
369            return write!(f, "#{r:02x}{g:02x}{b:02x}");
370        }
371
372        write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}")
373    }
374}
375
376impl Interpolable for Color {
377    /// Interpolates the color. Equivalent to [`Color::mix`].
378    fn interpolated(&self, other: Self, ratio: f32) -> Self {
379        self.mix(other, ratio)
380    }
381}
382
383/// A color in the [Oklab color space](https://en.wikipedia.org/wiki/Oklab_color_space),
384/// represented as Oklch.
385pub struct Oklch {
386    /// Perceptual lightness: 0 is pure black, 1 is pure white.
387    pub l: f32,
388    /// Chromatic intensity: 0 is achromatic, +0.5 is usually the upper limit.
389    pub c: f32,
390    /// Hue angle, in radians.
391    pub h: f32,
392    /// Alpha channel.
393    pub a: f32,
394}
395
396/// Creates a [`Color`] with shorter and cleaner syntax.
397///
398/// # Examples
399///
400/// ```
401/// # use iced_core::{Color, color};
402/// assert_eq!(color!(0, 0, 0), Color::BLACK);
403/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
404/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
405/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
406/// assert_eq!(color!(0x0000ff), Color::from_rgba(0.0, 0.0, 1.0, 1.0));
407/// ```
408#[macro_export]
409macro_rules! color {
410    ($r:expr, $g:expr, $b:expr) => {
411        $crate::Color::from_rgb8($r, $g, $b)
412    };
413    ($r:expr, $g:expr, $b:expr, $a:expr) => {{ $crate::Color::from_rgba8($r, $g, $b, $a) }};
414    ($hex:literal) => {{ $crate::color!($hex, 1.0) }};
415    ($hex:literal, $a:expr) => {{
416        let mut hex = $hex as u32;
417
418        // Shorthand notation: 0x123
419        if stringify!($hex).len() == 5 {
420            let r = hex & 0xF00;
421            let g = hex & 0xF0;
422            let b = hex & 0xF;
423
424            hex = (r << 12) | (r << 8) | (g << 8) | (g << 4) | (b << 4) | b;
425        }
426
427        debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
428
429        $crate::Color::from_packed_rgba8(hex, $a)
430    }};
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn parse() {
439        let tests = [
440            ("#ff0000", [255, 0, 0, 255], "#ff0000"),
441            ("00ff0080", [0, 255, 0, 128], "#00ff0080"),
442            ("#F80", [255, 136, 0, 255], "#ff8800"),
443            ("#00f1", [0, 0, 255, 17], "#0000ff11"),
444            ("#00ff", [0, 0, 255, 255], "#0000ff"),
445        ];
446
447        for (arg, expected_rgba8, expected_str) in tests {
448            let color = arg.parse::<Color>().expect("color must parse");
449
450            assert_eq!(color.into_rgba8(), expected_rgba8);
451            assert_eq!(color.to_string(), expected_str);
452        }
453
454        assert!("invalid".parse::<Color>().is_err());
455    }
456
457    const SHORTHAND: Color = color!(0x123);
458
459    #[test]
460    fn shorthand_notation() {
461        assert_eq!(SHORTHAND, Color::from_rgb8(0x11, 0x22, 0x33));
462    }
463}