better-color-tools
Advanced tools
Comparing version 0.0.1 to 0.0.2
# better-color-tools | ||
## 0.0.2 | ||
### Patch Changes | ||
- 181de13: Improve HSL accuracy by standardizing to 2 decimal places for all 3 values (before, hue had 3; saturation & lightness had 1) | ||
## 0.0.1 | ||
### Patch Changes | ||
- ce992b2: Fixed hex and HSL bugs |
@@ -141,10 +141,30 @@ import NP from 'number-precision'; | ||
const hsl = [0, 0, 0, 1]; | ||
const values = strVal | ||
.replace(/hsl\s*\(/i, '') | ||
let [h, s, l, a] = strVal | ||
.replace(/hsla?\s*\(/i, '') | ||
.replace(/\)\s*$/, '') | ||
.split(','); | ||
hsl[0] = parseFloat(values[0]); | ||
hsl[1] = values[1].includes('%') ? parseFloat(values[1]) / 100 : parseFloat(values[1]); | ||
hsl[2] = values[2].includes('%') ? parseFloat(values[2]) / 100 : parseFloat(values[2]); | ||
hsl[3] = parseFloat(values[3] || '1'); | ||
hsl[0] = parseFloat(h); | ||
if ((s.includes('%') && !l.includes('%')) || (!s.includes('%') && l.includes('%'))) | ||
throw new Error(`Mix of % and normalized (0–1) values in "${strVal}", prefer one or the other.`); | ||
if (s.includes('%')) { | ||
const sVal = parseFloat(s) / 100; | ||
const lVal = parseFloat(l) / 100; | ||
if (sVal < 0 || sVal > 100) | ||
throw new Error(`Saturation out of bounds for "${strVal}", must be between 0% and 100%`); | ||
if (lVal < 0 || lVal > 100) | ||
throw new Error(`Lightness out of bounds for "${strVal}", must be between 0% and 100%`); | ||
hsl[1] = sVal; | ||
hsl[2] = lVal; | ||
} | ||
else { | ||
const sVal = parseFloat(s); | ||
const lVal = parseFloat(l); | ||
if (sVal < 0 || sVal > 1) | ||
throw new Error(`Saturation out of bounds for "${strVal}", must be between 0 and 1 (or be a %)`); | ||
if (lVal < 0 || lVal > 1) | ||
throw new Error(`Lightness out of bounds for "${strVal}", must be between 0 and 1 (or be a %)`); | ||
hsl[1] = sVal; | ||
hsl[2] = lVal; | ||
} | ||
hsl[3] = parseFloat(a || '1'); | ||
const color = hslToRGB(hsl); | ||
@@ -158,8 +178,4 @@ return validate(color); | ||
throw new Error(`Unable to parse color "${rawColor}"`); | ||
const color = [ | ||
parseInt(values[0], 10), | ||
parseInt(values[1], 10), | ||
parseInt(values[2], 10), | ||
parseFloat((values[3] || '1').trim()), // a | ||
]; | ||
const [r, g, b, a] = values; | ||
const color = [parseInt(r, 10), parseInt(g, 10), parseInt(b, 10), parseFloat((a || '1').trim())]; | ||
return validate(color); | ||
@@ -268,3 +284,3 @@ } | ||
if (M == m) | ||
return [H, S, NP.round(L / 255, 3), A]; | ||
return [H, S, NP.round(L / 255, 4), A]; | ||
// if any other color, calculate hue & saturation | ||
@@ -293,3 +309,3 @@ const C = M - m; | ||
} | ||
return [NP.round(H, 3), NP.round(S, 3), NP.round(L / 255, 3), NP.round(A, 3)]; | ||
return [NP.round(H, 2), NP.round(S, 4), NP.round(L / 255, 4), A]; | ||
} | ||
@@ -296,0 +312,0 @@ export default { |
{ | ||
"name": "better-color-tools", | ||
"description": "Better color manipulation for Sass and JavaScript / TypeScript.", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"author": { | ||
@@ -28,9 +28,2 @@ "name": "Drew Powers", | ||
"types": "./dist/index.d.ts", | ||
"scripts": { | ||
"build": "rm -rf dist && tsc", | ||
"changeset": "changeset", | ||
"dev": "tsc -w", | ||
"lint": "eslint \"**/*.{js,ts}\"", | ||
"test": "mocha --parallel" | ||
}, | ||
"dependencies": { | ||
@@ -52,3 +45,11 @@ "number-precision": "^1.5.1" | ||
"typescript": "^4.5.2" | ||
}, | ||
"scripts": { | ||
"build": "rm -rf dist && tsc", | ||
"changeset": "changeset", | ||
"dev": "tsc -w", | ||
"lint": "eslint \"**/*.{js,ts}\"", | ||
"pretest": "npm run build", | ||
"test": "mocha --parallel" | ||
} | ||
} | ||
} |
@@ -59,4 +59,7 @@ # better-color-tools | ||
// mix color (0 = pure color1, 0.5 = even blend, 1 = pure color2) | ||
$mix: color.mix(#1a7f37, #cf222e, 0.4); | ||
$mix: color.mix(#1a7f37, #cf222e, 0); // 100% color 1, 0% color 2 | ||
$mix: color.mix(#1a7f37, #cf222e, 0.25); // 75%, 25% | ||
$mix: color.mix(#1a7f37, #cf222e, 0.5); // 50%, 50% | ||
$mix: color.mix(#1a7f37, #cf222e, 0.75); // 25%, 75% | ||
$mix: color.mix(#1a7f37, #cf222e, 1); // 0%, 100% | ||
``` | ||
@@ -66,4 +69,4 @@ | ||
⚠️ Still in development. It’s important to note that Sass’ new [`color.scale()`][sass-color-scale] tool is pretty advanced, and is actually good way to lighten / darken colors (previous methods have been lacking). Right now Sass’ `color.scale()` produces | ||
better results than this library, and I’m not happy with that 🙂. | ||
⚠️ Still in development. It’s important to note that Sass’ new [`color.scale()`][sass-color-scale] utility is now a fantastic way to lighten / darken colors (previous attempts had been lacking). `color.scale()` produces better results than this library, | ||
currently, and I’m not happy with that 🙂. | ||
@@ -73,7 +76,9 @@ ```scss | ||
// 0 = current color, 1 = white, -1 = black | ||
$lighter: color.lighten(#cf222e, 0.25); | ||
$lighter: color.lighten(#cf222e, 0); // 0% lighter (original color) | ||
$lighter: color.lighten(#cf222e, 0.25); // 25% lighter | ||
$lighter: color.lighten(#cf222e, 1); // 100% lighter (pure white) | ||
// 0 = current color, 1 = black, -1 = white | ||
$darker: color.darken(#cf222e, 0.25); | ||
$darker: color.darken(#cf222e, 0); // 0% darker (original color) | ||
$darker: color.darken(#cf222e, 0.25); // 25% darker | ||
$darker: color.darken(#cf222e, 1); // 100% darker (pure black) | ||
``` | ||
@@ -87,7 +92,12 @@ | ||
_Note: you’ll see `0xcf222e` in the examples which is just another way of writing `'#cf222e'`. It’s just replacing the `#` with `0x`. Use what you prefer!_ | ||
```ts | ||
import color from 'better-color-tools'; | ||
// mix color (0.5 = even blend, 0 = color 1, 1 = color 2) | ||
const mix = color.mix('#1a7f37', '#cf222e', 0.4); | ||
const mix = color.mix(0x1a7f37, 0xcf222e, 0); // 100% color 1, 0% color 2 | ||
const mix = color.mix(0x1a7f37, 0xcf222e, 0.25); // 75%, 25% | ||
const mix = color.mix(0x1a7f37, 0xcf222e, 0.5); // 50%, 50% | ||
const mix = color.mix(0x1a7f37, 0xcf222e, 0.75); // 25%, 75% | ||
const mix = color.mix(0x1a7f37, 0xcf222e, 1); // 0%, 100% | ||
``` | ||
@@ -102,7 +112,9 @@ | ||
// 1 = white, 0 = current color, -1 = black | ||
const lighter = color.lighten('#cf222e', 0.5); | ||
color.lighten(0xcf222e, 0); // 0% lighter (original color) | ||
color.lighten(0xcf222e, 0.25); // 25% lighter | ||
color.lighten(0xcf222e, 1); // 100% lighter (pure white) | ||
// 1 = black, 0 = current color, -1 = white | ||
const darker = color.darken('#cf222e', 0.5); | ||
color.darken(0xcf222e, 0); // 0% darker (original color) | ||
color.darken(0xcf222e, 0.25); // 25% darker | ||
color.darken(0xcf222e, 1); // 100% darker (pure black) | ||
``` | ||
@@ -114,21 +126,19 @@ | ||
It’s in HSL handling where approaches differ. Because HSL is a smaller color space than RGB, in order to use it, it **requires some use of decimals.** So any library that rounds out-of-the-box will yield different results. | ||
It’s in HSL handling where approaches differ. Because HSL is a smaller color space than RGB, in order to use it, it **requires at least 1 decimal place.** So any library that rounds out-of-the-box will produce inaccurate results (compare this library to | ||
[color-convert] converting from RGB -> HSL and back again): | ||
Compare this library to [color-convert], converting from RGB -> HSL -> RGB | ||
```ts | ||
color.from(color.from([167, 214, 65]).hsl).rgbVal; // ✅ [167, 214, 65] | ||
convert.hsl.rgb(convert.rgb.hsl(167, 214, 65)); // ❌ [168, 215, 66] | ||
``` | ||
The reason, again, is rounding by default. This is a [known limitation of HSL][hsl], so many libraries can disable rounding with overrides, but in addition to that not being default behavior it also produces noisy results: | ||
```ts | ||
const original = [167, 214, 65]; | ||
color.from(color.from(original).hsl).rgb; // ✅ [167, 214, 65] | ||
convert.hsl.rgb(convert.rgb.hsl(...original)); // ❌ [168, 215, 66] | ||
color.from([167, 214, 65]).hsl; // hsl(78.93, 64.5%, 54.71%) | ||
convert.rgb.hsl.raw([167, 214, 65]); // hsl(78.9261744966443, 64.50216450216452%, 54.70588235294118%) | ||
``` | ||
2 things are the cause of the difference: | ||
This library takes the opinion that **HSL should have RGB precision by default.** So this library generates values that support infinite conversions without quality loss that are still readable. | ||
1. **JavaScript’s rounding errors.** Many implementations are borrowed from other languages that don’t have JavaScript’s “[bad math][number-precision].” This implementation was written with JavaScript in mind and minimizes decimal calculations through | ||
tricks like normalizing to `255` rather than `1.` | ||
2. **No rounding by default.** As stated before, **HSL requires decimal places** to produce the full RGB color space. When a library rounds by default it will always make HSL conversions inaccurate. This is a known limitation, so libraries like | ||
color-convert will allow you to use decimals. But that generates numbers like `hsl(78.9261744966443, 64.50216450216452%, 54.70588235294118%)`. Compare that to better-color-tools: `hsl(78.926, 64.5%, 54.7%)`. Why do you have to choose between accuracy | ||
and utility? | ||
#### Usage | ||
@@ -155,5 +165,5 @@ | ||
// convert color to hsl | ||
color.from('#C4432B').hsl; // 'hsl(9.412, 64%, 46.9%, 1)' | ||
color.from(0xc4432b).hsl; // 'hsl(9.412, 64%, 46.9%, 1)' | ||
color.from('#C4432B').hslVal; // [9.412, 0.64, 0.469, 1] | ||
color.from('#C4432B').hsl; // 'hsl(9.41, 64.02%, 46.86%, 1)' | ||
color.from(0xc4432b).hsl; // 'hsl(9.41, 64.02%, 46.86%, 1)' | ||
color.from('#C4432B').hslVal; // [9.41, 0.6402, 0.4686, 1] | ||
@@ -164,6 +174,12 @@ // convert color names to hex | ||
## TODO / Roadmap | ||
- Adding color spaces like [Adobe](https://en.wikipedia.org/wiki/Adobe_RGB_color_space) and [Rec 709](https://en.wikipedia.org/wiki/Rec._709) to allow color mixing and lightening/darkening to use different perceptual color algorithms | ||
- This library currently only supports 8-bit RGB (web & apps); is 16-bit useful? (create an issue!) | ||
[color-convert]: https://github.com/Qix-/color-convert | ||
[computer-color]: https://www.youtube.com/watch?v=LKnqECcg6Gw&vl=en | ||
[hsl]: https://en.wikipedia.org/wiki/HSL_and_HSV#Disadvantages | ||
[number-precision]: https://github.com/nefe/number-precision | ||
[sass-color]: https://sass-lang.com/documentation/modules/color | ||
[sass-color-scale]: https://sass-lang.com/documentation/modules/color#scale |
@@ -143,10 +143,24 @@ import NP from 'number-precision'; | ||
const hsl: HSL = [0, 0, 0, 1]; | ||
const values = strVal | ||
.replace(/hsl\s*\(/i, '') | ||
let [h, s, l, a] = strVal | ||
.replace(/hsla?\s*\(/i, '') | ||
.replace(/\)\s*$/, '') | ||
.split(','); | ||
hsl[0] = parseFloat(values[0]); | ||
hsl[1] = values[1].includes('%') ? parseFloat(values[1]) / 100 : parseFloat(values[1]); | ||
hsl[2] = values[2].includes('%') ? parseFloat(values[2]) / 100 : parseFloat(values[2]); | ||
hsl[3] = parseFloat(values[3] || '1'); | ||
hsl[0] = parseFloat(h); | ||
if ((s.includes('%') && !l.includes('%')) || (!s.includes('%') && l.includes('%'))) throw new Error(`Mix of % and normalized (0–1) values in "${strVal}", prefer one or the other.`); | ||
if (s.includes('%')) { | ||
const sVal = parseFloat(s) / 100; | ||
const lVal = parseFloat(l) / 100; | ||
if (sVal < 0 || sVal > 100) throw new Error(`Saturation out of bounds for "${strVal}", must be between 0% and 100%`); | ||
if (lVal < 0 || lVal > 100) throw new Error(`Lightness out of bounds for "${strVal}", must be between 0% and 100%`); | ||
hsl[1] = sVal; | ||
hsl[2] = lVal; | ||
} else { | ||
const sVal = parseFloat(s); | ||
const lVal = parseFloat(l); | ||
if (sVal < 0 || sVal > 1) throw new Error(`Saturation out of bounds for "${strVal}", must be between 0 and 1 (or be a %)`); | ||
if (lVal < 0 || lVal > 1) throw new Error(`Lightness out of bounds for "${strVal}", must be between 0 and 1 (or be a %)`); | ||
hsl[1] = sVal; | ||
hsl[2] = lVal; | ||
} | ||
hsl[3] = parseFloat(a || '1'); | ||
const color = hslToRGB(hsl); | ||
@@ -159,8 +173,4 @@ return validate(color); | ||
if (values.length != 3 && values.length != 4) throw new Error(`Unable to parse color "${rawColor}"`); | ||
const color: RGBA = [ | ||
parseInt(values[0], 10), // r | ||
parseInt(values[1], 10), // g | ||
parseInt(values[2], 10), // b | ||
parseFloat((values[3] || '1').trim()), // a | ||
]; | ||
const [r, g, b, a] = values; | ||
const color: RGBA = [parseInt(r, 10), parseInt(g, 10), parseInt(b, 10), parseFloat((a || '1').trim())]; | ||
return validate(color); | ||
@@ -271,3 +281,3 @@ } | ||
// if white/black/gray, exit early | ||
if (M == m) return [H, S, NP.round(L / 255, 3), A]; | ||
if (M == m) return [H, S, NP.round(L / 255, 4), A]; | ||
@@ -300,3 +310,3 @@ // if any other color, calculate hue & saturation | ||
return [NP.round(H, 3), NP.round(S, 3), NP.round(L / 255, 3), NP.round(A, 3)]; | ||
return [NP.round(H, 2), NP.round(S, 4), NP.round(L / 255, 4), A]; | ||
} | ||
@@ -303,0 +313,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
145932
19
1191
178