• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.ux.material.libmonet.score;
18 
19 import com.google.ux.material.libmonet.hct.Hct;
20 import com.google.ux.material.libmonet.utils.MathUtils;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Comparator;
24 import java.util.List;
25 import java.util.Map;
26 
27 /**
28  * Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest
29  * based on suitability.
30  *
31  * <p>Enables use of a high cluster count for image quantization, thus ensuring colors aren't
32  * muddied, while curating the high cluster count to a much smaller number of appropriate choices.
33  */
34 public final class Score {
35   private static final double TARGET_CHROMA = 48.; // A1 Chroma
36   private static final double WEIGHT_PROPORTION = 0.7;
37   private static final double WEIGHT_CHROMA_ABOVE = 0.3;
38   private static final double WEIGHT_CHROMA_BELOW = 0.1;
39   private static final double CUTOFF_CHROMA = 5.;
40   private static final double CUTOFF_EXCITED_PROPORTION = 0.01;
41 
Score()42   private Score() {}
43 
score(Map<Integer, Integer> colorsToPopulation)44   public static List<Integer> score(Map<Integer, Integer> colorsToPopulation) {
45     // Fallback color is Google Blue.
46     return score(colorsToPopulation, 4, 0xff4285f4, true);
47   }
48 
score(Map<Integer, Integer> colorsToPopulation, int desired)49   public static List<Integer> score(Map<Integer, Integer> colorsToPopulation, int desired) {
50     return score(colorsToPopulation, desired, 0xff4285f4, true);
51   }
52 
score( Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb)53   public static List<Integer> score(
54       Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb) {
55     return score(colorsToPopulation, desired, fallbackColorArgb, true);
56   }
57 
58   /**
59    * Given a map with keys of colors and values of how often the color appears, rank the colors
60    * based on suitability for being used for a UI theme.
61    *
62    * @param colorsToPopulation map with keys of colors and values of how often the color appears,
63    *     usually from a source image.
64    * @param desired max count of colors to be returned in the list.
65    * @param fallbackColorArgb color to be returned if no other options available.
66    * @param filter whether to filter out undesireable combinations.
67    * @return Colors sorted by suitability for a UI theme. The most suitable color is the first item,
68    *     the least suitable is the last. There will always be at least one color returned. If all
69    *     the input colors were not suitable for a theme, a default fallback color will be provided,
70    *     Google Blue.
71    */
score( Map<Integer, Integer> colorsToPopulation, int desired, int fallbackColorArgb, boolean filter)72   public static List<Integer> score(
73       Map<Integer, Integer> colorsToPopulation,
74       int desired,
75       int fallbackColorArgb,
76       boolean filter) {
77 
78     // Get the HCT color for each Argb value, while finding the per hue count and
79     // total count.
80     List<Hct> colorsHct = new ArrayList<>();
81     int[] huePopulation = new int[360];
82     double populationSum = 0.;
83     for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) {
84       Hct hct = Hct.fromInt(entry.getKey());
85       colorsHct.add(hct);
86       int hue = (int) Math.floor(hct.getHue());
87       huePopulation[hue] += entry.getValue();
88       populationSum += entry.getValue();
89     }
90 
91     // Hues with more usage in neighboring 30 degree slice get a larger number.
92     double[] hueExcitedProportions = new double[360];
93     for (int hue = 0; hue < 360; hue++) {
94       double proportion = huePopulation[hue] / populationSum;
95       for (int i = hue - 14; i < hue + 16; i++) {
96         int neighborHue = MathUtils.sanitizeDegreesInt(i);
97         hueExcitedProportions[neighborHue] += proportion;
98       }
99     }
100 
101     // Scores each HCT color based on usage and chroma, while optionally
102     // filtering out values that do not have enough chroma or usage.
103     List<ScoredHCT> scoredHcts = new ArrayList<>();
104     for (Hct hct : colorsHct) {
105       int hue = MathUtils.sanitizeDegreesInt((int) Math.round(hct.getHue()));
106       double proportion = hueExcitedProportions[hue];
107       if (filter && (hct.getChroma() < CUTOFF_CHROMA || proportion <= CUTOFF_EXCITED_PROPORTION)) {
108         continue;
109       }
110 
111       double proportionScore = proportion * 100.0 * WEIGHT_PROPORTION;
112       double chromaWeight =
113           hct.getChroma() < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE;
114       double chromaScore = (hct.getChroma() - TARGET_CHROMA) * chromaWeight;
115       double score = proportionScore + chromaScore;
116       scoredHcts.add(new ScoredHCT(hct, score));
117     }
118     // Sorted so that colors with higher scores come first.
119     Collections.sort(scoredHcts, new ScoredComparator());
120 
121     // Iterates through potential hue differences in degrees in order to select
122     // the colors with the largest distribution of hues possible. Starting at
123     // 90 degrees(maximum difference for 4 colors) then decreasing down to a
124     // 15 degree minimum.
125     List<Hct> chosenColors = new ArrayList<>();
126     for (int differenceDegrees = 90; differenceDegrees >= 15; differenceDegrees--) {
127       chosenColors.clear();
128       for (ScoredHCT entry : scoredHcts) {
129         Hct hct = entry.hct;
130         boolean hasDuplicateHue = false;
131         for (Hct chosenHct : chosenColors) {
132           if (MathUtils.differenceDegrees(hct.getHue(), chosenHct.getHue()) < differenceDegrees) {
133             hasDuplicateHue = true;
134             break;
135           }
136         }
137         if (!hasDuplicateHue) {
138           chosenColors.add(hct);
139         }
140         if (chosenColors.size() >= desired) {
141           break;
142         }
143       }
144       if (chosenColors.size() >= desired) {
145         break;
146       }
147     }
148     List<Integer> colors = new ArrayList<>();
149     if (chosenColors.isEmpty()) {
150       colors.add(fallbackColorArgb);
151     }
152     for (Hct chosenHct : chosenColors) {
153       colors.add(chosenHct.toInt());
154     }
155     return colors;
156   }
157 
158   private static class ScoredHCT {
159     public final Hct hct;
160     public final double score;
161 
ScoredHCT(Hct hct, double score)162     public ScoredHCT(Hct hct, double score) {
163       this.hct = hct;
164       this.score = score;
165     }
166   }
167 
168   private static class ScoredComparator implements Comparator<ScoredHCT> {
ScoredComparator()169     public ScoredComparator() {}
170 
171     @Override
compare(ScoredHCT entry1, ScoredHCT entry2)172     public int compare(ScoredHCT entry1, ScoredHCT entry2) {
173       return Double.compare(entry2.score, entry1.score);
174     }
175   }
176 }
177