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