1#[cfg(all(not(feature = "std"), feature = "libm"))]
2#[allow(unused_imports)]
3use crate::utils::no_std::FloatExt;
4use crate::{
5 color::Argb,
6 hct::Hct,
7 utils::math::{difference_degrees, sanitize_degrees_int},
8 IndexMap,
9};
10#[cfg(not(feature = "std"))]
11use alloc::{vec, vec::Vec};
12#[cfg(feature = "std")]
13use std::{vec, vec::Vec};
14
15#[derive(Debug)]
16struct ScoredHCT {
17 hct: Hct,
18 score: f64,
19}
20
21pub struct Score;
28
29impl Score {
30 const TARGET_CHROMA: f64 = 48.0; const WEIGHT_PROPORTION: f64 = 0.7;
32 const WEIGHT_CHROMA_ABOVE: f64 = 0.3;
33 const WEIGHT_CHROMA_BELOW: f64 = 0.1;
34 const CUTOFF_CHROMA: f64 = 5.0;
35 const CUTOFF_EXCITED_PROPORTION: f64 = 0.01;
36 pub fn score(
54 colors_to_population: &IndexMap<Argb, u32>,
55 desired: Option<i32>,
56 fallback_color_argb: Option<Argb>,
57 filter: Option<bool>,
58 ) -> Vec<Argb> {
59 let desired = desired.unwrap_or(4);
60 let fallback_color_argb = fallback_color_argb.unwrap_or(Argb::new(255, 66, 133, 244));
61 let filter = filter.unwrap_or(true);
62 let mut colors_hct = vec![];
65 let mut hue_population = [0; 360];
66 let mut population_sum = 0.0;
67
68 for (argb, population) in colors_to_population {
69 let hct: Hct = (*argb).into();
70
71 let hue = hct.get_hue().floor() as i32;
72
73 colors_hct.push(hct);
74
75 hue_population[hue as usize] += population;
76 population_sum += f64::from(*population);
77 }
78
79 let mut hue_excited_proportions = [0.0; 360];
81
82 for (hue, population) in hue_population.into_iter().enumerate().take(360) {
83 let proportion = f64::from(population) / population_sum;
84
85 for i in ((hue as i32) - 14)..((hue as i32) + 16) {
86 let neighbor_hue = sanitize_degrees_int(i);
87
88 hue_excited_proportions[neighbor_hue as usize] += proportion;
89 }
90 }
91
92 let mut scored_hcts = vec![];
95
96 for hct in colors_hct {
97 let hue = hct.get_hue().round() as i32;
98
99 let hue = sanitize_degrees_int(hue);
100 let proportion = hue_excited_proportions[hue as usize];
101
102 if filter
103 && (hct.get_chroma() < Self::CUTOFF_CHROMA
104 || proportion <= Self::CUTOFF_EXCITED_PROPORTION)
105 {
106 continue;
107 }
108
109 let proportion_score = proportion * 100.0 * Self::WEIGHT_PROPORTION;
110 let chroma_weight = if hct.get_chroma() < Self::TARGET_CHROMA {
111 Self::WEIGHT_CHROMA_BELOW
112 } else {
113 Self::WEIGHT_CHROMA_ABOVE
114 };
115 let chroma_score = (hct.get_chroma() - Self::TARGET_CHROMA) * chroma_weight;
116 let score = proportion_score + chroma_score;
117
118 scored_hcts.push(ScoredHCT { hct, score });
119 }
120
121 scored_hcts.sort_by(|a, b| unsafe { b.score.partial_cmp(&a.score).unwrap_unchecked() });
124
125 let mut chosen_colors: Vec<Hct> = vec![];
130
131 for difference_degree in (15..=90).rev() {
132 chosen_colors.clear();
133
134 for entry in &scored_hcts {
135 let hct = entry.hct;
136
137 if !chosen_colors.iter().any(|color| {
138 difference_degrees(entry.hct.get_hue(), color.get_hue())
139 < f64::from(difference_degree)
140 }) {
141 chosen_colors.push(hct);
142 }
143
144 if chosen_colors.len() >= desired as usize {
145 break;
146 }
147 }
148
149 if chosen_colors.len() >= desired as usize {
150 break;
151 }
152 }
153
154 let mut colors = vec![];
155
156 if chosen_colors.is_empty() {
157 colors.push(fallback_color_argb);
158 }
159
160 for chosen_hct in chosen_colors {
161 colors.push(Argb::from(chosen_hct));
162 }
163
164 colors
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::Score;
171 use crate::{color::Argb, IndexMap};
172
173 #[test]
174 fn test_prioritizes_chroma() {
175 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
176 (Argb::from_u32(0xff000000), 1),
177 (Argb::from_u32(0xffffffff), 1),
178 (Argb::from_u32(0xff0000ff), 1),
179 ]);
180
181 let ranked = Score::score(&argb_to_population, None, None, None);
182
183 assert_eq!(ranked.len(), 1);
184 assert_eq!(ranked[0], Argb::from_u32(0xff0000ff));
185 }
186
187 #[test]
188 fn test_prioritizes_chroma_when_proportions_equal() {
189 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
190 (Argb::from_u32(0xffff0000), 1),
191 (Argb::from_u32(0xff00ff00), 1),
192 (Argb::from_u32(0xff0000ff), 1),
193 ]);
194
195 let ranked = Score::score(&argb_to_population, None, None, None);
196
197 assert_eq!(ranked.len(), 3);
198 assert_eq!(ranked[0], Argb::from_u32(0xffff0000));
199 assert_eq!(ranked[1], Argb::from_u32(0xff00ff00));
200 assert_eq!(ranked[2], Argb::from_u32(0xff0000ff));
201 }
202
203 #[test]
204 fn test_generates_gblue_when_no_colors_available() {
205 let argb_to_population: IndexMap<Argb, u32> =
206 IndexMap::from_iter([(Argb::from_u32(0xff000000), 1)]);
207
208 let ranked = Score::score(&argb_to_population, None, None, None);
209
210 assert_eq!(ranked.len(), 1);
211 assert_eq!(ranked[0], Argb::from_u32(0xff4285f4));
212 }
213
214 #[test]
215 fn test_dedupes_nearby_hues() {
216 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
217 (Argb::from_u32(0xff008772), 1),
218 (Argb::from_u32(0xff318477), 1),
219 ]);
220
221 let ranked = Score::score(&argb_to_population, None, None, None);
222
223 assert_eq!(ranked.len(), 1);
224 assert_eq!(ranked[0], Argb::from_u32(0xff008772));
225 }
226
227 #[test]
228 fn test_maximizes_hue_distance() {
229 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
230 (Argb::from_u32(0xff008772), 1),
231 (Argb::from_u32(0xff008587), 1),
232 (Argb::from_u32(0xff007ebc), 1),
233 ]);
234
235 let ranked = Score::score(&argb_to_population, Some(2), None, None);
236
237 assert_eq!(ranked.len(), 2);
238 assert_eq!(ranked[0], Argb::from_u32(0xff007ebc));
239 assert_eq!(ranked[1], Argb::from_u32(0xff008772));
240 }
241
242 #[test]
243 fn test_generated_scenario_one() {
244 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
245 (Argb::from_u32(0xff7ea16d), 67),
246 (Argb::from_u32(0xffd8ccae), 67),
247 (Argb::from_u32(0xff835c0d), 49),
248 ]);
249
250 let ranked = Score::score(
251 &argb_to_population,
252 Some(3),
253 Some(Argb::from_u32(0xff8d3819)),
254 Some(false),
255 );
256
257 assert_eq!(ranked.len(), 3);
258 assert_eq!(ranked[0], Argb::from_u32(0xff7ea16d));
259 assert_eq!(ranked[1], Argb::from_u32(0xffd8ccae));
260 assert_eq!(ranked[2], Argb::from_u32(0xff835c0d));
261 }
262
263 #[test]
264 fn test_generated_scenario_two() {
265 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
266 (Argb::from_u32(0xffd33881), 14),
267 (Argb::from_u32(0xff3205cc), 77),
268 (Argb::from_u32(0xff0b48cf), 36),
269 (Argb::from_u32(0xffa08f5d), 81),
270 ]);
271
272 let ranked = Score::score(
273 &argb_to_population,
274 None,
275 Some(Argb::from_u32(0xff7d772b)),
276 None,
277 );
278
279 assert_eq!(ranked.len(), 3);
280 assert_eq!(ranked[0], Argb::from_u32(0xff3205cc));
281 assert_eq!(ranked[1], Argb::from_u32(0xffa08f5d));
282 assert_eq!(ranked[2], Argb::from_u32(0xffd33881));
283 }
284
285 #[test]
286 fn test_generated_scenario_three() {
287 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
288 (Argb::from_u32(0xffbe94a6), 23),
289 (Argb::from_u32(0xffc33fd7), 42),
290 (Argb::from_u32(0xff899f36), 90),
291 (Argb::from_u32(0xff94c574), 82),
292 ]);
293
294 let ranked = Score::score(
295 &argb_to_population,
296 Some(3),
297 Some(Argb::from_u32(0xffaa79a4)),
298 None,
299 );
300
301 assert_eq!(ranked.len(), 3);
302 assert_eq!(ranked[0], Argb::from_u32(0xff94c574));
303 assert_eq!(ranked[1], Argb::from_u32(0xffc33fd7));
304 assert_eq!(ranked[2], Argb::from_u32(0xffbe94a6));
305 }
306
307 #[test]
308 fn test_generated_scenario_four() {
309 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
310 (Argb::from_u32(0xffdf241c), 85),
311 (Argb::from_u32(0xff685859), 44),
312 (Argb::from_u32(0xffd06d5f), 34),
313 (Argb::from_u32(0xff561c54), 27),
314 (Argb::from_u32(0xff713090), 88),
315 ]);
316
317 let ranked = Score::score(
318 &argb_to_population,
319 Some(5),
320 Some(Argb::from_u32(0xff58c19c)),
321 Some(false),
322 );
323
324 assert_eq!(ranked.len(), 2);
325 assert_eq!(ranked[0], Argb::from_u32(0xffdf241c));
326 assert_eq!(ranked[1], Argb::from_u32(0xff561c54));
327 }
328
329 #[test]
330 fn test_generated_scenario_five() {
331 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
332 (Argb::from_u32(0xffbe66f8), 41),
333 (Argb::from_u32(0xff4bbda9), 88),
334 (Argb::from_u32(0xff80f6f9), 44),
335 (Argb::from_u32(0xffab8017), 43),
336 (Argb::from_u32(0xffe89307), 65),
337 ]);
338
339 let ranked = Score::score(
340 &argb_to_population,
341 Some(3),
342 Some(Argb::from_u32(0xff916691)),
343 Some(false),
344 );
345
346 assert_eq!(ranked.len(), 3);
347 assert_eq!(ranked[0], Argb::from_u32(0xffab8017));
348 assert_eq!(ranked[1], Argb::from_u32(0xff4bbda9));
349 assert_eq!(ranked[2], Argb::from_u32(0xffbe66f8));
350 }
351
352 #[test]
353 fn test_generated_scenario_six() {
354 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
355 (Argb::from_u32(0xff18ea8f), 93),
356 (Argb::from_u32(0xff327593), 18),
357 (Argb::from_u32(0xff066a18), 74),
358 (Argb::from_u32(0xfffa8a23), 62),
359 (Argb::from_u32(0xff04ca1f), 65),
360 ]);
361
362 let ranked = Score::score(
363 &argb_to_population,
364 Some(2),
365 Some(Argb::from_u32(0xff4c377a)),
366 Some(false),
367 );
368
369 assert_eq!(ranked.len(), 2);
370 assert_eq!(ranked[0], Argb::from_u32(0xff18ea8f));
371 assert_eq!(ranked[1], Argb::from_u32(0xfffa8a23));
372 }
373
374 #[test]
375 fn test_generated_scenario_seven() {
376 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
377 (Argb::from_u32(0xff2e05ed), 23),
378 (Argb::from_u32(0xff153e55), 90),
379 (Argb::from_u32(0xff9ab220), 23),
380 (Argb::from_u32(0xff153379), 66),
381 (Argb::from_u32(0xff68bcc3), 81),
382 ]);
383
384 let ranked = Score::score(
385 &argb_to_population,
386 Some(2),
387 Some(Argb::from_u32(0xfff588dc)),
388 None,
389 );
390
391 assert_eq!(ranked.len(), 2);
392 assert_eq!(ranked[0], Argb::from_u32(0xff2e05ed));
393 assert_eq!(ranked[1], Argb::from_u32(0xff9ab220));
394 }
395
396 #[test]
397 fn test_generated_scenario_eight() {
398 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
399 (Argb::from_u32(0xff816ec5), 24),
400 (Argb::from_u32(0xff6dcb94), 19),
401 (Argb::from_u32(0xff3cae91), 98),
402 (Argb::from_u32(0xff5b542f), 25),
403 ]);
404
405 let ranked = Score::score(
406 &argb_to_population,
407 Some(1),
408 Some(Argb::from_u32(0xff84b0fd)),
409 Some(false),
410 );
411
412 assert_eq!(ranked.len(), 1);
413 assert_eq!(ranked[0], Argb::from_u32(0xff3cae91));
414 }
415
416 #[test]
417 fn test_generated_scenario_nine() {
418 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
419 (Argb::from_u32(0xff206f86), 52),
420 (Argb::from_u32(0xff4a620d), 96),
421 (Argb::from_u32(0xfff51401), 85),
422 (Argb::from_u32(0xff2b8ebf), 3),
423 (Argb::from_u32(0xff277766), 59),
424 ]);
425
426 let ranked = Score::score(
427 &argb_to_population,
428 Some(3),
429 Some(Argb::from_u32(0xff02b415)),
430 None,
431 );
432
433 assert_eq!(ranked.len(), 3);
434 assert_eq!(ranked[0], Argb::from_u32(0xfff51401));
435 assert_eq!(ranked[1], Argb::from_u32(0xff4a620d));
436 assert_eq!(ranked[2], Argb::from_u32(0xff2b8ebf));
437 }
438
439 #[test]
440 fn test_generated_scenario_ten() {
441 let argb_to_population: IndexMap<Argb, u32> = IndexMap::from_iter([
442 (Argb::from_u32(0xff8b1d99), 54),
443 (Argb::from_u32(0xff27effe), 43),
444 (Argb::from_u32(0xff6f558d), 2),
445 (Argb::from_u32(0xff77fdf2), 78),
446 ]);
447
448 let ranked = Score::score(
449 &argb_to_population,
450 None,
451 Some(Argb::from_u32(0xff5e7a10)),
452 None,
453 );
454
455 assert_eq!(ranked.len(), 3);
456 assert_eq!(ranked[0], Argb::from_u32(0xff27effe));
457 assert_eq!(ranked[1], Argb::from_u32(0xff8b1d99));
458 assert_eq!(ranked[2], Argb::from_u32(0xff6f558d));
459 }
460}