colour-tool
Version:
dev-tool that makes colours easier to deal with
358 lines (294 loc) • 11.6 kB
JavaScript
// Sample code for color conversions
// This was provided by w3 in the CSS4 specifications.
// Conversion can also be done using ICC profiles and a Color Management System
require('../../mathExtension')();
const { create, all } = require('mathjs');
// create a new math instance for matrix support
const config = { };
const math = create(all, config)
module.exports = {
// sRGB-related functions
lin_sRGB(RGB) {
// 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
return RGB.map(function (val) {
if (val < 0.04045) {
return val / 12.92;
}
return Math.pow((val + 0.055) / 1.055, 2.4);
});
},
gam_sRGB(RGB) {
// 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
return RGB.map(function (val) {
if (val > 0.0031308) {
return 1.055 * Math.pow(val, 1/2.4) - 0.055;
}
return 12.92 * val;
});
},
lin_sRGB_to_XYZ(rgb) {
// convert an array of linear-light 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
var M = math.matrix([
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041]
]);
return math.multiply(M, rgb).valueOf();
},
XYZ_to_lin_sRGB(XYZ) {
// convert XYZ to linear-light sRGB
var M = math.matrix([
[ 3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[ 0.0556434, -0.2040259, 1.0572252]
]);
return math.multiply(M, XYZ).valueOf();
},
// image-3-related functions
lin_P3(RGB) {
// convert an array of image-p3 RGB values in the range 0.0 - 1.0
// to linear light (un-companded) form.
return lin_sRGB(RGB); // same as sRGB
},
gam_P3(RGB) {
// convert an array of linear-light image-p3 RGB in the range 0.0-1.0
// to gamma corrected form
return gam_sRGB(RGB); // same as sRGB
},
lin_P3_to_XYZ(rgb) {
// convert an array of linear-light image-p3 values to CIE XYZ
// using D65 (no chromatic adaptation)
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
var M = math.matrix([
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
[0.0000000000000000, 0.04511338185890264, 1.043944368900976]
]);
// 0 was computed as -3.972075516933488e-17
return math.multiply(M, rgb).valueOf();
},
XYZ_to_lin_P3(XYZ) {
// convert XYZ to linear-light P3
var M = math.matrix([
[ 2.493496911941425, -0.9313836179191239, -0.40271078445071684],
[-0.8294889695615747, 1.7626640603183463, 0.023624685841943577],
[ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872]
]);
return math.multiply(M, XYZ).valueOf();
},
// prophoto-rgb functions
lin_ProPhoto(RGB) {
// convert an array of prophoto-rgb values in the range 0.0 - 1.0
// to linear light (un-companded) form.
// Transfer curve is gamma 1.8 with a small linear portion
return RGB.map(function (val) {
if (val < 0.031248) {
return val / 16;
}
return Math.pow(val, 1.8);
});
},
gam_ProPhoto(RGB) {
// convert an array of linear-light prophoto-rgb in the range 0.0-1.0
// to gamma corrected form
// Transfer curve is gamma 1.8 with a small linear portion
return RGB.map(function (val) {
if (val > 0.001953) {
return Math.pow(val, 1/1.8);
}
return 16 * val;
});
},
lin_ProPhoto_to_XYZ(rgb) {
// convert an array of linear-light 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
var M = Math.matrix([
[ 0.7977604896723027, 0.13518583717574031, 0.0313493495815248 ],
[ 0.2880711282292934, 0.7118432178101014, 0.00008565396060525902 ],
[ 0.0, 0.0, 0.8251046025104601 ]
]);
return Math.multiply(M, rgb).valueOf();
},
XYZ_to_lin_ProPhoto(XYZ) {
// convert XYZ to linear-light prophoto-rgb
var M = Math.matrix([
[ 1.3457989731028281, -0.25558010007997534, -0.05110628506753401 ],
[ -0.5446224939028347, 1.5082327413132781, 0.02053603239147973 ],
[ 0.0, 0.0, 1.2119675456389454 ]
]);
return Math.multiply(M, XYZ).valueOf();
},
// a98-rgb functions
lin_a98rgb(RGB) {
// convert an array of a98-rgb values in the range 0.0 - 1.0
// to linear light (un-companded) form.
return RGB.map(function (val) {
return Math.pow(val, 563/256);
});
},
gam_a98rgb(RGB) {
// convert an array of linear-light a98-rgb in the range 0.0-1.0
// to gamma corrected form
return RGB.map(function (val) {
return Math.pow(val, 256/563);
});
},
lin_a98rgb_to_XYZ(rgb) {
// convert an array of linear-light a98-rgb values to CIE XYZ
// using D50 (so no chromatic adaptation needed afterwards)
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
// which has greater numerical precsion than section 4.3.5.3 of
// https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
var M = Math.matrix([
[ 0.5766690429101305, 0.1855582379065463, 0.1882286462349947 ],
[ 0.29734497525053605, 0.6273635662554661, 0.07529145849399788 ],
[ 0.02703136138641234, 0.07068885253582723, 0.9913375368376388 ]
]);
return Math.multiply(M, rgb).valueOf();
},
XYZ_to_lin_a98rgb(XYZ) {
// convert XYZ to linear-light a98-rgb
var M = Math.matrix([
[ 2.0415879038107465, -0.5650069742788596, -0.34473135077832956 ],
[ -0.9692436362808795, 1.8759675015077202, 0.04155505740717557 ],
[ 0.013444280632031142, -0.11836239223101838, 1.0151749943912054 ]
]);
return Math.multiply(M, XYZ).valueOf();
},
//Rec. 2020-related functions
lin_2020(RGB) {
// convert an array of rec-2020 RGB values in the range 0.0 - 1.0
// to linear light (un-companded) form.
const α = 1.09929682680944 ;
const β = 0.018053968510807;
return RGB.map(function (val) {
if (val < β * 4.5 ) {
return val / 4.5;
}
return Math.pow((val + α -1 ) / α, 2.4);
});
},
//check with standard this really is 2.4 and 1/2.4, not 0.45 was wikipedia claims
gam_2020(RGB) {
// convert an array of linear-light rec-2020 RGB in the range 0.0-1.0
// to gamma corrected form
const α = 1.09929682680944 ;
const β = 0.018053968510807;
return RGB.map(function (val) {
if (val > β ) {
return α * Math.pow(val, 1/2.4) - (α - 1);
}
return 4.5 * val;
});
},
lin_2020_to_XYZ(rgb) {
// convert an array of linear-light rec-2020 values to CIE XYZ
// using D65 (no chromatic adaptation)
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
var M = math.matrix([
[0.6369580483012914, 0.14461690358620832, 0.1688809751641721],
[0.2627002120112671, 0.6779980715188708, 0.05930171646986196],
[0.000000000000000, 0.028072693049087428, 1.060985057710791]
]);
// 0 is actually calculated as 4.994106574466076e-17
return math.multiply(M, rgb).valueOf();
},
XYZ_to_lin_2020(XYZ) {
// convert XYZ to linear-light rec-2020
var M = math.matrix([
[1.7166511879712674, -0.35567078377639233, -0.25336628137365974],
[-0.6666843518324892, 1.6164812366349395, 0.01576854581391113],
[0.017639857445310783, -0.042770613257808524, 0.9421031212354738]
]);
return math.multiply(M, XYZ).valueOf();
},
// Chromatic adaptation
D65_to_D50(XYZ) {
// Bradford chromatic adaptation from D65 to D50
// The matrix below is the result of three operations:
// - convert from XYZ to retinal cone domain
// - scale components from one reference white to another
// - convert back to XYZ
// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
var M = math.matrix([
[ 1.0478112, 0.0228866, -0.0501270],
[ 0.0295424, 0.9904844, -0.0170491],
[-0.0092345, 0.0150436, 0.7521316]
]);
return math.multiply(M, XYZ).valueOf();
},
D50_to_D65(XYZ) {
// Bradford chromatic adaptation from D50 to D65
var M = math.matrix([
[ 0.9555766, -0.0230393, 0.0631636],
[-0.0282895, 1.0099416, 0.0210077],
[ 0.0122982, -0.0204830, 1.3299098]
]);
return math.multiply(M, XYZ).valueOf();
},
// Lab and LCH
XYZ_to_Lab(XYZ) {
// Assuming XYZ is relative to D50, convert to CIE Lab
// from CIE standard, which now defines these as a rational fraction
var ε = 216/24389; // 6^3/29^3
var κ = 24389/27; // 29^3/3^3
var white = [0.96422, 1.00000, 0.82521]; // D50 reference white
// var white = [0.95047, 1.00000, 1.08883]; // D65 reference white
// compute xyz, which is XYZ scaled relative to reference white
var xyz = XYZ.map((value, i) => value / white[i]);
// now compute f
var 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
];
},
Lab_to_XYZ(Lab) {
// Convert Lab to D50-adapted XYZ
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
var κ = 24389/27; // 29^3/3^3
var ε = 216/24389; // 6^3/29^3
var white = [0.96422, 1.00000, 0.82521]; // D50 reference white
// var white = [0.95047, 1.00000, 1.08883]; // D65 reference white
var f = [];
// compute f, starting with the luminance-related term
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]);
},
Lab_to_LCH(LAB) {
// pure white, pure black or no chroma, no hue information can be used
if(LAB[0] === 100 || LAB[1] === 0 ||LAB[0] === 0) return [LAB[0],0,0];
// Convert to polar form
var hue = Math.atan2(LAB[2], LAB[1]) * 180 / Math.PI;
return [
LAB[0], // L is still L
Math.sqrt(Math.pow(LAB[1], 2) + Math.pow(LAB[2], 2)), // Chroma
hue >= 0 ? hue : hue + 360 // Hue, in degrees [0 to 360)
];
},
LCH_to_Lab(LCH) {
// Convert from polar form
return [
LCH[0], // L is still L
LCH[1] * Math.cos(LCH[2] * Math.PI / 180), // a
LCH[1] * Math.sin(LCH[2] * Math.PI / 180), // b
];
}
}