Skip to content

Commit

Permalink
Implicit parsing of undefined channels.
Browse files Browse the repository at this point in the history
CSS has no way of expressing an unset channel, and so for example “transparent”
is defined arbitrarily as rgba(0, 0, 0, 0). This module, in contrast, uses NaN
channels to distinguish a channel that is unset. Therefore when parsing CSS
color specifiers, we must record the unset channels (such as the hue being
undefined when the color is black).
  • Loading branch information
mbostock committed Feb 3, 2016
1 parent a71da07 commit 5cfe466
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 139 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var c = d3.color("steelblue"); // {r: 70, g: 130, b: 180, opacity: 1}
Let’s try converting it to HSL:

```js
var c = d3.hsl("steelblue"); // {h: 207.27272727272728, s: 0.44, l: 0.4901960784313726, opacity: 1}
var c = d3.hsl("steelblue"); // {h: 207.27…, s: 0.44, l: 0.4902…, opacity: 1}
```

Now rotate the hue by 90°, bump up the saturation, and format as a string for CSS:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "d3-color",
"version": "0.4.0",
"version": "0.4.1",
"description": "Color spaces! RGB, HSL, Cubehelix, Lab and HCL (Lch).",
"keywords": [
"d3",
Expand Down
117 changes: 58 additions & 59 deletions src/color.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,44 +175,44 @@ color.prototype = Color.prototype = {
export default function color(format) {
var m;
format = (format + "").trim().toLowerCase();
return (m = reHex3.exec(format)) ? (m = parseInt(m[1], 16), new Rgb((m >> 8 & 0xf) | (m >> 4 & 0x0f0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf))) // #f00
return (m = reHex3.exec(format)) ? (m = parseInt(m[1], 16), new Rgb((m >> 8 & 0xf) | (m >> 4 & 0x0f0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1)) // #f00
: (m = reHex6.exec(format)) ? rgbn(parseInt(m[1], 16)) // #ff0000
: (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3]) // rgb(255, 0, 0)
: (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100) // rgb(100%, 0%, 0%)
: (m = reRgbaInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)
: (m = reRgbaPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1)
: (m = reHslPercent.exec(format)) ? new Hsl(m[1], m[2] / 100, m[3] / 100) // hsl(120, 50%, 50%)
: (m = reHslaPercent.exec(format)) ? new Hsl(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)
: (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0)
: (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%)
: (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)
: (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1)
: (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%)
: (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)
: named.hasOwnProperty(format) ? rgbn(named[format])
: format === "transparent" ? new Rgb(0, 0, 0, 0)
: format === "transparent" ? new Rgb(NaN, NaN, NaN, 0)
: null;
}

function rgbn(n) {
return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff);
return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1);
}

function rgba(r, g, b, a) {
if (a <= 0) r = g = b = NaN;
return new Rgb(r, g, b, a);
}

export function rgbConvert(o) {
if (!(o instanceof Color)) o = color(o);
if (!o) return new Rgb;
o = o.rgb();
return new Rgb(o.r, o.g, o.b, o.opacity);
}

export function rgb(r, g, b, opacity) {
if (arguments.length === 1) {
if (!(r instanceof Color)) r = color(r);
if (r) {
r = r.rgb();
opacity = r.opacity;
b = r.b;
g = r.g;
r = r.r;
} else {
r = NaN;
}
}
return new Rgb(r, g, b, opacity);
return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity);
}

export function Rgb(r, g, b, opacity) {
this.r = +r;
this.g = +g;
this.b = +b;
this.opacity = opacity == null ? 1 : +opacity;
this.opacity = +opacity;
}

var _rgb = rgb.prototype = Rgb.prototype = new Color;
Expand Down Expand Up @@ -247,49 +247,48 @@ _rgb.toString = function() {
+ (a === 1 ? ")" : ", " + a + ")");
};

