Skip to content

Commit ac660c6

Browse files
authored
{rgb,hsl}.clamp() (#102)
* {rgb,hsl}.clamp() * Update README
1 parent 70e3a04 commit ac660c6

File tree

4 files changed

+58
-17
lines changed

4 files changed

+58
-17
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ Constructs a new [RGB](https://en.wikipedia.org/wiki/RGB_color_model) color. The
152152

153153
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.
154154

155+
<a name="rgb_clamp" href="#rgb_clamp">#</a> *rgb*.<b>clamp</b>() [<>](https://github.com/d3/d3-color/blob/master/src/color.js "Source")
156+
157+
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].
158+
155159
<a name="hsl" href="#hsl">#</a> d3.<b>hsl</b>(<i>h</i>, <i>s</i>, <i>l</i>[, <i>opacity</i>]) [<>](https://github.com/d3/d3-color/blob/master/src/color.js "Source")<br>
156160
<a href="#hsl">#</a> d3.<b>hsl</b>(<i>specifier</i>)<br>
157161
<a href="#hsl">#</a> d3.<b>hsl</b>(<i>color</i>)<br>
@@ -160,6 +164,10 @@ Constructs a new [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) color. The cha
160164

161165
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.)
162166

167+
<a name="hsl_clamp" href="#hsl_clamp">#</a> *hsl*.<b>clamp</b>() [<>](https://github.com/d3/d3-color/blob/master/src/color.js "Source")
168+
169+
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].
170+
163171
<a name="lab" href="#lab">#</a> d3.<b>lab</b>(<i>l</i>, <i>a</i>, <i>b</i>[, <i>opacity</i>]) [<>](https://github.com/d3/d3-color/blob/master/src/lab.js "Source")<br>
164172
<a href="#lab">#</a> d3.<b>lab</b>(<i>specifier</i>)<br>
165173
<a href="#lab">#</a> d3.<b>lab</b>(<i>color</i>)<br>

src/color.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ define(Rgb, rgb, extend(Color, {
251251
rgb: function() {
252252
return this;
253253
},
254+
clamp: function() {
255+
return new Rgb(clampi(this.r), clampi(this.g), clampi(this.b), clampa(this.opacity));
256+
},
254257
displayable: function() {
255258
return (-0.5 <= this.r && this.r < 255.5)
256259
&& (-0.5 <= this.g && this.g < 255.5)
@@ -268,16 +271,20 @@ function rgb_formatHex() {
268271
}
269272

270273
function rgb_formatRgb() {
271-
var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
272-
return (a === 1 ? "rgb(" : "rgba(")
273-
+ Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", "
274-
+ Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", "
275-
+ Math.max(0, Math.min(255, Math.round(this.b) || 0))
276-
+ (a === 1 ? ")" : ", " + a + ")");
274+
const a = clampa(this.opacity);
275+
return `${a === 1 ? "rgb(" : "rgba("}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${a === 1 ? ")" : `, ${a})`}`;
276+
}
277+
278+
function clampa(opacity) {
279+
return isNaN(opacity) ? 1 : Math.max(0, Math.min(1, opacity));
280+
}
281+
282+
function clampi(value) {
283+
return Math.max(0, Math.min(255, Math.round(value) || 0));
277284
}
278285

279286
function hex(value) {
280-
value = Math.max(0, Math.min(255, Math.round(value) || 0));
287+
value = clampi(value);
281288
return (value < 16 ? "0" : "") + value.toString(16);
282289
}
283290

@@ -347,25 +354,29 @@ define(Hsl, hsl, extend(Color, {
347354
this.opacity
348355
);
349356
},
357+
clamp: function() {
358+
return new Hsl(clamph(this.h), clampt(this.s), clampt(this.l), clampa(this.opacity));
359+
},
350360
displayable: function() {
351361
return (0 <= this.s && this.s <= 1 || isNaN(this.s))
352362
&& (0 <= this.l && this.l <= 1)
353363
&& (0 <= this.opacity && this.opacity <= 1);
354364
},
355365
formatHsl: function() {
356-
var a = this.opacity,
357-
h = (this.h || 0) % 360,
358-
s = Math.max(0, Math.min(1, this.s || 0)),
359-
l = Math.max(0, Math.min(1, this.l || 0));
360-
a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
361-
return (a === 1 ? "hsl(" : "hsla(")
362-
+ (h < 0 ? h + 360 : h) + ", "
363-
+ s * 100 + "%, "
364-
+ l * 100 + "%"
365-
+ (a === 1 ? ")" : ", " + a + ")");
366+
const a = clampa(this.opacity);
367+
return `${a === 1 ? "hsl(" : "hsla("}${clamph(this.h)}, ${clampt(this.s) * 100}%, ${clampt(this.l) * 100}%${a === 1 ? ")" : `, ${a})`}`;
366368
}
367369
}));
368370

