Skip to main content

color_utils/
lib.rs

1//! Color utilities.
2
3#![feature(decl_macro)]
4
5use colorsys::Hsl;
6use lab::Lab;
7
8pub mod constants;
9pub mod color;
10mod traits;
11
12pub use self::constants::*;
13pub use self::color::*;
14pub use self::traits::*;
15
16pub use rgb;
17pub use rgb::{Rgb, Rgba, RGB8, RGBA8};
18
19/// Convert RGB8 to values in [0.0, 1.0]
20pub const fn normalize_rgb (Rgb { r, g, b } : RGB8) -> Rgb <f32> {
21  Rgb::new (
22    r as f32 / 255.0,
23    g as f32 / 255.0,
24    b as f32 / 255.0
25  )
26}
27
28/// Convert RGBA8 to values in [0.0, 1.0]
29pub const fn normalize_rgba (Rgba { r, g, b, a } : RGBA8) -> Rgba <f32> {
30  Rgba::new (
31    r as f32 / 255.0,
32    g as f32 / 255.0,
33    b as f32 / 255.0,
34    a as f32 / 255.0
35  )
36}
37
38/// Convert normalized to values to RGB8
39#[expect(clippy::cast_possible_truncation)]
40#[expect(clippy::cast_sign_loss)]
41pub const fn quantize_rgb (Rgb {r, g, b } : Rgb <f32>) -> RGB8 {
42  Rgb::new (
43    (r * 255.0) as u8,
44    (g * 255.0) as u8,
45    (b * 255.0) as u8
46  )
47}
48
49/// Convert normalized to values to RGBA8
50#[expect(clippy::cast_possible_truncation)]
51#[expect(clippy::cast_sign_loss)]
52pub const fn quantize_rgba (Rgba { r, g, b, a } : Rgba <f32>) -> RGBA8 {
53  Rgba::new (
54    (r * 255.0) as u8,
55    (g * 255.0) as u8,
56    (b * 255.0) as u8,
57    (a * 255.0) as u8
58  )
59}
60
61/// Return hue in [0, 360). Returns `None` if color is monochrome (grayscale).
62pub fn hue_deg (rgb : RGB8) -> Option <f32> {
63  let Rgb { r, g, b } = normalize_rgb (rgb);
64  let max   = f32::max (f32::max (r, g), b);
65  let min   = f32::min (f32::min (r, g), b);
66  let delta = max - min;
67  if delta == 0.0 {
68    return None
69  }
70  let mut hue;
71  if max == r {
72    hue = (g - b) / delta % 6.0
73  } else if max == g {
74    hue = ((b - r) / delta) + 2.0;
75  } else {
76    hue = ((r - g) / delta) + 4.0;
77  }
78  hue *= 60.0;
79  if hue < 0.0 {
80    hue += 360.0;
81  }
82  Some (hue)
83}
84
85/// Given a hue in [0, 360) and luminance [0, 100], return the RGB value.
86///
87/// Note that this is an iterative method that walks up or down HSL lightness space
88/// until the luminance reaches the desired value.
89pub fn hue_luminance_custom (hue_deg : f32, luminance : f32) -> RGB8 {
90  let mut rgb = hue_to_rgb (hue_deg);
91  loop {
92    let lum = luminance_custom (rgb);
93    let diff = lum - luminance;
94    if diff.abs() < 0.25 {
95      return rgb
96    }
97    let mut hsl = Hsl::from (colorsys::Rgb::from (rgb.into_array()));
98    let new_lightness = if lum > luminance {
99      hsl.lightness() - 1.0
100    } else {
101      debug_assert!(lum < luminance);
102      hsl.lightness() + 1.0
103    };
104    hsl.set_lightness (new_lightness);
105    let array : [u8; 3] = colorsys::Rgb::from (hsl).into();
106    rgb = Rgb::from (array);
107  }
108}
109
110/// Give a fully saturated RGB value for the given hue
111pub fn hue_to_rgb (hue : f32) -> RGB8 {
112  let h = hue.rem_euclid (360.0);
113  let c = 1.0; // saturation = 1, value = 1
114  let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
115  let [r1, g1, b1] = match h {
116    h if h < 60.0  => [c, x, 0.0],
117    h if h < 120.0 => [x, c, 0.0],
118    h if h < 180.0 => [0.0, c, x],
119    h if h < 240.0 => [0.0, x, c],
120    h if h < 300.0 => [x, 0.0, c],
121    _              => [c, 0.0, x]
122  };
123  quantize_rgb (Rgb::new (r1, g1, b1))
124}
125
126/// Returns luminance value from 0.0-100.0 with custom chroma bias.
127///
128/// Implements algorithm used by <http://www.workwithcolor.com/hsl-color-picker-01.htm>.
129#[expect(clippy::cast_possible_truncation)]
130pub fn luminance_custom (Rgb { r, g, b } : RGB8) -> f32 {
131  if r == g && g == b {
132    return (r as f32 / 255.0) * 100.0
133  }
134  // rgb -> CIELAB
135  let tmp_lab = Lab::from_rgb (&[r, g, b]);
136  // grayscale RGB of computed LAB luminance
137  let tmp_rgb = Lab { a: 0.0, b: 0.0, .. tmp_lab }.to_rgb();
138  // custom chroma bias
139  // maps a -> -5..5 (green-red)
140  let lum_lab_a = ((tmp_lab.a / 127.0) * 50.0) / 10.0;
141  // maps b -> -2.5..2.5 (blue-yellow)
142  let lum_lab_b = (((tmp_lab.a - tmp_lab.b) / 127.0) * 50.0) / 10.0 / 2.0;
143  // addition results in range of -7.5..7.5
144  let mut lum_lab_ab = lum_lab_a + lum_lab_b;
145  // chroma diff will be in the range -255..255
146  if tmp_lab.a > tmp_lab.b {
147    // if green-red chroma is greater than blue-yellow chroma,
148    // add the difference mapped to 0.0..10.0, resulting in a range of -7.5..17.5
149    lum_lab_ab += (((tmp_lab.a - tmp_lab.b) / 127.0) * 50.0) / 10.0;
150  }
151  // HSL from the grayscale RGB of computed LAB luminance, plus the custom luminance
152  // factor
153  Hsl::from (colorsys::Rgb::from (&tmp_rgb)).lightness() as f32 + lum_lab_ab
154}
155
156pub fn report_sizes() {
157  use std::mem::size_of;
158  macro_rules! show {
159    ($e:expr) => { println!("{}: {:?}", stringify!($e), $e); }
160  }
161  println!("report sizes...");
162  show!(size_of::<Color>());
163  println!("...report sizes");
164}
165
166#[cfg(test)]
167mod tests {
168  use super::*;
169
170  #[test]
171  fn hue() {
172    for hue in 0..360 {
173      let rgb = hue_to_rgb (hue as f32);
174      let hue_deg = hue_deg (rgb).unwrap();
175      assert!((hue as f32 - hue_deg).abs() < 1.0,
176        "rgb: {rgb:?}, hue: {hue}, hue_deg: {hue_deg}");
177    }
178  }
179
180  #[test]
181  fn hue_lum() {
182    assert_eq!([211, 0, 0], hue_luminance_custom (0.0, 44.0).into_array());
183    assert_eq!([121, 121, 0], hue_luminance_custom (60.0, 44.0).into_array());
184    assert_eq!([0, 145, 0], hue_luminance_custom (120.0, 44.0).into_array());
185    assert_eq!([0, 130, 130], hue_luminance_custom (180.0, 44.0).into_array());
186    assert_eq!([0, 0, 255], hue_luminance_custom (240.0, 44.0).into_array());
187    assert_eq!([159, 0, 159], hue_luminance_custom (300.0, 44.0).into_array());
188    // make sure alglorithm terminates
189    for hue in 0..360 {
190      for luminance in 0..100 {
191        let _rgb = hue_luminance_custom (hue as f32, luminance as f32);
192      }
193    }
194  }
195}