export function hsl(h, s, l, opacity) {
if (arguments.length === 1) {
if (h instanceof Hsl) {
opacity = h.opacity;
l = h.l;
s = h.s;
h = h.h;
} else {
if (!(h instanceof Color)) h = color(h);
if (h) {
if (h instanceof Hsl) return h;
h = h.rgb();
opacity = h.opacity;
var r = h.r / 255,
g = h.g / 255,
b = h.b / 255,
min = Math.min(r, g, b),
max = Math.max(r, g, b),
range = max - min;
l = (max + min) / 2;
if (range) {
s = l < 0.5 ? range / (max + min) : range / (2 - max - min);
if (r === max) h = (g - b) / range + (g < b) * 6;
else if (g === max) h = (b - r) / range + 2;
else h = (r - g) / range + 4;
h *= 60;
} else {
h = NaN;
s = l > 0 && l < 1 ? 0 : h;
}
} else {
h = NaN;
}
}
function hsla(h, s, l, a) {
if (a <= 0) h = s = l = NaN;
else if (l <= 0 || l >= 1) h = s = NaN;
else if (s <= 0) h = NaN;
return new Hsl(h, s, l, a);
}

export function hslConvert(o) {
if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);
if (!(o instanceof Color)) o = color(o);
if (!o) return new Hsl;
if (o instanceof Hsl) return o;
o = o.rgb();
var r = o.r / 255,
g = o.g / 255,
b = o.b / 255,
min = Math.min(r, g, b),
max = Math.max(r, g, b),
h = NaN,
s = max - min,
l = (max + min) / 2;
if (s) {
if (r === max) h = (g - b) / s + (g < b) * 6;
else if (g === max) h = (b - r) / s + 2;
else h = (r - g) / s + 4;
s /= l < 0.5 ? max + min : 2 - max - min;
h *= 60;
} else {
s = l > 0 && l < 1 ? 0 : h;
}
return new Hsl(h, s, l, opacity);
return new Hsl(h, s, l, o.opacity);
}

export function hsl(h, s, l, opacity) {
return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity);
}

function Hsl(h, s, l, opacity) {
this.h = +h;
this.s = +s;
this.l = +l;
this.opacity = opacity == null ? 1 : +opacity;
this.opacity = +opacity;
}

var _hsl = hsl.prototype = Hsl.prototype = new Color;
Expand Down
37 changes: 17 additions & 20 deletions src/cubehelix.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Color, rgb, Rgb, darker, brighter} from "./color";
import {Color, rgbConvert, Rgb, darker, brighter} from "./color";
import {deg2rad, rad2deg} from "./math";

var A = -0.14861,
Expand All @@ -10,32 +10,29 @@ var A = -0.14861,
EB = E * B,
BC_DA = B * C - D * A;

export default function cubehelix(h, s, l, opacity) {
if (arguments.length === 1) {
if (h instanceof Cubehelix) {
opacity = h.opacity;
l = h.l;
s = h.s;
h = h.h;
} else {
if (!(h instanceof Rgb)) h = rgb(h);
opacity = h.opacity;
var r = h.r / 255, g = h.g / 255, b = h.b / 255;
l = (BC_DA * b + ED * r - EB * g) / (BC_DA + ED - EB);
var bl = b - l, k = (E * (g - l) - C * bl) / D;
s = Math.sqrt(k * k + bl * bl) / (E * l * (1 - l)); // NaN if l=0 or l=1
function cubehelixConvert(o) {
if (o instanceof Cubehelix) return new Cubehelix(o.h, o.s, o.l, o.opacity);
if (!(o instanceof Rgb)) o = rgbConvert(o);
var r = o.r / 255,
g = o.g / 255,
b = o.b / 255,
l = (BC_DA * b + ED * r - EB * g) / (BC_DA + ED - EB),
bl = b - l,
k = (E * (g - l) - C * bl) / D,
s = Math.sqrt(k * k + bl * bl) / (E * l * (1 - l)), // NaN if l=0 or l=1
h = s ? Math.atan2(k, bl) * rad2deg - 120 : NaN;
if (h < 0) h += 360;
}
}
return new Cubehelix(h, s, l, opacity);
return new Cubehelix(h < 0 ? h + 360 : h, s, l, o.opacity);
}

export default function cubehelix(h, s, l, opacity) {
return arguments.length === 1 ? cubehelixConvert(h) : new Cubehelix(h, s, l, opacity == null ? 1 : opacity);
}

export function Cubehelix(h, s, l, opacity) {
this.h = +h;
this.s = +s;
this.l = +l;
this.opacity = opacity == null ? 1 : +opacity;
this.opacity = +opacity;
}

var _cubehelix = cubehelix.prototype = Cubehelix.prototype = new Color;
Expand Down
76 changes: 29 additions & 47 deletions src/lab.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Color, rgb, Rgb} from "./color";
import {Color, rgbConvert, Rgb} from "./color";
import {deg2rad, rad2deg} from "./math";

