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 import com.google.ux.material.libmonet.utils.MathUtils; 21*970e1046SAndroid Build Coastguard Worker 22*970e1046SAndroid Build Coastguard Worker /** 23*970e1046SAndroid Build Coastguard Worker * In traditional color spaces, a color can be identified solely by the observer's measurement of 24*970e1046SAndroid Build Coastguard Worker * the color. Color appearance models such as CAM16 also use information about the environment where 25*970e1046SAndroid Build Coastguard Worker * the color was observed, known as the viewing conditions. 26*970e1046SAndroid Build Coastguard Worker * 27*970e1046SAndroid Build Coastguard Worker * <p>For example, white under the traditional assumption of a midday sun white point is accurately 28*970e1046SAndroid Build Coastguard Worker * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100) 29*970e1046SAndroid Build Coastguard Worker * 30*970e1046SAndroid Build Coastguard Worker * <p>This class caches intermediate values of the CAM16 conversion process that depend only on 31*970e1046SAndroid Build Coastguard Worker * viewing conditions, enabling speed ups. 32*970e1046SAndroid Build Coastguard Worker */ 33*970e1046SAndroid Build Coastguard Worker public final class ViewingConditions { 34*970e1046SAndroid Build Coastguard Worker /** sRGB-like viewing conditions. */ 35*970e1046SAndroid Build Coastguard Worker public static final ViewingConditions DEFAULT = 36*970e1046SAndroid Build Coastguard Worker ViewingConditions.defaultWithBackgroundLstar(50.0); 37*970e1046SAndroid Build Coastguard Worker 38*970e1046SAndroid Build Coastguard Worker private final double aw; 39*970e1046SAndroid Build Coastguard Worker private final double nbb; 40*970e1046SAndroid Build Coastguard Worker private final double ncb; 41*970e1046SAndroid Build Coastguard Worker private final double c; 42*970e1046SAndroid Build Coastguard Worker private final double nc; 43*970e1046SAndroid Build Coastguard Worker private final double n; 44*970e1046SAndroid Build Coastguard Worker private final double[] rgbD; 45*970e1046SAndroid Build Coastguard Worker private final double fl; 46*970e1046SAndroid Build Coastguard Worker private final double flRoot; 47*970e1046SAndroid Build Coastguard Worker private final double z; 48*970e1046SAndroid Build Coastguard Worker getAw()49*970e1046SAndroid Build Coastguard Worker public double getAw() { 50*970e1046SAndroid Build Coastguard Worker return aw; 51*970e1046SAndroid Build Coastguard Worker } 52*970e1046SAndroid Build Coastguard Worker getN()53*970e1046SAndroid Build Coastguard Worker public double getN() { 54*970e1046SAndroid Build Coastguard Worker return n; 55*970e1046SAndroid Build Coastguard Worker } 56*970e1046SAndroid Build Coastguard Worker getNbb()57*970e1046SAndroid Build Coastguard Worker public double getNbb() { 58*970e1046SAndroid Build Coastguard Worker return nbb; 59*970e1046SAndroid Build Coastguard Worker } 60*970e1046SAndroid Build Coastguard Worker getNcb()61*970e1046SAndroid Build Coastguard Worker double getNcb() { 62*970e1046SAndroid Build Coastguard Worker return ncb; 63*970e1046SAndroid Build Coastguard Worker } 64*970e1046SAndroid Build Coastguard Worker getC()65*970e1046SAndroid Build Coastguard Worker double getC() { 66*970e1046SAndroid Build Coastguard Worker return c; 67*970e1046SAndroid Build Coastguard Worker } 68*970e1046SAndroid Build Coastguard Worker getNc()69*970e1046SAndroid Build Coastguard Worker double getNc() { 70*970e1046SAndroid Build Coastguard Worker return nc; 71*970e1046SAndroid Build Coastguard Worker } 72*970e1046SAndroid Build Coastguard Worker getRgbD()73*970e1046SAndroid Build Coastguard Worker public double[] getRgbD() { 74*970e1046SAndroid Build Coastguard Worker return rgbD; 75*970e1046SAndroid Build Coastguard Worker } 76*970e1046SAndroid Build Coastguard Worker getFl()77*970e1046SAndroid Build Coastguard Worker double getFl() { 78*970e1046SAndroid Build Coastguard Worker return fl; 79*970e1046SAndroid Build Coastguard Worker } 80*970e1046SAndroid Build Coastguard Worker getFlRoot()81*970e1046SAndroid Build Coastguard Worker public double getFlRoot() { 82*970e1046SAndroid Build Coastguard Worker return flRoot; 83*970e1046SAndroid Build Coastguard Worker } 84*970e1046SAndroid Build Coastguard Worker getZ()85*970e1046SAndroid Build Coastguard Worker double getZ() { 86*970e1046SAndroid Build Coastguard Worker return z; 87*970e1046SAndroid Build Coastguard Worker } 88*970e1046SAndroid Build Coastguard Worker 89*970e1046SAndroid Build Coastguard Worker /** 90*970e1046SAndroid Build Coastguard Worker * Create ViewingConditions from a simple, physically relevant, set of parameters. 91*970e1046SAndroid Build Coastguard Worker * 92*970e1046SAndroid Build Coastguard Worker * @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day 93*970e1046SAndroid Build Coastguard Worker * afternoon 94*970e1046SAndroid Build Coastguard Worker * @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in 95*970e1046SAndroid Build Coastguard Worker * the room where the color is viewed. Can be calculated from lux by multiplying lux by 96*970e1046SAndroid Build Coastguard Worker * 0.0586. default = 11.72, or 200 lux. 97*970e1046SAndroid Build Coastguard Worker * @param backgroundLstar The lightness of the area surrounding the color. measured by L* in 98*970e1046SAndroid Build Coastguard Worker * L*a*b*. default = 50.0 99*970e1046SAndroid Build Coastguard Worker * @param surround A general description of the lighting surrounding the color. 0 is pitch dark, 100*970e1046SAndroid Build Coastguard Worker * like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at 101*970e1046SAndroid Build Coastguard Worker * night. 2.0 means there is no difference between the lighting on the color and around it. 102*970e1046SAndroid Build Coastguard Worker * default = 2.0 103*970e1046SAndroid Build Coastguard Worker * @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting, 104*970e1046SAndroid Build Coastguard Worker * such as knowing an apple is still red in green light. default = false, the eye does not 105*970e1046SAndroid Build Coastguard Worker * perform this process on self-luminous objects like displays. 106*970e1046SAndroid Build Coastguard Worker */ make( double[] whitePoint, double adaptingLuminance, double backgroundLstar, double surround, boolean discountingIlluminant)107*970e1046SAndroid Build Coastguard Worker public static ViewingConditions make( 108*970e1046SAndroid Build Coastguard Worker double[] whitePoint, 109*970e1046SAndroid Build Coastguard Worker double adaptingLuminance, 110*970e1046SAndroid Build Coastguard Worker double backgroundLstar, 111*970e1046SAndroid Build Coastguard Worker double surround, 112*970e1046SAndroid Build Coastguard Worker boolean discountingIlluminant) { 113*970e1046SAndroid Build Coastguard Worker // A background of pure black is non-physical and leads to infinities that represent the idea 114*970e1046SAndroid Build Coastguard Worker // that any color viewed in pure black can't be seen. 115*970e1046SAndroid Build Coastguard Worker backgroundLstar = Math.max(0.1, backgroundLstar); 116*970e1046SAndroid Build Coastguard Worker // Transform white point XYZ to 'cone'/'rgb' responses 117*970e1046SAndroid Build Coastguard Worker double[][] matrix = Cam16.XYZ_TO_CAM16RGB; 118*970e1046SAndroid Build Coastguard Worker double[] xyz = whitePoint; 119*970e1046SAndroid Build Coastguard Worker double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]); 120*970e1046SAndroid Build Coastguard Worker double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]); 121*970e1046SAndroid Build Coastguard Worker double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]); 122*970e1046SAndroid Build Coastguard Worker double f = 0.8 + (surround / 10.0); 123*970e1046SAndroid Build Coastguard Worker double c = 124*970e1046SAndroid Build Coastguard Worker (f >= 0.9) 125*970e1046SAndroid Build Coastguard Worker ? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0)) 126*970e1046SAndroid Build Coastguard Worker : MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0)); 127*970e1046SAndroid Build Coastguard Worker double d = 128*970e1046SAndroid Build Coastguard Worker discountingIlluminant 129*970e1046SAndroid Build Coastguard Worker ? 1.0 130*970e1046SAndroid Build Coastguard Worker : f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0))); 131*970e1046SAndroid Build Coastguard Worker d = MathUtils.clampDouble(0.0, 1.0, d); 132*970e1046SAndroid Build Coastguard Worker double nc = f; 133*970e1046SAndroid Build Coastguard Worker double[] rgbD = 134*970e1046SAndroid Build Coastguard Worker new double[] { 135*970e1046SAndroid Build Coastguard Worker d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d, d * (100.0 / bW) + 1.0 - d 136*970e1046SAndroid Build Coastguard Worker }; 137*970e1046SAndroid Build Coastguard Worker double k = 1.0 / (5.0 * adaptingLuminance + 1.0); 138*970e1046SAndroid Build Coastguard Worker double k4 = k * k * k * k; 139*970e1046SAndroid Build Coastguard Worker double k4F = 1.0 - k4; 140*970e1046SAndroid Build Coastguard Worker double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(5.0 * adaptingLuminance)); 141*970e1046SAndroid Build Coastguard Worker double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]); 142*970e1046SAndroid Build Coastguard Worker double z = 1.48 + Math.sqrt(n); 143*970e1046SAndroid Build Coastguard Worker double nbb = 0.725 / Math.pow(n, 0.2); 144*970e1046SAndroid Build Coastguard Worker double ncb = nbb; 145*970e1046SAndroid Build Coastguard Worker double[] rgbAFactors = 146*970e1046SAndroid Build Coastguard Worker new double[] { 147*970e1046SAndroid Build Coastguard Worker Math.pow(fl * rgbD[0] * rW / 100.0, 0.42), 148*970e1046SAndroid Build Coastguard Worker Math.pow(fl * rgbD[1] * gW / 100.0, 0.42), 149*970e1046SAndroid Build Coastguard Worker Math.pow(fl * rgbD[2] * bW / 100.0, 0.42) 150*970e1046SAndroid Build Coastguard Worker }; 151*970e1046SAndroid Build Coastguard Worker 152*970e1046SAndroid Build Coastguard Worker double[] rgbA = 153*970e1046SAndroid Build Coastguard Worker new double[] { 154*970e1046SAndroid Build Coastguard Worker (400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13), 155*970e1046SAndroid Build Coastguard Worker (400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13), 156*970e1046SAndroid Build Coastguard Worker (400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13) 157*970e1046SAndroid Build Coastguard Worker }; 158*970e1046SAndroid Build Coastguard Worker 159*970e1046SAndroid Build Coastguard Worker double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb; 160*970e1046SAndroid Build Coastguard Worker return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z); 161*970e1046SAndroid Build Coastguard Worker } 162*970e1046SAndroid Build Coastguard Worker 163*970e1046SAndroid Build Coastguard Worker /** 164*970e1046SAndroid Build Coastguard Worker * Create sRGB-like viewing conditions with a custom background lstar. 165*970e1046SAndroid Build Coastguard Worker * 166*970e1046SAndroid Build Coastguard Worker * <p>Default viewing conditions have a lstar of 50, midgray. 167*970e1046SAndroid Build Coastguard Worker */ defaultWithBackgroundLstar(double lstar)168*970e1046SAndroid Build Coastguard Worker public static ViewingConditions defaultWithBackgroundLstar(double lstar) { 169*970e1046SAndroid Build Coastguard Worker return ViewingConditions.make( 170*970e1046SAndroid Build Coastguard Worker ColorUtils.whitePointD65(), 171*970e1046SAndroid Build Coastguard Worker (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f), 172*970e1046SAndroid Build Coastguard Worker lstar, 173*970e1046SAndroid Build Coastguard Worker 2.0, 174*970e1046SAndroid Build Coastguard Worker false); 175*970e1046SAndroid Build Coastguard Worker } 176*970e1046SAndroid Build Coastguard Worker 177*970e1046SAndroid Build Coastguard Worker /** 178*970e1046SAndroid Build Coastguard Worker * Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand 179*970e1046SAndroid Build Coastguard Worker * for technical color science terminology, this class would not benefit from documenting them 180*970e1046SAndroid Build Coastguard Worker * individually. A brief overview is available in the CAM16 specification, and a complete overview 181*970e1046SAndroid Build Coastguard Worker * requires a color science textbook, such as Fairchild's Color Appearance Models. 182*970e1046SAndroid Build Coastguard Worker */ ViewingConditions( double n, double aw, double nbb, double ncb, double c, double nc, double[] rgbD, double fl, double flRoot, double z)183*970e1046SAndroid Build Coastguard Worker private ViewingConditions( 184*970e1046SAndroid Build Coastguard Worker double n, 185*970e1046SAndroid Build Coastguard Worker double aw, 186*970e1046SAndroid Build Coastguard Worker double nbb, 187*970e1046SAndroid Build Coastguard Worker double ncb, 188*970e1046SAndroid Build Coastguard Worker double c, 189*970e1046SAndroid Build Coastguard Worker double nc, 190*970e1046SAndroid Build Coastguard Worker double[] rgbD, 191*970e1046SAndroid Build Coastguard Worker double fl, 192*970e1046SAndroid Build Coastguard Worker double flRoot, 193*970e1046SAndroid Build Coastguard Worker double z) { 194*970e1046SAndroid Build Coastguard Worker this.n = n; 195*970e1046SAndroid Build Coastguard Worker this.aw = aw; 196*970e1046SAndroid Build Coastguard Worker this.nbb = nbb; 197*970e1046SAndroid Build Coastguard Worker this.ncb = ncb; 198*970e1046SAndroid Build Coastguard Worker this.c = c; 199*970e1046SAndroid Build Coastguard Worker this.nc = nc; 200*970e1046SAndroid Build Coastguard Worker this.rgbD = rgbD; 201*970e1046SAndroid Build Coastguard Worker this.fl = fl; 202*970e1046SAndroid Build Coastguard Worker this.flRoot = flRoot; 203*970e1046SAndroid Build Coastguard Worker this.z = z; 204*970e1046SAndroid Build Coastguard Worker } 205*970e1046SAndroid Build Coastguard Worker } 206