import { clamp, mod, circularDelta, multMatrixVector, Vector3 } from './arithmetic';
import { yToMunsellValueTable } from './y-to-value-table';
import {
lToY,
labToLchab,
xyzToLab,
linearRgbToXyz,
rgbToLinearRgb,
rgb255ToRgb,
hexToRgb,
ILLUMINANT_C,
ILLUMINANT_D65,
SRGB,
} from './colorspace';
import { mhvcToLchab, mhvcToMunsell } from './convert';
/**
* Converts Y of XYZ to Munsell value. The round-trip error, `abs(Y -
* munsellValueToY(yToMunsellValue(Y))`, is guaranteed to be smaller than 1e-5 if
* Y is in [0, 1].
* @param Y - will be in [0, 1]. Clamped if it exceeds the interval.
* @returns {number} Munsell value
*/
export const yToMunsellValue = (Y: number): number => {
const y2000 = clamp(Y, 0, 1) * 2000;
const yFloor = Math.floor(y2000);
const yCeil = Math.ceil(y2000);
if (yFloor === yCeil) {
return yToMunsellValueTable[yFloor];
} else {
return (
(yCeil - y2000) * yToMunsellValueTable[yFloor] +
(y2000 - yFloor) * yToMunsellValueTable[yCeil]
);
}
};
/**
* Converts L* of CIELAB to Munsell value. The round-trip error, `abs(L* -
* munsellValueToL(lToMunsellValue(L*))`, is guaranteed to be smaller than 1e-3
* if L* is in [0, 100].
* @param lstar - will be in [0, 100]. Clamped if it exceeds the
* interval.
* @returns {number} Munsell value
*/
export const lToMunsellValue = (lstar: number): number => {
return yToMunsellValue(lToY(lstar));
};
/**
* ProcType specifies the action to be taken when a computation doesn't converge
* within the given number of iterations. The following options are available:
* - `"error"`: throws Error;
* - `"init"`: returns the initial rough approximation.
* - `"last"`: returns the last approximation.
*/
export type ProcType = 'error' | 'init' | 'last';
const invertMhvcToLchab = (
lstar: number,
cstarab: number,
hab: number,
initHue100: number,
initChroma: number,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
const value = lToMunsellValue(lstar);
if (value <= threshold || initChroma <= threshold) {
return [initHue100, value, initChroma];
}
let hue100 = initHue100;
let chroma = initChroma;
for (let i = 0; i < maxIteration; i++) {
const [, tmp_cstarab, tmp_hab] = mhvcToLchab(hue100, value, chroma);
const d_cstarab = cstarab - tmp_cstarab;
const d_hab = circularDelta(hab, tmp_hab, 360);
const d_hue100 = d_hab * 0.277777777778; // 100/360
const d_chroma = d_cstarab * 0.181818181818; // 1/5.5
if (Math.abs(d_hue100) <= threshold && Math.abs(d_chroma) <= threshold) {
return [mod(hue100, 100), value, chroma];
} else {
hue100 += factor * d_hue100;
chroma = Math.max(0, chroma + factor * d_chroma);
}
}
// If the loop has finished without achieving the required accuracy:
switch (ifReachMax) {
case 'error':
throw new Error(
'invertMhvcToLchab() reached maxIteration without achieving the required accuracy.',
);
case 'init':
return [initHue100, value, initChroma];
case 'last':
return [hue100, value, chroma];
default:
throw new SyntaxError(`Unknown ifReachMax specifier: ${ifReachMax}`);
}
};
/**
* Converts LCHab to Munsell HVC by inverting {@link mhvcToLchab}() with a
* simple iteration algorithm, which is almost the same as the one in "An
* Open-Source Inversion Algorithm for the Munsell Renotation" by Paul Centore,
* 2011:
* - V := {@link lToMunsellValue}(L*);
* - C0 := C*ab / 5.5;
* - H0 := hab * 100/360;
* - Cn+1 := Cn + factor * ΔCn;
* - Hn+1 := Hn + factor * ΔHn.
* ΔHn and ΔCn are internally calculated at every
* step. This function returns Munsell HVC values if C0 ≦ threshold
* or if V ≦ threshold or when max(ΔHn, ΔCn) falls
* below threshold.
* Note that the given values are assumed to be under **Illuminant C**.
* I don't recommend you use this function if you are not sure what that means.
* @param lstar
* @param cstarab
* @param hab
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {Array} [Hue, Value, Chroma]
*/
export const lchabToMhvc = (
lstar: number,
cstarab: number,
hab: number,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
return invertMhvcToLchab(
lstar,
cstarab,
hab,
hab * 0.277777777778,
cstarab * 0.181818181818,
threshold,
maxIteration,
ifReachMax,
factor,
);
};
/**
* Converts LCHab to Munsell string. Note that the given values are assumed to
* be under **Illuminant C**. I don't recommend you use this function
* if you are not sure what that means.
* @param lstar
* @param cstarab
* @param hab
* @param [digits] - is the number of digits after the decimal
* point. Must be non-negative integer. Note that the units digit of the hue
* prefix is assumed to be already after the decimal point.
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {string} Munsell Color code
* @see {@link lchabToMhvc}
*/
export const lchabToMunsell = (
lstar: number,
cstarab: number,
hab: number,
digits = 1,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): string => {
return mhvcToMunsell(
...lchabToMhvc(lstar, cstarab, hab, threshold, maxIteration, ifReachMax, factor),
digits,
);
};
/**
* Converts CIELAB to Munsell HVC. Note that the given values are assumed to be
* under **Illuminant C**. I don't recommend you use this function if you
* are not sure what that means.
* @param lstar
* @param astar
* @param bstar
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {Array} [Hue, Value, Chroma]
* @see {@link lchabToMhvc}
*/
export const labToMhvc = (
lstar: number,
astar: number,
bstar: number,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
return lchabToMhvc(
...labToLchab(lstar, astar, bstar),
threshold,
maxIteration,
ifReachMax,
factor,
);
};
/**
* Converts CIELAB to Munsell Color string. Note that the given values are assumed to
* be under **Illuminant C**. I don't recommend you use this function
* if you are not sure what that means.
* @param lstar
* @param astar
* @param bstar
* @param [digits] - is the number of digits after the decimal
* point. Must be non-negative integer. Note that the units digit of the hue
* prefix is assumed to be already after the decimal point.
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {string} Munsell Color code
* @see {@link lchabToMhvc}
*/
export const labToMunsell = (
lstar: number,
astar: number,
bstar: number,
digits = 1,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): string => {
return mhvcToMunsell(
...labToMhvc(lstar, astar, bstar, threshold, maxIteration, ifReachMax, factor),
digits,
);
};
/**
* Converts XYZ to Munsell HVC, where Bradford transformation is used as CAT.
* @param X
* @param Y
* @param Z
* @param [illuminant]
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {Array} [Hue, Value, Chroma]
* @see {@link lchabToMhvc}
*/
export const xyzToMhvc = (
X: number,
Y: number,
Z: number,
illuminant = ILLUMINANT_D65,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
return labToMhvc(
...xyzToLab(...multMatrixVector(illuminant.catMatrixThisToC, [X, Y, Z]), ILLUMINANT_C),
threshold,
maxIteration,
ifReachMax,
factor,
);
};
/**
* Converts XYZ to Munsell Color string, where the Bradford transformation is used
* as CAT.
* @param X
* @param Y
* @param Z
* @param [illuminant]
* @param [digits] - is the number of digits after the decimal
* point. Must be non-negative integer. Note that the units digit of the hue
* prefix is assumed to be already after the decimal point.
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {string} Munsell Color code
* @see {@link lchabToMhvc}
*/
export const xyzToMunsell = (
X: number,
Y: number,
Z: number,
illuminant = ILLUMINANT_D65,
digits = 1,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): string => {
return mhvcToMunsell(
...xyzToMhvc(X, Y, Z, illuminant, threshold, maxIteration, ifReachMax, factor),
digits,
);
};
/**
* Converts linear RGB to Munsell HVC.
* @param lr - will be in [0, 1] though any real number is accepted and
* properly processed as an out-of-gamut color.
* @param lg - ditto.
* @param lb - ditto.
* @param [rgbSpace]
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {Array} [Hue, Value, Chroma]
* @see {@link lchabToMhvc}
*/
export const linearRgbToMhvc = (
lr: number,
lg: number,
lb: number,
rgbSpace = SRGB,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
return xyzToMhvc(
...linearRgbToXyz(lr, lg, lb, rgbSpace),
rgbSpace.illuminant,
threshold,
maxIteration,
ifReachMax,
factor,
);
};
/**
* Converts linear RGB to Munsell Color string.
* @param lr - will be in [0, 1] though any real number is accepted and
* properly processed as an out-of-gamut color.
* @param lg - ditto.
* @param lb - ditto.
* @param [rgbSpace]
* @param [digits] - is the number of digits after the decimal
* point. Must be non-negative integer. Note that the units digit of the hue
* prefix is assumed to be already after the decimal point.
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {string} Munsell Color code
* @see {@link lchabToMhvc}
*/
export const linearRgbToMunsell = (
lr: number,
lg: number,
lb: number,
rgbSpace = SRGB,
digits = 1,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): string => {
return mhvcToMunsell(
...linearRgbToMhvc(lr, lg, lb, rgbSpace, threshold, maxIteration, ifReachMax, factor),
digits,
);
};
/**
* Converts gamma-corrected RGB to Munsell HVC.
* @param r - will be in [0, 1] though any real number is accepted and
* properly processed as an out-of-gamut color.
* @param g - ditto.
* @param b - ditto.
* @param [rgbSpace]
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {Array} [Hue, Value, Chroma]
* @see {@link lchabToMhvc}
*/
export const rgbToMhvc = (
r: number,
g: number,
b: number,
rgbSpace = SRGB,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
return linearRgbToMhvc(
...rgbToLinearRgb(r, g, b, rgbSpace),
rgbSpace,
threshold,
maxIteration,
ifReachMax,
factor,
);
};
/**
* Converts gamma-corrected RGB to Munsell Color string.
* @param r - will be in [0, 1] though any real number is accepted and
* properly processed as an out-of-gamut color.
* @param g - ditto.
* @param b - ditto.
* @param [rgbSpace]
* @param [digits] - is the number of digits after the decimal
* point. Must be non-negative integer. Note that the units digit of the hue
* prefix is assumed to be already after the decimal point.
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {string} Munsell Color code
* @see {@link lchabToMhvc}
*/
export const rgbToMunsell = (
r: number,
g: number,
b: number,
rgbSpace = SRGB,
digits = 1,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): string => {
return mhvcToMunsell(
...rgbToMhvc(r, g, b, rgbSpace, threshold, maxIteration, ifReachMax, factor),
digits,
);
};
/**
* Converts quantized RGB to Munsell HVC. Whether this conversion succeeds or
* not depends on the parameters though the following behavior is guaranteed
* and tested on Node.js:
* If `r255`, `g255`, and `b255` are in {0, 1, ..., 255} and the optional
* parameters have default values,
* 1. `rgb255ToMhvc()` successfully returns Munsell HVC before maxIteration
* 2. and the round-trip is invariant, i.e. {@link mhvcToRgb255}(rgb255ToMhvc(r255, g255, b255))
* returns `[r255, g255, b255]`.
* @param r255 - will be in {0, 1, ..., 255} though any integer is
* accepted and properly processed as an out-of-gamut color.
* @param g255 - ditto.
* @param b255 - ditto.
* @param [rgbSpace]
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {Array} [Hue, Value, Chroma]
* @see {@link lchabToMhvc}
*/
export const rgb255ToMhvc = (
r255: number,
g255: number,
b255: number,
rgbSpace = SRGB,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
return rgbToMhvc(
...rgb255ToRgb(r255, g255, b255),
rgbSpace,
threshold,
maxIteration,
ifReachMax,
factor,
);
};
/**
* Converts quantized RGB to Munsell Color string. Whether this conversion
* succeeds or not depends on the parameters though the following behaviour is
* guaranteed and tested on Node.js:
* If `r255`, `g255`, `b255` are in {0, 1, ..., 255} and the optional
* parameters except `digits` have defaultvalues, `rgb255ToMunsell()` successfully
* returns a Munsell Color string before `maxIteration`.
* @param r255 - will be in {0, 1, ..., 255} though any integer is
* accepted and properly processed as an out-of-gamut color.
* @param g255 - ditto.
* @param b255 - ditto.
* @param [rgbSpace]
* @param [digits] - is the number of digits after the decimal
* point. Must be non-negative integer. Note that the units digit of the hue
* prefix is assumed to be already after the decimal point.
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {string} Munsell Color code
* @see {@link lchabToMhvc}
*/
export const rgb255ToMunsell = (
r255: number,
g255: number,
b255: number,
rgbSpace = SRGB,
digits = 1,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): string => {
return mhvcToMunsell(
...rgb255ToMhvc(r255, g255, b255, rgbSpace, threshold, maxIteration, ifReachMax, factor),
digits,
);
};
/**
* Converts hex color to Munsell HVC. Whether this conversion succeeds or
* not depends on the parameters though the following behaviour is guaranteed
* and tested on Node.js:
* If the optional parameters have default values,
* 1. `hexToMhvc()` successfully returns Munsell HVC before `maxIteration`
* 2. and the round-trip is invariant for 24-bit hex colors, i.e.
* {@link mhvcToHex}(hexToMhvc(hex)) returns the same hex color.
* @param hex - may be 24-bit RGB (#XXXXXX), 12-bit RGB (#XXX), 32-bit
* RGBA, (#XXXXXXXX), or 16-bit RGBA (#XXXX). Alpha channel is ignored.
* @param [rgbSpace]
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {Array} [Hue, Value, Chroma]
* @see {@link lchabToMhvc}
*/
export const hexToMhvc = (
hex: string,
rgbSpace = SRGB,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): Vector3 => {
return rgbToMhvc(...hexToRgb(hex), rgbSpace, threshold, maxIteration, ifReachMax, factor);
};
/**
* Converts hex color to Munsell Color string. Whether this conversion
* succeeds or not depends on the parameters though the following behavior is
* guaranteed and tested on Node.js:
* If the optional parameters except `digits` have default values,
* `hexToMunsell()` successfully returns a Munsell Color string before `maxIteration`.
* @param hex - may be 24-bit RGB (#XXXXXX), 12-bit RGB (#XXX), 32-bit
* RGBA, (#XXXXXXXX), or 16-bit RGBA (#XXXX). Alpha channel is ignored.
* @param [rgbSpace]
* @param [digits] - is the number of digits after the decimal
* point. Must be non-negative integer. Note that the units digit of the hue
* prefix is assumed to be already after the decimal point.
* @param [threshold]
* @param [maxIteration]
* @param [ifReachMax]
* @param [factor]
* @returns {string} Munsell Color code
* @see {@link lchabToMhvc}
*/
export const hexToMunsell = (
hex: string,
rgbSpace = SRGB,
digits = 1,
threshold = 1e-6,
maxIteration = 200,
ifReachMax: ProcType = 'error',
factor = 0.5,
): string => {
return mhvcToMunsell(
...hexToMhvc(hex, rgbSpace, threshold, maxIteration, ifReachMax, factor),
digits,
);
};