material_colors/
score.rs

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
21/// Given a large set of colors, remove colors that are unsuitable for a UI
22/// theme, and rank the rest based on suitability.
23///
24/// Enables use of a high cluster count for image quantization, thus ensuring
25/// colors aren't muddied, while curating the high cluster count to a much
26///  smaller number of appropriate choices.
27pub struct Score;
28
29impl Score {
30    const TARGET_CHROMA: f64 = 48.0; // A1 Chroma
31    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    /// Given a map with keys of colors and values of how often the color appears,
37    /// rank the colors based on suitability for being used for a UI theme.
38    ///
39    /// - Parameters:
40    ///   `colorsToPopulation`: is a map with keys of colors and values of often
41    ///     the color appears, usually from a source image.
42    ///   `desired`: Max count of colors to be returned in the list.
43    ///   `fallbackColorArgb`: Color to be returned if no other options available.
44    ///   `filter`: Whether to filter out undesireable combinations.
45    ///
46    /// - Returns: A list of color `Int` that can be used when generating a theme.
47    ///   The list returned is of length <= `desired`. The recommended color is
48    ///   the first item, the least suitable is the last. There will always be at
49    ///   least one color returned. If all the input colors were not suitable for
50    ///   a theme, a default fallback color will be provided, Google Blue. The
51    ///   default number of colors returned is 4, simply because thats the # of
52    ///   colors display in Android 12's wallpaper picker.
53    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        // Get the HCT color for each Argb value, while finding the per hue count and
63        // total count.
64        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        // Hues with more usage in neighboring 30 degree slice get a larger number.
80        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        // Scores each HCT color based on usage and chroma, while optionally
93        // filtering out values that do not have enough chroma or usage.
94        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        // Sorted so that colors with higher scores come first.
122        // SAFETY: The score will never be NAN, so using `unwrap_unchecked` is completely safe
123        scored_hcts.sort_by(|a, b| unsafe { b.score.partial_cmp(&a.score).unwrap_unchecked() });
124
125        // Iterates through potential hue differences in degrees in order to select
126        // the colors with the largest distribution of hues possible. Starting at
127        // 90 degrees(maximum difference for 4 colors) then decreasing down to a
128        // 15 degree minimum.
129        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}