Created
June 16, 2022 20:12
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* 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