xref: /aosp_15_r20/external/libmonet/palettes/TonalPalette.java (revision 970e10460f970939fd510dd6ad3e0d65908272e3)
1*970e1046SAndroid Build Coastguard Worker /*
2*970e1046SAndroid Build Coastguard Worker  * Copyright 2021 Google LLC
3*970e1046SAndroid Build Coastguard Worker  *
4*970e1046SAndroid Build Coastguard Worker  * Licensed under the Apache License, Version 2.0 (the "License");
5*970e1046SAndroid Build Coastguard Worker  * you may not use this file except in compliance with the License.
6*970e1046SAndroid Build Coastguard Worker  * You may obtain a copy of the License at
7*970e1046SAndroid Build Coastguard Worker  *
8*970e1046SAndroid Build Coastguard Worker  *      http://www.apache.org/licenses/LICENSE-2.0
9*970e1046SAndroid Build Coastguard Worker  *
10*970e1046SAndroid Build Coastguard Worker  * Unless required by applicable law or agreed to in writing, software
11*970e1046SAndroid Build Coastguard Worker  * distributed under the License is distributed on an "AS IS" BASIS,
12*970e1046SAndroid Build Coastguard Worker  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*970e1046SAndroid Build Coastguard Worker  * See the License for the specific language governing permissions and
14*970e1046SAndroid Build Coastguard Worker  * limitations under the License.
15*970e1046SAndroid Build Coastguard Worker  */
16*970e1046SAndroid Build Coastguard Worker 
17*970e1046SAndroid Build Coastguard Worker package com.google.ux.material.libmonet.palettes;
18*970e1046SAndroid Build Coastguard Worker 
19*970e1046SAndroid Build Coastguard Worker import com.google.ux.material.libmonet.hct.Hct;
20*970e1046SAndroid Build Coastguard Worker import java.util.HashMap;
21*970e1046SAndroid Build Coastguard Worker import java.util.Map;
22*970e1046SAndroid Build Coastguard Worker 
23*970e1046SAndroid Build Coastguard Worker /**
24*970e1046SAndroid Build Coastguard Worker  * A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
25*970e1046SAndroid Build Coastguard Worker  */
26*970e1046SAndroid Build Coastguard Worker public final class TonalPalette {
27*970e1046SAndroid Build Coastguard Worker   Map<Integer, Integer> cache;
28*970e1046SAndroid Build Coastguard Worker   Hct keyColor;
29*970e1046SAndroid Build Coastguard Worker   double hue;
30*970e1046SAndroid Build Coastguard Worker   double chroma;
31*970e1046SAndroid Build Coastguard Worker 
32*970e1046SAndroid Build Coastguard Worker   /**
33*970e1046SAndroid Build Coastguard Worker    * Create tones using the HCT hue and chroma from a color.
34*970e1046SAndroid Build Coastguard Worker    *
35*970e1046SAndroid Build Coastguard Worker    * @param argb ARGB representation of a color
36*970e1046SAndroid Build Coastguard Worker    * @return Tones matching that color's hue and chroma.
37*970e1046SAndroid Build Coastguard Worker    */
fromInt(int argb)38*970e1046SAndroid Build Coastguard Worker   public static TonalPalette fromInt(int argb) {
39*970e1046SAndroid Build Coastguard Worker     return fromHct(Hct.fromInt(argb));
40*970e1046SAndroid Build Coastguard Worker   }
41*970e1046SAndroid Build Coastguard Worker 
42*970e1046SAndroid Build Coastguard Worker   /**
43*970e1046SAndroid Build Coastguard Worker    * Create tones using a HCT color.
44*970e1046SAndroid Build Coastguard Worker    *
45*970e1046SAndroid Build Coastguard Worker    * @param hct HCT representation of a color.
46*970e1046SAndroid Build Coastguard Worker    * @return Tones matching that color's hue and chroma.
47*970e1046SAndroid Build Coastguard Worker    */
fromHct(Hct hct)48*970e1046SAndroid Build Coastguard Worker   public static TonalPalette fromHct(Hct hct) {
49*970e1046SAndroid Build Coastguard Worker     return new TonalPalette(hct.getHue(), hct.getChroma(), hct);
50*970e1046SAndroid Build Coastguard Worker   }
51*970e1046SAndroid Build Coastguard Worker 
52*970e1046SAndroid Build Coastguard Worker   /**
53*970e1046SAndroid Build Coastguard Worker    * Create tones from a defined HCT hue and chroma.
54*970e1046SAndroid Build Coastguard Worker    *
55*970e1046SAndroid Build Coastguard Worker    * @param hue HCT hue
56*970e1046SAndroid Build Coastguard Worker    * @param chroma HCT chroma
57*970e1046SAndroid Build Coastguard Worker    * @return Tones matching hue and chroma.
58*970e1046SAndroid Build Coastguard Worker    */
fromHueAndChroma(double hue, double chroma)59*970e1046SAndroid Build Coastguard Worker   public static TonalPalette fromHueAndChroma(double hue, double chroma) {
60*970e1046SAndroid Build Coastguard Worker     final Hct keyColor = new KeyColor(hue, chroma).create();
61*970e1046SAndroid Build Coastguard Worker     return new TonalPalette(hue, chroma, keyColor);
62*970e1046SAndroid Build Coastguard Worker   }
63*970e1046SAndroid Build Coastguard Worker 
TonalPalette(double hue, double chroma, Hct keyColor)64*970e1046SAndroid Build Coastguard Worker   private TonalPalette(double hue, double chroma, Hct keyColor) {
65*970e1046SAndroid Build Coastguard Worker     cache = new HashMap<>();
66*970e1046SAndroid Build Coastguard Worker     this.hue = hue;
67*970e1046SAndroid Build Coastguard Worker     this.chroma = chroma;
68*970e1046SAndroid Build Coastguard Worker     this.keyColor = keyColor;
69*970e1046SAndroid Build Coastguard Worker   }
70*970e1046SAndroid Build Coastguard Worker 
71*970e1046SAndroid Build Coastguard Worker   /**
72*970e1046SAndroid Build Coastguard Worker    * Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
73*970e1046SAndroid Build Coastguard Worker    *
74*970e1046SAndroid Build Coastguard Worker    * @param tone HCT tone, measured from 0 to 100.
75*970e1046SAndroid Build Coastguard Worker    * @return ARGB representation of a color with that tone.
76*970e1046SAndroid Build Coastguard Worker    */
77*970e1046SAndroid Build Coastguard Worker   // AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923)
78*970e1046SAndroid Build Coastguard Worker   @SuppressWarnings("ComputeIfAbsentUseValue")
tone(int tone)79*970e1046SAndroid Build Coastguard Worker   public int tone(int tone) {
80*970e1046SAndroid Build Coastguard Worker     Integer color = cache.get(tone);
81*970e1046SAndroid Build Coastguard Worker     if (color == null) {
82*970e1046SAndroid Build Coastguard Worker       color = Hct.from(this.hue, this.chroma, tone).toInt();
83*970e1046SAndroid Build Coastguard Worker       cache.put(tone, color);
84*970e1046SAndroid Build Coastguard Worker     }
85*970e1046SAndroid Build Coastguard Worker     return color;
86*970e1046SAndroid Build Coastguard Worker   }
87*970e1046SAndroid Build Coastguard Worker 
88*970e1046SAndroid Build Coastguard Worker   /** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
getHct(double tone)89*970e1046SAndroid Build Coastguard Worker   public Hct getHct(double tone) {
90*970e1046SAndroid Build Coastguard Worker     return Hct.from(this.hue, this.chroma, tone);
91*970e1046SAndroid Build Coastguard Worker   }
92*970e1046SAndroid Build Coastguard Worker 
93*970e1046SAndroid Build Coastguard Worker   /** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
getChroma()94*970e1046SAndroid Build Coastguard Worker   public double getChroma() {
95*970e1046SAndroid Build Coastguard Worker     return this.chroma;
96*970e1046SAndroid Build Coastguard Worker   }
97*970e1046SAndroid Build Coastguard Worker 
98*970e1046SAndroid Build Coastguard Worker   /** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
getHue()99*970e1046SAndroid Build Coastguard Worker   public double getHue() {
100*970e1046SAndroid Build Coastguard Worker     return this.hue;
101*970e1046SAndroid Build Coastguard Worker   }
102*970e1046SAndroid Build Coastguard Worker 
103*970e1046SAndroid Build Coastguard Worker   /** The key color is the first tone, starting from T50, that matches the palette's chroma. */
getKeyColor()104*970e1046SAndroid Build Coastguard Worker   public Hct getKeyColor() {
105*970e1046SAndroid Build Coastguard Worker     return this.keyColor;
106*970e1046SAndroid Build Coastguard Worker   }
107*970e1046SAndroid Build Coastguard Worker 
108*970e1046SAndroid Build Coastguard Worker   /** Key color is a color that represents the hue and chroma of a tonal palette. */
109*970e1046SAndroid Build Coastguard Worker   private static final class KeyColor {
110*970e1046SAndroid Build Coastguard Worker     private final double hue;
111*970e1046SAndroid Build Coastguard Worker     private final double requestedChroma;
112*970e1046SAndroid Build Coastguard Worker 
113*970e1046SAndroid Build Coastguard Worker     // Cache that maps tone to max chroma to avoid duplicated HCT calculation.
114*970e1046SAndroid Build Coastguard Worker     private final Map<Integer, Double> chromaCache = new HashMap<>();
115*970e1046SAndroid Build Coastguard Worker     private static final double MAX_CHROMA_VALUE = 200.0;
116*970e1046SAndroid Build Coastguard Worker 
117*970e1046SAndroid Build Coastguard Worker     /** Key color is a color that represents the hue and chroma of a tonal palette */
KeyColor(double hue, double requestedChroma)118*970e1046SAndroid Build Coastguard Worker     public KeyColor(double hue, double requestedChroma) {
119*970e1046SAndroid Build Coastguard Worker       this.hue = hue;
120*970e1046SAndroid Build Coastguard Worker       this.requestedChroma = requestedChroma;
121*970e1046SAndroid Build Coastguard Worker     }
122*970e1046SAndroid Build Coastguard Worker 
123*970e1046SAndroid Build Coastguard Worker     /**
124*970e1046SAndroid Build Coastguard Worker      * Creates a key color from a [hue] and a [chroma]. The key color is the first tone, starting
125*970e1046SAndroid Build Coastguard Worker      * from T50, matching the given hue and chroma.
126*970e1046SAndroid Build Coastguard Worker      *
127*970e1046SAndroid Build Coastguard Worker      * @return Key color [Hct]
128*970e1046SAndroid Build Coastguard Worker      */
create()129*970e1046SAndroid Build Coastguard Worker     public Hct create() {
130*970e1046SAndroid Build Coastguard Worker       // Pivot around T50 because T50 has the most chroma available, on
131*970e1046SAndroid Build Coastguard Worker       // average. Thus it is most likely to have a direct answer.
132*970e1046SAndroid Build Coastguard Worker       final int pivotTone = 50;
133*970e1046SAndroid Build Coastguard Worker       final int toneStepSize = 1;
134*970e1046SAndroid Build Coastguard Worker       // Epsilon to accept values slightly higher than the requested chroma.
135*970e1046SAndroid Build Coastguard Worker       final double epsilon = 0.01;
136*970e1046SAndroid Build Coastguard Worker 
137*970e1046SAndroid Build Coastguard Worker       // Binary search to find the tone that can provide a chroma that is closest
138*970e1046SAndroid Build Coastguard Worker       // to the requested chroma.
139*970e1046SAndroid Build Coastguard Worker       int lowerTone = 0;
140*970e1046SAndroid Build Coastguard Worker       int upperTone = 100;
141*970e1046SAndroid Build Coastguard Worker       while (lowerTone < upperTone) {
142*970e1046SAndroid Build Coastguard Worker         final int midTone = (lowerTone + upperTone) / 2;
143*970e1046SAndroid Build Coastguard Worker         boolean isAscending = maxChroma(midTone) < maxChroma(midTone + toneStepSize);
144*970e1046SAndroid Build Coastguard Worker         boolean sufficientChroma = maxChroma(midTone) >= requestedChroma - epsilon;
145*970e1046SAndroid Build Coastguard Worker 
146*970e1046SAndroid Build Coastguard Worker         if (sufficientChroma) {
147*970e1046SAndroid Build Coastguard Worker           // Either range [lowerTone, midTone] or [midTone, upperTone] has
148*970e1046SAndroid Build Coastguard Worker           // the answer, so search in the range that is closer the pivot tone.
149*970e1046SAndroid Build Coastguard Worker           if (Math.abs(lowerTone - pivotTone) < Math.abs(upperTone - pivotTone)) {
150*970e1046SAndroid Build Coastguard Worker             upperTone = midTone;
151*970e1046SAndroid Build Coastguard Worker           } else {
152*970e1046SAndroid Build Coastguard Worker             if (lowerTone == midTone) {
153*970e1046SAndroid Build Coastguard Worker               return Hct.from(this.hue, this.requestedChroma, lowerTone);
154*970e1046SAndroid Build Coastguard Worker             }
155*970e1046SAndroid Build Coastguard Worker             lowerTone = midTone;
156*970e1046SAndroid Build Coastguard Worker           }
157*970e1046SAndroid Build Coastguard Worker         } else {
158*970e1046SAndroid Build Coastguard Worker           // As there is no sufficient chroma in the midTone, follow the direction to the chroma
159*970e1046SAndroid Build Coastguard Worker           // peak.
160*970e1046SAndroid Build Coastguard Worker           if (isAscending) {
161*970e1046SAndroid Build Coastguard Worker             lowerTone = midTone + toneStepSize;
162*970e1046SAndroid Build Coastguard Worker           } else {
163*970e1046SAndroid Build Coastguard Worker             // Keep midTone for potential chroma peak.
164*970e1046SAndroid Build Coastguard Worker             upperTone = midTone;
165*970e1046SAndroid Build Coastguard Worker           }
166*970e1046SAndroid Build Coastguard Worker         }
167*970e1046SAndroid Build Coastguard Worker       }
168*970e1046SAndroid Build Coastguard Worker 
169*970e1046SAndroid Build Coastguard Worker       return Hct.from(this.hue, this.requestedChroma, lowerTone);
170*970e1046SAndroid Build Coastguard Worker     }
171*970e1046SAndroid Build Coastguard Worker 
172*970e1046SAndroid Build Coastguard Worker     // Find the maximum chroma for a given tone
maxChroma(int tone)173*970e1046SAndroid Build Coastguard Worker     private double maxChroma(int tone) {
174*970e1046SAndroid Build Coastguard Worker       return chromaCache.computeIfAbsent(
175*970e1046SAndroid Build Coastguard Worker           tone, (Integer key) -> Hct.from(hue, MAX_CHROMA_VALUE, key).getChroma());
176*970e1046SAndroid Build Coastguard Worker     }
177*970e1046SAndroid Build Coastguard Worker   }
178*970e1046SAndroid Build Coastguard Worker }
179