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