UNPKG

@poupe/material-color-utilities

Version:

Algorithms and utilities that power the Material Design 3 (M3) color system, including choosing theme colors from images and creating tones of colors; all in a new color space.

357 lines 17.4 kB
/** * @license * 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. */ import * as utils from '../utils/color_utils.js'; import * as math from '../utils/math_utils.js'; import { ViewingConditions } from './viewing_conditions.js'; /** * CAM16, a color appearance model. Colors are not just defined by their hex * code, but rather, a hex code and viewing conditions. * * CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, * b*, or jstar, astar, bstar in code. CAM16-UCS is included in the CAM16 * specification, and should be used when measuring distances between colors. * * 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) */ export class Cam16 { /** * All of the CAM16 dimensions can be calculated from 3 of the dimensions, in * the following combinations: * - {j or q} and {c, m, or s} and hue * - jstar, astar, bstar * Prefer using a static method that constructs from 3 of those dimensions. * This constructor is intended for those methods to use to return all * possible dimensions. * * @param hue * @param chroma informally, colorfulness / color intensity. like saturation * in HSL, except perceptually accurate. * @param j lightness * @param q brightness; ratio of lightness to white point's lightness * @param m colorfulness * @param s saturation; ratio of chroma to white point's chroma * @param jstar CAM16-UCS J coordinate * @param astar CAM16-UCS a coordinate * @param bstar CAM16-UCS b coordinate */ constructor(hue, chroma, j, q, m, s, jstar, astar, bstar) { this.hue = hue; this.chroma = chroma; this.j = j; this.q = q; this.m = m; this.s = s; this.jstar = jstar; this.astar = astar; this.bstar = bstar; } /** * CAM16 instances also have coordinates in the CAM16-UCS space, called J*, * a*, b*, or jstar, astar, bstar in code. CAM16-UCS is included in the CAM16 * specification, and is used to measure distances between colors. */ distance(other) { const dJ = this.jstar - other.jstar; const dA = this.astar - other.astar; const dB = this.bstar - other.bstar; const dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB); const dE = 1.41 * Math.pow(dEPrime, 0.63); return dE; } /** * @param argb ARGB representation of a color. * @return CAM16 color, assuming the color was viewed in default viewing * conditions. */ static fromInt(argb) { return Cam16.fromIntInViewingConditions(argb, ViewingConditions.DEFAULT); } /** * @param argb ARGB representation of a color. * @param viewingConditions Information about the environment where the color * was observed. * @return CAM16 color. */ static fromIntInViewingConditions(argb, viewingConditions) { const red = (argb & 0x00ff0000) >> 16; const green = (argb & 0x0000ff00) >> 8; const blue = (argb & 0x000000ff); const redL = utils.linearized(red); const greenL = utils.linearized(green); const blueL = utils.linearized(blue); const x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL; const y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL; const z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL; const rC = 0.401288 * x + 0.650173 * y - 0.051461 * z; const gC = -0.250268 * x + 1.204414 * y + 0.045854 * z; const bC = -0.002079 * x + 0.048952 * y + 0.953127 * z; const rD = viewingConditions.rgbD[0] * rC; const gD = viewingConditions.rgbD[1] * gC; const bD = viewingConditions.rgbD[2] * bC; const rAF = Math.pow((viewingConditions.fl * Math.abs(rD)) / 100.0, 0.42); const gAF = Math.pow((viewingConditions.fl * Math.abs(gD)) / 100.0, 0.42); const bAF = Math.pow((viewingConditions.fl * Math.abs(bD)) / 100.0, 0.42); const rA = (math.signum(rD) * 400.0 * rAF) / (rAF + 27.13); const gA = (math.signum(gD) * 400.0 * gAF) / (gAF + 27.13); const bA = (math.signum(bD) * 400.0 * bAF) / (bAF + 27.13); const a = (11.0 * rA + -12.0 * gA + bA) / 11.0; const b = (rA + gA - 2.0 * bA) / 9.0; const u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0; const p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0; const atan2 = Math.atan2(b, a); const atanDegrees = (atan2 * 180.0) / Math.PI; const hue = atanDegrees < 0 ? atanDegrees + 360.0 : atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees; const hueRadians = (hue * Math.PI) / 180.0; const ac = p2 * viewingConditions.nbb; const j = 100.0 * Math.pow(ac / viewingConditions.aw, viewingConditions.c * viewingConditions.z); const q = (4.0 / viewingConditions.c) * Math.sqrt(j / 100.0) * (viewingConditions.aw + 4.0) * viewingConditions.fLRoot; const huePrime = hue < 20.14 ? hue + 360 : hue; const eHue = 0.25 * (Math.cos((huePrime * Math.PI) / 180.0 + 2.0) + 3.8); const p1 = (50000.0 / 13.0) * eHue * viewingConditions.nc * viewingConditions.ncb; const t = (p1 * Math.sqrt(a * a + b * b)) / (u + 0.305); const alpha = Math.pow(t, 0.9) * Math.pow(1.64 - Math.pow(0.29, viewingConditions.n), 0.73); const c = alpha * Math.sqrt(j / 100.0); const m = c * viewingConditions.fLRoot; const s = 50.0 * Math.sqrt((alpha * viewingConditions.c) / (viewingConditions.aw + 4.0)); const jstar = ((1.0 + 100.0 * 0.007) * j) / (1.0 + 0.007 * j); const mstar = (1.0 / 0.0228) * Math.log(1.0 + 0.0228 * m); const astar = mstar * Math.cos(hueRadians); const bstar = mstar * Math.sin(hueRadians); return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar); } /** * @param j CAM16 lightness * @param c CAM16 chroma * @param h CAM16 hue */ static fromJch(j, c, h) { return Cam16.fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT); } /** * @param j CAM16 lightness * @param c CAM16 chroma * @param h CAM16 hue * @param viewingConditions Information about the environment where the color * was observed. */ static fromJchInViewingConditions(j, c, h, viewingConditions) { const q = (4.0 / viewingConditions.c) * Math.sqrt(j / 100.0) * (viewingConditions.aw + 4.0) * viewingConditions.fLRoot; const m = c * viewingConditions.fLRoot; const alpha = c / Math.sqrt(j / 100.0); const s = 50.0 * Math.sqrt((alpha * viewingConditions.c) / (viewingConditions.aw + 4.0)); const hueRadians = (h * Math.PI) / 180.0; const jstar = ((1.0 + 100.0 * 0.007) * j) / (1.0 + 0.007 * j); const mstar = (1.0 / 0.0228) * Math.log(1.0 + 0.0228 * m); const astar = mstar * Math.cos(hueRadians); const bstar = mstar * Math.sin(hueRadians); return new Cam16(h, c, j, q, m, s, jstar, astar, bstar); } /** * @param jstar CAM16-UCS lightness. * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian * coordinate on the Y axis. * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian * coordinate on the X axis. */ static fromUcs(jstar, astar, bstar) { return Cam16.fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT); } /** * @param jstar CAM16-UCS lightness. * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian * coordinate on the Y axis. * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian * coordinate on the X axis. * @param viewingConditions Information about the environment where the color * was observed. */ static fromUcsInViewingConditions(jstar, astar, bstar, viewingConditions) { const a = astar; const b = bstar; const m = Math.sqrt(a * a + b * b); const M = (Math.exp(m * 0.0228) - 1.0) / 0.0228; const c = M / viewingConditions.fLRoot; let h = Math.atan2(b, a) * (180.0 / Math.PI); if (h < 0.0) { h += 360.0; } const j = jstar / (1 - (jstar - 100) * 0.007); return Cam16.fromJchInViewingConditions(j, c, h, viewingConditions); } /** * @return ARGB representation of color, assuming the color was viewed in * default viewing conditions, which are near-identical to the default * viewing conditions for sRGB. */ toInt() { return this.viewed(ViewingConditions.DEFAULT); } /** * @param viewingConditions Information about the environment where the color * will be viewed. * @return ARGB representation of color */ viewed(viewingConditions) { const alpha = this.chroma === 0.0 || this.j === 0.0 ? 0.0 : this.chroma / Math.sqrt(this.j / 100.0); const t = Math.pow(alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.n), 0.73), 1.0 / 0.9); const hRad = (this.hue * Math.PI) / 180.0; const eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8); const ac = viewingConditions.aw * Math.pow(this.j / 100.0, 1.0 / viewingConditions.c / viewingConditions.z); const p1 = eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb; const p2 = ac / viewingConditions.nbb; const hSin = Math.sin(hRad); const hCos = Math.cos(hRad); const gamma = (23.0 * (p2 + 0.305) * t) / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin); const a = gamma * hCos; const b = gamma * hSin; const rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0; const gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0; const bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0; const rCBase = Math.max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA))); const rC = math.signum(rA) * (100.0 / viewingConditions.fl) * Math.pow(rCBase, 1.0 / 0.42); const gCBase = Math.max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA))); const gC = math.signum(gA) * (100.0 / viewingConditions.fl) * Math.pow(gCBase, 1.0 / 0.42); const bCBase = Math.max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA))); const bC = math.signum(bA) * (100.0 / viewingConditions.fl) * Math.pow(bCBase, 1.0 / 0.42); const rF = rC / viewingConditions.rgbD[0]; const gF = gC / viewingConditions.rgbD[1]; const bF = bC / viewingConditions.rgbD[2]; const x = 1.86206786 * rF - 1.01125463 * gF + 0.14918677 * bF; const y = 0.38752654 * rF + 0.62144744 * gF - 0.00897398 * bF; const z = -0.01584150 * rF - 0.03412294 * gF + 1.04996444 * bF; const argb = utils.argbFromXyz(x, y, z); return argb; } /// Given color expressed in XYZ and viewed in [viewingConditions], convert to /// CAM16. static fromXyzInViewingConditions(x, y, z, viewingConditions) { // Transform XYZ to 'cone'/'rgb' responses const rC = 0.401288 * x + 0.650173 * y - 0.051461 * z; const gC = -0.250268 * x + 1.204414 * y + 0.045854 * z; const bC = -0.002079 * x + 0.048952 * y + 0.953127 * z; // Discount illuminant const rD = viewingConditions.rgbD[0] * rC; const gD = viewingConditions.rgbD[1] * gC; const bD = viewingConditions.rgbD[2] * bC; // chromatic adaptation const rAF = Math.pow(viewingConditions.fl * Math.abs(rD) / 100.0, 0.42); const gAF = Math.pow(viewingConditions.fl * Math.abs(gD) / 100.0, 0.42); const bAF = Math.pow(viewingConditions.fl * Math.abs(bD) / 100.0, 0.42); const rA = math.signum(rD) * 400.0 * rAF / (rAF + 27.13); const gA = math.signum(gD) * 400.0 * gAF / (gAF + 27.13); const bA = math.signum(bD) * 400.0 * bAF / (bAF + 27.13); // redness-greenness const a = (11.0 * rA + -12.0 * gA + bA) / 11.0; // yellowness-blueness const b = (rA + gA - 2.0 * bA) / 9.0; // auxiliary components const u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0; const p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0; // hue const atan2 = Math.atan2(b, a); const atanDegrees = atan2 * 180.0 / Math.PI; const hue = atanDegrees < 0 ? atanDegrees + 360.0 : atanDegrees >= 360 ? atanDegrees - 360 : atanDegrees; const hueRadians = hue * Math.PI / 180.0; // achromatic response to color const ac = p2 * viewingConditions.nbb; // CAM16 lightness and brightness const J = 100.0 * Math.pow(ac / viewingConditions.aw, viewingConditions.c * viewingConditions.z); const Q = (4.0 / viewingConditions.c) * Math.sqrt(J / 100.0) * (viewingConditions.aw + 4.0) * (viewingConditions.fLRoot); const huePrime = (hue < 20.14) ? hue + 360 : hue; const eHue = (1.0 / 4.0) * (Math.cos(huePrime * Math.PI / 180.0 + 2.0) + 3.8); const p1 = 50000.0 / 13.0 * eHue * viewingConditions.nc * viewingConditions.ncb; const t = p1 * Math.sqrt(a * a + b * b) / (u + 0.305); const alpha = Math.pow(t, 0.9) * Math.pow(1.64 - Math.pow(0.29, viewingConditions.n), 0.73); // CAM16 chroma, colorfulness, chroma const C = alpha * Math.sqrt(J / 100.0); const M = C * viewingConditions.fLRoot; const s = 50.0 * Math.sqrt((alpha * viewingConditions.c) / (viewingConditions.aw + 4.0)); // CAM16-UCS components const jstar = (1.0 + 100.0 * 0.007) * J / (1.0 + 0.007 * J); const mstar = Math.log(1.0 + 0.0228 * M) / 0.0228; const astar = mstar * Math.cos(hueRadians); const bstar = mstar * Math.sin(hueRadians); return new Cam16(hue, C, J, Q, M, s, jstar, astar, bstar); } /// XYZ representation of CAM16 seen in [viewingConditions]. xyzInViewingConditions(viewingConditions) { const alpha = (this.chroma === 0.0 || this.j === 0.0) ? 0.0 : this.chroma / Math.sqrt(this.j / 100.0); const t = Math.pow(alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.n), 0.73), 1.0 / 0.9); const hRad = this.hue * Math.PI / 180.0; const eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8); const ac = viewingConditions.aw * Math.pow(this.j / 100.0, 1.0 / viewingConditions.c / viewingConditions.z); const p1 = eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb; const p2 = (ac / viewingConditions.nbb); const hSin = Math.sin(hRad); const hCos = Math.cos(hRad); const gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin); const a = gamma * hCos; const b = gamma * hSin; const rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0; const gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0; const bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0; const rCBase = Math.max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA))); const rC = math.signum(rA) * (100.0 / viewingConditions.fl) * Math.pow(rCBase, 1.0 / 0.42); const gCBase = Math.max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA))); const gC = math.signum(gA) * (100.0 / viewingConditions.fl) * Math.pow(gCBase, 1.0 / 0.42); const bCBase = Math.max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA))); const bC = math.signum(bA) * (100.0 / viewingConditions.fl) * Math.pow(bCBase, 1.0 / 0.42); const rF = rC / viewingConditions.rgbD[0]; const gF = gC / viewingConditions.rgbD[1]; const bF = bC / viewingConditions.rgbD[2]; const x = 1.86206786 * rF - 1.01125463 * gF + 0.14918677 * bF; const y = 0.38752654 * rF + 0.62144744 * gF - 0.00897398 * bF; const z = -0.01584150 * rF - 0.03412294 * gF + 1.04996444 * bF; return [x, y, z]; } } //# sourceMappingURL=cam16.js.map