/* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.ux.material.libmonet.hct; import com.google.ux.material.libmonet.utils.ColorUtils; import com.google.ux.material.libmonet.utils.MathUtils; /** * In traditional color spaces, a color can be identified solely by the observer's measurement of * the color. Color appearance models such as CAM16 also use information about the environment where * the color was observed, known as the viewing conditions. * *

For example, white under the traditional assumption of a midday sun white point is accurately * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100) * *

This class caches intermediate values of the CAM16 conversion process that depend only on * viewing conditions, enabling speed ups. */ public final class ViewingConditions { /** sRGB-like viewing conditions. */ public static final ViewingConditions DEFAULT = ViewingConditions.defaultWithBackgroundLstar(50.0); private final double aw; private final double nbb; private final double ncb; private final double c; private final double nc; private final double n; private final double[] rgbD; private final double fl; private final double flRoot; private final double z; public double getAw() { return aw; } public double getN() { return n; } public double getNbb() { return nbb; } double getNcb() { return ncb; } double getC() { return c; } double getNc() { return nc; } public double[] getRgbD() { return rgbD; } double getFl() { return fl; } public double getFlRoot() { return flRoot; } double getZ() { return z; } /** * Create ViewingConditions from a simple, physically relevant, set of parameters. * * @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day * afternoon * @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in * the room where the color is viewed. Can be calculated from lux by multiplying lux by * 0.0586. default = 11.72, or 200 lux. * @param backgroundLstar The lightness of the area surrounding the color. measured by L* in * L*a*b*. default = 50.0 * @param surround A general description of the lighting surrounding the color. 0 is pitch dark, * like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at * night. 2.0 means there is no difference between the lighting on the color and around it. * default = 2.0 * @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting, * such as knowing an apple is still red in green light. default = false, the eye does not * perform this process on self-luminous objects like displays. */ public static ViewingConditions make( double[] whitePoint, double adaptingLuminance, double backgroundLstar, double surround, boolean discountingIlluminant) { // A background of pure black is non-physical and leads to infinities that represent the idea // that any color viewed in pure black can't be seen. backgroundLstar = Math.max(0.1, backgroundLstar); // Transform white point XYZ to 'cone'/'rgb' responses double[][] matrix = Cam16.XYZ_TO_CAM16RGB; double[] xyz = whitePoint; double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]); double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]); double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]); double f = 0.8 + (surround / 10.0); double c = (f >= 0.9) ? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0)) : MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0)); double d = discountingIlluminant ? 1.0 : f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0))); d = MathUtils.clampDouble(0.0, 1.0, d); double nc = f; double[] rgbD = new double[] { d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d, d * (100.0 / bW) + 1.0 - d }; double k = 1.0 / (5.0 * adaptingLuminance + 1.0); double k4 = k * k * k * k; double k4F = 1.0 - k4; double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(5.0 * adaptingLuminance)); double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]); double z = 1.48 + Math.sqrt(n); double nbb = 0.725 / Math.pow(n, 0.2); double ncb = nbb; double[] rgbAFactors = new double[] { Math.pow(fl * rgbD[0] * rW / 100.0, 0.42), Math.pow(fl * rgbD[1] * gW / 100.0, 0.42), Math.pow(fl * rgbD[2] * bW / 100.0, 0.42) }; double[] rgbA = new double[] { (400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13), (400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13), (400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13) }; double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb; return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z); } /** * Create sRGB-like viewing conditions with a custom background lstar. * *

Default viewing conditions have a lstar of 50, midgray. */ public static ViewingConditions defaultWithBackgroundLstar(double lstar) { return ViewingConditions.make( ColorUtils.whitePointD65(), (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f), lstar, 2.0, false); } /** * Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand * for technical color science terminology, this class would not benefit from documenting them * individually. A brief overview is available in the CAM16 specification, and a complete overview * requires a color science textbook, such as Fairchild's Color Appearance Models. */ private ViewingConditions( double n, double aw, double nbb, double ncb, double c, double nc, double[] rgbD, double fl, double flRoot, double z) { this.n = n; this.aw = aw; this.nbb = nbb; this.ncb = ncb; this.c = c; this.nc = nc; this.rgbD = rgbD; this.fl = fl; this.flRoot = flRoot; this.z = z; } }