Skip to content

Commit

Permalink
{rgb,hsl}.clamp() (#102)
Browse files Browse the repository at this point in the history
* {rgb,hsl}.clamp()

* Update README
  • Loading branch information
mbostock authored Mar 28, 2022
1 parent 70e3a04 commit ac660c6
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 17 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a name="rgb_clamp" href="#rgb_clamp">#</a> *rgb*.<b>clamp</b>() [<>](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].

<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>
<a href="#hsl">#</a> d3.<b>hsl</b>(<i>specifier</i>)<br>
<a href="#hsl">#</a> d3.<b>hsl</b>(<i>color</i>)<br>
Expand All @@ -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.)

<a name="hsl_clamp" href="#hsl_clamp">#</a> *hsl*.<b>clamp</b>() [<>](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].

<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>
<a href="#lab">#</a> d3.<b>lab</b>(<i>specifier</i>)<br>
<a href="#lab">#</a> d3.<b>lab</b>(<i>color</i>)<br>
Expand Down
45 changes: 28 additions & 17 deletions src/color.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions test/hsl-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions test/rgb-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit ac660c6

Please sign in to comment.