colorjs.io
Version:
Color space agnostic color manipulation library
2,032 lines (1,714 loc) • 75.8 kB
JavaScript
// A is m x n. B is n x p. product is m x p.
function multiplyMatrices(A, B) {
let m = A.length;
if (!Array.isArray(A[0])) {
// A is vector, convert to [[a, b, c, ...]]
A = [A];
}
if (!Array.isArray(B[0])) {
// B is vector, convert to [[a], [b], [c], ...]]
B = B.map(x => [x]);
}
let p = B[0].length;
let B_cols = B[0].map((_, i) => B.map(x => x[i])); // transpose B
let product = A.map(row => B_cols.map(col => {
if (!Array.isArray(row)) {
return col.reduce((a, c) => a + c * row, 0);
}
return row.reduce((a, c, i) => a + c * (col[i] || 0), 0);
}));
if (m === 1) {
product = product[0]; // Avoid [[a, b, c, ...]]
}
if (p === 1) {
return product.map(x => x[0]); // Avoid [[a], [b], [c], ...]]
}
return product;
}
/**
* Check if a value is a string (including a String object)
* @param {*} str - Value to check
* @returns {boolean}
*/
function isString (str) {
return type(str) === "string";
}
/**
* Determine the internal JavaScript [[Class]] of an object.
* @param {*} o - Value to check
* @returns {string}
*/
function type (o) {
let str = Object.prototype.toString.call(o);
return (str.match(/^\[object\s+(.*?)\]$/)[1] || "").toLowerCase();
}
/**
* Like Object.assign() but copies property descriptors (including symbols)
* @param {Object} target - Object to copy to
* @param {...Object} sources - Objects to copy from
* @returns {Object} target
*/
function extend (target, ...sources) {
for (let source of sources) {
if (source) {
let descriptors = Object.getOwnPropertyDescriptors(source);
Object.defineProperties(target, descriptors);
}
}
return target;
}
/**
* Copy a descriptor from one object to another
* @param {Object} target - Object to copy to
* @param {Object} source - Object to copy from
* @param {string} prop - Name of property
*/
function copyDescriptor (target, source, prop) {
let descriptor = Object.getOwnPropertyDescriptor(source, prop);
Object.defineProperty(target, prop, descriptor);
}
/**
* Uppercase the first letter of a string
* @param {string} str - String to capitalize
* @returns Capitalized string
*/
function capitalize(str) {
if (!str) {
return str;
}
return str[0].toUpperCase() + str.slice(1);
}
/**
* Round a number to a certain number of significant digits based on a range
* @param {number} n - The number to round
* @param {number} precision - Number of significant digits
* @param {Array[2]} range - Range to base decimals on
*/
function toPrecision(n, precision, range = [0, 1]) {
precision = +precision;
let digits = ((range[1] || range[0] || 1) + "").length;
let decimals = Math.max(0, precision + 1 - digits);
return +n.toFixed(decimals);
}
function parseCoord(coord) {
if (coord.indexOf(".") > 0) {
// Reduce a coordinate of a certain color space until the color is in gamut
let [spaceId, coordName] = coord.split(".");
let space = Color.space(spaceId);
if (!(coordName in space.coords)) {
throw new ReferenceError(`Color space "${space.name}" has no "${coordName}" coordinate.`);
}
return [space, coordName];
}
}
function value(obj, prop, value) {
let props = prop.split(".");
let lastProp = props.pop();
obj = props.reduceRight((acc, cur) => {
return acc && acc[cur];
}, obj);
if (obj) {
if (value === undefined) {
// Get
return obj[lastProp];
}
else {
// Set
return obj[lastProp] = value;
}
}
}
var util = /*#__PURE__*/Object.freeze({
__proto__: null,
isString: isString,
type: type,
extend: extend,
copyDescriptor: copyDescriptor,
capitalize: capitalize,
toPrecision: toPrecision,
parseCoord: parseCoord,
value: value,
multiplyMatrices: multiplyMatrices
});
/**
* Module version of Bliss.Hooks.
* @author Lea Verou
*/
class Hooks {
add (name, callback, first) {
if (typeof arguments[0] != "string") {
// Multiple hooks
for (var name in arguments[0]) {
this.add(name, arguments[0][name], arguments[1]);
}
return;
}
(Array.isArray(name)? name : [name]).forEach(function(name) {
this[name] = this[name] || [];
if (callback) {
this[name][first? "unshift" : "push"](callback);
}
}, this);
}
run (name, env) {
this[name] = this[name] || [];
this[name].forEach(function(callback) {
callback.call(env && env.context? env.context : env, env);
});
}
}
const ε = .000075;
const hasDOM = typeof document !== "undefined";
class Color$1 {
// Signatures:
// new Color(stringToParse)
// new Color(otherColor)
// new Color(coords, alpha) // defaults to sRGB
// new Color(CSS variable [, root])
constructor (...args) {
let str, color;
// new Color(color)
// new Color({spaceId, coords})
// new Color({space, coords})
if (args[0] && typeof args[0] === "object" && (args[0].space || args[0].spaceId) && args[0].coords) {
color = args[0];
}
else if (isString(args[0])) {
// new Color("--foo" [, root])
if (hasDOM && args[0].indexOf("--") === 0) {
// CSS variable
let root = arguments[1] && arguments[1].nodeType === 1? arguments[1] : document.documentElement;
str = getComputedStyle(root).getPropertyValue(arguments[0]);
}
// new Color(string)
else if (args.length === 1) {
str = args[0];
}
if (str) {
color = Color$1.parse(str);
}
}
if (color) {
if ("spaceId" in color) {
this.spaceId = color.spaceId;
}
else {
this.space = color.space;
}
this.coords = color.coords.slice();
this.alpha = color.alpha;
}
else { // default signature new Color([ColorSpace,] array [, alpha])
let spaceId, coords, alpha;
if (Array.isArray(args[0])) {
// No color space provided, default to sRGB
[spaceId, coords, alpha] = ["sRGB", ...args];
}
else {
[spaceId, coords, alpha] = args;
}
this.spaceId = spaceId || "sRGB";
this.coords = coords? coords.slice() : [0, 0, 0];
this.alpha = alpha;
}
this.alpha = this.alpha < 1? this.alpha : 1; // this also deals with NaN etc
// Convert "NaN" to NaN
for (let i = 0; i < this.coords.length; i++) {
if (this.coords[i] === "NaN") {
this.coords[i] = NaN;
}
}
}
get space () {
return Color$1.spaces[this.spaceId];
}
set space (value) {
// Setting spaceId works with color space objects too
return this.spaceId = value;
}
get spaceId () {
return this._spaceId;
}
// Handle dynamic changes of color space
set spaceId (id) {
let newSpace = Color$1.space(id);
id = newSpace.id;
if (this.space && newSpace && this.space !== newSpace) {
// We’re not setting this for the first time, need to:
// a) Convert coords
this.coords = this[id];
// b) Remove instance properties from previous color space
for (let prop in this.space.instance) {
if (this.hasOwnProperty(prop)) {
delete this[prop];
}
}
}
this._spaceId = id;
// Add new instance properties from new color space
extend(this, this.space.instance);
}
get white () {
return this.space.white || Color$1.whites.D50;
}
// Set properties and return current instance
set (prop, value$1) {
if (arguments.length === 1 && type(arguments[0]) === "object") {
// Argument is an object literal
let object = arguments[0];
for (let p in object) {
this.set(p, object[p]);
}
}
else {
if (typeof value$1 === "function") {
let current = value(this, prop);
value(this, prop, value$1.call(this, current));
}
else {
value(this, prop, value$1);
}
}
return this;
}
lighten (amount = .25) {
let ret = new Color$1(this);
let lightness = ret.lightness;
ret.lightness = lightness * (1 + amount);
return ret;
}
darken (amount = .25) {
let ret = new Color$1(this);
let lightness = ret.lightness;
ret.lightness = lightness * (1 - amount);
return ret;
}
// Euclidean distance of colors in an arbitrary color space
distance (color, space = "lab") {
color = Color$1.get(color);
space = Color$1.space(space);
let coords1 = this[space.id];
let coords2 = color[space.id];
return Math.sqrt(coords1.reduce((a, c, i) => {
if (isNaN(c) || isNaN(coords2[i])) {
return a;
}
return a + (coords2[i] - c) ** 2;
}, 0));
}
deltaE (color, o = {}) {
if (isString(o)) {
o = {method: o};
}
let {method = Color$1.defaults.deltaE, ...rest} = o;
color = Color$1.get(color);
if (this["deltaE" + method]) {
return this["deltaE" + method](color, rest);
}
return this.deltaE76(color);
}
// 1976 DeltaE. 2.3 is the JND
deltaE76 (color) {
return this.distance(color, "lab");
}
// Relative luminance
get luminance () {
return this.xyz.Y;
}
set luminance (value) {
this.xyz.Y = value;
}
// WCAG 2.0 contrast https://www.w3.org/TR/WCAG20-TECHS/G18.html
contrast (color) {
color = Color$1.get(color);
let L1 = this.luminance;
let L2 = color.luminance;
if (L2 > L1) {
[L1, L2] = [L2, L1];
}
return (L1 + .05) / (L2 + .05);
}
// Chromaticity coordinates
get uv () {
let [X, Y, Z] = this.xyz;
let denom = X + 15 * Y + 3 * Z;
return [4 * X / denom, 9 * Y / denom];
}
get xy () {
let [X, Y, Z] = this.xyz;
let sum = X + Y + Z;
return [X / sum, Y / sum];
}
// no setters, as lightness information is lost
// when converting color to chromaticity
// Get formatted coords
getCoords ({inGamut, precision = Color$1.defaults.precision} = {}) {
let coords = this.coords;
if (inGamut && !this.inGamut()) {
coords = this.toGamut(inGamut === true? undefined : inGamut).coords;
}
if (precision !== undefined && precision !== null) {
let bounds = this.space.coords? Object.values(this.space.coords) : [];
coords = coords.map((n, i) => toPrecision(n, precision, bounds[i]));
}
return coords;
}
/**
* @return {Boolean} Is the color in gamut?
*/
inGamut (space = this.space, options) {
space = Color$1.space(space);
return Color$1.inGamut(space, this[space.id], options);
}
static inGamut (space, coords, {epsilon = ε} = {}) {
space = Color$1.space(space);
if (space.inGamut) {
return space.inGamut(coords);
}
else {
if (!space.coords) {
return true;
}
// No color-space specific inGamut() function, just check if coords are within reference range
let bounds = Object.values(space.coords);
return coords.every((c, i) => {
if (Number.isNaN(c)) {
return true;
}
let [min, max] = bounds[i];
return (min === undefined || c >= min - epsilon)
&& (max === undefined || c <= max + epsilon);
});
}
}
/**
* Force coordinates in gamut of a certain color space and return the result
* @param {Object} options
* @param {string} options.method - How to force into gamut.
* If "clip", coordinates are just clipped to their reference range.
* If in the form [colorSpaceId].[coordName], that coordinate is reduced
* until the color is in gamut. Please note that this may produce nonsensical
* results for certain coordinates (e.g. hue) or infinite loops if reducing the coordinate never brings the color in gamut.
* @param {ColorSpace|string} options.space - The space whose gamut we want to map to
* @param {boolean} options.inPlace - If true, modify the current color, otherwise return a new one.
*/
toGamut ({method = Color$1.defaults.gamutMapping, space = this.space, inPlace} = {}) {
if (isString(arguments[0])) {
space = arguments[0];
}
space = Color$1.space(space);
if (this.inGamut(space, {epsilon: 0})) {
return this;
}
// 3 spaces:
// this.space: current color space
// space: space whose gamut we are mapping to
// mapSpace: space with the coord we're reducing
let color = this.to(space);
if (method.indexOf(".") > 0 && !this.inGamut(space)) {
let clipped = color.toGamut({method: "clip", space});
// distance of original color from gamut boundary
let base_error = this.deltaE(clipped, {method: "2000"});
// console.log(base_error);
if (this.deltaE(clipped, {method: "2000"}) > 2.3) {
// Reduce a coordinate of a certain color space until the color is in gamut
let [mapSpace, coordName] = parseCoord(method);
let mappedColor = color.to(mapSpace);
let bounds = mapSpace.coords[coordName];
let min = bounds[0];
let ε = .001; // for deltaE
let low = min;
let high = mappedColor[coordName];
// distance of current estimate from original color
let error = color.deltaE(mappedColor, {method: "2000"});
// let i = 0;
while ((high - low > ε) && (error < base_error)) {
let clipped = mappedColor.toGamut({space, method: "clip"});
let deltaE = mappedColor.deltaE(clipped, {method: "2000"});
error = color.deltaE(mappedColor, {method: "2000"});
if (deltaE - 2 < ε) {
low = mappedColor[coordName];
// console.log(++i, "in", mappedColor.chroma, mappedColor.srgb, error);
}
else {
// console.log(++i, "out", mappedColor.chroma, mappedColor.srgb, clipped.srgb, deltaE, error);
if (Math.abs(deltaE - 2) < ε) {
// We've found the boundary
break;
}
high = mappedColor[coordName];
}
mappedColor[coordName] = (high + low) / 2;
}
color = mappedColor.to(space);
}
else {
color = clipped;
}
}
if (method === "clip" // Dumb coord clipping
// finish off smarter gamut mapping with clip to get rid of ε, see #17
|| !color.inGamut(space, {epsilon: 0})
) {
let bounds = Object.values(space.coords);
color.coords = color.coords.map((c, i) => {
let [min, max] = bounds[i];
if (min !== undefined) {
c = Math.max(min, c);
}
if (max !== undefined) {
c = Math.min(c, max);
}
return c;
});
}
if (space.id !== this.spaceId) {
color = color.to(this.space);
}
if (inPlace) {
this.coords = color.coords;
return this;
}
else {
return color;
}
}
clone () {
return new Color$1(this.spaceId, this.coords, this.alpha);
}
/**
* Convert to color space and return a new color
* @param {Object|string} space - Color space object or id
* @param {Object} options
* @param {boolean} options.inGamut - Whether to force resulting color in gamut
* @returns {Color}
*/
to (space, {inGamut} = {}) {
space = Color$1.space(space);
let id = space.id;
let color = new Color$1(id, this[id], this.alpha);
if (inGamut) {
color.toGamut({inPlace: true});
}
return color;
}
toJSON () {
return {
spaceId: this.spaceId,
coords: this.coords,
alpha: this.alpha
};
}
/**
* Generic toString() method, outputs a color(spaceId ...coords) function
* @param {Object} options
* @param {number} options.precision - Significant digits
* @param {boolean} options.commas - Whether to use commas to separate arguments or spaces (and a slash for alpha) [default: false]
* @param {Function|String|Array} options.format - If function, maps all coordinates. Keywords tap to colorspace-specific formats (e.g. "hex")
* @param {boolean} options.inGamut - Adjust coordinates to fit in gamut first? [default: false]
* @param {string} options.name - Function name [default: color]
*/
toString ({
precision = Color$1.defaults.precision,
format, commas, inGamut,
name = "color",
fallback
} = {}) {
let strAlpha = this.alpha < 1? ` ${commas? "," : "/"} ${this.alpha}` : "";
let coords = this.getCoords({inGamut, precision});
// Convert NaN to zeros to have a chance at a valid CSS color
// Also convert -0 to 0
coords = coords.map(c => c? c : 0);
if (isString(format)) {
if (format === "%") {
let maximumSignificantDigits = precision;
if (!Number.isInteger(precision) || precision > 21) {
maximumSignificantDigits = 21;
}
format = c => c.toLocaleString("en-US", {
style: "percent",
maximumSignificantDigits
});
}
}
if (typeof format === "function") {
coords = coords.map(format);
}
let args = [...coords];
if (name === "color") {
// If output is a color() function, add colorspace id as first argument
args.unshift(this.space? this.space.cssId || this.space.id : "XYZ");
}
let ret = `${name}(${args.join(commas? ", " : " ")}${strAlpha})`;
if (fallback) {
// Return a CSS string that's actually supported by the current browser
// Return as a String object, so we can also hang the color object on it
// in case it's different than this. That way third party code can use that
// for e.g. computing text color, indicating out of gamut etc
if (!hasDOM || !self.CSS || CSS.supports("color", ret)) {
ret = new String(ret);
ret.color = this;
return ret;
}
let fallbacks = Array.isArray(fallback)? fallback.slice() : Color$1.defaults.fallbackSpaces;
for (let i = 0, fallbackSpace; fallbackSpace = fallbacks[i]; i++) {
if (Color$1.spaces[fallbackSpace]) {
let color = this.to(fallbackSpace);
ret = color.toString({precision});
if (CSS.supports("color", ret)) {
ret = new String(ret);
ret.color = color;
return ret;
}
else if (fallbacks === Color$1.defaults.fallbackSpaces) {
// Drop this space from the default fallbacks since it's not supported
fallbacks.splice(i, 1);
i--;
}
}
}
// None of the fallbacks worked, return in the most conservative form possible
let color = this.to("srgb");
ret = new String(color.toString({commas: true}));
ret.color = color;
}
return ret;
}
equals (color) {
color = Color$1.get(color);
return this.spaceId === color.spaceId
&& this.alpha === color.alpha
&& this.coords.every((c, i) => c === color.coords[i]);
}
// Adapt XYZ from white point W1 to W2
static chromaticAdaptation (W1, W2, XYZ, options = {}) {
W1 = W1 || Color$1.whites.D50;
W2 = W2 || Color$1.whites.D50;
if (W1 === W2) {
return XYZ;
}
let env = {W1, W2, XYZ, options};
Color$1.hooks.run("chromatic-adaptation-start", env);
if (!env.M) {
if (env.W1 === Color$1.whites.D65 && env.W2 === Color$1.whites.D50) {
// Linear Bradford CAT
env.M = [
[ 1.0478112, 0.0228866, -0.0501270],
[ 0.0295424, 0.9904844, -0.0170491],
[-0.0092345, 0.0150436, 0.7521316]
];
}
else if (env.W1 === Color$1.whites.D50 && env.W2 === Color$1.whites.D65) {
env.M = [
[ 0.9555766, -0.0230393, 0.0631636],
[-0.0282895, 1.0099416, 0.0210077],
[ 0.0122982, -0.0204830, 1.3299098]
];
}
}
Color$1.hooks.run("chromatic-adaptation-end", env);
if (env.M) {
return multiplyMatrices(env.M, env.XYZ);
}
else {
throw new TypeError("Only Bradford CAT with white points D50 and D65 supported for now.");
}
}
// CSS color to Color object
static parse (str) {
let env = {str};
Color$1.hooks.run("parse-start", env);
if (env.color) {
return env.color;
}
env.parsed = Color$1.parseFunction(env.str);
Color$1.hooks.run("parse-function-start", env);
if (env.color) {
return env.color;
}
// Try colorspace-specific parsing
for (let space of Object.values(Color$1.spaces)) {
if (space.parse) {
let color = space.parse(env.str, env.parsed);
if (color) {
return color;
}
}
}
let name = env.parsed && env.parsed.name;
if (!/^color|^rgb/.test(name) && hasDOM && document.head) {
// Use browser to parse when a DOM is available
// we mainly use this for color names right now if keywords.js is not included
// and for future-proofing
let previousColor = document.head.style.color;
document.head.style.color = "";
document.head.style.color = str;
if (document.head.style.color !== previousColor) {
let computed = getComputedStyle(document.head).color;
document.head.style.color = previousColor;
if (computed) {
str = computed;
env.parsed = Color$1.parseFunction(computed);
name = env.parsed.name;
}
}
}
if (env.parsed) {
// It's a function
if (name === "rgb" || name === "rgba") {
let args = env.parsed.args.map((c, i) => i < 3 && !c.percentage? c / 255 : +c);
return {
spaceId: "srgb",
coords: args.slice(0, 3),
alpha: args[3]
};
}
else if (name === "color") {
let spaceId = env.parsed.args.shift().toLowerCase();
let space = Object.values(Color$1.spaces).find(space => (space.cssId || space.id) === spaceId);
if (space) {
// From https://drafts.csswg.org/css-color-4/#color-function
// If more <number>s or <percentage>s are provided than parameters that the colorspace takes, the excess <number>s at the end are ignored.
// If less <number>s or <percentage>s are provided than parameters that the colorspace takes, the missing parameters default to 0. (This is particularly convenient for multichannel printers where the additional inks are spot colors or varnishes that most colors on the page won’t use.)
let argCount = Object.keys(space.coords).length;
let alpha = env.parsed.rawArgs.indexOf("/") > 0? env.parsed.args.pop() : 1;
let coords = Array(argCount).fill(0);
coords.forEach((_, i) => coords[i] = env.parsed.args[i] || 0);
return {spaceId: space.id, coords, alpha};
}
else {
throw new TypeError(`Color space ${spaceId} not found. Missing a plugin?`);
}
}
}
throw new TypeError(`Could not parse ${str} as a color. Missing a plugin?`);
}
/**
* Parse a CSS function, regardless of its name and arguments
* @param String str String to parse
* @return Object An object with {name, args, rawArgs}
*/
static parseFunction (str) {
if (!str) {
return;
}
str = str.trim();
const isFunctionRegex = /^([a-z]+)\((.+?)\)$/i;
const isNumberRegex = /^-?[\d.]+$/;
let parts = str.match(isFunctionRegex);
if (parts) {
// It is a function, parse args
let args = parts[2].match(/([-\w.]+(?:%|deg)?)/g);
args = args.map(arg => {
if (/%$/.test(arg)) {
// Convert percentages to 0-1 numbers
let n = new Number(+arg.slice(0, -1) / 100);
n.percentage = true;
return n;
}
else if (/deg$/.test(arg)) {
// Drop deg from degrees and convert to number
let n = new Number(+arg.slice(0, -3));
n.deg = true;
return n;
}
else if (isNumberRegex.test(arg)) {
// Convert numerical args to numbers
return +arg;
}
// Return everything else as-is
return arg;
});
return {
name: parts[1].toLowerCase(),
rawName: parts[1],
rawArgs: parts[2],
// An argument could be (as of css-color-4):
// a number, percentage, degrees (hue), ident (in color())
args
};
}
}
// One-off convert between color spaces
static convert (coords, fromSpace, toSpace) {
fromSpace = Color$1.space(fromSpace);
toSpace = Color$1.space(toSpace);
if (fromSpace === toSpace) {
// Same space, no change needed
return coords;
}
// Convert NaN to 0, which seems to be valid in every coordinate of every color space
coords = coords.map(c => Number.isNaN(c)? 0 : c);
let fromId = fromSpace.id;
let toId = toSpace.id;
// Do we have a more specific conversion function?
// Avoids round-tripping to & from XYZ
if (toSpace.from && toSpace.from[fromId]) {
// No white point adaptation, we assume the custom function takes care of it
return toSpace.from[fromId](coords);
}
if (fromSpace.to && fromSpace.to[toId]) {
// No white point adaptation, we assume the custom function takes care of it
return fromSpace.to[toId](coords);
}
let XYZ = fromSpace.toXYZ(coords);
if (toSpace.white !== fromSpace.white) {
// Different white point, perform white point adaptation
XYZ = Color$1.chromaticAdaptation(fromSpace.white, toSpace.white, XYZ);
}
return toSpace.fromXYZ(XYZ);
}
/**
* Get a color from the argument passed
* Basically gets us the same result as new Color(color) but doesn't clone an existing color object
*/
static get (color, ...args) {
if (color instanceof Color$1) {
return color;
}
return new Color$1(color, ...args);
}
/**
* Return a color space object from an id or color space object
* Mainly used internally, so that functions can easily accept either
*/
static space (space) {
let type$1 = type(space);
if (type$1 === "string") {
// It's a color space id
let ret = Color$1.spaces[space.toLowerCase()];
if (!ret) {
throw new TypeError(`No color space found with id = "${space}"`);
}
return ret;
}
else if (space && type$1 === "object") {
return space;
}
throw new TypeError(`${space} is not a valid color space`);
}
// Define a new color space
static defineSpace ({id, inherits}) {
let space = Color$1.spaces[id] = arguments[0];
if (inherits) {
const except = ["id", "parse", "instance", "properties"];
let parent = Color$1.spaces[inherits];
for (let prop in parent) {
if (!except.includes(prop) && !(prop in space)) {
copyDescriptor(space, parent, prop);
}
}
}
let coords = space.coords;
if (space.properties) {
extend(Color$1.prototype, space.properties);
}
if (!space.fromXYZ && !space.toXYZ) {
// Using a different connection space, define from/to XYZ functions based on that
let connectionSpace;
// What are we using as a connection space?
if (space.from && space.to) {
let from = new Set(Object.keys(space.from));
let to = new Set(Object.keys(space.to));
// Find spaces we can both convert to and from
let candidates = [...from].filter(id => {
if (to.has(id)) {
// Of those, only keep those that have fromXYZ and toXYZ
let space = Color$1.spaces[id];
return space && space.fromXYZ && space.toXYZ;
}
});
if (candidates.length > 0) {
// Great, we found connection spaces! Pick the first one
connectionSpace = Color$1.spaces[candidates[0]];
}
}
if (connectionSpace) {
// Define from/to XYZ functions based on the connection space
Object.assign(space, {
// ISSUE do we need white point adaptation here?
fromXYZ(XYZ) {
let newCoords = connectionSpace.fromXYZ(XYZ);
return this.from[connectionSpace.id](newCoords);
},
toXYZ(coords) {
let newCoords = this.to[connectionSpace.id](coords);
return connectionSpace.toXYZ(newCoords);
}
});
}
else {
throw new ReferenceError(`No connection space found for ${space.name}.`);
}
}
let coordNames = Object.keys(coords);
// Define getters and setters for color[spaceId]
// e.g. color.lch on *any* color gives us the lch coords
Object.defineProperty(Color$1.prototype, id, {
// Convert coords to coords in another colorspace and return them
// Source colorspace: this.spaceId
// Target colorspace: id
get () {
let ret = Color$1.convert(this.coords, this.spaceId, id);
if (!self.Proxy) {
return ret;
}
// Enable color.spaceId.coordName syntax
return new Proxy(ret, {
has: (obj, property) => {
return coordNames.includes(property) || Reflect.has(obj, property);
},
get: (obj, property, receiver) => {
let i = coordNames.indexOf(property);
if (i > -1) {
return obj[i];
}
return Reflect.get(obj, property, receiver);
},
set: (obj, property, value, receiver) => {
let i = coordNames.indexOf(property);
if (property > -1) { // Is property a numerical index?
i = property; // next if will take care of modifying the color
}
if (i > -1) {
obj[i] = value;
// Update color.coords
this.coords = Color$1.convert(obj, id, this.spaceId);
return true;
}
return Reflect.set(obj, property, value, receiver);
},
});
},
// Convert coords in another colorspace to internal coords and set them
// Target colorspace: this.spaceId
// Source colorspace: id
set (coords) {
this.coords = Color$1.convert(coords, id, this.spaceId);
},
configurable: true,
enumerable: true
});
return space;
}
// Define a shortcut property, e.g. color.lightness instead of color.lch.lightness
// Shorcut is looked up on Color.shortcuts at calling time
// If `long` is provided, it's added to Color.shortcuts as well, otherwise it's assumed to be already there
static defineShortcut(prop, obj = Color$1.prototype, long) {
if (long) {
Color$1.shortcuts[prop] = long;
}
Object.defineProperty(obj, prop, {
get () {
return value(this, Color$1.shortcuts[prop]);
},
set (value$1) {
return value(this, Color$1.shortcuts[prop], value$1);
},
configurable: true,
enumerable: true
});
}
// Define static versions of all instance methods
static statify(names = []) {
names = names || Object.getOwnPropertyNames(Color$1.prototype);
for (let prop of Object.getOwnPropertyNames(Color$1.prototype)) {
let descriptor = Object.getOwnPropertyDescriptor(Color$1.prototype, prop);
if (descriptor.get || descriptor.set) {
continue; // avoid accessors
}
let method = descriptor.value;
if (typeof method === "function" && !(prop in Color$1)) {
// We have a function, and no static version already
Color$1[prop] = function(color, ...args) {
color = Color$1.get(color);
return color[prop](...args);
};
}
}
}
}
Object.assign(Color$1, {
util,
hooks: new Hooks(),
whites: {
D50: [0.96422, 1.00000, 0.82521],
D65: [0.95047, 1.00000, 1.08883],
},
spaces: {},
// These will be available as getters and setters on EVERY color instance.
// They refer to LCH by default, but can be set to anything
// and you can add more by calling Color.defineShortcut()
shortcuts: {
"lightness": "lch.lightness",
"chroma": "lch.chroma",
"hue": "lch.hue",
},
// Global defaults one may want to configure
defaults: {
gamutMapping: "lch.chroma",
precision: 5,
deltaE: "76", // Default deltaE method
fallbackSpaces: ["p3", "srgb"]
}
});
Color$1.defineSpace({
id: "xyz",
name: "XYZ",
coords: {
X: [],
Y: [],
Z: []
},
white: Color$1.whites.D50,
inGamut: coords => true,
toXYZ: coords => coords,
fromXYZ: coords => coords
});
for (let prop in Color$1.shortcuts) {
Color$1.defineShortcut(prop);
}
// Make static methods for all instance methods
Color$1.statify();
// Color.DEBUGGING = true;
Color$1.defineSpace({
id: "lab",
name: "Lab",
coords: {
L: [0, 100],
a: [-100, 100],
b: [-100, 100]
},
inGamut: coords => true,
// Assuming XYZ is relative to D50, convert to CIE Lab
// from CIE standard, which now defines these as a rational fraction
white: Color$1.whites.D50,
ε: 216/24389, // 6^3/29^3
κ: 24389/27, // 29^3/3^3
fromXYZ(XYZ) {
const {κ, ε, white} = this;
// compute xyz, which is XYZ scaled relative to reference white
let xyz = XYZ.map((value, i) => value / white[i]);
// now compute f
let f = xyz.map(value => value > ε ? Math.cbrt(value) : (κ * value + 16)/116);
return [
(116 * f[1]) - 16, // L
500 * (f[0] - f[1]), // a
200 * (f[1] - f[2]) // b
];
},
toXYZ(Lab) {
// Convert Lab to D50-adapted XYZ
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
const {κ, ε, white} = this;
// compute f, starting with the luminance-related term
let f = [];
f[1] = (Lab[0] + 16)/116;
f[0] = Lab[1]/500 + f[1];
f[2] = f[1] - Lab[2]/200;
// compute xyz
var xyz = [
Math.pow(f[0], 3) > ε ? Math.pow(f[0], 3) : (116*f[0]-16)/κ,
Lab[0] > κ * ε ? Math.pow((Lab[0]+16)/116, 3) : Lab[0]/κ,
Math.pow(f[2], 3) > ε ? Math.pow(f[2], 3) : (116*f[2]-16)/κ
];
// Compute XYZ by scaling xyz by reference white
return xyz.map((value, i) => value * white[i]);
},
parse (str, parsed = Color$1.parseFunction(str)) {
if (parsed && parsed.name === "lab") {
let L = parsed.args[0];
// Percentages in lab() don't translate to a 0-1 range, but a 0-100 range
if (L.percentage) {
parsed.args[0] = L * 100;
}
return {
spaceId: "lab",
coords: parsed.args.slice(0, 3),
alpha: parsed.args.slice(3)[0]
};
}
},
instance: {
toString ({format, ...rest} = {}) {
if (!format) {
format = (c, i) => i === 0? c + "%" : c;
}
return Color$1.prototype.toString.call(this, {name: "lab", format, ...rest});
}
}
});
const range = [0, 360];
range.isAngle = true;
function constrain (angle) {
return ((angle % 360) + 360) % 360;
}
function adjust (arc, angles) {
if (arc === "raw") {
return angles;
}
let [a1, a2] = angles.map(constrain);
let angleDiff = a2 - a1;
if (arc === "increasing") {
if (angleDiff < 0) {
a2 += 360;
}
}
else if (arc === "decreasing") {
if (angleDiff > 0) {
a1 += 360;
}
}
else if (arc === "longer") {
if (-180 < angleDiff && angleDiff < 180) {
if (angleDiff > 0) {
a2 += 360;
}
else {
a1 += 360;
}
}
}
else if (arc === "shorter") {
if (angleDiff > 180) {
a1 += 360;
}
else if (angleDiff < -180) {
a2 += 360;
}
}
return [a1, a2];
}
Color$1.defineSpace({
id: "lch",
name: "LCH",
coords: {
lightness: [0, 100],
chroma: [0, 150],
hue: range,
},
inGamut: coords => true,
white: Color$1.whites.D50,
from: {
lab (Lab) {
// Convert to polar form
let [L, a, b] = Lab;
let hue;
const ε = 0.0005;
if (Math.abs(a) < ε && Math.abs(b) < ε) {
hue = NaN;
}
else {
hue = Math.atan2(b, a) * 180 / Math.PI;
}
return [
L, // L is still L
Math.sqrt(a ** 2 + b ** 2), // Chroma
constrain(hue) // Hue, in degrees [0 to 360)
];
}
},
to: {
lab (LCH) {
// Convert from polar form
let [Lightness, Chroma, Hue] = LCH;
// Clamp any negative Chroma
if (Chroma < 0) {
Chroma = 0;
} // Deal with NaN Hue
if (isNaN(Hue)) {
Hue = 0;
}
return [
Lightness, // L is still L
Chroma * Math.cos(Hue * Math.PI / 180), // a
Chroma * Math.sin(Hue * Math.PI / 180) // b
];
}
},
parse (str, parsed = Color$1.parseFunction(str)) {
if (parsed && parsed.name === "lch") {
let L = parsed.args[0];
// Percentages in lch() don't translate to a 0-1 range, but a 0-100 range
if (L.percentage) {
parsed.args[0] = L * 100;
}
return {
spaceId: "lch",
coords: parsed.args.slice(0, 3),
alpha: parsed.args.slice(3)[0]
};
}
},
instance: {
toString ({format, ...rest} = {}) {
if (!format) {
format = (c, i) => i === 0? c + "%" : c;
}
return Color$1.prototype.toString.call(this, {name: "lch", format, ...rest});
}
}
});
Color$1.defineSpace({
id: "srgb",
name: "sRGB",
coords: {
red: [0, 1],
green: [0, 1],
blue: [0, 1]
},
white: Color$1.whites.D65,
// convert an array of sRGB values in the range 0.0 - 1.0
// to linear light (un-companded) form.
// https://en.wikipedia.org/wiki/SRGB
toLinear(RGB) {
return RGB.map(function (val) {
if (val < 0.04045) {
return val / 12.92;
}
return Math.pow((val + 0.055) / 1.055, 2.4);
});
},
// convert an array of linear-light sRGB values in the range 0.0-1.0
// to gamma corrected form
// https://en.wikipedia.org/wiki/SRGB
toGamma(RGB) {
return RGB.map(function (val) {
if (val > 0.0031308) {
return 1.055 * Math.pow(val, 1/2.4) - 0.055;
}
return 12.92 * val;
});
},
toXYZ_M: [
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041]
],
fromXYZ_M: [
[ 3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[ 0.0556434, -0.2040259, 1.0572252]
],
// convert an array of sRGB values to CIE XYZ
// using sRGB's own white, D65 (no chromatic adaptation)
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
// also
// https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
toXYZ(rgb) {
rgb = this.toLinear(rgb);
return multiplyMatrices(this.toXYZ_M, rgb);
},
fromXYZ(XYZ) {
return this.toGamma(multiplyMatrices(this.fromXYZ_M, XYZ));
},
// Properties added to Color.prototype
properties: {
toHex({
alpha = true, // include alpha in hex?
collapse = true // collapse to 3-4 digit hex when possible?
} = {}) {
let coords = this.to("srgb", {inGamut: true}).coords;
if (this.alpha < 1 && alpha) {
coords.push(this.alpha);
}
coords = coords.map(c => Math.round(c * 255));
let collapsible = collapse && coords.every(c => c % 17 === 0);
let hex = coords.map(c => {
if (collapsible) {
return (c/17).toString(16);
}
return c.toString(16).padStart(2, "0");
}).join("");
return "#" + hex;
},
get hex() {
return this.toHex();
}
},
// Properties present only on sRGB colors
instance: {
toString ({inGamut = true, commas, format = "%", ...rest} = {}) {
if (format === 255) {
format = c => c * 255;
}
else if (format === "hex") {
return this.toHex(arguments[0]);
}
return Color$1.prototype.toString.call(this, {
inGamut, commas, format,
name: "rgb" + (commas && this.alpha < 1? "a" : ""),
...rest
});
}
},
parseHex (str) {
if (str.length <= 5) {
// #rgb or #rgba, duplicate digits
str = str.replace(/[a-f0-9]/gi, "$&$&");
}
let rgba = [];
str.replace(/[a-f0-9]{2}/gi, component => {
rgba.push(parseInt(component, 16) / 255);
});
return {
spaceId: "srgb",
coords: rgba.slice(0, 3),
alpha: rgba.slice(3)[0]
};
}
});
Color$1.hooks.add("parse-start", env => {
let str = env.str;
if (/^#([a-f0-9]{3,4}){1,2}$/i.test(str)) {
env.color = Color$1.spaces.srgb.parseHex(str);
}
});
Color$1.defineSpace({
id: "hsl",
name: "HSL",
coords: {
hue: range,
saturation: [0, 100],
lightness: [0, 100]
},
inGamut (coords) {
let rgb = this.to.srgb(coords);
return Color$1.inGamut("srgb", rgb);
},
white: Color$1.whites.D65,
// Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
from: {
srgb (rgb) {
rgb = rgb.map(c => c * 100);
let max = Math.max.apply(Math, rgb);
let min = Math.min.apply(Math, rgb);
let [r, g, b] = rgb;
let [h, s, l] = [NaN, 0, (min + max)/2];
let d = max - min;
if (d !== 0) {
s = d * 100 / (100 - Math.abs(2 * l - 100));
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4;
}
h = h * 60;
}
return [h, s, l];
}
},
// Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
to: {
srgb (hsl) {
let [h, s, l] = hsl;
h = h % 360;
if (h < 0) {
h += 360;
}
s /= 100;
l /= 100;
function f(n) {
let k = (n + h/30) % 12;
let a = s * Math.min(l, 1 - l);
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
}
return [f(0), f(8), f(4)];
}
},
parse (str, parsed = Color$1.parseFunction(str)) {
if (parsed && /^hsla?$/.test(parsed.name)) {
let hsl = parsed.args;
// percentages are converted to [0, 1] by parseFunction
hsl[1] *= 100;
hsl[2] *= 100;
return {
spaceId: "hsl",
coords: hsl.slice(0, 3),
alpha: hsl[3]
};
}
},
instance: {
toString ({precision, commas, format, inGamut, ...rest} = {}) {
if (!format) {
format = (c, i) => i > 0? c + "%" : c;
}
return Color$1.prototype.toString.call(this, {
inGamut: true, // hsl() out of gamut makes no sense
commas, format,
name: "hsl" + (commas && this.alpha < 1? "a" : ""),
...rest
});
}
}
});
// The Hue, Whiteness Blackness (HWB) colorspace
// See https://drafts.csswg.org/css-color-4/#the-hwb-notation
// Note that, like HSL, calculations are done directly on
// gamma-corrected sRGB values rather than linearising them first.
Color$1.defineSpace({
id: "hwb",
name: "HWB",
coords: {
hue: range,
whiteness: [0, 100],
blackness: [0, 100]
},
inGamut (coords) {
let rgb = this.to.srgb(coords);
return Color$1.inGamut("srgb", rgb);
},
white: Color$1.whites.D65,
from: {
srgb (rgb) {
let hsl = Color$1.spaces.hsl.from.srgb(rgb);
let h = hsl[0];
// calculate white and black
let w = Math.min(...rgb);
let b = 1 - Math.max(...rgb);
w *= 100;
b *= 100;
return [h, w, b];
},
hsv (hsv) {
let [h, s, v] = hsv;
return [h, v * (100 - s) / 100, 100 - v];
},
hsl (hsl) {
let hsv = Color$1.spaces.hsv.from.hsl(hsl);
return this.hsv(hsv);
}
},
to: {
srgb (hwb) {
let [h, w, b] = hwb;
// Now convert percentages to [0..1]
w /= 100;
b /= 100;
// Achromatic check (white plus black >= 1)
let sum = w + b;
if (sum >= 1) {
let gray = w / sum;
return [gray, gray, gray];
}
// From https://drafts.csswg.org/css-color-4/#hwb-to-rgb
let rgb = Color$1.spaces.hsl.to.srgb([h, 100, 50]);
for (var i = 0; i < 3; i++) {
rgb[i] *= (1 - w - b);
rgb[i] += w;
}
return rgb;
},
hsv (hwb) {
let [h, w, b] = hwb;
// Now convert percentages to [0..1]
w /= 100;
b /= 100;
// Achromatic check (white plus black >= 1)
let sum = w + b;
if (sum >= 1) {
let gray = w / sum;
return [h, 0, gray];
}
let v = 1 - b;
let s = 100 - (100 * w) / (100 - b);
return [h, s, v * 100];
},
hsl (hwb) {
let hsv = Color$1.spaces.hwb.to.hsv(hwb);
return (Color$1.spaces.hsv.to.hsl(hsv));
}
},
parse (str, parsed = Color$1.parseFunction(str)) {
if (parsed && /^hwba?$/.test(parsed.name)) {
let hwb = parsed.args;
// white and black percentages are converted to [0, 1] by parseFunction
hwb[1] *= 100;
hwb[2] *= 100;
return {
spaceId: "hwb",
coords: hwb.slice(0, 3),
alpha: hwb[3]
};
}
},
instance: {
toString ({format, commas, inGamut, ...rest} = {}) {
if (!format) {
format = (c, i) => i > 0? c + "%" : c;
}
return Color$1.prototype.toString.call(this, {
inGamut: true, // hwb() out of gamut makes no sense
commas: false, // never commas
format,
name: "hwb",
...rest
});
}
}
});
// The Hue, Whiteness Blackness (HWB) colorspace
// See https://drafts.csswg.org/css-color-4/#the-hwb-notation
// Note that, like HSL, calculations are done directly on
// gamma-corrected sRGB values rather than linearising them first.
Color$1.defineSpace({
id: "hsv",
name: "HSV",
coords: {
hue: range,
saturation: [0, 100],
value: [0, 100]
},
inGamut (coords) {
let hsl = this.to.hsl(coords);
return Color$1.spaces.hsl.inGamut(hsl);
},
white: Color$1.whites.D65,
from: {
// https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion
hsl (hsl) {
let [h, s, l] = hsl;
s /= 100;
l /= 100;
let v = l + s * Math.min(l, 1 - l);
return [
h, // h is the same
v === 0? 0 : 200 * (1 - l / v), // s
100 * v
];
},
},
to: {
// https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion
hsl (hsv) {
let [h, s, v] = hsv;
s /= 100;
v /= 100;
let l = 100 * v * (1 - s/2);
return [
h, // h is the same
(l === 0 || l === 1)? 0 : (v - l) / Math.min(l, 1 - l),
l
];
}
}
});
Color$1.defineSpace({
inherits: "srgb",
id: "p3",
name: "P3",
cssId: "display-p3",
// Gamma correction is the same as sRGB
// convert an array of display-p3 values to CIE XYZ
// using D65 (no chromatic adaptation)
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
// Functions are the same as sRGB, just with different matrices
toXYZ_M: [
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
[0.0000000000000000, 0.04511338185890264, 1.043944368900976]
],
fromXYZ_M: [
[ 2.493496911941425, -0.9313836179191239, -0.40271078445071684],
[-0.8294889695615747, 1.7626640603183463, 0.023624685841943577],
[ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872]
]
});
Color$1.defineSpace({
inherits: "srgb",
id: "a98rgb",
name: "Adobe 98 RGB compatible",
cssId: "a98-rgb",
toLinear(RGB) {
return RGB.map(val => Math.pow(Math.abs(val), 563/256)*Math.sign(val));
},
toGamma(RGB) {
return RGB.map(val => Math.pow(Math.abs(val), 256/563)*Math.sign(val));
},
// convert an array of linear-light a98-rgb values to CIE XYZ
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
// has greater numerical precision than section 4.3.5.3 of
// https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
// but the values below were calculated from first principles
// from the chromaticity coordinates of R G B W
toXYZ_M: [
[ 0.5766690429101305, 0.1855582379065463, 0.1882286462349947 ],
[ 0.29734497525053605, 0.6273635662554661, 0.07529145849399788 ],
[ 0.02703136138641234, 0.07068885253582723, 0.9913375368376388 ]
],
fromXYZ_M: [
[ 2.0415879038107465, -0.5650069742788596, -0.34473135077832956 ],
[ -0.9692436362808795, 1.8759675015077202, 0.04155505740717557 ],
[ 0.013444280632031142, -0.11836239223101838, 1.0151749943912054 ]
]
});
Color$1.defineSpace({
inherits: "srgb",
id: "prophoto",
name: "ProPhoto",
cssId: "prophoto-rgb",
white: Color$1.whites.D50,
toLinear(RGB) {
// Transfer curve is gamma 1.8 with a small linear portion
const Et2 = 16/512;
return RGB.map(function (val) {
if (val <= Et2) {
return val / 16;
}
return Math.pow(val, 1.8);
});
},
toGamma(RGB) {
const Et = 1/512;
return RGB.map(function (val) {
if (val >= Et) {
return Math.pow(val, 1/1.8);
}
return 16 * val;
});
},
// convert an array of prophoto-rgb values to CIE XYZ
// using D50 (so no chromatic adaptation needed afterwards)
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
toXYZ_M: [
[ 0.7977604896723027, 0.13518583717574031, 0.0313493495815248 ],
[ 0.2880711282292934, 0.7118432178101014, 0.00008565396060525902 ],
[ 0.0, 0.0, 0.8251046025104601 ]
],
fromXYZ_M: [
[ 1.3457989731028281, -0.25558010007997534, -0.05110628506753401 ],
[ -0.5446224939028347, 1.5082327413132781, 0.02053603239147973 ],
[ 0.0, 0.0, 1.2119675456389454 ]
]
});
Color$1.defineSpace({
inherits: "srgb",
id: "rec2020",
name: "REC.2020",
α: 1.09929682680944,
β: 0.018053968510807,
toLinear(RGB) {
const {α, β} = this;
return RGB.map(function (val) {
if (val < β * 4.5 ) {
return val / 4.5;
}
return Math.pow((val + α -1 ) / α, 2.4);
});
},
toGamma(RGB) {
const {α, β} = this;
return RGB.map(function (val) {
if (val > β ) {
return α * Math.pow(val, 1/2.4) - (α - 1);
}
return 4.5 * val;
});
},
// convert an array of linear-light rec2020 values to CIE XYZ
// using D65 (no chromatic adaptation)
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
// 0 is actually calculated as 4.994106574466076e-17
toXYZ_M: [
[ 0.6369580483012914, 0.14461690358620832, 0.1688809751641721 ],
[ 0.2627002120112671, 0.6779980715188708, 0.05930171646986196 ],
[ 0.000000000000000, 0.028072693049087428, 1.060985057710791 ]
],
// from ITU-R BT.2124-0 Annex 2 p.3
fromXYZ_M: [
[ 1.716651187971268, -0.355670783776392, -0.253366281373660 ],
[ -0.666684351832489, 1.616481236634939, 0.0157685458139111 ],
[ 0.017639857445311, -0.042770613257809, 0.942103121235474 ]
]
});
Color$1.defineSpace({
// Absolute CIE XYZ, with a D65 whitepoint,
// as used in most HDR colorspaces as a starting point.
// SDR spaces are converted per BT.2048
// so that diffuse, media white is 203 cd/m²
id: "absxyzd65",
name: "Absolute XYZ D65",
coords: {
Xa: [0, 9504.7],
Ya: [0, 10000],
Za: [0, 10888.3]
},
white: Color$1.whites.D65,
Yw: 203, // absolute luminance of media white
inGamut: coords => true,
fromXYZ (XYZ) {
// First adapt from D50 to D65, with linear Bradford default
const W1 = Color$1.whites.D50;
const W2 = Color$1.whites.D65;
XYZ = Color$1.chromaticAdaptation(W1, W2, XYZ);
const {Yw} = this;
// Then make XYZ absolute, not relative to media white
// Maximum luminance in PQ is 10,000 cd/m²
// Relative XYZ has Y=1 for media white
return XYZ.map (function (val) {
return Math.max(val * Yw, 0);
});
},
toXYZ (AbsXYZ) {
// First convert to media-white relative XYZ
const {Yw} = this;
let XYZ = AbsXYZ.map (function (val) {
return Math.max(val / Yw, 0);
});
// Then adapt to D50
const W1 = Color$1.whites.D65;
const W2 = Color$1.whites.D50;
return Color$1.chromaticAdaptation(W1, W2, XYZ);
}
});
Color$1.defineSpace({
id: "jzazbz",
cssid: "Jzazbz",
name: "Jzazbz",
coords: {
Jz: [0, 1],
az: [-0.5, 0.5],
bz: [-0.5, 0.5]
},
inGamut: coords => true,
// Note that XYZ is relative to D65
white: Color$1.whites.D65,
b: 1.15,
g: 0.66,
n:2610 / (2 ** 14),
ninv: (2 ** 14) / 2610,
c1: 3424 / (2 ** 12),
c2: 2413 / (2 ** 7),
c3: 2392 / (2 ** 7),
p: 1.7 * 2523 / (2 ** 5),
pinv: (2 ** 5) / (1.7 * 2523),
d: -0.56,
d0: 1.6295499532821566E-11,
XYZtoCone_M: [
[ 0.41478972, 0.579999, 0.0146480 ],
[ -0.2015100, 1.120649, 0.0531008 ],
[ -0.0166008, 0.264800, 0.6684799 ]
],
// XYZtoCone_M inverted
ConetoXYZ_M: [
[ 1.9242264357876067, -1.0047923125953657, 0.037651404030618 ],
[ 0.35031676209499907, 0.7264811939316552, -0.06538442294808501 ],
[ -0.09098281098284752, -0.3127282905230739, 1.5227665613052603 ]
],
ConetoIab_M: [
[ 0.5, 0.5, 0 ],
[ 3.524000, -4.066708, 0.542708 ],
[ 0.199076, 1.096799, -1.295875 ]
],
// ConetoIab_M inverted
IabtoCone_M: [
[ 1, 0.1386050432715393, 0.05804731615611886 ],
[ 0.9999999999999999, -0.1386050432715393, -0.05804731615611886 ],
[ 0.9999999999999998, -0.09601924202631895, -0.8118918960560388 ]
],
fromXYZ (XYZ) {
const {b, g, n, p, c1, c2, c3, d, d0, XYZtoCone_M, ConetoIab_M} = this;
// First make XYZ absolute, not relative to media white
// Maximum luminance in PQ is 10,000 cd/m²
// Relative XYZ has Y=1 for media whit