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