+
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,20 @@ color.mix(Color("yellow"), 0.3) // cyan -> rgb(77, 255, 179)
color.green(100).grayscale().lighten(0.6)
```

### Color Rounding

Due to the prevalence of floating point errors in JavaScript, options for rounding colors are limited. The library provides three methods for rounding.

```js
// Rounds all elements to the nearest integer (chainable)
color.round(); // hsl(100.25, 50.4%, 50%) -> hsl(100, 50%, 50%)

// Returns the internal color elements rounded with `toFixed`
color.toFixed(precision); // hsl(100.25, 50.4%, 50%), 1 -> ['100.3', '50.4', '50']

// Returns a formatted color string rounded to the given precision
color.toString(precision); // hsl(100.25, 50.4%, 50%), 1 -> "hsl(100.3, 50.4%, 50%)"
```

## Propers
The API was inspired by [color-js](https://github.com/brehaut/color-js). Manipulation functions by CSS tools like Sass, LESS, and Stylus.
41 changes: 23 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,21 @@ Color.prototype = {
return this[this.model]();
},

string(places) {
let self = this.model in colorString.to ? this : this.rgb();
self = self.round(typeof places === 'number' ? places : 1);
const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha];
toFixed(precision) {
return roundColor(this.color, precision);
},

string(precision) {
const self = this.model in colorString.to ? this : this.rgb();
const roundedColor = roundColor(self.color, precision);
const arguments_ = self.valpha === 1 ? roundedColor : [...roundedColor, this.valpha];
return colorString.to[self.model](...arguments_);
},

percentString(places) {
const self = this.rgb().round(typeof places === 'number' ? places : 1);
const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha];
percentString(precision) {
const self = this.rgb();
const roundedColor = roundColor(self.color, precision);
const arguments_ = self.valpha === 1 ? roundedColor : [...roundedColor, this.valpha];
return colorString.to.rgb.percent(...arguments_);
},

Expand Down Expand Up @@ -179,9 +184,12 @@ Color.prototype = {
return rgb;
},

round(places) {
places = Math.max(places || 0, 0);
return new Color([...this.color.map(roundToPlace(places)), this.valpha], this.model);
round() {
if ((arguments?.length ?? 0) !== 0) {
console.warn('Color.round() no longer accepts a precision argument and now rounds to the nearest integer. Consider using Color.toFixed() if you want to retrieve color elements with a precision argument.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't log anything to users since this is a CLI widely used package. Your log may break some apps.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, but I need some sort of safeguard here.

Calling with an unused argument (the old way) will not cause any error per se but will cause the library to return colours that are rounded differently. It's a silent data corruption that users would need to spend time finding the root cause of.

There is a breaking change notice describing this API change, but of course many people upgrade breaking versions without reading changelogs. I could explicitly throw, which would at least more predictably solve the underlying issue I want to solve. But doing and logging nothing would likely be detrimental here.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be released as a new major since it is breaking. Let's just throw new TypeError('.round() takes no arguments') here.

Also you can just do if (arguments?.length) looking at this again.

}

return new Color([...this.color.map(number => Math.round(Number(number))), this.valpha], this.model);
},

alpha(value) {
Expand Down Expand Up @@ -432,14 +440,11 @@ for (const model of Object.keys(convert)) {
};
}

function roundTo(number, places) {
return Number(number.toFixed(places));
}

function roundToPlace(places) {
return function (number) {
return roundTo(number, places);
};
function roundColor(colorArray, precision) {
return colorArray.map(unroundedArgument => {
const rounded = unroundedArgument.toFixed(typeof precision === 'number' ? precision : 1);
return rounded.replace(/\.(\d*[1-9])?0+$/, (_, subgroup) => subgroup ? `.${subgroup}` : '');
});
}

function getset(model, channel, modifier) {
Expand Down
144 changes: 144 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,58 @@ it('Translations', () => {
});
});

it('Output to string (default rounding to 1)', () => {
deepEqual(Color.rgb(10, 30, 25).rgb().string(), 'rgb(10, 30, 25)');
deepEqual(Color.rgb(10, 30, 25).hsl().string(), 'hsl(165, 50%, 7.8%)');
deepEqual(Color.rgb(10, 30, 25).hwb().string(), 'hwb(165, 3.9%, 88.2%)');
});

it('Output to string (rounding to 0)', () => {
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hsl().string(0), 'hsl(164, 48%, 8%)');
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hwb().string(0), 'hwb(164, 4%, 88%)');
});

it('Output to string (rounding to 1)', () => {
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hsl().string(1), 'hsl(164.5, 48.5%, 8%)');
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hwb().string(1), 'hwb(164.5, 4.1%, 88.1%)');
});

it('Output to string (rounding to 2)', () => {
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hsl().string(2), 'hsl(164.5, 48.46%, 7.99%)');
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hwb().string(2), 'hwb(164.5, 4.12%, 88.14%)');
});

it('Output to string (rounding to 3)', () => {
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hsl().string(3), 'hsl(164.499, 48.46%, 7.99%)');
deepEqual(Color.hsl(164.4992, 48.46, 7.99).hwb().string(3), 'hwb(164.499, 4.118%, 88.138%)');
deepEqual(Color.hsl(165, 50, 8).hwb().string(3), 'hwb(165, 4%, 88%)'); // No superfluous zeros.
});

it('Output to string (rgba)', () => {
equal(Color.rgb(10, 30, 25, 0.5).toString(), 'rgba(10, 30, 25, 0.5)');
});

it('Output to string (hsl)', () => {
equal(Color.hsl(165, 50, 8).toString(), 'hsl(165, 50%, 8%)');
});

it('Output to string (hsla)', () => {
equal(Color.hsl(165, 50, 8, 0.3).toString(), 'hsla(165, 50%, 8%, 0.3)');
});

it('Output to string (hwb)', () => {
equal(Color.hwb(165, 4, 88).toString(), 'hwb(165, 4%, 88%)');
});

it('Output to string (hwb with alpha)', () => {
equal(Color.hwb(165, 4, 88, 0.7).toString(), 'hwb(165, 4%, 88%, 0.7)');
});

it('Output to string (toString and string consistency)', () => {
const color = Color.rgb(255, 128, 64, 0.8);
equal(color.toString(), color.string());
});

it('Array getters', () => {
deepEqual(Color({
r: 10,
Expand Down Expand Up @@ -788,3 +840,95 @@ it('Should parse alphas in RGBA hex notation correctly', () => {
Color('#000000aa').alpha(),
);
});

it('Should round to integers with round', () => {
deepEqual(Color.rgb(10.7, 30.2, 25.9).round().rgb().object(), {
r: 11,
g: 30,
b: 26,
});

deepEqual(Color.rgb(255.4, 128.6, 0.1).round().rgb().object(), {
r: 255,
g: 129,
b: 0,
});

deepEqual(Color.rgb(10.7, 30.2, 25.9, 0.456).round().rgb().object(), {
r: 11,
g: 30,
b: 26,
alpha: 0.456,
});

deepEqual(Color.hsl(164.7, 48.3, 7.8).round().hsl().object(), {
h: 165,
s: 48,
l: 8,
});

deepEqual(Color.hsv(164.7, 48.3, 7.8).round().hsv().object(), {
h: 165,
s: 48,
v: 8,
});

deepEqual(Color.hwb(164.7, 48.3, 7.8).round().hwb().object(), {
h: 165,
w: 48,
b: 8,
});

deepEqual(Color.cmyk(10.7, 30.2, 25.9, 15.4).round().cmyk().object(), {
c: 11,
m: 30,
y: 26,
k: 15,
});
});

it('Should round deterministically', () => {
const color = Color.rgb(10.7, 30.2, 25.9);
const rounded1 = color.round();
const rounded2 = color.round();
ok(rounded1 !== rounded2);
deepEqual(rounded1.rgb().object(), rounded2.rgb().object());
});

it('Should return arbitrary-precision strings with toFixed (precision 0)', () => {
deepEqual(Color.rgb(10.789, 30.234, 25.567).toFixed(0), ['11', '30', '26']);
});

it('Should return arbitrary-precision strings with toFixed (precision 1, default)', () => {
deepEqual(Color.rgb(10.789, 30.234, 25.567).toFixed(1), ['10.8', '30.2', '25.6']);

// eslint-disable-next-line unicorn/require-number-to-fixed-digits-argument
deepEqual(Color.rgb(10.789, 30.234, 25.567).toFixed(), ['10.8', '30.2', '25.6']);
});

it('Should return arbitrary-precision strings with toFixed (precision 2)', () => {
deepEqual(Color.rgb(10.789, 30.234, 25.567).toFixed(2), ['10.79', '30.23', '25.57']);
});

it('Should return arbitrary-precision strings with toFixed (precision 3)', () => {
deepEqual(Color.rgb(10.789, 30.234, 25.567).toFixed(3), ['10.789', '30.234', '25.567']);
});

it('Should remove trailing zeros when using toFixed', () => {
// eslint-disable-next-line unicorn/no-zero-fractions
deepEqual(Color.rgb(10.0, 30.100, 25.000).toFixed(2), ['10', '30.1', '25']);
});

it('Should return arbitrary-precision strings with toFixed regardless of the color model', () => {
deepEqual(Color.hsl(164.789, 48.234, 7.567).toFixed(1), ['164.8', '48.2', '7.6']);
deepEqual(Color.hsv(164.789, 48.234, 7.567).toFixed(2), ['164.79', '48.23', '7.57']);
deepEqual(Color.hwb(164.789, 48.234, 7.567).toFixed(0), ['165', '48', '8']);
deepEqual(Color.cmyk(10.789, 30.234, 25.567, 15.123).toFixed(1), ['10.8', '30.2', '25.6', '15.1']);
});

it('Should not mutate the original color when calling toFixed', () => {
const color = Color.rgb(10.789, 30.234, 25.567);
const fixed = color.toFixed(2);
deepEqual(fixed, ['10.79', '30.23', '25.57']);
equal(color.red(), 10.789);
});
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载