Skip to main content

dinamika_cpu/
color.rs

1//! Colors: non-premultiplied (`Color`, `ColorU8`) and premultiplied
2//! (`PremultipliedColor`, `PremultipliedColorU8`) representations.
3//!
4//! Pixmap stores pixels in premultiplied RGBA format at 8 bits per channel.
5
6#[inline]
7fn clamp01(v: f32) -> f32 {
8    v.clamp(0.0, 1.0)
9}
10
11#[inline]
12fn to_u8(v: f32) -> u8 {
13    (clamp01(v) * 255.0 + 0.5) as u8
14}
15
16/// A color in floating-point RGBA format in the range `0.0..=1.0`,
17/// non-premultiplied alpha.
18#[derive(Copy, Clone, Debug, PartialEq)]
19pub struct Color {
20    r: f32,
21    g: f32,
22    b: f32,
23    a: f32,
24}
25
26impl Color {
27    pub const TRANSPARENT: Color = Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
28    pub const BLACK: Color = Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
29    pub const WHITE: Color = Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
30
31    /// Creates a color from floating-point components (values are clamped to `0..=1`).
32    pub fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
33        Color { r: clamp01(r), g: clamp01(g), b: clamp01(b), a: clamp01(a) }
34    }
35
36    /// Creates a color from 8-bit components.
37    pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Color {
38        Color {
39            r: r as f32 / 255.0,
40            g: g as f32 / 255.0,
41            b: b as f32 / 255.0,
42            a: a as f32 / 255.0,
43        }
44    }
45
46    #[inline]
47    pub fn red(&self) -> f32 {
48        self.r
49    }
50    #[inline]
51    pub fn green(&self) -> f32 {
52        self.g
53    }
54    #[inline]
55    pub fn blue(&self) -> f32 {
56        self.b
57    }
58    #[inline]
59    pub fn alpha(&self) -> f32 {
60        self.a
61    }
62
63    /// Whether the color is fully opaque.
64    #[inline]
65    pub fn is_opaque(&self) -> bool {
66        self.a >= 1.0
67    }
68
69    /// Sets the alpha (clamped to `0..=1`).
70    pub fn set_alpha(&mut self, a: f32) {
71        self.a = clamp01(a);
72    }
73
74    /// Converts to a premultiplied floating-point color.
75    #[inline]
76    pub fn premultiply(&self) -> PremultipliedColor {
77        PremultipliedColor { r: self.r * self.a, g: self.g * self.a, b: self.b * self.a, a: self.a }
78    }
79
80    /// Converts to an 8-bit non-premultiplied color.
81    pub fn to_color_u8(&self) -> ColorU8 {
82        ColorU8 { r: to_u8(self.r), g: to_u8(self.g), b: to_u8(self.b), a: to_u8(self.a) }
83    }
84}
85
86/// An 8-bit non-premultiplied RGBA color.
87#[derive(Copy, Clone, Debug, PartialEq, Eq)]
88pub struct ColorU8 {
89    r: u8,
90    g: u8,
91    b: u8,
92    a: u8,
93}
94
95impl ColorU8 {
96    pub const fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> ColorU8 {
97        ColorU8 { r, g, b, a }
98    }
99    #[inline]
100    pub fn red(&self) -> u8 {
101        self.r
102    }
103    #[inline]
104    pub fn green(&self) -> u8 {
105        self.g
106    }
107    #[inline]
108    pub fn blue(&self) -> u8 {
109        self.b
110    }
111    #[inline]
112    pub fn alpha(&self) -> u8 {
113        self.a
114    }
115
116    pub fn to_color(self) -> Color {
117        Color::from_rgba8(self.r, self.g, self.b, self.a)
118    }
119
120    /// Premultiplies the color (each channel is multiplied by alpha).
121    pub fn premultiply(self) -> PremultipliedColorU8 {
122        let a = self.a as u16;
123        let m = |c: u8| ((c as u16 * a + 127) / 255) as u8;
124        PremultipliedColorU8 { r: m(self.r), g: m(self.g), b: m(self.b), a: self.a }
125    }
126}
127
128/// A premultiplied floating-point color (RGB already multiplied by alpha).
129#[derive(Copy, Clone, Debug, PartialEq)]
130pub struct PremultipliedColor {
131    pub r: f32,
132    pub g: f32,
133    pub b: f32,
134    pub a: f32,
135}
136
137impl PremultipliedColor {
138    /// Converts to an 8-bit premultiplied color (what the pixmap stores).
139    pub fn to_color_u8(&self) -> PremultipliedColorU8 {
140        // RGB must not exceed alpha — clamp for correctness.
141        let a = clamp01(self.a);
142        PremultipliedColorU8 {
143            r: to_u8(self.r.min(a)),
144            g: to_u8(self.g.min(a)),
145            b: to_u8(self.b.min(a)),
146            a: to_u8(a),
147        }
148    }
149}
150
151/// An 8-bit premultiplied RGBA color — the pixel storage format.
152#[derive(Copy, Clone, Debug, PartialEq, Eq)]
153pub struct PremultipliedColorU8 {
154    r: u8,
155    g: u8,
156    b: u8,
157    a: u8,
158}
159
160impl PremultipliedColorU8 {
161    pub const TRANSPARENT: PremultipliedColorU8 = PremultipliedColorU8 { r: 0, g: 0, b: 0, a: 0 };
162
163    /// Creates a value without checking the `rgb <= a` invariant.
164    pub const fn from_rgba_unchecked(r: u8, g: u8, b: u8, a: u8) -> PremultipliedColorU8 {
165        PremultipliedColorU8 { r, g, b, a }
166    }
167
168    #[inline]
169    pub fn red(&self) -> u8 {
170        self.r
171    }
172    #[inline]
173    pub fn green(&self) -> u8 {
174        self.g
175    }
176    #[inline]
177    pub fn blue(&self) -> u8 {
178        self.b
179    }
180    #[inline]
181    pub fn alpha(&self) -> u8 {
182        self.a
183    }
184
185    /// Restores the non-premultiplied color.
186    pub fn demultiply(&self) -> ColorU8 {
187        if self.a == 0 {
188            ColorU8 { r: 0, g: 0, b: 0, a: 0 }
189        } else {
190            let a = self.a as u16;
191            let d = |c: u8| (((c as u16) * 255 + a / 2) / a).min(255) as u8;
192            ColorU8 { r: d(self.r), g: d(self.g), b: d(self.b), a: self.a }
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn premultiply_roundtrip() {
203        let c = ColorU8::from_rgba(200, 100, 50, 128);
204        let p = c.premultiply();
205        let back = p.demultiply();
206        // allow a small rounding error
207        assert!((back.red() as i32 - 200).abs() <= 2);
208        assert!((back.green() as i32 - 100).abs() <= 2);
209        assert_eq!(back.alpha(), 128);
210    }
211
212    #[test]
213    fn opaque_premultiply_identity() {
214        let c = Color::from_rgba8(10, 20, 30, 255).premultiply().to_color_u8();
215        assert_eq!(c, PremultipliedColorU8::from_rgba_unchecked(10, 20, 30, 255));
216    }
217}