1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
//! Technical implementation of a subset of CSS Color Module Level 4 (WD).

use palette::rgb::{Rgb, Rgba};
use palette::{Alpha, ConvertInto, Hsl, Lab, Mix};

pub mod encoding;
mod named;

/// Nonlinear Display-P3.
pub type DisplayP3<T = f32> = Rgb<encoding::DisplayP3, T>;

/// Nonlinear Display-P3 with an alpha component.
pub type DisplayP3a<T = f32> = Rgba<encoding::DisplayP3, T>;

pub use palette::{Srgb, Srgba};

#[derive(Copy, Clone, Debug, PartialEq)]
enum ColorComponents {
    Transparent,

    /// sRGB is the default color space in CSS for colors specified with the
    /// `rgb(...)` or `rgba(...)` syntax. On iOS devices, it seems the default
    /// color space is (similar to) sRGB. As always, Apple is sparse on details.
    /// It apparently also is the default on Android devices. Note that is the
    /// default even for devices that support wide gamut: that's opt-in.
    StandardRGB(Srgb),

    /// Display P3 is another color space using the RGB model.
    DisplayP3(DisplayP3),
}

/// A color in a color-space with an associated alpha channel.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Color {
    components: ColorComponents,
    alpha: f32,
}

impl Color {
    /// This function returns a new translucent color in the sRGB color space
    /// with the given coordinates and alpha channel.
    pub fn rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Color {
        Color {
            components: ColorComponents::StandardRGB(Srgb::new(
                red as f32 / 255.0,
                green as f32 / 255.0,
                blue as f32 / 255.0,
            )),
            alpha,
        }
    }

    /// This function returns a new opaque color in the sRGB color space with
    /// the given coordinates.
    pub fn rgb(red: u8, green: u8, blue: u8) -> Color {
        Self::rgba(red, green, blue, 1.0)
    }

    /// This function returns a new translucent color in the sRGB color space
    /// with the given hex-encoded coordinates.
    pub fn hexa(hex: u32, alpha: f32) -> Color {
        Self::rgba(
            ((hex >> 16) & 0xff) as u8,
            ((hex >> 8) & 0xff) as u8,
            ((hex >> 0) & 0xff) as u8,
            alpha,
        )
    }

    /// This function returns a new opaque color in the sRGB color space with
    /// the given hex-encoded coordinates.
    pub fn hex(hex: u32) -> Color {
        Self::hexa(hex, 1.0)
    }

    /// This function returns a new translucent color in the sRGB color space
    /// with the given coordinates after converting from the alternative HSL
    /// representation to RGB.
    pub fn hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color {
        Color {
            components: ColorComponents::StandardRGB(Hsl::new(hue, saturation, lightness).into()),
            alpha,
        }
    }

    /// This function returns a new opaque color in the sRGB color space with
    /// the given coordinates after converting from the alternative HSL
    /// representation to RGB.
    pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> Color {
        Self::hsla(hue, saturation, lightness, 1.0)
    }

    /// This function returns a new translucent color in the Display-P3 color
    /// space with the given coordinates and alpha channel.
    pub fn display_p3_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Color {
        Color {
            components: ColorComponents::DisplayP3(DisplayP3::new(
                red as f32 / 255.0,
                green as f32 / 255.0,
                blue as f32 / 255.0,
            )),
            alpha,
        }
    }

    /// This functions returns a new opaque color in the Display-P3 color space
    /// with the given coordinates.
    pub fn display_p3_rgb(red: u8, green: u8, blue: u8) -> Color {
        Self::display_p3_rgba(red, green, blue, 1.0)
    }

    /// This function returns an unbiased transparent color. Specifically, when
    /// blending a non-transparent color with a transparent color, the resulting
    /// color will have the same color space and coordinate as the
    /// non-transparent color, and only the alpha channel will be blended.
    pub fn transparent() -> Color {
        Color {
            components: ColorComponents::Transparent,
            alpha: 0.0,
        }
    }

    /// This function returns the additional alpha-component of this color.
    pub fn alpha(&self) -> f32 {
        self.alpha
    }

    /// Blends the given color with another color using the given factor (between
    /// 0.0 and 1.0).
    pub fn mix(self, other: Color, factor: f32) -> Color {
        let factor = factor.max(0.0).min(1.0);

        Color {
            components: ColorComponents::StandardRGB(Srgb::from_linear(
                self.to_srgb()
                    .color
                    .into_linear()
                    .mix(&other.to_srgb().color.into_linear(), factor),
            )),
            alpha: self.alpha * factor + other.alpha * (1.0 - factor),
        }
    }

    /// This function converts the color to the Display-P3 color space (if
    /// necessary) and returns the result.
    pub fn to_display_p3(&self) -> DisplayP3a {
        Alpha {
            color: match self.components {
                ColorComponents::StandardRGB(rgb) => {
                    let lab: Lab = rgb.convert_into();
                    lab.convert_into()
                }
                ColorComponents::DisplayP3(rgb) => rgb,
                ColorComponents::Transparent => DisplayP3::new(0.0, 0.0, 0.0),
            },
            alpha: self.alpha,
        }
    }

    /// This function converts the color to the sRGB color space (if necessary)
    /// and returns the result.
    pub fn to_srgb(&self) -> Srgba {
        Alpha {
            color: match self.components {
                ColorComponents::StandardRGB(rgb) => rgb,
                ColorComponents::DisplayP3(rgb) => {
                    let lab: Lab = rgb.convert_into();
                    lab.convert_into()
                }
                ColorComponents::Transparent => Srgb::new(0.0, 0.0, 0.0),
            },
            alpha: self.alpha,
        }
    }

    /// Converts this color to sRGB and returns the resulting hex code.
    pub fn to_hex(&self) -> u32 {
        let srgb = self.to_srgb();

        (((srgb.red * 255.0) as u32) << 16)
            | (((srgb.green * 255.0) as u32) << 8)
            | (((srgb.blue * 255.0) as u32) << 0)
    }
}

impl Default for Color {
    fn default() -> Self {
        Color::transparent()
    }
}

pub use named::NamedColor;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_srgb_to_display_p3() {
        let color = Color::rgb(255, 0, 0);
        let p3 = color.to_display_p3();

        assert!((p3.red - 234.0 / 255.0) < (1.0 / 255.0));
        assert!((p3.green - 51.0 / 255.0) < (1.0 / 255.0));
        assert!((p3.blue - 35.0 / 255.0) < (1.0 / 255.0));
    }

    #[test]
    fn test_display_p3_to_srgb() {
        let color = Color::display_p3_rgb(234, 51, 35);
        assert_eq!(color.to_srgb(), Srgba::new(1.0, 0.0, 0.0, 1.0));
    }

    #[test]
    fn test_named_color() {
        use NamedColor;

        let color = Color::coral();
        assert_eq!(color, Color::rgb(255, 127, 80));
    }

    #[test]
    fn test_gamma_correct_blending() {
        let blue = Color::hex(0x0073FF);
        let red = Color::hex(0xFF003D);
        let mix = blue.mix(red, 0.5);
        assert_eq!(mix.to_hex(), 0xBB52BF);
    }
}