GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/color/math.rs (4.8K)
1 //! Colour arithmetic: sRGB/linear conversion (the hex parsers come from `cce_core::color`),
2 //! OKLab, and the perceptual fade.
3
4 use super::*;
5
6 pub use cce_core::color::{linear_to_srgb, parse_hex_bytes, parse_hex_rgb, parse_hex_rgba, parse_hex_rgba_linear, srgb_to_linear};
7
8 pub fn to_linear(color: [f32; 4]) -> [f32; 4] {
9 [
10 srgb_to_linear(color[0]),
11 srgb_to_linear(color[1]),
12 srgb_to_linear(color[2]),
13 color[3],
14 ]
15 }
16
17 pub fn to_srgb(color: [f32; 4]) -> [f32; 4] {
18 [
19 linear_to_srgb(color[0]),
20 linear_to_srgb(color[1]),
21 linear_to_srgb(color[2]),
22 color[3],
23 ]
24 }
25
26 pub fn to_linear_rgb(color: [f32; 3]) -> [f32; 3] {
27 [
28 srgb_to_linear(color[0]),
29 srgb_to_linear(color[1]),
30 srgb_to_linear(color[2]),
31 ]
32 }
33
34 pub fn to_srgb_rgb(color: [f32; 3]) -> [f32; 3] {
35 [
36 linear_to_srgb(color[0]),
37 linear_to_srgb(color[1]),
38 linear_to_srgb(color[2]),
39 ]
40 }
41
42 pub fn linear_srgb_to_oklab(rgb: [f32; 3]) -> [f32; 3] {
43 let l = 0.4122214708 * rgb[0] + 0.5363325363 * rgb[1] + 0.0514459929 * rgb[2];
44 let m = 0.2119034982 * rgb[0] + 0.6806995451 * rgb[1] + 0.1073969566 * rgb[2];
45 let s = 0.0883024619 * rgb[0] + 0.2817188376 * rgb[1] + 0.6299787005 * rgb[2];
46
47 let l_ = l.max(0.0).powf(1.0 / 3.0);
48 let m_ = m.max(0.0).powf(1.0 / 3.0);
49 let s_ = s.max(0.0).powf(1.0 / 3.0);
50
51 [
52 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
53 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
54 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
55 ]
56 }
57
58 pub fn oklab_to_linear_srgb(lab: [f32; 3]) -> [f32; 3] {
59 let l_ = lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2];
60 let m_ = lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2];
61 let s_ = lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2];
62
63 let l = l_ * l_ * l_;
64 let m = m_ * m_ * m_;
65 let s = s_ * s_ * s_;
66
67 [
68 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
69 -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
70 -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
71 ]
72 }
73
74 /// The alpha that makes an overlay's fade-out read as an EVEN fade to the eye.
75 ///
76 /// `t` runs 0 (overlay at `max_alpha`) to 1 (fully faded); `overlay` and `backdrop` are
77 /// linear-light RGB, the backdrop being whatever the fade composites onto.
78 ///
79 /// A linear alpha ramp is not a linear fade: the surface is an sRGB attachment, so the GPU
80 /// blends in LINEAR light, and perceived lightness goes as roughly the cube root of that. A
81 /// straight alpha ramp therefore hangs bright through the middle and then dives near the
82 /// end. This walks perceived lightness linearly instead and solves back for the alpha that
83 /// lands on it — exactly, since the composite is linear in alpha:
84 ///
85 /// ```text
86 /// Y(a) = a·Y_overlay + (1 - a)·Y_backdrop (blending, in linear light)
87 /// L ≈ cbrt(Y) (perception — OKLab's L, and CIE L* to within a hair)
88 /// ```
89 ///
90 /// Solved on luminance rather than per channel because a single alpha can only satisfy one
91 /// axis, and lightness is the one the eye reads a fade by. With no lightness contrast to
92 /// shape (a fade between equally-bright colors) it falls back to the linear ramp.
93 pub fn perceptual_fade_alpha(t: f32, overlay: [f32; 3], backdrop: [f32; 3], max_alpha: f32) -> f32 {
94 let t = t.clamp(0.0, 1.0);
95 let luminance = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
96 let y_over = luminance(overlay);
97 let y_back = luminance(backdrop);
98 let contrast = y_over - y_back;
99 if contrast.abs() < 1e-4 {
100 return max_alpha * (1.0 - t);
101 }
102 // Walk L from the fully-applied composite down to the bare backdrop.
103 let l = |y: f32| y.max(0.0).cbrt();
104 let y_full = max_alpha * y_over + (1.0 - max_alpha) * y_back;
105 let l_t = l(y_full) + (l(y_back) - l(y_full)) * t;
106 let y_t = l_t * l_t * l_t;
107 ((y_t - y_back) / contrast).clamp(0.0, max_alpha)
108 }
109
110 pub fn sidebar_bg_color() -> [f32; 4] {
111 load_colors_once();
112 let mut color = *SIDEBAR_BG_COLOR.read().unwrap();
113 if let Some(opacity) = read_opacity_if_configured() {
114 color[3] = opacity;
115 }
116 color
117 }
118
119 pub fn set_sidebar_bg_color(color: [f32; 4]) {
120 style_write(&SIDEBAR_BG_COLOR, color);
121 }
122
123 pub fn highlight_primary_color() -> [f32; 4] {
124 load_colors_once();
125 style_read(&HIGHLIGHT_PRIMARY_COLOR)
126 }
127
128 pub fn set_highlight_primary_color(color: [f32; 4]) {
129 if let Ok(mut lock) = HIGHLIGHT_PRIMARY_COLOR.write() {
130 *lock = [color[0], color[1], color[2], 0.12];
131 }
132 }
133
134 pub fn menubar_tab_label_color() -> [f32; 4] {
135 load_colors_once();
136 style_read(&MENUBAR_TAB_LABEL_COLOR)
137 }
138
139 pub fn set_menubar_tab_label_color(color: [f32; 4]) {
140 style_write(&MENUBAR_TAB_LABEL_COLOR, color);
141 }