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.hct; 18 19 import com.google.ux.material.libmonet.utils.ColorUtils; 20 21 /** 22 * A color system built using CAM16 hue and chroma, and L* from L*a*b*. 23 * 24 * <p>Using L* creates a link between the color system, contrast, and thus accessibility. Contrast 25 * ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can 26 * be calculated from Y. 27 * 28 * <p>Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones. 29 * 30 * <p>Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A 31 * difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50 32 * guarantees a contrast ratio >= 4.5. 33 */ 34 35 /** 36 * HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color 37 * measurement system that can also accurately render what colors will appear as in different 38 * lighting environments. 39 */ 40 public final class Hct { 41 private double hue; 42 private double chroma; 43 private double tone; 44 private int argb; 45 46 /** 47 * Create an HCT color from hue, chroma, and tone. 48 * 49 * @param hue 0 <= hue < 360; invalid values are corrected. 50 * @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower than 51 * the requested chroma. Chroma has a different maximum for any given hue and tone. 52 * @param tone 0 <= tone <= 100; invalid values are corrected. 53 * @return HCT representation of a color in default viewing conditions. 54 */ from(double hue, double chroma, double tone)55 public static Hct from(double hue, double chroma, double tone) { 56 int argb = HctSolver.solveToInt(hue, chroma, tone); 57 return new Hct(argb); 58 } 59 60 /** 61 * Create an HCT color from a color. 62 * 63 * @param argb ARGB representation of a color. 64 * @return HCT representation of a color in default viewing conditions 65 */ fromInt(int argb)66 public static Hct fromInt(int argb) { 67 return new Hct(argb); 68 } 69 Hct(int argb)70 private Hct(int argb) { 71 setInternalState(argb); 72 } 73 getHue()74 public double getHue() { 75 return hue; 76 } 77 getChroma()78 public double getChroma() { 79 return chroma; 80 } 81 getTone()82 public double getTone() { 83 return tone; 84 } 85 toInt()86 public int toInt() { 87 return argb; 88 } 89 90 /** 91 * Set the hue of this color. Chroma may decrease because chroma has a different maximum for any 92 * given hue and tone. 93 * 94 * @param newHue 0 <= newHue < 360; invalid values are corrected. 95 */ setHue(double newHue)96 public void setHue(double newHue) { 97 setInternalState(HctSolver.solveToInt(newHue, chroma, tone)); 98 } 99 100 /** 101 * Set the chroma of this color. Chroma may decrease because chroma has a different maximum for 102 * any given hue and tone. 103 * 104 * @param newChroma 0 <= newChroma < ? 105 */ setChroma(double newChroma)106 public void setChroma(double newChroma) { 107 setInternalState(HctSolver.solveToInt(hue, newChroma, tone)); 108 } 109 110 /** 111 * Set the tone of this color. Chroma may decrease because chroma has a different maximum for any 112 * given hue and tone. 113 * 114 * @param newTone 0 <= newTone <= 100; invalid valids are corrected. 115 */ setTone(double newTone)116 public void setTone(double newTone) { 117 setInternalState(HctSolver.solveToInt(hue, chroma, newTone)); 118 } 119 120 /** 121 * Translate a color into different ViewingConditions. 122 * 123 * <p>Colors change appearance. They look different with lights on versus off, the same color, as 124 * in hex code, on white looks different when on black. This is called color relativity, most 125 * famously explicated by Josef Albers in Interaction of Color. 126 * 127 * <p>In color science, color appearance models can account for this and calculate the appearance 128 * of a color in different settings. HCT is based on CAM16, a color appearance model, and uses it 129 * to make these calculations. 130 * 131 * <p>See ViewingConditions.make for parameters affecting color appearance. 132 */ inViewingConditions(ViewingConditions vc)133 public Hct inViewingConditions(ViewingConditions vc) { 134 // 1. Use CAM16 to find XYZ coordinates of color in specified VC. 135 Cam16 cam16 = Cam16.fromInt(toInt()); 136 double[] viewedInVc = cam16.xyzInViewingConditions(vc, null); 137 138 // 2. Create CAM16 of those XYZ coordinates in default VC. 139 Cam16 recastInVc = 140 Cam16.fromXyzInViewingConditions( 141 viewedInVc[0], viewedInVc[1], viewedInVc[2], ViewingConditions.DEFAULT); 142 143 // 3. Create HCT from: 144 // - CAM16 using default VC with XYZ coordinates in specified VC. 145 // - L* converted from Y in XYZ coordinates in specified VC. 146 return Hct.from( 147 recastInVc.getHue(), recastInVc.getChroma(), ColorUtils.lstarFromY(viewedInVc[1])); 148 } 149 setInternalState(int argb)150 private void setInternalState(int argb) { 151 this.argb = argb; 152 Cam16 cam = Cam16.fromInt(argb); 153 hue = cam.getHue(); 154 chroma = cam.getChroma(); 155 this.tone = ColorUtils.lstarFromArgb(argb); 156 } 157 } 158