var Kn = 18,
Expand All @@ -10,41 +10,31 @@ var Kn = 18,
t2 = 3 * t1 * t1,
t3 = t1 * t1 * t1;

export default function lab(l, a, b, opacity) {
if (arguments.length === 1) {
if (l instanceof Lab) {
opacity = l.opacity;
b = l.b;
a = l.a;
l = l.l;
} else if (l instanceof Hcl) {
var h = l.h * deg2rad;
opacity = l.opacity;
b = Math.sin(h) * l.c;
a = Math.cos(h) * l.c;
l = l.l;
} else {
if (!(l instanceof Rgb)) l = rgb(l);
opacity = l.opacity;
b = rgb2xyz(l.r);
a = rgb2xyz(l.g);
l = rgb2xyz(l.b);
var x = xyz2lab((0.4124564 * b + 0.3575761 * a + 0.1804375 * l) / Xn),
y = xyz2lab((0.2126729 * b + 0.7151522 * a + 0.0721750 * l) / Yn),
z = xyz2lab((0.0193339 * b + 0.1191920 * a + 0.9503041 * l) / Zn);
b = 200 * (y - z);
a = 500 * (x - y);
l = 116 * y - 16;
}
function labConvert(o) {
if (o instanceof Lab) return new Lab(o.l, o.a, o.b, o.opacity);
if (o instanceof Hcl) {
var h = o.h * deg2rad;
return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity);
}
return new Lab(l, a, b, opacity);
if (!(o instanceof Rgb)) o = rgbConvert(o);
var b = rgb2xyz(o.r),
a = rgb2xyz(o.g),
l = rgb2xyz(o.b),
x = xyz2lab((0.4124564 * b + 0.3575761 * a + 0.1804375 * l) / Xn),
y = xyz2lab((0.2126729 * b + 0.7151522 * a + 0.0721750 * l) / Yn),
z = xyz2lab((0.0193339 * b + 0.1191920 * a + 0.9503041 * l) / Zn);
return new Lab(116 * y - 16, 500 * (x - y), 200 * (y - z), o.opacity);
}

export default function lab(l, a, b, opacity) {
return arguments.length === 1 ? labConvert(l) : new Lab(l, a, b, opacity == null ? 1 : opacity);
}

export function Lab(l, a, b, opacity) {
this.l = +l;
this.a = +a;
this.b = +b;
this.opacity = opacity == null ? 1 : +opacity;
this.opacity = +opacity;
}

var _lab = lab.prototype = Lab.prototype = new Color;
Expand Down Expand Up @@ -88,30 +78,22 @@ function rgb2xyz(x) {
return (x /= 255) <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}

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);
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);
}

export function hcl(h, c, l, opacity) {
if (arguments.length === 1) {
if (h instanceof Hcl) {
opacity = h.opacity;
l = h.l;
c = h.c;
h = h.h;
} else {
if (!(h instanceof Lab)) h = lab(h);
opacity = h.opacity;
l = h.l;
c = Math.sqrt(h.a * h.a + h.b * h.b);
h = Math.atan2(h.b, h.a) * rad2deg;
if (h < 0) h += 360;
}
}
return new Hcl(h, c, l, opacity);
return arguments.length === 1 ? hclConvert(h) : new Hcl(h, c, l, opacity == null ? 1 : opacity);
}

