• 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.palettes;
18 
19 import com.google.ux.material.libmonet.hct.Hct;
20 import java.util.HashMap;
21 import java.util.Map;
22 
23 /**
24  * A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
25  *
26  * <p>TonalPalette is intended for use in a single thread due to its stateful caching.
27  */
28 public final class TonalPalette {
29   Map<Integer, Integer> cache;
30   Hct keyColor;
31   double hue;
32   double chroma;
33 
34   /**
35    * Create tones using the HCT hue and chroma from a color.
36    *
37    * @param argb ARGB representation of a color
38    * @return Tones matching that color's hue and chroma.
39    */
fromInt(int argb)40   public static TonalPalette fromInt(int argb) {
41     return fromHct(Hct.fromInt(argb));
42   }
43 
44   /**
45    * Create tones using a HCT color.
46    *
47    * @param hct HCT representation of a color.
48    * @return Tones matching that color's hue and chroma.
49    */
fromHct(Hct hct)50   public static TonalPalette fromHct(Hct hct) {
51     return new TonalPalette(hct.getHue(), hct.getChroma(), hct);
52   }
53 
54   /**
55    * Create tones from a defined HCT hue and chroma.
56    *
57    * @param hue HCT hue
58    * @param chroma HCT chroma
59    * @return Tones matching hue and chroma.
60    */
fromHueAndChroma(double hue, double chroma)61   public static TonalPalette fromHueAndChroma(double hue, double chroma) {
62     final Hct keyColor = new KeyColor(hue, chroma).create();
63     return new TonalPalette(hue, chroma, keyColor);
64   }
65 
TonalPalette(double hue, double chroma, Hct keyColor)66   private TonalPalette(double hue, double chroma, Hct keyColor) {
67     cache = new HashMap<>();
68     this.hue = hue;
69     this.chroma = chroma;
70     this.keyColor = keyColor;
71   }
72 
73   /**
74    * Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
75    *
76    * @param tone HCT tone, measured from 0 to 100.
77    * @return ARGB representation of a color with that tone.
78    */
tone(int tone)79   public int tone(int tone) {
80     Integer color = cache.get(tone);
81     if (color == null) {
82       color = Hct.from(this.hue, this.chroma, tone).toInt();
83       cache.put(tone, color);
84     }
85     return color;
86   }
87 
88   /** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
getHct(double tone)89   public Hct getHct(double tone) {
90     return Hct.from(this.hue, this.chroma, tone);
91   }
92 
93   /** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
getChroma()94   public double getChroma() {
95     return this.chroma;
96   }
97 
98   /** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
getHue()99   public double getHue() {
100     return this.hue;
101   }
102 
103   /** The key color is the first tone, starting from T50, that matches the palette's chroma. */
getKeyColor()104   public Hct getKeyColor() {
105     return this.keyColor;
106   }
107 
108   /** Key color is a color that represents the hue and chroma of a tonal palette. */
109   private static final class KeyColor {
110     private final double hue;
111     private final double requestedChroma;
112 
113     // Cache that maps tone to max chroma to avoid duplicated HCT calculation.
114     private final Map<Integer, Double> chromaCache = new HashMap<>();
115     private static final double MAX_CHROMA_VALUE = 200.0;
116 
117     /** Key color is a color that represents the hue and chroma of a tonal palette */
KeyColor(double hue, double requestedChroma)118     public KeyColor(double hue, double requestedChroma) {
119       this.hue = hue;
120       this.requestedChroma = requestedChroma;
121     }
122 
123     /**
124      * Creates a key color from a [hue] and a [chroma]. The key color is the first tone, starting
125      * from T50, matching the given hue and chroma.
126      *
127      * @return Key color [Hct]
128      */
create()129     public Hct create() {
130       // Pivot around T50 because T50 has the most chroma available, on
131       // average. Thus it is most likely to have a direct answer.
132       final int pivotTone = 50;
133       final int toneStepSize = 1;
134       // Epsilon to accept values slightly higher than the requested chroma.
135       final double epsilon = 0.01;
136 
137       // Binary search to find the tone that can provide a chroma that is closest
138       // to the requested chroma.
139       int lowerTone = 0;
140       int upperTone = 100;
141       while (lowerTone < upperTone) {
142         final int midTone = (lowerTone + upperTone) / 2;
143         boolean isAscending = maxChroma(midTone) < maxChroma(midTone + toneStepSize);
144         boolean sufficientChroma = maxChroma(midTone) >= requestedChroma - epsilon;
145 
146         if (sufficientChroma) {
147           // Either range [lowerTone, midTone] or [midTone, upperTone] has
148           // the answer, so search in the range that is closer the pivot tone.
149           if (Math.abs(lowerTone - pivotTone) < Math.abs(upperTone - pivotTone)) {
150             upperTone = midTone;
151           } else {
152             if (lowerTone == midTone) {
153               return Hct.from(this.hue, this.requestedChroma, lowerTone);
154             }
155             lowerTone = midTone;
156           }
157         } else {
158           // As there is no sufficient chroma in the midTone, follow the direction to the chroma
159           // peak.
160           if (isAscending) {
161             lowerTone = midTone + toneStepSize;
162           } else {
163             // Keep midTone for potential chroma peak.
164             upperTone = midTone;
165           }
166         }
167       }
168 
169       return Hct.from(this.hue, this.requestedChroma, lowerTone);
170     }
171 
172     // Find the maximum chroma for a given tone
maxChroma(int tone)173     private double maxChroma(int tone) {
174       if (chromaCache.get(tone) == null) {
175         Double newChroma = Hct.from(hue, MAX_CHROMA_VALUE, tone).getChroma();
176         if (newChroma != null) {
177           chromaCache.put(tone, newChroma);
178         }
179       }
180       return chromaCache.get(tone);
181     }
182   }
183 }
184