@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
JavaScript
/**
* @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