• 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 public final class TonalPalette {
27   Map<Integer, Integer> cache;
28   Hct keyColor;
29   double hue;
30   double chroma;
31 
32   /**
33    * Create tones using the HCT hue and chroma from a color.
34    *
35    * @param argb ARGB representation of a color
36    * @return Tones matching that color's hue and chroma.
37    */
fromInt(int argb)38   public static TonalPalette fromInt(int argb) {
39     return fromHct(Hct.fromInt(argb));
40   }
41 
42   /**
43    * Create tones using a HCT color.
44    *
45    * @param hct HCT representation of a color.
46    * @return Tones matching that color's hue and chroma.
47    */
fromHct(Hct hct)48   public static TonalPalette fromHct(Hct hct) {
49     return new TonalPalette(hct.getHue(), hct.getChroma(), hct);
50   }
51 
52   /**
53    * Create tones from a defined HCT hue and chroma.
54    *
55    * @param hue HCT hue
56    * @param chroma HCT chroma
57    * @return Tones matching hue and chroma.
58    */
fromHueAndChroma(double hue, double chroma)59   public static TonalPalette fromHueAndChroma(double hue, double chroma) {
60     return new TonalPalette(hue, chroma, createKeyColor(hue, chroma));
61   }
62 
TonalPalette(double hue, double chroma, Hct keyColor)63   private TonalPalette(double hue, double chroma, Hct keyColor) {
64     cache = new HashMap<>();
65     this.hue = hue;
66     this.chroma = chroma;
67     this.keyColor = keyColor;
68   }
69 
70   /** The key color is the first tone, starting from T50, matching the given hue and chroma. */
createKeyColor(double hue, double chroma)71   private static Hct createKeyColor(double hue, double chroma) {
72     double startTone = 50.0;
73     Hct smallestDeltaHct = Hct.from(hue, chroma, startTone);
74     double smallestDelta = Math.abs(smallestDeltaHct.getChroma() - chroma);
75     // Starting from T50, check T+/-delta to see if they match the requested
76     // chroma.
77     //
78     // Starts from T50 because T50 has the most chroma available, on
79     // average. Thus it is most likely to have a direct answer and minimize
80     // iteration.
81     for (double delta = 1.0; delta < 50.0; delta += 1.0) {
82       // Termination condition rounding instead of minimizing delta to avoid
83       // case where requested chroma is 16.51, and the closest chroma is 16.49.
84       // Error is minimized, but when rounded and displayed, requested chroma
85       // is 17, key color's chroma is 16.
86       if (Math.round(chroma) == Math.round(smallestDeltaHct.getChroma())) {
87         return smallestDeltaHct;
88       }
89 
90       final Hct hctAdd = Hct.from(hue, chroma, startTone + delta);
91       final double hctAddDelta = Math.abs(hctAdd.getChroma() - chroma);
92       if (hctAddDelta < smallestDelta) {
93         smallestDelta = hctAddDelta;
94         smallestDeltaHct = hctAdd;
95       }
96 
97       final Hct hctSubtract = Hct.from(hue, chroma, startTone - delta);
98       final double hctSubtractDelta = Math.abs(hctSubtract.getChroma() - chroma);
99       if (hctSubtractDelta < smallestDelta) {
100         smallestDelta = hctSubtractDelta;
101         smallestDeltaHct = hctSubtract;
102       }
103     }
104 
105     return smallestDeltaHct;
106   }
107 
108   /**
109    * Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
110    *
111    * @param tone HCT tone, measured from 0 to 100.
112    * @return ARGB representation of a color with that tone.
113    */
114   // AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923)
115   @SuppressWarnings("ComputeIfAbsentUseValue")
tone(int tone)116   public int tone(int tone) {
117     Integer color = cache.get(tone);
118     if (color == null) {
119       color = Hct.from(this.hue, this.chroma, tone).toInt();
120       cache.put(tone, color);
121     }
122     return color;
123   }
124 
125   /** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
getHct(double tone)126   public Hct getHct(double tone) {
127     return Hct.from(this.hue, this.chroma, tone);
128   }
129 
130   /** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
getChroma()131   public double getChroma() {
132     return this.chroma;
133   }
134 
135   /** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
getHue()136   public double getHue() {
137     return this.hue;
138   }
139 
140   /** The key color is the first tone, starting from T50, that matches the palette's chroma. */
getKeyColor()141   public Hct getKeyColor() {
142     return this.keyColor;
143   }
144 }
145