1#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct ColorMatrix {
15 pub rows: [[f32; 4]; 5],
17}
18
19impl Default for ColorMatrix {
20 fn default() -> Self {
21 Self::IDENTITY
22 }
23}
24
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
29pub enum ColorMatrixAlphaMode {
30 #[default]
33 Premultiplied,
34 Straight,
38}
39
40pub const LUMINANCE_WEIGHTS: [f32; 3] = [0.2125, 0.7154, 0.0721];
42
43impl ColorMatrix {
44 pub const IDENTITY: ColorMatrix = ColorMatrix {
46 rows: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], [0.0; 4]],
47 };
48
49 pub const fn new(rows: [[f32; 4]; 5]) -> ColorMatrix {
51 ColorMatrix { rows }
52 }
53
54 pub fn from_matrix5x4(m: [f32; 20]) -> ColorMatrix {
57 let mut rows = [[0.0; 4]; 5];
58 for (index, value) in m.into_iter().enumerate() {
59 rows[index / 4][index % 4] = value;
60 }
61 ColorMatrix { rows }
62 }
63
64 pub fn saturation(saturation: f32) -> ColorMatrix {
68 let s = saturation;
69 ColorMatrix::new([
70 [0.213 + 0.787 * s, 0.213 - 0.213 * s, 0.213 - 0.213 * s, 0.0],
71 [0.715 - 0.715 * s, 0.715 + 0.285 * s, 0.715 - 0.715 * s, 0.0],
72 [0.072 - 0.072 * s, 0.072 - 0.072 * s, 0.072 + 0.928 * s, 0.0],
73 [0.0, 0.0, 0.0, 1.0],
74 [0.0; 4],
75 ])
76 }
77
78 pub fn luminance_to_alpha() -> ColorMatrix {
81 let [r, g, b] = LUMINANCE_WEIGHTS;
82 ColorMatrix::new([[0.0, 0.0, 0.0, r], [0.0, 0.0, 0.0, g], [0.0, 0.0, 0.0, b], [0.0; 4], [0.0; 4]])
83 }
84
85 pub fn tint(tint: [f32; 4]) -> ColorMatrix {
88 ColorMatrix::new([
89 [tint[0], 0.0, 0.0, 0.0],
90 [0.0, tint[1], 0.0, 0.0],
91 [0.0, 0.0, tint[2], 0.0],
92 [0.0, 0.0, 0.0, tint[3]],
93 [0.0; 4],
94 ])
95 }
96
97 pub fn invert() -> ColorMatrix {
100 ColorMatrix::new([
101 [-1.0, 0.0, 0.0, 0.0],
102 [0.0, -1.0, 0.0, 0.0],
103 [0.0, 0.0, -1.0, 0.0],
104 [0.0, 0.0, 0.0, 1.0],
105 [1.0, 1.0, 1.0, 0.0],
106 ])
107 }
108
109 pub fn grayscale() -> ColorMatrix {
112 let (r, g, b) = (0.299, 0.587, 0.114);
113 ColorMatrix::new([[r, r, r, 0.0], [g, g, g, 0.0], [b, b, b, 0.0], [0.0, 0.0, 0.0, 1.0], [0.0; 4]])
114 }
115
116 pub fn scale_and_offset(scale: [f32; 4], offset: [f32; 4]) -> ColorMatrix {
119 let mut rows = [[0.0; 4]; 5];
120 for channel in 0..4 {
121 rows[channel][channel] = scale[channel];
122 }
123 rows[4] = offset;
124 ColorMatrix { rows }
125 }
126
127 pub fn is_identity(&self) -> bool {
129 *self == ColorMatrix::IDENTITY
130 }
131
132 pub fn then(&self, next: &ColorMatrix) -> ColorMatrix {
134 let mut rows = [[0.0; 4]; 5];
135 for (row, out) in rows.iter_mut().enumerate() {
136 for (column, value) in out.iter_mut().enumerate() {
137 let mut sum = if row == 4 { next.rows[4][column] } else { 0.0 };
138 for k in 0..4 {
139 sum += self.rows[row][k] * next.rows[k][column];
140 }
141 *value = sum;
142 }
143 }
144 ColorMatrix { rows }
145 }
146
147 pub fn apply(&self, color: [f32; 4]) -> [f32; 4] {
150 let mut out = self.rows[4];
151 for (channel, value) in color.into_iter().enumerate() {
152 for (column, sum) in out.iter_mut().enumerate() {
153 *sum += value * self.rows[channel][column];
154 }
155 }
156 out.map(|value| if value.is_nan() { 0.0 } else { value.clamp(0.0, 1.0) })
157 }
158
159 pub fn apply_premultiplied(&self, color: [f32; 4], mode: ColorMatrixAlphaMode) -> [f32; 4] {
163 match mode {
164 ColorMatrixAlphaMode::Straight => {
165 let [r, g, b, a] = self.apply(color);
166 [r.min(a), g.min(a), b.min(a), a]
167 }
168 ColorMatrixAlphaMode::Premultiplied => {
169 let alpha = color[3];
170 let straight = |value: f32| if alpha > 0.0 { (value / alpha).min(1.0) } else { 0.0 };
171 let [r, g, b, a] = self.apply([straight(color[0]), straight(color[1]), straight(color[2]), alpha]);
172 [r * a, g * a, b * a, a]
173 }
174 }
175 }
176
177 pub fn apply_to_rgba8(&self, pixels: &mut [u8], mode: ColorMatrixAlphaMode) {
180 if mode == ColorMatrixAlphaMode::Premultiplied {
181 return self.apply_to_premultiplied_rgba8(pixels);
182 }
183 for pixel in pixels.chunks_exact_mut(4) {
184 let color = [0, 1, 2, 3].map(|channel| pixel[channel] as f32 / 255.0);
185 let out = self.apply_premultiplied(color, mode);
186 for channel in 0..4 {
187 pixel[channel] = (out[channel] * 255.0).round().clamp(0.0, 255.0) as u8;
188 }
189 }
190 }
191
192 pub fn apply_to_premultiplied_rgba8(&self, pixels: &mut [u8]) {
196 for pixel in pixels.chunks_exact_mut(4) {
197 let alpha = pixel[3] as f32 / 255.0;
198 let straight = |value: u8| if alpha > 0.0 { (value as f32 / 255.0 / alpha).min(1.0) } else { 0.0 };
199 let [r, g, b, a] = self.apply([straight(pixel[0]), straight(pixel[1]), straight(pixel[2]), alpha]);
200 let premultiplied = |value: f32| (value * a * 255.0).round().clamp(0.0, 255.0) as u8;
201 pixel[0] = premultiplied(r);
202 pixel[1] = premultiplied(g);
203 pixel[2] = premultiplied(b);
204 pixel[3] = (a * 255.0).round().clamp(0.0, 255.0) as u8;
205 }
206 }
207
208 pub fn to_row_major_4x5(&self) -> [f32; 20] {
211 let mut out = [0.0; 20];
212 for output in 0..4 {
213 for input in 0..5 {
214 out[output * 5 + input] = self.rows[input][output];
215 }
216 }
217 out
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn close(a: [f32; 4], b: [f32; 4]) -> bool {
226 a.iter().zip(b.iter()).all(|(a, b)| (a - b).abs() < 1e-4)
227 }
228
229 #[test]
230 fn identity_changes_nothing() {
231 let color = [0.2, 0.4, 0.6, 0.8];
232 assert!(close(ColorMatrix::IDENTITY.apply(color), color));
233 assert!(ColorMatrix::default().is_identity());
234 }
235
236 #[test]
237 fn rows_follow_the_matrix5x4_of_winui() {
238 let mut members = [0.0; 20];
240 members[3] = 1.0;
241 members[19] = 0.25;
242 let matrix = ColorMatrix::from_matrix5x4(members);
243 assert_eq!(matrix.rows[0][3], 1.0);
244 assert_eq!(matrix.rows[4][3], 0.25);
245 assert!(close(matrix.apply([0.5, 0.0, 0.0, 0.0]), [0.0, 0.0, 0.0, 0.75]));
246 }
247
248 #[test]
249 fn saturation_zero_is_grey_and_one_is_unchanged() {
250 let color = [1.0, 0.0, 0.0, 1.0];
251 let grey = ColorMatrix::saturation(0.0).apply(color);
252 assert!((grey[0] - grey[1]).abs() < 1e-4 && (grey[1] - grey[2]).abs() < 1e-4);
253 assert!(close(ColorMatrix::saturation(1.0).apply(color), color));
254 }
255
256 #[test]
257 fn luminance_to_alpha_uses_the_weights_of_winui() {
258 let out = ColorMatrix::luminance_to_alpha().apply([1.0, 1.0, 1.0, 1.0]);
259 assert!(close(out, [0.0, 0.0, 0.0, 1.0]));
260 let out = ColorMatrix::luminance_to_alpha().apply([0.0, 1.0, 0.0, 0.3]);
261 assert!(close(out, [0.0, 0.0, 0.0, 0.7154]));
262 }
263
264 #[test]
265 fn invert_keeps_alpha() {
266 assert!(close(ColorMatrix::invert().apply([0.25, 0.5, 1.0, 0.5]), [0.75, 0.5, 0.0, 0.5]));
267 }
268
269 #[test]
270 fn then_applies_in_order() {
271 let a = ColorMatrix::scale_and_offset([0.5, 1.0, 1.0, 1.0], [0.0; 4]);
272 let b = ColorMatrix::scale_and_offset([1.0; 4], [0.25, 0.0, 0.0, 0.0]);
273 let color = [1.0, 0.0, 0.0, 1.0];
274 assert!(close(a.then(&b).apply(color), b.apply(a.apply(color))));
275 assert!(close(a.then(&b).apply(color), [0.75, 0.0, 0.0, 1.0]));
276 }
277
278 #[test]
279 fn premultiplied_pixels_are_transformed_straight() {
280 let mut pixels = [128u8, 0, 0, 128];
282 ColorMatrix::invert().apply_to_premultiplied_rgba8(&mut pixels);
283 assert_eq!(pixels[0], 0);
285 assert!((pixels[1] as i32 - 128).abs() <= 1);
286 assert_eq!(pixels[3], 128);
287 }
288
289 #[test]
290 fn premultiplied_mode_applies_to_premultiplied_values() {
291 let matrix = ColorMatrix::scale_and_offset([2.0; 4], [0.0; 4]);
293 let out = matrix.apply_premultiplied([0.25, 0.25, 0.25, 0.5], ColorMatrixAlphaMode::Straight);
294 assert!(close(out, [0.5, 0.5, 0.5, 1.0]));
295 let out = ColorMatrix::scale_and_offset([1.0, 1.0, 1.0, 0.5], [0.0; 4])
297 .apply_premultiplied([0.5, 0.5, 0.5, 0.5], ColorMatrixAlphaMode::Straight);
298 assert!(close(out, [0.25, 0.25, 0.25, 0.25]));
299 let mut pixels = [64u8, 64, 64, 128];
300 matrix.apply_to_rgba8(&mut pixels, ColorMatrixAlphaMode::Straight);
301 assert_eq!(pixels, [128, 128, 128, 255]);
302 }
303
304 #[test]
305 fn row_major_form_is_a_row_per_output_channel() {
306 let matrix = ColorMatrix::luminance_to_alpha();
307 let row_major = matrix.to_row_major_4x5();
308 assert_eq!(&row_major[15..18], &LUMINANCE_WEIGHTS);
310 assert_eq!(row_major[0], 0.0);
311 }
312}