Skip to main content

blockworx_paint/
color.rs

1/// An sRGBA color, 8 bits per channel, with **premultiplied alpha** — the
2/// non-linear ("gamma") values are the ones multiplied by the alpha, matching
3/// ecolor's `Color32` byte for byte so the two convert without a color shift.
4///
5/// Every operation here works in gamma space. That is not physically correct,
6/// but it is what the palette and both render backends already assume, and it
7/// is perceptually more even than the linear-space alternative.
8#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
9pub struct Color([u8; 4]);
10
11impl Color {
12    /// The absence of a color: a role mapped to nothing, a cell that draws no
13    /// fill.
14    pub const TRANSPARENT: Self = Self::from_rgba_premultiplied(0, 0, 0, 0);
15
16    /// The "recolor me" sentinel a text layout carries until something paints
17    /// it. Not a color, and not a valid premultiplied one either.
18    pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128);
19
20    pub const BLACK: Self = Self::from_rgb(0, 0, 0);
21    pub const WHITE: Self = Self::from_rgb(255, 255, 255);
22    pub const GRAY: Self = Self::from_rgb(160, 160, 160);
23
24    /// Opaque.
25    #[inline]
26    pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
27        Self([r, g, b, 255])
28    }
29
30    #[inline]
31    pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
32        Self([r, g, b, a])
33    }
34
35    /// From the "normal" RGBA a color picker hands out, with alpha kept
36    /// separate. [`Self::to_srgba_unmultiplied`] inverts it, up to rounding.
37    #[inline]
38    pub const fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
39        match a {
40            0 => Self::TRANSPARENT,
41            255 => Self::from_rgb(r, g, b),
42            a => {
43                let alpha = a as f32 / 255.0;
44                Self::from_rgba_premultiplied(
45                    round_u8(r as f32 * alpha),
46                    round_u8(g as f32 * alpha),
47                    round_u8(b as f32 * alpha),
48                    a,
49                )
50            }
51        }
52    }
53
54    /// Red, multiplied by alpha.
55    #[inline]
56    pub const fn r(self) -> u8 {
57        self.0[0]
58    }
59
60    /// Green, multiplied by alpha.
61    #[inline]
62    pub const fn g(self) -> u8 {
63        self.0[1]
64    }
65
66    /// Blue, multiplied by alpha.
67    #[inline]
68    pub const fn b(self) -> u8 {
69        self.0[2]
70    }
71
72    #[inline]
73    pub const fn a(self) -> u8 {
74        self.0[3]
75    }
76
77    #[inline]
78    pub const fn is_opaque(self) -> bool {
79        self.a() == 255
80    }
81
82    /// Premultiplied RGBA.
83    #[inline]
84    pub const fn to_array(self) -> [u8; 4] {
85        self.0
86    }
87
88    /// Premultiplied RGBA.
89    #[inline]
90    pub const fn to_tuple(self) -> (u8, u8, u8, u8) {
91        (self.r(), self.g(), self.b(), self.a())
92    }
93
94    /// Back to separate alpha — the inverse of
95    /// [`Self::from_rgba_unmultiplied`], up to rounding on transparent colors.
96    #[inline]
97    pub fn to_srgba_unmultiplied(self) -> [u8; 4] {
98        let [r, g, b, a] = self.0;
99        match a {
100            0 | 255 => self.0,
101            a => {
102                let factor = 255.0 / a as f32;
103                [
104                    round_u8(factor * r as f32),
105                    round_u8(factor * g as f32),
106                    round_u8(factor * b as f32),
107                    a,
108                ]
109            }
110        }
111    }
112
113    /// Scale every channel, alpha included, in gamma space: `0.5` makes the
114    /// color half as opaque, perceptually.
115    #[must_use]
116    #[inline]
117    pub fn gamma_multiply(self, factor: f32) -> Self {
118        debug_assert!(
119            0.0 <= factor && factor.is_finite(),
120            "gamma_multiply factor should be finite and non-negative, but was {factor}"
121        );
122        let Self([r, g, b, a]) = self;
123        Self([
124            (r as f32 * factor + 0.5) as u8,
125            (g as f32 * factor + 0.5) as u8,
126            (b as f32 * factor + 0.5) as u8,
127            (a as f32 * factor + 0.5) as u8,
128        ])
129    }
130}
131
132/// Nearest-integer rounding as ecolor does it — the `+ 0.5` truncation, not
133/// `f32::round`, because the two disagree on ties and the byte has to match.
134#[inline]
135const fn round_u8(value: f32) -> u8 {
136    (value + 0.5) as u8
137}
138
139impl std::fmt::Debug for Color {
140    /// Premultiplied, as stored.
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        let [r, g, b, a] = self.0;
143        write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
144    }
145}