Skip to content

Instantly share code, notes, and snippets.

@fuweichin
Created June 16, 2022 20:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fuweichin/164a7f556928c2680b839282aeea702d to your computer and use it in GitHub Desktop.
Save fuweichin/164a7f556928c2680b839282aeea702d to your computer and use it in GitHub Desktop.
Get nearest color of wave length in sRGB, Display P3, Rec. 2020 or CIE 1931 XYZ
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wave Length to Color Example</title>
<style>
body, p{
margin: 0;
}
.canvas{
background-color: #000000;
}
</style>
</head>
<body>
<canvas id="canvas" class="canvas" width="400" height="100"></canvas>
<p>wavelength to color, in sRGB</p>
<canvas id="canvas3" class="canvas" width="400" height="100"></canvas>
<p>wavelength to color, in Display P3 (balck if not suppported)</p>
<canvas id="canvas4" class="canvas" width="400" height="100"></canvas>
<p>wavelength to color, in Rec. 2020 (balck if not suppported)</p>
<script type="module">
import {default as wavelength2color, wavelength2rgb} from './wavelength2color.js';
const $ = (s, c) => (c ? c : document).querySelector(s);
const $$ = (s, c) => Array.prototype.slice.call((c ? c : document).querySelectorAll(s));
function wavelength2color1(l) {
let a = wavelength2rgb(l);
return `rgb(${a[0]},${a[1]},${a[2]})`;
// let a = wavelength2color(l, 'srgb');
// return `color(srgb ${a[0].toFixed(5)} ${a[1].toFixed(5)} ${a[2].toFixed(5)})`;
}
function wavelength2color2(l) {
let a = wavelength2color(l, 'display-p3');
return `color(display-p3 ${a[0].toFixed(5)} ${a[1].toFixed(5)} ${a[2].toFixed(5)})`;
}
function wavelength2color3(l) {
let a = wavelength2color(l, 'rec2020');
return `color(rec2020 ${a[0].toFixed(5)} ${a[1].toFixed(5)} ${a[2].toFixed(5)})`;
}
function main() {
{
let canvas = document.getElementById('canvas');
console.log(canvas.style.cssText, canvas.width, canvas.height, window.devicePixelRatio);
let context = canvas.getContext('2d');
console.time('1');
plot(context, new DOMRect(0, 0, canvas.width, canvas.height), wavelength2color1);
console.timeEnd('1');
}
let p3Color = 'color(display-p3 1 0 0)';
if (CSS.supports('(color: ' + p3Color + ')') && window.matchMedia('(color-gamut: p3)').matches) {
let canvas = document.getElementById('canvas3');
console.log(canvas.style.cssText, canvas.width, canvas.height, window.devicePixelRatio);
let context = canvas.getContext('2d', {colorSpace: 'display-p3'});
context.fillStyle = p3Color;
if (context.getContextAttributes().colorSpace === 'display-p3' && context.fillStyle === p3Color) {
console.time('3');
plot(context, new DOMRect(0, 0, canvas.width, canvas.height), wavelength2color2);
console.timeEnd('3');
}
}
let rec2020Color = 'color(rec2020 1 0 0)';
if (CSS.supports('(color: ' + rec2020Color + ')') && window.matchMedia('(color-gamut: rec2020)').matches) {
let canvas = document.getElementById('canvas4');
console.log(canvas.style.cssText, canvas.width, canvas.height, window.devicePixelRatio);
let context = canvas.getContext('2d', {colorSpace: 'rec2020'});
context.fillStyle = p3Color;
if (context.getContextAttributes().colorSpace === 'rec2020' && context.fillStyle === p3Color) {
console.time('4');
plot(context, new DOMRect(0, 0, canvas.width, canvas.height), wavelength2color3);
console.timeEnd('4');
}
}
console.log('plot complete');
}
function plot(context, rect, wavelength2color, colorSpace) {
let {left, top, width, right, bottom} = rect;
context.lineWidth = 1;
let startL = 700, endL = 400;
let step = (startL - endL) / width;
for (let x = left, l = startL; x < right; x += 1, l -= step) {
let c = wavelength2color(l);
context.strokeStyle = c;
context.beginPath();
context.moveTo(x, top);
context.lineTo(x, bottom);
context.stroke();
}
}
document.addEventListener('DOMContentLoaded', main);
</script>
</body>
</html>
/*!
* Get nearest color of wave length in sRGB, Display P3, Rec. 2020 or CIE 1931 XYZ
* @see https://stackoverflow.com/questions/1472514/convert-light-frequency-to-rgb#answer-39446403
* @see https://drafts.csswg.org/css-color-4/#color-conversion-code
*/
/**
* Simple matrix (and vector) multiplication
* Warning: No error handling for incompatible dimensions!
* @author Lea Verou 2020 MIT License
*/
// 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;
}
function XYZ_to_lin_sRGB(XYZ) {
// convert XYZ to linear-light sRGB
var M = [
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
];
return multiplyMatrices(M, XYZ.map((n) => [n]));
}
function 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
// Extended transfer function:
// For negative values, linear portion extends on reflection
// of axis, then uses reflected pow below that
return RGB.map(function (val) {
let sign = val < 0 ? -1 : 1;
let abs = Math.abs(val);
if (abs > 0.0031308) {
return sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
}
return 12.92 * val;
});
}
function XYZ_to_lin_P3(XYZ) {
// convert XYZ to linear-light P3
var M = [
[2.493496911941425, -0.9313836179191239, -0.40271078445071684],
[-0.8294889695615747, 1.7626640603183463, 0.023624685841943577],
[0.03584583024378447, -0.07617238926804182, 0.9568845240076872]
];
return multiplyMatrices(M, XYZ);
}
function gam_P3(RGB) {
// convert an array of linear-light display-p3 RGB in the range 0.0-1.0
// to gamma corrected form
return gam_sRGB(RGB); // same as sRGB
}
function XYZ_to_lin_2020(XYZ) {
// convert XYZ to linear-light rec2020
var M = [
[1.7166511879712674, -0.35567078377639233, -0.25336628137365974],
[-0.6666843518324892, 1.6164812366349395, 0.01576854581391113],
[0.017639857445310783, -0.042770613257808524, 0.9421031212354738]
];
return multiplyMatrices(M, XYZ);
}
function gam_2020(RGB) {
// convert an array of linear-light rec2020 RGB in the range 0.0-1.0
// to gamma corrected form
// ITU-R BT.2020-2 p.4
const α = 1.09929682680944;
const β = 0.018053968510807;
return RGB.map(function (val) {
let sign = val < 0 ? -1 : 1;
let abs = Math.abs(val);
if (abs > β ) {
return sign * (α * Math.pow(abs, 0.45) - (α - 1));
}
return 4.5 * val;
});
}
// CIE 1964 supplementary standard colorimetric observer
const LEN_MIN = 380;
const LEN_MAX = 780;
const LEN_STEP = 5;
const X = [
0.000160, 0.000662, 0.002362, 0.007242, 0.019110, 0.043400, 0.084736, 0.140638, 0.204492, 0.264737,
0.314679, 0.357719, 0.383734, 0.386726, 0.370702, 0.342957, 0.302273, 0.254085, 0.195618, 0.132349,
0.080507, 0.041072, 0.016172, 0.005132, 0.003816, 0.015444, 0.037465, 0.071358, 0.117749, 0.172953,
0.236491, 0.304213, 0.376772, 0.451584, 0.529826, 0.616053, 0.705224, 0.793832, 0.878655, 0.951162,
1.014160, 1.074300, 1.118520, 1.134300, 1.123990, 1.089100, 1.030480, 0.950740, 0.856297, 0.754930,
0.647467, 0.535110, 0.431567, 0.343690, 0.268329, 0.204300, 0.152568, 0.112210, 0.081261, 0.057930,
0.040851, 0.028623, 0.019941, 0.013842, 0.009577, 0.006605, 0.004553, 0.003145, 0.002175, 0.001506,
0.001045, 0.000727, 0.000508, 0.000356, 0.000251, 0.000178, 0.000126, 0.000090, 0.000065, 0.000046,
0.000033
];
const Y = [
0.000017, 0.000072, 0.000253, 0.000769, 0.002004, 0.004509, 0.008756, 0.014456, 0.021391, 0.029497,
0.038676, 0.049602, 0.062077, 0.074704, 0.089456, 0.106256, 0.128201, 0.152761, 0.185190, 0.219940,
0.253589, 0.297665, 0.339133, 0.395379, 0.460777, 0.531360, 0.606741, 0.685660, 0.761757, 0.823330,
0.875211, 0.923810, 0.961988, 0.982200, 0.991761, 0.999110, 0.997340, 0.982380, 0.955552, 0.915175,
0.868934, 0.825623, 0.777405, 0.720353, 0.658341, 0.593878, 0.527963, 0.461834, 0.398057, 0.339554,
0.283493, 0.228254, 0.179828, 0.140211, 0.107633, 0.081187, 0.060281, 0.044096, 0.031800, 0.022602,
0.015905, 0.011130, 0.007749, 0.005375, 0.003718, 0.002565, 0.001768, 0.001222, 0.000846, 0.000586,
0.000407, 0.000284, 0.000199, 0.000140, 0.000098, 0.000070, 0.000050, 0.000036, 0.000025, 0.000018,
0.000013
];
const Z = [
0.000705, 0.002928, 0.010482, 0.032344, 0.086011, 0.197120, 0.389366, 0.656760, 0.972542, 1.282500,
1.553480, 1.798500, 1.967280, 2.027300, 1.994800, 1.900700, 1.745370, 1.554900, 1.317560, 1.030200,
0.772125, 0.570060, 0.415254, 0.302356, 0.218502, 0.159249, 0.112044, 0.082248, 0.060709, 0.043050,
0.030451, 0.020584, 0.013676, 0.007918, 0.003988, 0.001091, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000
];
/**
* @param {Array} values
* @param {number} index
* @param {number} offset
* @return {number}
*/
function Interpolate(values, index, offset) {
let y0 = values[index];
return offset === 0 ? y0 : y0 + offset * (values[index + 1] - y0) / LEN_STEP;
}
/**
*
* @param {number} len - visible light wave length, 780 ~ 380 (nm)
* @param {*} colorSpace - one of 'srgb', 'display-p3', 'rec2020' or 'xyz'
* @returns array used to create css color module 4 expression, like 'color(display-p3, 0.1231, 0.9343, 0.2344)'
*/
function wavelength2color(len, colorSpace) {
if (len < LEN_MIN || len > LEN_MAX)
return [0, 0, 0];
let wavLen = len - LEN_MIN;
let index = (wavLen / LEN_STEP) | 0;
let offset = wavLen - LEN_STEP * index;
let x = Interpolate(X, index, offset);
let y = Interpolate(Y, index, offset);
let z = Interpolate(Z, index, offset);
let xyz = [x, y, z];
switch (colorSpace) {
case 'srgb':
return clamp(gam_sRGB(XYZ_to_lin_sRGB(xyz)));
case 'display-p3':
return clamp(gam_P3(XYZ_to_lin_P3(xyz)));
case 'rec2020':
return clamp(gam_2020(XYZ_to_lin_2020(xyz)));
case 'xyz':
return xyz;
default:
throw new Error('Unsupported colorSpace ' + colorSpace);
}
}
function clamp(arr) {
for (let i = 0; i < arr.length; i += 1) {
let e = arr[i];
if (e < 0) {
arr[i] = 0;
} else if (e > 1) {
arr[i] = 1;
}
}
return arr;
}
function wavelength2rgb(len) {
if (len < LEN_MIN || len > LEN_MAX)
return [0, 0, 0];
let wavLen = len - LEN_MIN;
let index = (wavLen / LEN_STEP) | 0;
let offset = wavLen - LEN_STEP * index;
let x = Interpolate(X, index, offset);
let y = Interpolate(Y, index, offset);
let z = Interpolate(Z, index, offset);
let a = clamp(gam_sRGB(XYZ_to_lin_sRGB([x, y, z])));
return [Math.round(a[0] * 255), Math.round(a[1] * 255), Math.round(a[2] * 255)];
}
export {
wavelength2color as default,
wavelength2rgb,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment