1*970e1046SAndroid Build Coastguard Worker /* 2*970e1046SAndroid Build Coastguard Worker * Copyright 2022 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.temperature; 18*970e1046SAndroid Build Coastguard Worker 19*970e1046SAndroid Build Coastguard Worker import com.google.ux.material.libmonet.hct.Hct; 20*970e1046SAndroid Build Coastguard Worker import com.google.ux.material.libmonet.utils.ColorUtils; 21*970e1046SAndroid Build Coastguard Worker import com.google.ux.material.libmonet.utils.MathUtils; 22*970e1046SAndroid Build Coastguard Worker import java.util.ArrayList; 23*970e1046SAndroid Build Coastguard Worker import java.util.Collections; 24*970e1046SAndroid Build Coastguard Worker import java.util.Comparator; 25*970e1046SAndroid Build Coastguard Worker import java.util.HashMap; 26*970e1046SAndroid Build Coastguard Worker import java.util.List; 27*970e1046SAndroid Build Coastguard Worker import java.util.Map; 28*970e1046SAndroid Build Coastguard Worker 29*970e1046SAndroid Build Coastguard Worker /** 30*970e1046SAndroid Build Coastguard Worker * Design utilities using color temperature theory. 31*970e1046SAndroid Build Coastguard Worker * 32*970e1046SAndroid Build Coastguard Worker * <p>Analogous colors, complementary color, and cache to efficiently, lazily, generate data for 33*970e1046SAndroid Build Coastguard Worker * calculations when needed. 34*970e1046SAndroid Build Coastguard Worker */ 35*970e1046SAndroid Build Coastguard Worker public final class TemperatureCache { 36*970e1046SAndroid Build Coastguard Worker private final Hct input; 37*970e1046SAndroid Build Coastguard Worker 38*970e1046SAndroid Build Coastguard Worker private Hct precomputedComplement; 39*970e1046SAndroid Build Coastguard Worker private List<Hct> precomputedHctsByTemp; 40*970e1046SAndroid Build Coastguard Worker private List<Hct> precomputedHctsByHue; 41*970e1046SAndroid Build Coastguard Worker private Map<Hct, Double> precomputedTempsByHct; 42*970e1046SAndroid Build Coastguard Worker TemperatureCache()43*970e1046SAndroid Build Coastguard Worker private TemperatureCache() { 44*970e1046SAndroid Build Coastguard Worker throw new UnsupportedOperationException(); 45*970e1046SAndroid Build Coastguard Worker } 46*970e1046SAndroid Build Coastguard Worker 47*970e1046SAndroid Build Coastguard Worker /** 48*970e1046SAndroid Build Coastguard Worker * Create a cache that allows calculation of ex. complementary and analogous colors. 49*970e1046SAndroid Build Coastguard Worker * 50*970e1046SAndroid Build Coastguard Worker * @param input Color to find complement/analogous colors of. Any colors will have the same tone, 51*970e1046SAndroid Build Coastguard Worker * and chroma as the input color, modulo any restrictions due to the other hues having lower 52*970e1046SAndroid Build Coastguard Worker * limits on chroma. 53*970e1046SAndroid Build Coastguard Worker */ TemperatureCache(Hct input)54*970e1046SAndroid Build Coastguard Worker public TemperatureCache(Hct input) { 55*970e1046SAndroid Build Coastguard Worker this.input = input; 56*970e1046SAndroid Build Coastguard Worker } 57*970e1046SAndroid Build Coastguard Worker 58*970e1046SAndroid Build Coastguard Worker /** 59*970e1046SAndroid Build Coastguard Worker * A color that complements the input color aesthetically. 60*970e1046SAndroid Build Coastguard Worker * 61*970e1046SAndroid Build Coastguard Worker * <p>In art, this is usually described as being across the color wheel. History of this shows 62*970e1046SAndroid Build Coastguard Worker * intent as a color that is just as cool-warm as the input color is warm-cool. 63*970e1046SAndroid Build Coastguard Worker */ getComplement()64*970e1046SAndroid Build Coastguard Worker public Hct getComplement() { 65*970e1046SAndroid Build Coastguard Worker if (precomputedComplement != null) { 66*970e1046SAndroid Build Coastguard Worker return precomputedComplement; 67*970e1046SAndroid Build Coastguard Worker } 68*970e1046SAndroid Build Coastguard Worker 69*970e1046SAndroid Build Coastguard Worker double coldestHue = getColdest().getHue(); 70*970e1046SAndroid Build Coastguard Worker double coldestTemp = getTempsByHct().get(getColdest()); 71*970e1046SAndroid Build Coastguard Worker 72*970e1046SAndroid Build Coastguard Worker double warmestHue = getWarmest().getHue(); 73*970e1046SAndroid Build Coastguard Worker double warmestTemp = getTempsByHct().get(getWarmest()); 74*970e1046SAndroid Build Coastguard Worker double range = warmestTemp - coldestTemp; 75*970e1046SAndroid Build Coastguard Worker boolean startHueIsColdestToWarmest = isBetween(input.getHue(), coldestHue, warmestHue); 76*970e1046SAndroid Build Coastguard Worker double startHue = startHueIsColdestToWarmest ? warmestHue : coldestHue; 77*970e1046SAndroid Build Coastguard Worker double endHue = startHueIsColdestToWarmest ? coldestHue : warmestHue; 78*970e1046SAndroid Build Coastguard Worker double directionOfRotation = 1.; 79*970e1046SAndroid Build Coastguard Worker double smallestError = 1000.; 80*970e1046SAndroid Build Coastguard Worker Hct answer = getHctsByHue().get((int) Math.round(input.getHue())); 81*970e1046SAndroid Build Coastguard Worker 82*970e1046SAndroid Build Coastguard Worker double complementRelativeTemp = (1. - getRelativeTemperature(input)); 83*970e1046SAndroid Build Coastguard Worker // Find the color in the other section, closest to the inverse percentile 84*970e1046SAndroid Build Coastguard Worker // of the input color. This is the complement. 85*970e1046SAndroid Build Coastguard Worker for (double hueAddend = 0.; hueAddend <= 360.; hueAddend += 1.) { 86*970e1046SAndroid Build Coastguard Worker double hue = MathUtils.sanitizeDegreesDouble( 87*970e1046SAndroid Build Coastguard Worker startHue + directionOfRotation * hueAddend); 88*970e1046SAndroid Build Coastguard Worker if (!isBetween(hue, startHue, endHue)) { 89*970e1046SAndroid Build Coastguard Worker continue; 90*970e1046SAndroid Build Coastguard Worker } 91*970e1046SAndroid Build Coastguard Worker Hct possibleAnswer = getHctsByHue().get((int) Math.round(hue)); 92*970e1046SAndroid Build Coastguard Worker double relativeTemp = 93*970e1046SAndroid Build Coastguard Worker (getTempsByHct().get(possibleAnswer) - coldestTemp) / range; 94*970e1046SAndroid Build Coastguard Worker double error = Math.abs(complementRelativeTemp - relativeTemp); 95*970e1046SAndroid Build Coastguard Worker if (error < smallestError) { 96*970e1046SAndroid Build Coastguard Worker smallestError = error; 97*970e1046SAndroid Build Coastguard Worker answer = possibleAnswer; 98*970e1046SAndroid Build Coastguard Worker } 99*970e1046SAndroid Build Coastguard Worker } 100*970e1046SAndroid Build Coastguard Worker precomputedComplement = answer; 101*970e1046SAndroid Build Coastguard Worker return precomputedComplement; 102*970e1046SAndroid Build Coastguard Worker } 103*970e1046SAndroid Build Coastguard Worker 104*970e1046SAndroid Build Coastguard Worker /** 105*970e1046SAndroid Build Coastguard Worker * 5 colors that pair well with the input color. 106*970e1046SAndroid Build Coastguard Worker * 107*970e1046SAndroid Build Coastguard Worker * <p>The colors are equidistant in temperature and adjacent in hue. 108*970e1046SAndroid Build Coastguard Worker */ getAnalogousColors()109*970e1046SAndroid Build Coastguard Worker public List<Hct> getAnalogousColors() { 110*970e1046SAndroid Build Coastguard Worker return getAnalogousColors(5, 12); 111*970e1046SAndroid Build Coastguard Worker } 112*970e1046SAndroid Build Coastguard Worker 113*970e1046SAndroid Build Coastguard Worker /** 114*970e1046SAndroid Build Coastguard Worker * A set of colors with differing hues, equidistant in temperature. 115*970e1046SAndroid Build Coastguard Worker * 116*970e1046SAndroid Build Coastguard Worker * <p>In art, this is usually described as a set of 5 colors on a color wheel divided into 12 117*970e1046SAndroid Build Coastguard Worker * sections. This method allows provision of either of those values. 118*970e1046SAndroid Build Coastguard Worker * 119*970e1046SAndroid Build Coastguard Worker * <p>Behavior is undefined when count or divisions is 0. When divisions < count, colors repeat. 120*970e1046SAndroid Build Coastguard Worker * 121*970e1046SAndroid Build Coastguard Worker * @param count The number of colors to return, includes the input color. 122*970e1046SAndroid Build Coastguard Worker * @param divisions The number of divisions on the color wheel. 123*970e1046SAndroid Build Coastguard Worker */ getAnalogousColors(int count, int divisions)124*970e1046SAndroid Build Coastguard Worker public List<Hct> getAnalogousColors(int count, int divisions) { 125*970e1046SAndroid Build Coastguard Worker // The starting hue is the hue of the input color. 126*970e1046SAndroid Build Coastguard Worker int startHue = (int) Math.round(input.getHue()); 127*970e1046SAndroid Build Coastguard Worker Hct startHct = getHctsByHue().get(startHue); 128*970e1046SAndroid Build Coastguard Worker double lastTemp = getRelativeTemperature(startHct); 129*970e1046SAndroid Build Coastguard Worker 130*970e1046SAndroid Build Coastguard Worker List<Hct> allColors = new ArrayList<>(); 131*970e1046SAndroid Build Coastguard Worker allColors.add(startHct); 132*970e1046SAndroid Build Coastguard Worker 133*970e1046SAndroid Build Coastguard Worker double absoluteTotalTempDelta = 0.f; 134*970e1046SAndroid Build Coastguard Worker for (int i = 0; i < 360; i++) { 135*970e1046SAndroid Build Coastguard Worker int hue = MathUtils.sanitizeDegreesInt(startHue + i); 136*970e1046SAndroid Build Coastguard Worker Hct hct = getHctsByHue().get(hue); 137*970e1046SAndroid Build Coastguard Worker double temp = getRelativeTemperature(hct); 138*970e1046SAndroid Build Coastguard Worker double tempDelta = Math.abs(temp - lastTemp); 139*970e1046SAndroid Build Coastguard Worker lastTemp = temp; 140*970e1046SAndroid Build Coastguard Worker absoluteTotalTempDelta += tempDelta; 141*970e1046SAndroid Build Coastguard Worker } 142*970e1046SAndroid Build Coastguard Worker 143*970e1046SAndroid Build Coastguard Worker int hueAddend = 1; 144*970e1046SAndroid Build Coastguard Worker double tempStep = absoluteTotalTempDelta / (double) divisions; 145*970e1046SAndroid Build Coastguard Worker double totalTempDelta = 0.0; 146*970e1046SAndroid Build Coastguard Worker lastTemp = getRelativeTemperature(startHct); 147*970e1046SAndroid Build Coastguard Worker while (allColors.size() < divisions) { 148*970e1046SAndroid Build Coastguard Worker int hue = MathUtils.sanitizeDegreesInt(startHue + hueAddend); 149*970e1046SAndroid Build Coastguard Worker Hct hct = getHctsByHue().get(hue); 150*970e1046SAndroid Build Coastguard Worker double temp = getRelativeTemperature(hct); 151*970e1046SAndroid Build Coastguard Worker double tempDelta = Math.abs(temp - lastTemp); 152*970e1046SAndroid Build Coastguard Worker totalTempDelta += tempDelta; 153*970e1046SAndroid Build Coastguard Worker 154*970e1046SAndroid Build Coastguard Worker double desiredTotalTempDeltaForIndex = (allColors.size() * tempStep); 155*970e1046SAndroid Build Coastguard Worker boolean indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex; 156*970e1046SAndroid Build Coastguard Worker int indexAddend = 1; 157*970e1046SAndroid Build Coastguard Worker // Keep adding this hue to the answers until its temperature is 158*970e1046SAndroid Build Coastguard Worker // insufficient. This ensures consistent behavior when there aren't 159*970e1046SAndroid Build Coastguard Worker // `divisions` discrete steps between 0 and 360 in hue with `tempStep` 160*970e1046SAndroid Build Coastguard Worker // delta in temperature between them. 161*970e1046SAndroid Build Coastguard Worker // 162*970e1046SAndroid Build Coastguard Worker // For example, white and black have no analogues: there are no other 163*970e1046SAndroid Build Coastguard Worker // colors at T100/T0. Therefore, they should just be added to the array 164*970e1046SAndroid Build Coastguard Worker // as answers. 165*970e1046SAndroid Build Coastguard Worker while (indexSatisfied && allColors.size() < divisions) { 166*970e1046SAndroid Build Coastguard Worker allColors.add(hct); 167*970e1046SAndroid Build Coastguard Worker desiredTotalTempDeltaForIndex = ((allColors.size() + indexAddend) * tempStep); 168*970e1046SAndroid Build Coastguard Worker indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex; 169*970e1046SAndroid Build Coastguard Worker indexAddend++; 170*970e1046SAndroid Build Coastguard Worker } 171*970e1046SAndroid Build Coastguard Worker lastTemp = temp; 172*970e1046SAndroid Build Coastguard Worker hueAddend++; 173*970e1046SAndroid Build Coastguard Worker 174*970e1046SAndroid Build Coastguard Worker if (hueAddend > 360) { 175*970e1046SAndroid Build Coastguard Worker while (allColors.size() < divisions) { 176*970e1046SAndroid Build Coastguard Worker allColors.add(hct); 177*970e1046SAndroid Build Coastguard Worker } 178*970e1046SAndroid Build Coastguard Worker break; 179*970e1046SAndroid Build Coastguard Worker } 180*970e1046SAndroid Build Coastguard Worker } 181*970e1046SAndroid Build Coastguard Worker 182*970e1046SAndroid Build Coastguard Worker List<Hct> answers = new ArrayList<>(); 183*970e1046SAndroid Build Coastguard Worker answers.add(input); 184*970e1046SAndroid Build Coastguard Worker 185*970e1046SAndroid Build Coastguard Worker int ccwCount = (int) Math.floor(((double) count - 1.0) / 2.0); 186*970e1046SAndroid Build Coastguard Worker for (int i = 1; i < (ccwCount + 1); i++) { 187*970e1046SAndroid Build Coastguard Worker int index = 0 - i; 188*970e1046SAndroid Build Coastguard Worker while (index < 0) { 189*970e1046SAndroid Build Coastguard Worker index = allColors.size() + index; 190*970e1046SAndroid Build Coastguard Worker } 191*970e1046SAndroid Build Coastguard Worker if (index >= allColors.size()) { 192*970e1046SAndroid Build Coastguard Worker index = index % allColors.size(); 193*970e1046SAndroid Build Coastguard Worker } 194*970e1046SAndroid Build Coastguard Worker answers.add(0, allColors.get(index)); 195*970e1046SAndroid Build Coastguard Worker } 196*970e1046SAndroid Build Coastguard Worker 197*970e1046SAndroid Build Coastguard Worker int cwCount = count - ccwCount - 1; 198*970e1046SAndroid Build Coastguard Worker for (int i = 1; i < (cwCount + 1); i++) { 199*970e1046SAndroid Build Coastguard Worker int index = i; 200*970e1046SAndroid Build Coastguard Worker while (index < 0) { 201*970e1046SAndroid Build Coastguard Worker index = allColors.size() + index; 202*970e1046SAndroid Build Coastguard Worker } 203*970e1046SAndroid Build Coastguard Worker if (index >= allColors.size()) { 204*970e1046SAndroid Build Coastguard Worker index = index % allColors.size(); 205*970e1046SAndroid Build Coastguard Worker } 206*970e1046SAndroid Build Coastguard Worker answers.add(allColors.get(index)); 207*970e1046SAndroid Build Coastguard Worker } 208*970e1046SAndroid Build Coastguard Worker 209*970e1046SAndroid Build Coastguard Worker return answers; 210*970e1046SAndroid Build Coastguard Worker } 211*970e1046SAndroid Build Coastguard Worker 212*970e1046SAndroid Build Coastguard Worker /** 213*970e1046SAndroid Build Coastguard Worker * Temperature relative to all colors with the same chroma and tone. 214*970e1046SAndroid Build Coastguard Worker * 215*970e1046SAndroid Build Coastguard Worker * @param hct HCT to find the relative temperature of. 216*970e1046SAndroid Build Coastguard Worker * @return Value on a scale from 0 to 1. 217*970e1046SAndroid Build Coastguard Worker */ getRelativeTemperature(Hct hct)218*970e1046SAndroid Build Coastguard Worker public double getRelativeTemperature(Hct hct) { 219*970e1046SAndroid Build Coastguard Worker double range = getTempsByHct().get(getWarmest()) - getTempsByHct().get(getColdest()); 220*970e1046SAndroid Build Coastguard Worker double differenceFromColdest = 221*970e1046SAndroid Build Coastguard Worker getTempsByHct().get(hct) - getTempsByHct().get(getColdest()); 222*970e1046SAndroid Build Coastguard Worker // Handle when there's no difference in temperature between warmest and 223*970e1046SAndroid Build Coastguard Worker // coldest: for example, at T100, only one color is available, white. 224*970e1046SAndroid Build Coastguard Worker if (range == 0.) { 225*970e1046SAndroid Build Coastguard Worker return 0.5; 226*970e1046SAndroid Build Coastguard Worker } 227*970e1046SAndroid Build Coastguard Worker return differenceFromColdest / range; 228*970e1046SAndroid Build Coastguard Worker } 229*970e1046SAndroid Build Coastguard Worker 230*970e1046SAndroid Build Coastguard Worker /** 231*970e1046SAndroid Build Coastguard Worker * Value representing cool-warm factor of a color. Values below 0 are considered cool, above, 232*970e1046SAndroid Build Coastguard Worker * warm. 233*970e1046SAndroid Build Coastguard Worker * 234*970e1046SAndroid Build Coastguard Worker * <p>Color science has researched emotion and harmony, which art uses to select colors. Warm-cool 235*970e1046SAndroid Build Coastguard Worker * is the foundation of analogous and complementary colors. See: - Li-Chen Ou's Chapter 19 in 236*970e1046SAndroid Build Coastguard Worker * Handbook of Color Psychology (2015). - Josef Albers' Interaction of Color chapters 19 and 21. 237*970e1046SAndroid Build Coastguard Worker * 238*970e1046SAndroid Build Coastguard Worker * <p>Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space. 239*970e1046SAndroid Build Coastguard Worker * Return value has these properties:<br> 240*970e1046SAndroid Build Coastguard Worker * - Values below 0 are cool, above 0 are warm.<br> 241*970e1046SAndroid Build Coastguard Worker * - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.<br> 242*970e1046SAndroid Build Coastguard Worker * - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130. 243*970e1046SAndroid Build Coastguard Worker */ rawTemperature(Hct color)244*970e1046SAndroid Build Coastguard Worker public static double rawTemperature(Hct color) { 245*970e1046SAndroid Build Coastguard Worker double[] lab = ColorUtils.labFromArgb(color.toInt()); 246*970e1046SAndroid Build Coastguard Worker double hue = MathUtils.sanitizeDegreesDouble(Math.toDegrees(Math.atan2(lab[2], lab[1]))); 247*970e1046SAndroid Build Coastguard Worker double chroma = Math.hypot(lab[1], lab[2]); 248*970e1046SAndroid Build Coastguard Worker return -0.5 249*970e1046SAndroid Build Coastguard Worker + 0.02 250*970e1046SAndroid Build Coastguard Worker * Math.pow(chroma, 1.07) 251*970e1046SAndroid Build Coastguard Worker * Math.cos(Math.toRadians(MathUtils.sanitizeDegreesDouble(hue - 50.))); 252*970e1046SAndroid Build Coastguard Worker } 253*970e1046SAndroid Build Coastguard Worker 254*970e1046SAndroid Build Coastguard Worker /** Coldest color with same chroma and tone as input. */ getColdest()255*970e1046SAndroid Build Coastguard Worker private Hct getColdest() { 256*970e1046SAndroid Build Coastguard Worker return getHctsByTemp().get(0); 257*970e1046SAndroid Build Coastguard Worker } 258*970e1046SAndroid Build Coastguard Worker 259*970e1046SAndroid Build Coastguard Worker /** 260*970e1046SAndroid Build Coastguard Worker * HCTs for all colors with the same chroma/tone as the input. 261*970e1046SAndroid Build Coastguard Worker * 262*970e1046SAndroid Build Coastguard Worker * <p>Sorted by hue, ex. index 0 is hue 0. 263*970e1046SAndroid Build Coastguard Worker */ getHctsByHue()264*970e1046SAndroid Build Coastguard Worker private List<Hct> getHctsByHue() { 265*970e1046SAndroid Build Coastguard Worker if (precomputedHctsByHue != null) { 266*970e1046SAndroid Build Coastguard Worker return precomputedHctsByHue; 267*970e1046SAndroid Build Coastguard Worker } 268*970e1046SAndroid Build Coastguard Worker List<Hct> hcts = new ArrayList<>(); 269*970e1046SAndroid Build Coastguard Worker for (double hue = 0.; hue <= 360.; hue += 1.) { 270*970e1046SAndroid Build Coastguard Worker Hct colorAtHue = Hct.from(hue, input.getChroma(), input.getTone()); 271*970e1046SAndroid Build Coastguard Worker hcts.add(colorAtHue); 272*970e1046SAndroid Build Coastguard Worker } 273*970e1046SAndroid Build Coastguard Worker precomputedHctsByHue = Collections.unmodifiableList(hcts); 274*970e1046SAndroid Build Coastguard Worker return precomputedHctsByHue; 275*970e1046SAndroid Build Coastguard Worker } 276*970e1046SAndroid Build Coastguard Worker 277*970e1046SAndroid Build Coastguard Worker /** 278*970e1046SAndroid Build Coastguard Worker * HCTs for all colors with the same chroma/tone as the input. 279*970e1046SAndroid Build Coastguard Worker * 280*970e1046SAndroid Build Coastguard Worker * <p>Sorted from coldest first to warmest last. 281*970e1046SAndroid Build Coastguard Worker */ 282*970e1046SAndroid Build Coastguard Worker // Prevent lint for Comparator not being available on Android before API level 24, 7.0, 2016. 283*970e1046SAndroid Build Coastguard Worker // "AndroidJdkLibsChecker" for one linter, "NewApi" for another. 284*970e1046SAndroid Build Coastguard Worker // A java_library Bazel rule with an Android constraint cannot skip these warnings without this 285*970e1046SAndroid Build Coastguard Worker // annotation; another solution would be to create an android_library rule and supply 286*970e1046SAndroid Build Coastguard Worker // AndroidManifest with an SDK set higher than 23. 287*970e1046SAndroid Build Coastguard Worker @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"}) getHctsByTemp()288*970e1046SAndroid Build Coastguard Worker private List<Hct> getHctsByTemp() { 289*970e1046SAndroid Build Coastguard Worker if (precomputedHctsByTemp != null) { 290*970e1046SAndroid Build Coastguard Worker return precomputedHctsByTemp; 291*970e1046SAndroid Build Coastguard Worker } 292*970e1046SAndroid Build Coastguard Worker 293*970e1046SAndroid Build Coastguard Worker List<Hct> hcts = new ArrayList<>(getHctsByHue()); 294*970e1046SAndroid Build Coastguard Worker hcts.add(input); 295*970e1046SAndroid Build Coastguard Worker Comparator<Hct> temperaturesComparator = 296*970e1046SAndroid Build Coastguard Worker Comparator.comparing((Hct arg) -> getTempsByHct().get(arg), Double::compareTo); 297*970e1046SAndroid Build Coastguard Worker Collections.sort(hcts, temperaturesComparator); 298*970e1046SAndroid Build Coastguard Worker precomputedHctsByTemp = hcts; 299*970e1046SAndroid Build Coastguard Worker return precomputedHctsByTemp; 300*970e1046SAndroid Build Coastguard Worker } 301*970e1046SAndroid Build Coastguard Worker 302*970e1046SAndroid Build Coastguard Worker /** Keys of HCTs in getHctsByTemp, values of raw temperature. */ getTempsByHct()303*970e1046SAndroid Build Coastguard Worker private Map<Hct, Double> getTempsByHct() { 304*970e1046SAndroid Build Coastguard Worker if (precomputedTempsByHct != null) { 305*970e1046SAndroid Build Coastguard Worker return precomputedTempsByHct; 306*970e1046SAndroid Build Coastguard Worker } 307*970e1046SAndroid Build Coastguard Worker 308*970e1046SAndroid Build Coastguard Worker List<Hct> allHcts = new ArrayList<>(getHctsByHue()); 309*970e1046SAndroid Build Coastguard Worker allHcts.add(input); 310*970e1046SAndroid Build Coastguard Worker 311*970e1046SAndroid Build Coastguard Worker Map<Hct, Double> temperaturesByHct = new HashMap<>(); 312*970e1046SAndroid Build Coastguard Worker for (Hct hct : allHcts) { 313*970e1046SAndroid Build Coastguard Worker temperaturesByHct.put(hct, rawTemperature(hct)); 314*970e1046SAndroid Build Coastguard Worker } 315*970e1046SAndroid Build Coastguard Worker 316*970e1046SAndroid Build Coastguard Worker precomputedTempsByHct = temperaturesByHct; 317*970e1046SAndroid Build Coastguard Worker return precomputedTempsByHct; 318*970e1046SAndroid Build Coastguard Worker } 319*970e1046SAndroid Build Coastguard Worker 320*970e1046SAndroid Build Coastguard Worker /** Warmest color with same chroma and tone as input. */ getWarmest()321*970e1046SAndroid Build Coastguard Worker private Hct getWarmest() { 322*970e1046SAndroid Build Coastguard Worker return getHctsByTemp().get(getHctsByTemp().size() - 1); 323*970e1046SAndroid Build Coastguard Worker } 324*970e1046SAndroid Build Coastguard Worker 325*970e1046SAndroid Build Coastguard Worker /** Determines if an angle is between two other angles, rotating clockwise. */ isBetween(double angle, double a, double b)326*970e1046SAndroid Build Coastguard Worker private static boolean isBetween(double angle, double a, double b) { 327*970e1046SAndroid Build Coastguard Worker if (a < b) { 328*970e1046SAndroid Build Coastguard Worker return a <= angle && angle <= b; 329*970e1046SAndroid Build Coastguard Worker } 330*970e1046SAndroid Build Coastguard Worker return a <= angle || angle <= b; 331*970e1046SAndroid Build Coastguard Worker } 332*970e1046SAndroid Build Coastguard Worker } 333