diff --git a/README.md b/README.md
index 22ce38c..a7f2d52 100644
--- a/README.md
+++ b/README.md
@@ -152,6 +152,10 @@ Constructs a new [RGB](https://en.wikipedia.org/wiki/RGB_color_model) color. The
If *r*, *g* and *b* are specified, these represent the channel values of the returned color; an *opacity* may also be specified. If a CSS Color Module Level 3 *specifier* string is specified, it is parsed and then converted to the RGB color space. See [color](#color) for examples. If a [*color*](#color) instance is specified, it is converted to the RGB color space using [*color*.rgb](#color_rgb). Note that unlike [*color*.rgb](#color_rgb) this method *always* returns a new instance, even if *color* is already an RGB color.
+# *rgb*.clamp() [<>](https://github.com/d3/d3-color/blob/master/src/color.js "Source")
+
+Returns a new RGB color where the `r`, `g`, and `b` channels are clamped to the range [0, 255] and rounded to the nearest integer value, and the `opacity` is clamped to the range [0, 1].
+
# d3.hsl(h, s, l[, opacity]) [<>](https://github.com/d3/d3-color/blob/master/src/color.js "Source")
# d3.hsl(specifier)
# d3.hsl(color)
@@ -160,6 +164,10 @@ Constructs a new [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) color. The cha
If *h*, *s* and *l* are specified, these represent the channel values of the returned color; an *opacity* may also be specified. If a CSS Color Module Level 3 *specifier* string is specified, it is parsed and then converted to the HSL color space. See [color](#color) for examples. If a [*color*](#color) instance is specified, it is converted to the RGB color space using [*color*.rgb](#color_rgb) and then converted to HSL. (Colors already in the HSL color space skip the conversion to RGB.)
+# *hsl*.clamp() [<>](https://github.com/d3/d3-color/blob/master/src/color.js "Source")
+
+Returns a new HSL color where the `h` channel is clamped to the range [0, 360), and the `s`, `l`, and `opacity` channels are clamped to the range [0, 1].
+
# d3.lab(l, a, b[, opacity]) [<>](https://github.com/d3/d3-color/blob/master/src/lab.js "Source")
# d3.lab(specifier)
# d3.lab(color)
diff --git a/src/color.js b/src/color.js
index 3af25a9..6552e3d 100644
--- a/src/color.js
+++ b/src/color.js
@@ -251,6 +251,9 @@ define(Rgb, rgb, extend(Color, {
rgb: function() {
return this;
},
+ clamp: function() {
+ return new Rgb(clampi(this.r), clampi(this.g), clampi(this.b), clampa(this.opacity));
+ },
displayable: function() {
return (-0.5 <= this.r && this.r < 255.5)
&& (-0.5 <= this.g && this.g < 255.5)
@@ -268,16 +271,20 @@ function rgb_formatHex() {
}
function rgb_formatRgb() {
- var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
- return (a === 1 ? "rgb(" : "rgba(")
- + Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", "
- + Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", "
- + Math.max(0, Math.min(255, Math.round(this.b) || 0))
- + (a === 1 ? ")" : ", " + a + ")");
+ const a = clampa(this.opacity);
+ return `${a === 1 ? "rgb(" : "rgba("}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${a === 1 ? ")" : `, ${a})`}`;
+}
+
+function clampa(opacity) {
+ return isNaN(opacity) ? 1 : Math.max(0, Math.min(1, opacity));
+}
+
+function clampi(value) {
+ return Math.max(0, Math.min(255, Math.round(value) || 0));
}
function hex(value) {
- value = Math.max(0, Math.min(255, Math.round(value) || 0));
+ value = clampi(value);
return (value < 16 ? "0" : "") + value.toString(16);
}
@@ -347,25 +354,29 @@ define(Hsl, hsl, extend(Color, {
this.opacity
);
},
+ clamp: function() {
+ return new Hsl(clamph(this.h), clampt(this.s), clampt(this.l), clampa(this.opacity));
+ },
displayable: function() {
return (0 <= this.s && this.s <= 1 || isNaN(this.s))
&& (0 <= this.l && this.l <= 1)
&& (0 <= this.opacity && this.opacity <= 1);
},
formatHsl: function() {
- var a = this.opacity,
- h = (this.h || 0) % 360,
- s = Math.max(0, Math.min(1, this.s || 0)),
- l = Math.max(0, Math.min(1, this.l || 0));
- a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
- return (a === 1 ? "hsl(" : "hsla(")
- + (h < 0 ? h + 360 : h) + ", "
- + s * 100 + "%, "
- + l * 100 + "%"
- + (a === 1 ? ")" : ", " + a + ")");
+ const a = clampa(this.opacity);
+ return `${a === 1 ? "hsl(" : "hsla("}${clamph(this.h)}, ${clampt(this.s) * 100}%, ${clampt(this.l) * 100}%${a === 1 ? ")" : `, ${a})`}`;
}
}));
+function clamph(value) {
+ value = (value || 0) % 360;
+ return value < 0 ? value + 360 : value;
+}
+
+function clampt(value) {
+ return Math.max(0, Math.min(1, value || 0));
+}
+
/* From FvD 13.37, CSS Color Module Level 3 */
function hsl2rgb(h, m1, m2) {
return (h < 60 ? m1 + (m2 - m1) * h / 60
diff --git a/test/hsl-test.js b/test/hsl-test.js
index d963916..930092d 100644
--- a/test/hsl-test.js
+++ b/test/hsl-test.js
@@ -89,6 +89,18 @@ it("hsl(h, s, l) does not clamp s and l channel values to [0,1]", () => {
assertHslEqual(hsl(120, 0.2, 1.1), 120, 0.2, 1.1, 1);
});
+it("hsl(h, s, l).clamp() clamps channel values", () => {
+ assertHslEqual(hsl(120, -0.1, -0.2).clamp(), 120, 0, 0, 1);
+ assertHslEqual(hsl(120, 1.1, 1.2).clamp(), 120, 1, 1, 1);
+ assertHslEqual(hsl(120, 2.1, 2.2).clamp(), 120, 1, 1, 1);
+ assertHslEqual(hsl(420, -0.1, -0.2).clamp(), 60, 0, 0, 1);
+ assertHslEqual(hsl(-420, -0.1, -0.2).clamp(), 300, 0, 0, 1);
+ assert.strictEqual(hsl(-420, -0.1, -0.2, NaN).clamp().opacity, 1);
+ assert.strictEqual(hsl(-420, -0.1, -0.2, 0.5).clamp().opacity, 0.5);
+ assert.strictEqual(hsl(-420, -0.1, -0.2, -1).clamp().opacity, 0);
+ assert.strictEqual(hsl(-420, -0.1, -0.2, 2).clamp().opacity, 1);
+});
+
it("hsl(h, s, l, opacity) does not clamp opacity to [0,1]", () => {
assertHslEqual(hsl(120, 0.1, 0.5, -0.2), 120, 0.1, 0.5, -0.2);
assertHslEqual(hsl(120, 0.9, 0.5, 1.2), 120, 0.9, 0.5, 1.2);
diff --git a/test/rgb-test.js b/test/rgb-test.js
index 32ebdf2..158a841 100644
--- a/test/rgb-test.js
+++ b/test/rgb-test.js
@@ -91,6 +91,16 @@ it("rgb(r, g, b) does not clamp channel values", () => {
assertRgbApproxEqual(rgb(300, 400, 500), 300, 400, 500, 1);
});
+it("rgb(r, g, b).clamp() rounds and clamps channel values", () => {
+ assertRgbApproxEqual(rgb(-10, -20, -30).clamp(), 0, 0, 0, 1);
+ assertRgbApproxEqual(rgb(10.5, 20.5, 30.5).clamp(), 11, 21, 31, 1);
+ assertRgbApproxEqual(rgb(300, 400, 500).clamp(), 255, 255, 255, 1);
+ assert.strictEqual(rgb(10.5, 20.5, 30.5, -1).clamp().opacity, 0);
+ assert.strictEqual(rgb(10.5, 20.5, 30.5, 0.5).clamp().opacity, 0.5);
+ assert.strictEqual(rgb(10.5, 20.5, 30.5, 2).clamp().opacity, 1);
+ assert.strictEqual(rgb(10.5, 20.5, 30.5, NaN).clamp().opacity, 1);
+});
+
it("rgb(r, g, b, opacity) does not clamp opacity", () => {
assertRgbApproxEqual(rgb(-10, -20, -30, -0.2), -10, -20, -30, -0.2);
assertRgbApproxEqual(rgb(300, 400, 500, 1.2), 300, 400, 500, 1.2);