git.lucas.co / cce-ui
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 }