diff --git a/src/lab.js b/src/lab.js index 5db7302..ae13ec7 100644 --- a/src/lab.js +++ b/src/lab.js @@ -2,7 +2,7 @@ import define, {extend} from "./define"; import {Color, rgbConvert, Rgb} from "./color"; import {deg2rad, rad2deg} from "./math"; -// https://beta.observablehq.com/@mbostock/lab-and-rgb +// https://observablehq.com/@mbostock/lab-and-rgb var K = 18, Xn = 0.96422, Yn = 1, @@ -10,15 +10,12 @@ var K = 18, t0 = 4 / 29, t1 = 6 / 29, t2 = 3 * t1 * t1, - t3 = t1 * t1 * t1; + t3 = t1 * t1 * t1, + dc = 0.1; function labConvert(o) { if (o instanceof Lab) return new Lab(o.l, o.a, o.b, o.opacity); - if (o instanceof Hcl) { - if (isNaN(o.h)) return new Lab(o.l, 0, 0, o.opacity); - var h = o.h * deg2rad; - return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity); - } + if (o instanceof Hcl) return hcl2lab(o); if (!(o instanceof Rgb)) o = rgbConvert(o); var r = rgb2lrgb(o.r), g = rgb2lrgb(o.g), @@ -88,7 +85,7 @@ function rgb2lrgb(x) { function hclConvert(o) { if (o instanceof Hcl) return new Hcl(o.h, o.c, o.l, o.opacity); if (!(o instanceof Lab)) o = labConvert(o); - if (o.a === 0 && o.b === 0) return new Hcl(NaN, 0 < o.l ? 0 : NaN, o.l, o.opacity); + if (o.a === 0 && o.b === 0) return new Hcl(NaN, 0 < o.l && o.l < 100 ? 0 : NaN, o.l, o.opacity); var h = Math.atan2(o.b, o.a) * rad2deg; return new Hcl(h < 0 ? h + 360 : h, Math.sqrt(o.a * o.a + o.b * o.b), o.l, o.opacity); } @@ -108,6 +105,16 @@ export function Hcl(h, c, l, opacity) { this.opacity = +opacity; } +function hcl2lab(o) { + if (isNaN(o.h)) return new Lab(o.l, 0, 0, o.opacity); + var h = o.h * deg2rad; + return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity); +} + +function hcl2rgb(o) { + return hcl2lab(o).rgb(); +} + define(Hcl, hcl, extend(Color, { brighter: function(k) { return new Hcl(this.h, this.c, this.l + K * (k == null ? 1 : k), this.opacity); @@ -116,6 +123,20 @@ define(Hcl, hcl, extend(Color, { return new Hcl(this.h, this.c, this.l - K * (k == null ? 1 : k), this.opacity); }, rgb: function() { - return labConvert(this).rgb(); + return hcl2rgb(this); + }, + toString: function() { + if ((rgb = hcl2rgb(this)).displayable()) return rgb + ""; + var c0, c1, rgb, hcl = new Hcl(this.h, 0, this.l, 1); + if ((rgb = hcl2rgb(hcl)).displayable()) { + c0 = 0, c1 = this.c; + while (c1 - c0 > dc) { + hcl.c = (c0 + c1) * 0.5; + if ((rgb = hcl2rgb(hcl)).displayable()) c0 = hcl.c; + else c1 = hcl.c; + } + } + rgb.opacity = this.opacity; + return rgb + ""; } })); diff --git a/test/hcl-test.js b/test/hcl-test.js index 9436fd6..b2cd4b5 100644 --- a/test/hcl-test.js +++ b/test/hcl-test.js @@ -16,17 +16,17 @@ tape("hcl(…) exposes h, c, and l channel values", function(test) { test.end(); }); -tape("hcl(…) returns defined hue and undefined chroma for black", function(test) { +tape("hcl(…) returns defined hue and undefined chroma for black and white", function(test) { test.hclEqual(color.hcl("black"), NaN, NaN, 0, 1); test.hclEqual(color.hcl("#000"), NaN, NaN, 0, 1); test.hclEqual(color.hcl(color.lab("#000")), NaN, NaN, 0, 1); + test.hclEqual(color.hcl("white"), NaN, NaN, 100, 1); + test.hclEqual(color.hcl("#fff"), NaN, NaN, 100, 1); + test.hclEqual(color.hcl(color.lab("#fff")), NaN, NaN, 100, 1); test.end(); }); -tape("hcl(…) returns defined hue and chroma for gray and white", function(test) { - test.hclEqual(color.hcl("white"), NaN, 0, 100, 1); - test.hclEqual(color.hcl("#fff"), NaN, 0, 100, 1); - test.hclEqual(color.hcl(color.lab("#fff")), NaN, 0, 100, 1); +tape("hcl(…) returns undefined hue and zero chroma for gray", function(test) { test.hclEqual(color.hcl("gray"), NaN, 0, 53.585013, 1); test.hclEqual(color.hcl(color.lab("gray")), NaN, 0, 53.585013, 1); test.end(); @@ -70,6 +70,12 @@ tape("hcl.toString() treats undefined channel values as 0", function(test) { test.end(); }); +tape("hcl.toString() clamps chroma", function(test) { + test.equal(color.hcl(302, 130, 0, 0.4) + "", "rgba(0, 0, 0, 0.4)"); + test.equal(color.hcl(302, 130, 100, 0.4) + "", "rgba(255, 255, 255, 0.4)"); + test.end(); +}); + tape("hcl(h, c, l) does not wrap hue to [0,360)", function(test) { test.hclEqual(color.hcl(-10, 40, 50), -10, 40, 50, 1); test.hclEqual(color.hcl(0, 40, 50), 0, 40, 50, 1); @@ -146,7 +152,7 @@ tape("hcl(hcl) copies an HCL color", function(test) { tape("hcl(lab) returns h = NaN if a and b are zero", function(test) { test.hclEqual(color.hcl(color.lab(0, 0, 0)), NaN, NaN, 0, 1); test.hclEqual(color.hcl(color.lab(50, 0, 0)), NaN, 0, 50, 1); - test.hclEqual(color.hcl(color.lab(100, 0, 0)), NaN, 0, 100, 1); + test.hclEqual(color.hcl(color.lab(100, 0, 0)), NaN, NaN, 100, 1); test.hclEqual(color.hcl(color.lab(0, 10, 0)), 0, 10, 0, 1); test.hclEqual(color.hcl(color.lab(50, 10, 0)), 0, 10, 50, 1); test.hclEqual(color.hcl(color.lab(100, 10, 0)), 0, 10, 100, 1);