1use std::num::NonZeroUsize;
2
3use almost::equal;
4use palette::{convert::FromColorUnclamped, ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba};
5
6pub fn steps<C>(c: C, len: NonZeroUsize) -> Vec<Srgba>
10where
11 Oklcha: FromColor<C>,
12{
13 let mut c = Oklcha::from_color(c);
14 let mut steps = Vec::with_capacity(len.get());
15
16 for i in 0..len.get() {
17 let lightness = i as f32 / (len.get() - 1) as f32;
18 c.l = lightness;
19 steps.push(oklch_to_srgba_nearest_chroma(c))
20 }
21 steps
22}
23
24pub fn get_index(base_index: usize, steps: usize, step_len: usize, is_dark: bool) -> Option<usize> {
26 if is_dark {
27 base_index.checked_add(steps)
28 } else {
29 base_index.checked_sub(steps)
30 }
31 .filter(|i| *i < step_len)
32}
33
34pub fn get_surface_color(
36 base_index: usize,
37 steps: usize,
38 step_array: &[Srgba],
39 mut is_dark: bool,
40 fallback: &Srgba,
41) -> Srgba {
42 assert!(step_array.len() == 100);
43
44 is_dark = is_dark || base_index < 91;
45
46 *get_index(base_index, steps, step_array.len(), is_dark)
47 .and_then(|i| step_array.get(i))
48 .unwrap_or(fallback)
49}
50
51#[must_use]
53pub fn get_small_widget_color(
54 base_index: usize,
55 steps: usize,
56 step_array: &[Srgba],
57 fallback: &Srgba,
58) -> Srgba {
59 assert!(step_array.len() == 100);
60
61 let is_dark = base_index <= 40 || (base_index > 51 && base_index < 65);
62
63 let res = *get_index(base_index, steps, step_array.len(), is_dark)
64 .and_then(|i| step_array.get(i))
65 .unwrap_or(fallback);
66
67 let mut lch = Lch::from_color(res);
68 if lch.chroma / Lch::<f32>::max_chroma() > 0.03 {
69 lch.chroma = 0.03 * Lch::<f32>::max_chroma();
70 lch.clamp_assign();
71 Srgba::from_color(lch)
72 } else {
73 res
74 }
75}
76
77pub fn get_text(
79 base_index: usize,
80 step_array: &[Srgba],
81 fallback: &Srgba,
82 tint_array: Option<&[Srgba]>,
83) -> Srgba {
84 assert!(step_array.len() == 100);
85 let step_array = if let Some(tint_array) = tint_array {
86 assert!(tint_array.len() == 100);
87 tint_array
88 } else {
89 step_array
90 };
91
92 let is_dark = base_index < 60;
93
94 let index = get_index(base_index, 70, step_array.len(), is_dark)
95 .or_else(|| get_index(base_index, 50, step_array.len(), is_dark))
96 .unwrap_or_else(|| if is_dark { 99 } else { 0 });
97
98 *step_array.get(index).unwrap_or(fallback)
99}
100
101pub fn color_index<C>(c: C, array_len: usize) -> usize
104where
105 Oklcha: FromColor<C>,
106{
107 let c = Oklcha::from_color(c);
108 ((c.l * array_len as f32).round() as usize).clamp(0, array_len - 1)
109}
110
111pub fn oklch_to_srgba_nearest_chroma(mut c: Oklcha) -> Srgba {
113 let mut r_chroma = c.chroma;
114 let mut l_chroma = 0.0;
115 let mut new_c = Srgba::from_color_unclamped(c);
117
118 if is_valid_srgb(new_c) {
119 new_c.clamp_assign();
120 return new_c;
121 }
122
123 for _ in 0..64 {
125 let new_c = Srgba::from_color_unclamped(c);
126 if is_valid_srgb(new_c) {
127 l_chroma = c.chroma;
128 c.chroma = (c.chroma + r_chroma) / 2.0;
129 } else {
130 r_chroma = c.chroma;
131 c.chroma = (c.chroma + l_chroma) / 2.0;
132 }
133 }
134 Srgba::from_color(c)
135}
136
137pub fn is_valid_srgb(c: Srgba) -> bool {
139 (equal(c.red, Srgb::max_red()) || (c.red >= Srgb::min_red() && c.red <= Srgb::max_red()))
140 && (equal(c.blue, Srgb::max_blue())
141 || (c.blue >= Srgb::min_blue() && c.blue <= Srgb::max_blue()))
142 && (equal(c.green, Srgb::max_green())
143 || (c.green >= Srgb::min_green() && c.green <= Srgb::max_green()))
144}
145
146#[cfg(test)]
147mod tests {
148 use almost::equal;
149 use palette::{OklabHue, Srgba};
150
151 use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma};
152
153 #[test]
154 fn test_valid_check() {
155 assert!(is_valid_srgb(Srgba::new(1.0, 1.0, 1.0, 1.0)));
156 assert!(is_valid_srgb(Srgba::new(0.0, 0.0, 0.0, 1.0)));
157 assert!(is_valid_srgb(Srgba::new(0.5, 0.5, 0.5, 1.0)));
158 assert!(!is_valid_srgb(Srgba::new(-0.1, 0.0, 0.0, 1.0)));
159 assert!(!is_valid_srgb(Srgba::new(0.0, -0.1, 0.0, 1.0)));
160 assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -0.1, 1.0)));
161 assert!(!is_valid_srgb(Srgba::new(-100.1, 0.0, 0.0, 1.0)));
162 assert!(!is_valid_srgb(Srgba::new(0.0, -100.1, 0.0, 1.0)));
163 assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, -100.1, 1.0)));
164 assert!(!is_valid_srgb(Srgba::new(1.1, 0.0, 0.0, 1.0)));
165 assert!(!is_valid_srgb(Srgba::new(0.0, 1.1, 0.0, 1.0)));
166 assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 1.1, 1.0)));
167 assert!(!is_valid_srgb(Srgba::new(100.1, 0.0, 0.0, 1.0)));
168 assert!(!is_valid_srgb(Srgba::new(0.0, 100.1, 0.0, 1.0)));
169 assert!(!is_valid_srgb(Srgba::new(-0.0, 0.0, 100.1, 1.0)));
170 }
171
172 #[test]
173 fn test_conversion_boundaries() {
174 let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
175 let srgb = oklch_to_srgba_nearest_chroma(c1);
176 equal(srgb.red, 0.0);
177 equal(srgb.blue, 0.0);
178 equal(srgb.green, 0.0);
179
180 let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0);
181 let srgb = oklch_to_srgba_nearest_chroma(c1);
182
183 equal(srgb.red, 1.0);
184 equal(srgb.blue, 1.0);
185 equal(srgb.green, 1.0);
186 }
187
188 #[test]
189 fn test_conversion_colors() {
190 let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0);
191 let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
192 assert!(srgb.red == 133);
193 assert!(srgb.green == 69);
194 assert!(srgb.blue == 0);
195
196 let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0);
197 let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
198 assert!(srgb.red == 78);
199 assert!(srgb.green == 27);
200 assert!(srgb.blue == 15);
201
202 let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0);
203 let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
204 assert!(srgb.red == 192);
205 assert!(srgb.green == 153);
206 assert!(srgb.blue == 253);
207 }
208
209 #[test]
210 fn test_conversion_fallback_colors() {
211 let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0);
212 let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
213 assert!(srgb.red == 255);
214 assert!(srgb.green == 103);
215 assert!(srgb.blue == 65);
216
217 let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0);
218 let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
219 assert!(srgb.red == 193);
220 assert!(srgb.green == 152);
221 assert!(srgb.blue == 255);
222
223 let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0);
224 let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::<u8, u8>();
225 assert!(srgb.red == 1);
226 assert!(srgb.green == 19);
227 assert!(srgb.blue == 0);
228 }
229}