export function Hcl(h, c, l, opacity) {
this.h = +h;
this.c = +c;
this.l = +l;
this.opacity = opacity == null ? 1 : +opacity;
this.opacity = +opacity;
}

var _hcl = hcl.prototype = Hcl.prototype = new Color;
Expand All @@ -125,5 +107,5 @@ _hcl.darker = function(k) {
};

_hcl.rgb = function() {
return lab(this).rgb();
return labConvert(this).rgb();
};
12 changes: 6 additions & 6 deletions test/color-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ tape("color(format) parses CSS color names (e.g., \"rebeccapurple\")", function(
test.rgbEqual(color.color("aliceblue"), 240, 248, 255, 1);
test.rgbEqual(color.color("yellow"), 255, 255, 0, 1);
test.rgbEqual(color.color("rebeccapurple"), 102, 51, 153, 1);
test.rgbEqual(color.color("transparent"), 0, 0, 0, 0);
test.rgbEqual(color.color("transparent"), NaN, NaN, NaN, 0);
test.end();
});

Expand Down Expand Up @@ -80,13 +80,13 @@ tape("color(format) allows number signs", function(test) {
test.rgbEqual(color.color("rgb(+120,+30,+50)"), 120, 30, 50, 1);
test.hslEqual(color.color("hsl(+120,+30%,+50%)"), 120, 0.3, 0.5, 1);
test.rgbEqual(color.color("rgb(-120,-30,-50)"), -120, -30, -50, 1);
test.hslEqual(color.color("hsl(-120,-30%,-50%)"), -120, -0.3, -0.5, 1);
test.hslEqual(color.color("hsl(-120,-30%,-50%)"), NaN, NaN, -0.5, 1);
test.rgbEqual(color.color("rgba(12,34,56,+0.4)"), 12, 34, 56, 0.4);
test.rgbEqual(color.color("rgba(12,34,56,-0.4)"), 12, 34, 56, -0.4);
test.rgbEqual(color.color("rgba(12,34,56,-0.4)"), NaN, NaN, NaN, -0.4);
test.rgbEqual(color.color("rgba(12%,34%,56%,+0.4)"), 31, 87, 143, 0.4);
test.rgbEqual(color.color("rgba(12%,34%,56%,-0.4)"), 31, 87, 143, -0.4);
test.rgbEqual(color.color("rgba(12%,34%,56%,-0.4)"), NaN, NaN, NaN, -0.4);
test.hslEqual(color.color("hsla(60,100%,20%,+0.4)"), 60, 1, 0.2, 0.4);
test.hslEqual(color.color("hsla(60,100%,20%,-0.4)"), 60, 1, 0.2, -0.4);
test.hslEqual(color.color("hsla(60,100%,20%,-0.4)"), NaN, NaN, NaN, -0.4);
test.end();
});

Expand Down Expand Up @@ -128,7 +128,7 @@ tape("color(format) does not allow whitespace before open paren or percent sign"

tape("color(format) is case-insensitive", function(test) {
test.rgbEqual(color.color("aLiCeBlUE"), 240, 248, 255, 1);
test.rgbEqual(color.color("transPARENT"), 0, 0, 0, 0);
test.rgbEqual(color.color("transPARENT"), NaN, NaN, NaN, 0);
test.rgbEqual(color.color(" #aBc\t\n"), 170, 187, 204, 1);
test.rgbEqual(color.color(" #aaBBCC\t\n"), 170, 187, 204, 1);
test.rgbEqual(color.color(" rGB(120,30,50)\t\n"), 120, 30, 50, 1);
Expand Down
2 changes: 1 addition & 1 deletion test/hcl-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ tape("hcl(format) parses the specified format and converts to HCL", function(tes
});

tape("hcl(format) returns undefined channel values for unknown formats", function(test) {
test.hclEqual(color.hcl("invalid"), NaN, NaN, NaN, 1);
test.hclEqual(color.hcl("invalid"), NaN, NaN, NaN, NaN);
test.end();
});

Expand Down

0 comments on commit 5cfe466

Please sign in to comment.