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