munsell
Version:
Library for Munsell Color System
244 lines (219 loc) • 7.3 kB
text/typescript
import {
polarToCartesian,
cartesianToPolar,
multMatrixVector,
clamp as _clamp,
Vector3,
Matrix33,
} from './arithmetic';
const CONST1 = 216 / 24389;
const CONST2 = 24389 / 27 / 116;
const CONST3 = 16 / 116;
export const functionF = (x: number): number => {
// Called in XYZ -> Lab conversion
if (x > CONST1) {
return Math.pow(x, 0.3333333333333333);
} else {
return CONST2 * x + CONST3;
}
};
export const labToLchab = (lstar: number, astar: number, bstar: number): Vector3 => {
return [lstar, ...cartesianToPolar(astar, bstar, 360)];
};
export const lchabToLab = (lstar: number, Cstarab: number, hab: number): Vector3 => {
return [lstar, ...polarToCartesian(Cstarab, hab, 360)];
};
export class Illuminant {
X: number;
Z: number;
catMatrixCToThis: Matrix33;
catMatrixThisToC: Matrix33;
constructor(X: number, Z: number, catMatrixCToThis: Matrix33, catMatrixThisToC: Matrix33) {
this.X = X;
this.Z = Z;
this.catMatrixCToThis = catMatrixCToThis;
this.catMatrixThisToC = catMatrixThisToC;
}
}
// The following data are based on dufy. I use the Bradford transformation as CAT.
export const ILLUMINANT_D65 = new Illuminant(
0.950428061568676,
1.08891545904089,
[
[0.9904112147597705, -0.00718628493839008, -0.011587161829988951],
[-0.012395677058354078, 1.01560663662526, -0.0029181533414322086],
[-0.003558889496942143, 0.006762494889396557, 0.9182865019746504],
],
[
[1.0098158523233767, 0.007060316533713093, 0.012764537821734395],
[0.012335983421444891, 0.9846986027789835, 0.003284857773421468],
[0.003822773174044815, -0.007224207660971385, 1.0890100329203007],
],
);
export const ILLUMINANT_C = new Illuminant(
0.9807171421603395,
1.182248923134197,
[
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
],
[
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
],
);
const DELTA = 6 / 29;
const CONST4 = 3 * DELTA * DELTA;
export const lToY = (lstar: number): number => {
const fy = (lstar + 16) / 116;
return fy > DELTA ? fy * fy * fy : (fy - CONST3) * CONST4;
};
export const labToXyz = (
lstar: number,
astar: number,
bstar: number,
illuminant = ILLUMINANT_D65,
): Vector3 => {
const fy = (lstar + 16) / 116;
const fx = fy + astar * 0.002;
const fz = fy - bstar * 0.005;
const Xw = illuminant.X;
const Zw = illuminant.Z;
return [
fx > DELTA ? fx * fx * fx * Xw : (fx - CONST3) * CONST4 * Xw,
fy > DELTA ? fy * fy * fy : (fy - CONST3) * CONST4,
fz > DELTA ? fz * fz * fz * Zw : (fz - CONST3) * CONST4 * Zw,
];
};
export const xyzToLab = (X: number, Y: number, Z: number, illuminant = ILLUMINANT_D65): Vector3 => {
const [fX, fY, fZ] = [X / illuminant.X, Y, Z / illuminant.Z].map(functionF);
return [116 * fY - 16, 500 * (fX - fY), 200 * (fY - fZ)];
};
const createLinearizer = (gamma: number): ((x: number) => number) => {
// Returns a function for inverse gamma-correction (not used for sRGB).
const reciprocal = 1 / gamma;
return (x) => {
return x >= 0 ? Math.pow(x, reciprocal) : -Math.pow(-x, reciprocal);
};
};
const createDelinearizer = (gamma: number): ((x: number) => number) => {
// Returns a function for gamma correction (not used for sRGB).
return (x) => {
return x >= 0 ? Math.pow(x, gamma) : -Math.pow(-x, gamma);
};
};
export class RGBSpace {
matrixThisToXyz: Matrix33;
matrixXyzToThis: Matrix33;
linearizer: (x: number) => number;
delinearizer: (x: number) => number;
illuminant: Illuminant;
constructor(
matrixThisToXyz: Matrix33,
matrixXyzToThis: Matrix33,
linearizer = createLinearizer(2.2),
delinearizer = createDelinearizer(2.2),
illuminant = ILLUMINANT_D65,
) {
this.matrixThisToXyz = matrixThisToXyz;
this.matrixXyzToThis = matrixXyzToThis;
this.linearizer = linearizer;
this.delinearizer = delinearizer;
this.illuminant = illuminant;
}
}
const CONST5 = 0.0031308 * 12.92;
// The following data are based on dufy.
export const SRGB = new RGBSpace(
[
[0.4124319639872968, 0.3575780371782625, 0.1804592355313134],
[0.21266023143094992, 0.715156074356525, 0.07218369421252536],
[0.01933274831190452, 0.11919267905942081, 0.9504186404649174],
],
[
[3.240646461582504, -1.537229731776316, -0.49856099408961585],
[-0.969260718909152, 1.876000564872059, 0.04155578980259398],
[0.05563672378977863, -0.2040013205625215, 1.0570977520057931],
],
(x) => {
// Below is actually the linearizer of bg-sRGB.
if (x > CONST5) {
return Math.pow((0.055 + x) / 1.055, 2.4);
} else if (x < -CONST5) {
return -Math.pow((0.055 - x) / 1.055, 2.4);
} else {
return x / 12.92;
}
},
(x) => {
// Below is actually the delinearizer of bg-sRGB.
if (x > 0.0031308) {
return Math.pow(x, 1 / 2.4) * 1.055 - 0.055;
} else if (x < -0.0031308) {
return -Math.pow(-x, 1 / 2.4) * 1.055 + 0.055;
} else {
return x * 12.92;
}
},
);
export const ADOBE_RGB = new RGBSpace(
[
[0.5766645233146432, 0.18556215235063508, 0.18820138590339738],
[0.29734264483411293, 0.6273768008045281, 0.07528055436135896],
[0.027031149530373878, 0.07069034375262295, 0.991193965757893],
],
[
[2.0416039047109305, -0.5650114025085637, -0.3447340526026908],
[-0.969223190031607, 1.8759279278672774, 0.04155418080089159],
[0.01344622799042258, -0.11837953662156253, 1.015322039041507],
],
createDelinearizer(563 / 256),
createLinearizer(563 / 256),
);
export const xyzToLinearRgb = (X: number, Y: number, Z: number, rgbSpace = SRGB): Vector3 => {
return multMatrixVector(rgbSpace.matrixXyzToThis, [X, Y, Z]);
};
export const linearRgbToXyz = (lr: number, lg: number, lb: number, rgbSpace = SRGB): Vector3 => {
return multMatrixVector(rgbSpace.matrixThisToXyz, [lr, lg, lb]);
};
export const linearRgbToRgb = (lr: number, lg: number, lb: number, rgbSpace = SRGB): Vector3 => {
return [lr, lg, lb].map(rgbSpace.delinearizer) as Vector3;
};
export const rgbToLinearRgb = (r: number, g: number, b: number, rgbSpace = SRGB): Vector3 => {
return [r, g, b].map(rgbSpace.linearizer) as Vector3;
};
export const rgbToRgb255 = (r: number, g: number, b: number, clamp = true): Vector3 => {
if (clamp) {
return [r, g, b].map((x) => _clamp(Math.round(x * 255), 0, 255)) as Vector3;
} else {
return [r, g, b].map((x) => Math.round(x * 255)) as Vector3;
}
};
export const rgb255ToRgb = (r255: number, g255: number, b255: number): Vector3 => {
return [r255 / 255, g255 / 255, b255 / 255];
};
export const rgbToHex = (r: number, g: number, b: number): string => {
const toHex = (x: number) =>
Math.round(_clamp(x, 0, 1) * 255)
.toString(16)
.padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
export const hexToRgb = (hex: string): Vector3 => {
switch (hex.length) {
case 7: // #XXXXXX
case 9: // #XXXXXXXX
return [
parseInt(hex.slice(1, 3), 16) / 255,
parseInt(hex.slice(3, 5), 16) / 255,
parseInt(hex.slice(5, 7), 16) / 255,
];
case 4: // #XXX
case 5: // #XXXX
return [parseInt(hex[1], 16) / 15, parseInt(hex[2], 16) / 15, parseInt(hex[3], 16) / 15];
default:
throw SyntaxError(`The length of hex color is invalid: ${hex}`);
}
};