From dfccd37f3b10905d3c4200ffb0046fb89cc1cced Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Thu, 13 Apr 2023 21:29:29 -0700 Subject: [PATCH] Use d50 referent instead of d65 for lab (and by extension hcl) color space. See https://github.com/maplibre/maplibre-gl-js/issues/2382 --- CHANGELOG.md | 2 ++ src/function/index.test.ts | 4 ++-- src/util/color_spaces.test.ts | 40 +++++++++++++++++------------------ src/util/color_spaces.ts | 23 ++++++++++++-------- src/util/interpolate.test.ts | 12 +++++------ 5 files changed, 44 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad322f76..652a30e48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### 🐛 Bug fixes +* Fix color interpolation in HCL and LAB color spaces to use d50 white point instead of d65, in conformance with CSS spec [#146](https://github.com/maplibre/maplibre-style-spec/pull/146) + * Fix incorrect color interpolation in HCL and LAB color spaces when interpolation results are outside the sRGB gamut. [#94](https://github.com/maplibre/maplibre-style-spec/pull/94) ## 18.0.0 diff --git a/src/function/index.test.ts b/src/function/index.test.ts index be4d41fc8..ea1fb66cc 100644 --- a/src/function/index.test.ts +++ b/src/function/index.test.ts @@ -191,7 +191,7 @@ describe('exponential function', () => { }).evaluate; expectToMatchColor(f({zoom: 0}, undefined), 'rgb(14.12% 38.43% 14.12% / .6)', 4); - expectToMatchColor(f({zoom: 5}, undefined), 'rgb(0% 35.71% 46.73% / .8)', 4); + expectToMatchColor(f({zoom: 5}, undefined), 'rgb(0.00% 35.13% 43.84% / 0.8)', 4); expectToMatchColor(f({zoom: 10}, undefined), 'rgb(8.64% 22.66% 55.36% / 1)', 4); }); @@ -205,7 +205,7 @@ describe('exponential function', () => { }).evaluate; expectToMatchColor(f({zoom: 0}, undefined), 'rgb(0% 0% 0% / .6)'); - expectToMatchColor(f({zoom: 5}, undefined), 'rgb(14.29% 46.73% 46.63% / .8)', 4); + expectToMatchColor(f({zoom: 5}, undefined), 'rgb(14.15% 46.74% 46.63% / 0.8)', 4); expectToMatchColor(f({zoom: 10}, undefined), 'rgb(0% 100% 100% / 1)'); }); diff --git a/src/util/color_spaces.test.ts b/src/util/color_spaces.test.ts index c8d490d3a..b97af91c8 100644 --- a/src/util/color_spaces.test.ts +++ b/src/util/color_spaces.test.ts @@ -8,21 +8,21 @@ describe('color spaces', () => { test('should convert colors from sRGB to LAB color space', () => { expectCloseToArray(rgbToLab([0, 0, 0, 1]), [0, 0, 0, 1]); expectCloseToArray(rgbToLab([1, 1, 1, 1]), [100, 0, 0, 1], 4); - expectCloseToArray(rgbToLab([0, 1, 0, 1]), [87.73, -86.18, 83.18, 1], 2); - expectCloseToArray(rgbToLab([0, 1, 1, 1]), [91.11, -48.09, -14.13, 1], 2); - expectCloseToArray(rgbToLab([0, 0, 1, 1]), [32.3, 79.19, -107.86, 1], 2); - expectCloseToArray(rgbToLab([1, 1, 0, 1]), [97.14, -21.55, 94.48, 1], 2); - expectCloseToArray(rgbToLab([1, 0, 0, 1]), [53.24, 80.09, 67.2, 1], 2); + expectCloseToArray(rgbToLab([0, 1, 0, 1]), [87.82, -79.29, 80.99, 1], 2); + expectCloseToArray(rgbToLab([0, 1, 1, 1]), [90.67, -50.67, -14.96, 1], 2); + expectCloseToArray(rgbToLab([0, 0, 1, 1]), [29.57, 68.3, -112.03, 1], 2); + expectCloseToArray(rgbToLab([1, 1, 0, 1]), [97.61, -15.75, 93.39, 1], 2); + expectCloseToArray(rgbToLab([1, 0, 0, 1]), [54.29, 80.81, 69.89, 1], 2); }); test('should convert colors from LAB to sRGB color space', () => { expectCloseToArray(labToRgb([0, 0, 0, 1]), [0, 0, 0, 1]); expectCloseToArray(labToRgb([100, 0, 0, 1]), [1, 1, 1, 1]); - expectCloseToArray(labToRgb([50, 50, 0, 1]), [0.7605, 0.3096, 0.4734, 1], 4); - expectCloseToArray(labToRgb([70, -45, 0, 1]), [0.0469, 0.7537, 0.6656, 1], 4); - expectCloseToArray(labToRgb([70, 0, 70, 1]), [0.7955, 0.6590, 0.0818, 1], 4); - expectCloseToArray(labToRgb([55, 0, -60, 1]), [0, 0.5403, 0.9255, 1], 4); - expectCloseToArray(labToRgb([32.3, 79.19, -107.86, 1]), [0, 0, 1, 1], 3); + expectCloseToArray(labToRgb([50, 50, 0, 1]), [0.7562, 0.3045, 0.4756, 1], 4); + expectCloseToArray(labToRgb([70, -45, 0, 1]), [0.1079, 0.7556, 0.6640, 1], 4); + expectCloseToArray(labToRgb([70, 0, 70, 1]), [0.7663, 0.6636, 0.0558, 1], 4); + expectCloseToArray(labToRgb([55, 0, -60, 1]), [0.1281, 0.5310, 0.9276, 1], 4); + expectCloseToArray(labToRgb([29.57, 68.3, -112.03, 1]), [0, 0, 1, 1], 3); }); }); @@ -32,21 +32,21 @@ describe('color spaces', () => { test('should convert colors from sRGB to HCL color space', () => { expectCloseToArray(rgbToHcl([0, 0, 0, 1]), [NaN, 0, 0, 1]); expectCloseToArray(rgbToHcl([1, 1, 1, 1]), [NaN, 0, 100, 1], 4); - expectCloseToArray(rgbToHcl([0, 1, 0, 1]), [136.02, 119.78, 87.73, 1], 2); - expectCloseToArray(rgbToHcl([0, 1, 1, 1]), [196.38, 50.12, 91.11, 1], 2); - expectCloseToArray(rgbToHcl([0, 0, 1, 1]), [306.28, 133.81, 32.30, 1], 2); - expectCloseToArray(rgbToHcl([1, 1, 0, 1]), [102.85, 96.91, 97.14, 1], 2); - expectCloseToArray(rgbToHcl([1, 0, 0, 1]), [40.00, 104.55, 53.24, 1], 2); + expectCloseToArray(rgbToHcl([0, 1, 0, 1]), [134.39, 113.34, 87.82, 1], 2); + expectCloseToArray(rgbToHcl([0, 1, 1, 1]), [196.45, 52.83, 90.67, 1], 2); + expectCloseToArray(rgbToHcl([0, 0, 1, 1]), [301.37, 131.21, 29.57, 1], 2); + expectCloseToArray(rgbToHcl([1, 1, 0, 1]), [99.57, 94.71, 97.61, 1], 2); + expectCloseToArray(rgbToHcl([1, 0, 0, 1]), [40.85, 106.84, 54.29, 1], 2); }); test('should convert colors from HCL to sRGB color space', () => { expectCloseToArray(hclToRgb([0, 0, 0, 1]), [0, 0, 0, 1]); expectCloseToArray(hclToRgb([0, 0, 100, 1]), [1, 1, 1, 1]); - expectCloseToArray(hclToRgb([0, 50, 50, 1]), [0.7605, 0.3096, 0.4734, 1], 4); - expectCloseToArray(hclToRgb([180, 45, 70, 1]), [0.0469, 0.7537, 0.6656, 1], 4); - expectCloseToArray(hclToRgb([90, 70, 70, 1]), [0.7955, 0.6590, 0.0818, 1], 4); - expectCloseToArray(hclToRgb([270, 60, 55, 1]), [0, 0.5403, 0.9255, 1], 4); - expectCloseToArray(hclToRgb([306.28, 133.81, 32.30, 1]), [0, 0, 1, 1], 3); + expectCloseToArray(hclToRgb([0, 50, 50, 1]), [0.7562, 0.3045, 0.4756, 1], 4); + expectCloseToArray(hclToRgb([180, 45, 70, 1]), [0.1079, 0.7556, 0.6640, 1], 4); + expectCloseToArray(hclToRgb([90, 70, 70, 1]), [0.7663, 0.6636, 0.0558, 1], 4); + expectCloseToArray(hclToRgb([270, 60, 55, 1]), [0.1281, 0.5310, 0.9276, 1], 4); + expectCloseToArray(hclToRgb([301.37, 131.21, 29.57, 1]), [0, 0, 1, 1], 3); }); }); diff --git a/src/util/color_spaces.ts b/src/util/color_spaces.ts index 236281836..31a57f02a 100644 --- a/src/util/color_spaces.ts +++ b/src/util/color_spaces.ts @@ -30,10 +30,10 @@ export type HCLColor = [h: number, c: number, l: number, alpha: number]; */ export type LABColor = [l: number, a: number, b: number, alpha: number]; -// Constants -const Xn = 0.950470, // D65 standard referent +// See https://observablehq.com/@mbostock/lab-and-rgb +const Xn = 0.96422, Yn = 1, - Zn = 1.088830, + Zn = 0.82521, t0 = 4 / 29, t1 = 6 / 29, t2 = 3 * t1 * t1, @@ -53,9 +53,14 @@ export function rgbToLab([r, g, b, alpha]: RGBColor): LABColor { r = rgb2xyz(r); g = rgb2xyz(g); b = rgb2xyz(b); - const x = xyz2lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn); - const y = xyz2lab((0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / Yn); - const z = xyz2lab((0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / Zn); + let x, z; + const y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn); + if (r === g && g === b) { + x = z = y; + } else { + x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn); + z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn); + } const l = 116 * y - 16; return [(l < 0) ? 0 : l, 500 * (x - y), 200 * (y - z), alpha]; @@ -79,9 +84,9 @@ export function labToRgb([l, a, b, alpha]: LABColor): RGBColor { z = Zn * lab2xyz(z); return [ - xyz2rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z), // D65 -> sRGB - xyz2rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z), - xyz2rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z), + xyz2rgb(3.1338561 * x - 1.6168667 * y - 0.4906146 * z), // D50 -> sRGB + xyz2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z), + xyz2rgb(0.0719453 * x - 0.2289914 * y + 1.4052427 * z), alpha, ]; } diff --git a/src/util/interpolate.test.ts b/src/util/interpolate.test.ts index bb226923a..bc6e123a1 100644 --- a/src/util/interpolate.test.ts +++ b/src/util/interpolate.test.ts @@ -57,9 +57,9 @@ describe('interpolate', () => { const i11nFn = (t: number) => interpolate.color(color, targetColor, t, 'hcl'); expectToMatchColor(i11nFn(0.00), 'rgb(0% 0% 100% / 1)'); - expectToMatchColor(i11nFn(0.25), 'rgb(0% 53.05% 100% / 0.9)', 4); - expectToMatchColor(i11nFn(0.50), 'rgb(0% 72.97% 100% / 0.8)', 4); - expectToMatchColor(i11nFn(0.75), 'rgb(0% 88.42% 67.80% / 0.7)', 4); + expectToMatchColor(i11nFn(0.25), 'rgb(0% 49.37% 100% / 0.9)', 4); + expectToMatchColor(i11nFn(0.50), 'rgb(0% 70.44% 100% / 0.8)', 4); + expectToMatchColor(i11nFn(0.75), 'rgb(0% 87.54% 63.18% / 0.7)', 4); expectToMatchColor(i11nFn(1.00), 'rgb(0% 100% 0% / 0.6)'); }); @@ -69,9 +69,9 @@ describe('interpolate', () => { const i11nFn = (t: number) => interpolate.color(color, targetColor, t, 'lab'); expectToMatchColor(i11nFn(0.00), 'rgb(0% 0% 100% / 1)'); - expectToMatchColor(i11nFn(0.25), 'rgb(42.40% 35.65% 82.90% / 0.9)', 4); - expectToMatchColor(i11nFn(0.50), 'rgb(49.19% 57.81% 65.10% / 0.8)', 4); - expectToMatchColor(i11nFn(0.75), 'rgb(43.61% 78.93% 44.66% / 0.7)', 4); + expectToMatchColor(i11nFn(0.25), 'rgb(39.64% 34.55% 83.36% / 0.9)', 4); + expectToMatchColor(i11nFn(0.50), 'rgb(46.42% 56.82% 65.91% / 0.8)', 4); + expectToMatchColor(i11nFn(0.75), 'rgb(41.45% 78.34% 45.62% / 0.7)', 4); expectToMatchColor(i11nFn(1.00), 'rgb(0% 100% 0% / 0.6)'); });