feather_ui/
color.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use num_traits::NumCast;
5use wide::f32x4;
6
7macro_rules! gen_channel_accessors {
8    ($rgba:ident, $r:ident, $g:ident, $b:ident, $a:ident) => {
9        pub fn as_array(&self) -> &[f32; 4] {
10            self.$rgba.as_array_ref()
11        }
12
13        pub const fn new($r: f32, $g: f32, $b: f32, $a: f32) -> Self {
14            Self {
15                $rgba: f32x4::new([$r, $g, $b, $a]),
16            }
17        }
18
19        pub fn $r(&self) -> f32 {
20            self.as_array()[0]
21        }
22
23        pub fn $g(&self) -> f32 {
24            self.as_array()[1]
25        }
26
27        pub fn $b(&self) -> f32 {
28            self.as_array()[2]
29        }
30
31        pub fn $a(&self) -> f32 {
32            self.as_array()[3]
33        }
34    };
35}
36
37pub fn mat4_x_vec4(l: f32x4, r: [f32x4; 4]) -> f32x4 {
38    let v = l.as_array_ref();
39    ((r[0]) * f32x4::splat(v[0]))
40        + ((r[1]) * f32x4::splat(v[1]))
41        + ((r[2]) * f32x4::splat(v[2]))
42        + ((r[3]) * f32x4::splat(v[3]))
43}
44
45fn fconv<T: num_traits::ToPrimitive, U: NumCast>(v: T) -> U {
46    U::from(v).unwrap()
47}
48
49pub fn srgb_to_linear<T: num_traits::Float>(c: T) -> T {
50    if c <= fconv(0.04045) {
51        c / fconv(12.92)
52    } else {
53        ((c + fconv(0.055)) / fconv(1.055)).powf(fconv(2.4))
54    }
55}
56
57/// https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
58/// http://www.ericbrasseur.org/gamma.html?i=2#formulas
59pub fn linear_to_srgb<T: num_traits::Float>(c: T) -> T {
60    if c < fconv(0.0031308) {
61        c * fconv(12.92)
62    } else {
63        (c.powf(<T as NumCast>::from(1.0).unwrap() / fconv(2.4)) * fconv(1.055)) - fconv(0.055)
64    }
65}
66
67/// Applies a function to all color channels, leaving the alpha channel
68/// untouched
69pub fn map_color(c: f32x4, f: impl Fn(f32) -> f32) -> f32x4 {
70    let v = c.to_array();
71    f32x4::new([f(v[0]), f(v[1]), f(v[2]), v[3]])
72}
73
74pub trait ColorSpace: Premultiplied {
75    /// Everything must provide a way to translate into standard XYZ space. The
76    /// 4th component is alpha, which is left unchanged in all color space
77    /// transforms
78    fn xyz(&self) -> XYZ;
79
80    /// This uses the standard formulation to transform into OkLab from XYZ
81    /// but this can be overridden if there's a more efficient pathway
82    fn oklab(&self) -> OkLab {
83        let xyz = self.xyz();
84
85        let mut lms = mat4_x_vec4(xyz.xyza, XYZ_OKLAB_M1);
86
87        let v = lms.as_array_mut();
88        for v in v.iter_mut().take(3) {
89            *v = v.powf(1.0 / 3.0);
90        }
91
92        OkLab {
93            laba: mat4_x_vec4(lms, XYZ_OKLAB_M2),
94        }
95    }
96
97    /// This pathway goes through the linear Raw_sRGB pathway first, because
98    /// only linear Raw_sRGB to XYZ and vice-versa is defined, but a color
99    /// space can override this with a faster pathway.
100    fn srgb(&self) -> Raw_sRGB<false, false> {
101        let srgb = self.linear_srgb();
102
103        Raw_sRGB {
104            rgba: map_color(srgb.rgba, linear_to_srgb),
105        }
106    }
107
108    /// This automatically converts from XYZ, but can be overridden if there's a
109    /// faster pathway
110    fn linear_srgb(&self) -> Raw_sRGB<true, false> {
111        let xyz = self.xyz();
112
113        let linear_rgba = mat4_x_vec4(xyz.xyza, XYZ_SRGB);
114        Raw_sRGB { rgba: linear_rgba }
115    }
116}
117
118/// Represents a color space that is premultiplied. You cannot un-premultiply a
119/// colorspace, so this is a one-way transformation.
120pub trait Premultiplied {
121    /// Transforms this colorspace into premultiplied non-linear sRGB. This
122    /// should always linearize the colorspace before performing the
123    /// premultiplication, which will then be delinearized back into sRGB
124    /// after the premultiplication has happened.
125    fn srgb_pre(&self) -> Raw_sRGB<false, true> {
126        let srgb = self.linear_srgb_pre();
127
128        Raw_sRGB {
129            rgba: map_color(srgb.rgba, linear_to_srgb),
130        }
131    }
132
133    fn linear_srgb_pre(&self) -> Raw_sRGB<true, true>;
134}
135
136#[derive(Debug, Default, Clone, Copy, PartialEq)]
137pub struct XYZ {
138    xyza: f32x4,
139}
140
141impl XYZ {
142    gen_channel_accessors! {xyza, x, y, z, a}
143}
144
145impl ColorSpace for XYZ {
146    fn xyz(&self) -> XYZ {
147        *self
148    }
149}
150
151impl Premultiplied for XYZ {
152    fn linear_srgb_pre(&self) -> Raw_sRGB<true, true> {
153        self.linear_srgb().premultiply()
154    }
155}
156
157const XYZ_OKLAB_M1: [f32x4; 4] = [
158    f32x4::new([0.818_933, 0.361_866_74, -0.128_859_71, 0.0]),
159    f32x4::new([0.032_984_544, 0.929_311_9, 0.036_145_64, 0.0]),
160    f32x4::new([0.048_200_3, 0.264_366_27, 0.633_851_7, 0.0]),
161    f32x4::new([0.0, 0.0, 0.0, 1.0]),
162];
163
164const OKLAB_XYZ_M1: [f32x4; 4] = [
165    f32x4::new([1.22701, -0.5578, 0.281256, 0.0]),
166    f32x4::new([-0.0405802, 1.11226, -0.0716767, 0.0]),
167    f32x4::new([-0.0763813, -0.421482, 1.58616, 0.0]),
168    f32x4::new([0.0, 0.0, 0.0, 1.0]),
169];
170
171const XYZ_OKLAB_M2: [f32x4; 4] = [
172    f32x4::new([0.210_454_26, 0.793_617_8, -0.004_072_047, 0.0]),
173    f32x4::new([1.977_998_5, -2.428_592_2, 0.450_593_7, 0.0]),
174    f32x4::new([0.025904037, 0.782_771_77, -0.808_675_77, 0.0]),
175    f32x4::new([0.0, 0.0, 0.0, 1.0]),
176];
177
178const OKLAB_XYZ_M2: [f32x4; 4] = [
179    f32x4::new([1.0, 0.396338, 0.215804, 0.0]),
180    f32x4::new([1.0, -0.105561, -0.0638542, 0.0]),
181    f32x4::new([1.0, -0.0894842, -1.29149, 0.0]),
182    f32x4::new([0.0, 0.0, 0.0, 1.0]),
183];
184
185#[derive(Debug, Default, Clone, Copy, PartialEq)]
186pub struct OkLab {
187    laba: f32x4,
188}
189
190impl OkLab {
191    gen_channel_accessors! {laba, l, c, h, a}
192}
193
194impl ColorSpace for OkLab {
195    fn xyz(&self) -> XYZ {
196        let mut lms = mat4_x_vec4(self.laba, OKLAB_XYZ_M2);
197
198        let v = lms.as_array_mut();
199        for v in v.iter_mut().take(3) {
200            *v = v.powf(3.0);
201        }
202
203        XYZ {
204            xyza: mat4_x_vec4(lms, OKLAB_XYZ_M1),
205        }
206    }
207
208    fn oklab(&self) -> OkLab {
209        *self
210    }
211
212    // Based on the somewhat cursed code provided here: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
213    fn linear_srgb(&self) -> Raw_sRGB<true, false> {
214        let v = self.laba.as_array_ref();
215        let l_ = v[0] + 0.396_337_78 * v[1] + 0.215_803_76 * v[2];
216        let m_ = v[0] - 0.105_561_346 * v[1] - 0.063_854_17 * v[2];
217        let s_ = v[0] - 0.089_484_18 * v[1] - 1.291_485_5 * v[2];
218
219        let l = l_ * l_ * l_;
220        let m = m_ * m_ * m_;
221        let s = s_ * s_ * s_;
222
223        Raw_sRGB {
224            rgba: f32x4::new([
225                4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s,
226                -1.268_438 * l + 2.609_757_4 * m - 0.341_319_38 * s,
227                -0.0041960863 * l - 0.703_418_6 * m + 1.707_614_7 * s,
228                v[3],
229            ]),
230        }
231    }
232}
233
234impl Premultiplied for OkLab {
235    fn linear_srgb_pre(&self) -> Raw_sRGB<true, true> {
236        self.linear_srgb().premultiply()
237    }
238}
239
240const XYZ_SRGB: [f32x4; 4] = [
241    f32x4::new([3.2404542, -1.5371385, -0.4985314, 0.0]),
242    f32x4::new([-0.969_266, 1.8760108, 0.0415560, 0.0]),
243    f32x4::new([0.0556434, -0.2040259, 1.0572252, 0.0]),
244    f32x4::new([0.0, 0.0, 0.0, 1.0]),
245];
246const SRGB_XYZ: [f32x4; 4] = [
247    f32x4::new([0.4124564, 0.3575761, 0.1804375, 0.0]),
248    f32x4::new([0.2126729, 0.7151522, 0.0721750, 0.0]),
249    f32x4::new([0.0193339, 0.119_192, 0.9503041, 0.0]),
250    f32x4::new([0.0, 0.0, 0.0, 1.0]),
251];
252
253#[derive(Debug, Default, Clone, Copy, PartialEq)]
254#[allow(non_camel_case_types)]
255pub struct Raw_sRGB<const LINEAR: bool, const PREMULTIPLY: bool> {
256    pub rgba: f32x4,
257}
258
259impl<const LINEAR: bool, const PREMULTIPLY: bool> Raw_sRGB<LINEAR, PREMULTIPLY> {
260    gen_channel_accessors! {rgba, r, g, b, a}
261
262    // Conveniently, white and black are the same in EVERY sRGB variant.
263
264    /// Returns transparent black (all zeroes)
265    pub const fn transparent() -> Self {
266        Self { rgba: f32x4::ZERO }
267    }
268
269    /// Returns opaque black
270    pub const fn black() -> Self {
271        Self {
272            rgba: f32x4::new([0.0, 0.0, 0.0, 1.0]),
273        }
274    }
275
276    /// Returns pure white (all ones)
277    pub const fn white() -> Self {
278        Self { rgba: f32x4::ONE }
279    }
280}
281
282impl Raw_sRGB<false, false> {
283    /// Returns this color as an array of 8-bit channels. This is only
284    /// implemented on nonlinear sRGB that hasn't been premultiplied,
285    /// because otherwise you'll lose precision when sending the color to a
286    /// shader, unless you are writing directly to the texture atlas, in
287    /// which case you want `as_bgra` implemented on premultiplied sRGB.
288    pub fn as_8bit(&self) -> [u8; 4] {
289        self.as_array().map(|x| (x * 255.0).round() as u8)
290    }
291
292    /// Returns this color in a 32-bit integer. This is only valid for nonlinear
293    /// sRGB that isn't premultiplied to avoid precision loss.
294    pub fn as_32bit(&self) -> sRGB32 {
295        sRGB32 {
296            rgba: u32::from_be_bytes(self.as_8bit()),
297        }
298    }
299}
300
301impl Raw_sRGB<false, true> {
302    /// Returns this color as an array of 8-bit channels, with the red and blue
303    /// channels swapped. This is used exclusively to write values directly
304    /// to a premultiplied sRGB texture like the texture atlas.
305    pub fn as_bgra(&self) -> [u8; 4] {
306        let mut rgba = self.as_array().map(|x| (x * 255.0).round() as u8);
307        rgba.swap(0, 2);
308        rgba
309    }
310}
311
312impl<const LINEAR: bool, const PREMULTIPLY: bool> From<[f32; 4]> for Raw_sRGB<LINEAR, PREMULTIPLY> {
313    fn from(value: [f32; 4]) -> Self {
314        Self {
315            rgba: f32x4::new(value),
316        }
317    }
318}
319
320impl ColorSpace for Raw_sRGB<false, false> {
321    fn xyz(&self) -> XYZ {
322        self.linear_srgb().xyz()
323    }
324
325    fn srgb(&self) -> Raw_sRGB<false, false> {
326        *self
327    }
328
329    fn linear_srgb(&self) -> Raw_sRGB<true, false> {
330        Raw_sRGB {
331            rgba: map_color(self.rgba, srgb_to_linear),
332        }
333    }
334}
335
336impl Premultiplied for Raw_sRGB<false, false> {
337    fn linear_srgb_pre(&self) -> Raw_sRGB<true, true> {
338        self.linear_srgb().premultiply()
339    }
340}
341
342impl ColorSpace for Raw_sRGB<true, false> {
343    fn xyz(&self) -> XYZ {
344        // We can't really un-premultiply the alpha here, and XYZ can be premultiplied,
345        // so we just leave it
346        XYZ {
347            xyza: mat4_x_vec4(self.rgba, SRGB_XYZ),
348        }
349    }
350
351    fn oklab(&self) -> OkLab {
352        let v = self.as_array();
353        let l = 0.412_221_46 * v[0] + 0.536_332_55 * v[1] + 0.051_445_995 * v[2];
354        let m = 0.211_903_5 * v[0] + 0.680_699_5 * v[1] + 0.107_396_96 * v[2];
355        let s = 0.088_302_46 * v[0] + 0.281_718_85 * v[1] + 0.629_978_7 * v[2];
356
357        let l_ = l.powf(1.0 / 3.0);
358        let m_ = m.powf(1.0 / 3.0);
359        let s_ = s.powf(1.0 / 3.0);
360
361        OkLab {
362            laba: f32x4::new([
363                0.210_454_26 * l_ + 0.793_617_8 * m_ - 0.004_072_047 * s_,
364                1.977_998_5 * l_ - 2.428_592_2 * m_ + 0.450_593_7 * s_,
365                0.025_904_037 * l_ + 0.782_771_77 * m_ - 0.808_675_77 * s_,
366                v[3],
367            ]),
368        }
369    }
370
371    fn linear_srgb(&self) -> Raw_sRGB<true, false> {
372        *self
373    }
374}
375
376impl Premultiplied for Raw_sRGB<true, false> {
377    fn linear_srgb_pre(&self) -> Raw_sRGB<true, true> {
378        self.premultiply()
379    }
380}
381
382impl Raw_sRGB<true, false> {
383    // Premultiplies the alpha
384    fn premultiply(&self) -> Raw_sRGB<true, true> {
385        let a = self.a();
386        Raw_sRGB {
387            rgba: map_color(self.rgba, |x| x * a),
388        }
389    }
390}
391
392impl Premultiplied for Raw_sRGB<false, true> {
393    fn srgb_pre(&self) -> Raw_sRGB<false, true> {
394        *self
395    }
396
397    fn linear_srgb_pre(&self) -> Raw_sRGB<true, true> {
398        Raw_sRGB {
399            rgba: map_color(self.rgba, srgb_to_linear),
400        }
401    }
402}
403
404impl Premultiplied for Raw_sRGB<true, true> {
405    fn linear_srgb_pre(&self) -> Raw_sRGB<true, true> {
406        *self
407    }
408}
409
410/// Standard sRGB colorspace
411#[allow(non_camel_case_types)]
412pub type sRGB = Raw_sRGB<false, false>;
413/// Linear sRGB colorspace
414#[allow(non_camel_case_types)]
415pub type Linear_sRGB = Raw_sRGB<true, false>;
416/// Premultiplied sRGB colorspace
417#[allow(non_camel_case_types)]
418pub type Pre_sRGB = Raw_sRGB<false, true>;
419/// Premultiplied Linear sRGB colorspace
420#[allow(non_camel_case_types)]
421pub type PreLinear_sRGB = Raw_sRGB<true, true>;
422
423// We only implement this conversion for sRGB because cosmic_text expects sRGB
424// colors.
425impl From<sRGB> for cosmic_text::Color {
426    fn from(val: sRGB) -> Self {
427        let v = val.as_8bit();
428        cosmic_text::Color::rgba(v[0], v[1], v[2], v[3])
429    }
430}
431
432/// Represents an sRGB color (not premultiplied) as a 32-bit signed integer
433#[allow(non_camel_case_types)]
434#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
435pub struct sRGB32 {
436    pub rgba: u32,
437}
438
439impl sRGB32 {
440    pub const fn as_array(&self) -> [u8; 4] {
441        self.rgba.to_be_bytes()
442    }
443
444    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
445        Self {
446            rgba: u32::from_be_bytes([r, g, b, a]),
447        }
448    }
449
450    /// Returns transparent black (zero)
451    pub const fn transparent() -> Self {
452        Self { rgba: 0 }
453    }
454
455    /// Returns opaque black
456    pub const fn black() -> Self {
457        Self {
458            rgba: u32::from_be_bytes([0, 0, 0, 255]),
459        }
460    }
461
462    /// Returns pure white
463    pub const fn white() -> Self {
464        Self { rgba: u32::MAX }
465    }
466
467    pub const fn from_alpha(alpha: u8) -> Self {
468        Self {
469            rgba: u32::from_be_bytes([255, 255, 255, alpha]),
470        }
471    }
472
473    pub const fn r(&self) -> u8 {
474        self.as_array()[0]
475    }
476
477    pub const fn g(&self) -> u8 {
478        self.as_array()[1]
479    }
480
481    pub const fn b(&self) -> u8 {
482        self.as_array()[2]
483    }
484
485    pub const fn a(&self) -> u8 {
486        self.as_array()[3]
487    }
488
489    pub fn as_f32(&self) -> sRGB {
490        sRGB {
491            rgba: f32x4::new(self.as_array().map(|x| x as f32 / 255.0)),
492        }
493    }
494}
495
496/// Represents an sRGB color (not premultiplied) as a 64-bit signed integer,
497/// 16-bits per channel
498#[allow(non_camel_case_types)]
499#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
500pub struct sRGB64 {
501    pub rgba: u64,
502}
503
504impl sRGB64 {
505    pub const fn as_array(&self) -> [u16; 4] {
506        [
507            ((self.rgba >> 48) & 0xFFFF) as u16,
508            ((self.rgba >> 32) & 0xFFFF) as u16,
509            ((self.rgba >> 16) & 0xFFFF) as u16,
510            (self.rgba & 0xFFFF) as u16,
511        ]
512    }
513
514    pub const fn new(r: u16, g: u16, b: u16, a: u16) -> Self {
515        Self {
516            rgba: ((r as u64) << 48) | ((g as u64) << 32) | ((b as u64) << 16) | (a as u64),
517        }
518    }
519
520    /// Returns transparent black (zero)
521    pub const fn transparent() -> Self {
522        Self { rgba: 0 }
523    }
524
525    /// Returns opaque black
526    pub const fn black() -> Self {
527        Self {
528            rgba: 0x000000000000FFFF,
529        }
530    }
531
532    /// Returns pure white
533    pub const fn white() -> Self {
534        Self { rgba: u64::MAX }
535    }
536
537    pub const fn from_alpha(alpha: u16) -> Self {
538        Self {
539            rgba: 0xFFFFFFFFFFFF0000 | alpha as u64,
540        }
541    }
542
543    pub const fn r(&self) -> u16 {
544        self.as_array()[0]
545    }
546
547    pub const fn g(&self) -> u16 {
548        self.as_array()[1]
549    }
550
551    pub const fn b(&self) -> u16 {
552        self.as_array()[2]
553    }
554
555    pub const fn a(&self) -> u16 {
556        self.as_array()[3]
557    }
558
559    pub fn as_f32(&self) -> sRGB {
560        sRGB {
561            rgba: f32x4::new(self.as_array().map(|x| x as f32 / 65535.0)),
562        }
563    }
564
565    /// 16-bit precision has a potentially arbitrary 1.0 point, since it can
566    /// potentially store HDR data from different formats.
567    pub fn as_f32_scaled(&self, scale: f32) -> sRGB {
568        sRGB {
569            rgba: f32x4::new(self.as_array().map(|x| x as f32 / (65535.0 / scale))),
570        }
571    }
572}