Skip to main content

ecolor/
lib.rs

1//! Color conversions and types.
2//!
3//! This crate is built for the wants and needs of [`egui`](https://github.com/emilk/egui/).
4//!
5//! If you want an actual _good_ color crate, use [`color`](https://crates.io/crates/color) instead.
6//!
7//! If you want a compact color representation, use [`Color32`].
8//! If you want to manipulate RGBA colors in linear space use [`Rgba`].
9//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
10//!
11//! ## Conventions
12//! The word "gamma" or "srgb" is used to refer to values in the non-linear space defined by
13//! [the sRGB transfer function](https://en.wikipedia.org/wiki/SRGB).
14//! We use `u8` for anything in the "gamma" space.
15//!
16//! We use `f32` in 0-1 range for anything in the linear space.
17//!
18//! ## Feature flags
19#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
20//!
21
22#![expect(clippy::wrong_self_convention)]
23
24#[cfg(feature = "cint")]
25mod cint_impl;
26
27mod color32;
28pub use color32::*;
29
30mod hsva_gamma;
31pub use hsva_gamma::*;
32
33mod hsva;
34pub use hsva::*;
35
36#[cfg(feature = "color-hex")]
37mod hex_color_macro;
38#[cfg(feature = "color-hex")]
39#[doc(hidden)]
40pub use color_hex;
41
42mod rgba;
43pub use rgba::*;
44
45mod hex_color_runtime;
46pub use hex_color_runtime::*;
47
48// ----------------------------------------------------------------------------
49// Color conversion:
50
51impl From<Color32> for Rgba {
52    fn from(srgba: Color32) -> Self {
53        let [r, g, b, a] = srgba.to_array();
54        if a == 0 {
55            // Additive, or completely transparent
56            Self([
57                linear_f32_from_gamma_u8(r),
58                linear_f32_from_gamma_u8(g),
59                linear_f32_from_gamma_u8(b),
60                0.0,
61            ])
62        } else {
63            let a = linear_f32_from_linear_u8(a);
64            Self([
65                linear_from_gamma(r as f32 / (255.0 * a)) * a,
66                linear_from_gamma(g as f32 / (255.0 * a)) * a,
67                linear_from_gamma(b as f32 / (255.0 * a)) * a,
68                a,
69            ])
70        }
71    }
72}
73
74impl From<Rgba> for Color32 {
75    fn from(rgba: Rgba) -> Self {
76        let [r, g, b, a] = rgba.to_array();
77        if a == 0.0 {
78            // Additive, or completely transparent
79            Self([
80                gamma_u8_from_linear_f32(r),
81                gamma_u8_from_linear_f32(g),
82                gamma_u8_from_linear_f32(b),
83                0,
84            ])
85        } else {
86            Self([
87                fast_round(gamma_u8_from_linear_f32(r / a) as f32 * a),
88                fast_round(gamma_u8_from_linear_f32(g / a) as f32 * a),
89                fast_round(gamma_u8_from_linear_f32(b / a) as f32 * a),
90                linear_u8_from_linear_f32(a),
91            ])
92        }
93    }
94}
95
96/// gamma [0, 255] -> linear [0, 1].
97pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
98    if s <= 10 {
99        s as f32 / 3294.6
100    } else {
101        ((s as f32 + 14.025) / 269.025).powf(2.4)
102    }
103}
104
105/// linear [0, 255] -> linear [0, 1].
106/// Useful for alpha-channel.
107#[inline(always)]
108pub const fn linear_f32_from_linear_u8(a: u8) -> f32 {
109    a as f32 / 255.0
110}
111
112/// linear [0, 1] -> gamma [0, 255] (clamped).
113/// Values outside this range will be clamped to the range.
114pub fn gamma_u8_from_linear_f32(l: f32) -> u8 {
115    if l <= 0.0 {
116        0
117    } else if l <= 0.0031308 {
118        fast_round(3294.6 * l)
119    } else if l <= 1.0 {
120        fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025)
121    } else {
122        255
123    }
124}
125
126/// linear [0, 1] -> linear [0, 255] (clamped).
127/// Useful for alpha-channel.
128#[inline(always)]
129pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
130    fast_round(a * 255.0)
131}
132
133const fn fast_round(r: f32) -> u8 {
134    (r + 0.5) as _ // rust does a saturating cast since 1.45
135}
136
137/// Compute val * (frac/255) with no floating point or divisions.
138#[inline]
139const fn mul_frac_round(val: u8, frac: u8) -> u8 {
140    // Treat this as a simple fixed point calculation
141    let p = (val as u16) * (frac as u16) + 128;
142    ((p + (p >> 8)) >> 8) as u8
143    // Logic split out a bit more.
144    //let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate.
145    //return ((p + 1 + (p >> 8)) >> 8) as u8;
146}
147
148#[test]
149pub fn test_srgba_conversion() {
150    for b in 0..=255 {
151        let l = linear_f32_from_gamma_u8(b);
152        assert!(0.0 <= l && l <= 1.0);
153        assert_eq!(gamma_u8_from_linear_f32(l), b);
154    }
155}
156
157/// gamma [0, 1] -> linear [0, 1] (not clamped).
158/// Works for numbers outside this range (e.g. negative numbers).
159pub fn linear_from_gamma(gamma: f32) -> f32 {
160    if gamma < 0.0 {
161        -linear_from_gamma(-gamma)
162    } else if gamma <= 0.04045 {
163        gamma / 12.92
164    } else {
165        ((gamma + 0.055) / 1.055).powf(2.4)
166    }
167}
168
169/// linear [0, 1] -> gamma [0, 1] (not clamped).
170/// Works for numbers outside this range (e.g. negative numbers).
171pub fn gamma_from_linear(linear: f32) -> f32 {
172    if linear < 0.0 {
173        -gamma_from_linear(-linear)
174    } else if linear <= 0.0031308 {
175        12.92 * linear
176    } else {
177        1.055 * linear.powf(1.0 / 2.4) - 0.055
178    }
179}
180
181// ----------------------------------------------------------------------------
182
183/// Cheap and ugly.
184/// Made for graying out disabled `Ui`s.
185pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 {
186    let [mut r, mut g, mut b, mut a] = color.to_array();
187
188    if a == 0 {
189        r /= 2;
190        g /= 2;
191        b /= 2;
192    } else if a < 170 {
193        // Cheapish and looks ok.
194        // Works for e.g. grid stripes.
195        let div = (2 * 255 / a as i32) as u8;
196        r = r / 2 + target.r() / div;
197        g = g / 2 + target.g() / div;
198        b = b / 2 + target.b() / div;
199        a /= 2;
200    } else {
201        r = r / 2 + target.r() / 2;
202        g = g / 2 + target.g() / 2;
203        b = b / 2 + target.b() / 2;
204    }
205    Color32::from_rgba_premultiplied(r, g, b, a)
206}