371+
function clamph(value) {
372+
value = (value || 0) % 360;
373+
return value < 0 ? value + 360 : value;
374+
}
375+
376+
function clampt(value) {
377+
return Math.max(0, Math.min(1, value || 0));
378+
}
379+
369380
/* From FvD 13.37, CSS Color Module Level 3 */
370381
function hsl2rgb(h, m1, m2) {
371382
return (h < 60 ? m1 + (m2 - m1) * h / 60

test/hsl-test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ it("hsl(h, s, l) does not clamp s and l channel values to [0,1]", () => {
8989
assertHslEqual(hsl(120, 0.2, 1.1), 120, 0.2, 1.1, 1);
9090
});
9191

92+
it("hsl(h, s, l).clamp() clamps channel values", () => {
93+
assertHslEqual(hsl(120, -0.1, -0.2).clamp(), 120, 0, 0, 1);
94+
assertHslEqual(hsl(120, 1.1, 1.2).clamp(), 120, 1, 1, 1);
95+
assertHslEqual(hsl(120, 2.1, 2.2).clamp(), 120, 1, 1, 1);
96+
assertHslEqual(hsl(420, -0.1, -0.2).clamp(), 60, 0, 0, 1);
97+
assertHslEqual(hsl(-420, -0.1, -0.2).clamp(), 300, 0, 0, 1);
98+
assert.strictEqual(hsl(-420, -0.1, -0.2, NaN).clamp().opacity, 1);
99+
assert.strictEqual(hsl(-420, -0.1, -0.2, 0.5).clamp().opacity, 0.5);
100+
assert.strictEqual(hsl(-420, -0.1, -0.2, -1).clamp().opacity, 0);
101+
assert.strictEqual(hsl(-420, -0.1, -0.2, 2).clamp().opacity, 1);
102+
});
103+
92104
it("hsl(h, s, l, opacity) does not clamp opacity to [0,1]", () => {
93105
assertHslEqual(hsl(120, 0.1, 0.5, -0.2), 120, 0.1, 0.5, -0.2);
94106
assertHslEqual(hsl(120, 0.9, 0.5, 1.2), 120, 0.9, 0.5, 1.2);

test/rgb-test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ it("rgb(r, g, b) does not clamp channel values", () => {
9191
assertRgbApproxEqual(rgb(300, 400, 500), 300, 400, 500, 1);
9292
});
9393

94+
it("rgb(r, g, b).clamp() rounds and clamps channel values", () => {
95+
assertRgbApproxEqual(rgb(-10, -20, -30).clamp(), 0, 0, 0, 1);
96+
assertRgbApproxEqual(rgb(10.5, 20.5, 30.5).clamp(), 11, 21, 31, 1);
97+
assertRgbApproxEqual(rgb(300, 400, 500).clamp(), 255, 255, 255, 1);
98+
assert.strictEqual(rgb(10.5, 20.5, 30.5, -1).clamp().opacity, 0);
99+
assert.strictEqual(rgb(10.5, 20.5, 30.5, 0.5).clamp().opacity, 0.5);
100+
assert.strictEqual(rgb(10.5, 20.5, 30.5, 2).clamp().opacity, 1);
101+
assert.strictEqual(rgb(10.5, 20.5, 30.5, NaN).clamp().opacity, 1);
102+
});
103+
94104
it("rgb(r, g, b, opacity) does not clamp opacity", () => {
95105
assertRgbApproxEqual(rgb(-10, -20, -30, -0.2), -10, -20, -30, -0.2);
96106
assertRgbApproxEqual(rgb(300, 400, 500, 1.2), 300, 400, 500, 1.2);

0 commit comments

Comments
 (0)