Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

better-color-tools

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

better-color-tools - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

dist/index.min.js

6

CHANGELOG.md
# better-color-tools
## 0.2.0
### Minor Changes
- 90b85ee: Improve mix(), lighten(), and darken()
## 0.1.0

@@ -4,0 +10,0 @@

15

dist/index.d.ts

@@ -5,3 +5,3 @@ export declare type RGB = [number, number, number];

export declare type P3 = [number, number, number, number];
export declare type Color = string | number | RGB | RGBA;
export declare type Color = string | number | RGB | RGBA | HSL | P3;
export declare type ColorOutput = {

@@ -35,9 +35,10 @@ hex: string;

/**
* Mix colors using more-correct equation
* Mix colors with gamma correction
* @param {Color} color1
* @param {Color} color2
* @param {number} weight
* Explanation: https://www.youtube.com/watch?v=LKnqECcg6Gw
* @param {number} gamma {default: 2.2}
* https://observablehq.com/@sebastien/srgb-rgb-gamma
*/
export declare function mix(color1: Color, color2: Color, weight?: number): RGBA;
export declare function mix(color1: Color, color2: Color, weight?: number, γ?: number): ColorOutput;
/** Convert any number of user inputs into RGBA array */

@@ -51,3 +52,3 @@ export declare function parse(rawColor: Color): RGBA;

*/
export declare function alpha(rawColor: Color, value: number): RGBA;
export declare function alpha(rawColor: Color, value: number): ColorOutput;
/**

@@ -60,3 +61,3 @@ * Darken

*/
export declare function darken(color: Color, value: number): RGBA;
export declare function darken(color: Color, value: number): ColorOutput;
/**

@@ -69,3 +70,3 @@ * Lighten

*/
export declare function lighten(color: Color, value: number): RGBA;
export declare function lighten(color: Color, value: number): ColorOutput;
/**

@@ -72,0 +73,0 @@ * HSL to RGB

@@ -9,6 +9,6 @@ import NP from 'number-precision';

export const HEX_RE = /^#?[0-9a-f]{3,8}$/i;
export const RGB_RE = new RegExp(`^rgba?\\(\\s*(?<R>${FLOAT})${COMMA}(?<G>${FLOAT})${COMMA}(?<B>${FLOAT})(${COMMA}(?<A>${FLOAT}))?\\s*\\)$`, 'i');
export const HSL_RE = new RegExp(`^hsla?\\(\\s*(?<H>${FLOAT})${COMMA}(?<S>${FLOAT})%${COMMA}(?<L>${FLOAT})%(${COMMA}(?<A>${FLOAT}))?\\s*\\)$`, 'i');
export const P3_RE = new RegExp(`^color\\(\\s*display-p3\\s+(?<R>${FLOAT})\\s+(?<G>${FLOAT})\\s+(?<B>${FLOAT})(\\s*\\/\\s*(?<A>${FLOAT}))?\\s*\\)$`, 'i');
const { plus, minus, times, divide, round, strip } = NP;
export const RGB_RE = new RegExp(['^rgba?\\(\\s*', `(?<R>${FLOAT}%?)`, COMMA, `(?<G>${FLOAT}%?)`, COMMA, `(?<B>${FLOAT}%?)`, `(${COMMA}(?<A>${FLOAT}%?))?`, '\\s*\\)$'].join(''), 'i');
export const HSL_RE = new RegExp(['^hsla?\\(\\s*', `(?<H>${FLOAT})`, COMMA, `(?<S>${FLOAT})%`, COMMA, `(?<L>${FLOAT})%`, `(${COMMA}(?<A>${FLOAT})%?)?`, '\\s*\\)$'].join(''), 'i');
export const P3_RE = new RegExp(['^color\\(\\s*display-p3\\s+', `(?<R>${FLOAT}%?)`, '\\s+', `(?<G>${FLOAT}%?)`, '\\s+', `(?<B>${FLOAT}%?)`, `(\\s*\\/\\s*(?<A>${FLOAT}%?))?`, '\\s*\\)$'].join(''), 'i');
const { round, strip } = NP;
/**

@@ -32,4 +32,4 @@ * Parse any valid CSS color color string and convert to:

if (n < 3)
return leftPad(round(v, 0).toString(16), 2);
return v < 1 ? round(times(255, v), 0).toString(16) : '';
return leftPad(round(v * 255, 0).toString(16), 2);
return v < 1 ? round(v * 255, 0).toString(16) : '';
})

@@ -41,4 +41,4 @@ .join('')}`;

if (n < 3)
return leftPad(round(v, 0).toString(16), 2);
return v < 1 ? leftPad(times(256, v).toString(16), 2) : '';
return leftPad(round(v * 255, 0).toString(16), 2);
return v < 1 ? leftPad((v * 256).toString(16), 2) : '';
});

@@ -49,6 +49,6 @@ return parseInt(`0x${hex.join('')}`, 16);

if (color[3] == 1) {
return `rgb(${round(color[0], 0)}, ${round(color[1], 0)}, ${round(color[2], 0)})`;
return `rgb(${round(color[0] * 255, 0)}, ${round(color[1] * 255, 0)}, ${round(color[2] * 255, 0)})`;
}
else {
return `rgba(${round(color[0], 0)}, ${round(color[1], 0)}, ${round(color[2], 0)}, ${round(color[3], P)})`;
return `rgba(${round(color[0] * 255, 0)}, ${round(color[1] * 255, 0)}, ${round(color[2] * 255, 0)}, ${round(color[3], P)})`;
}

@@ -58,3 +58,3 @@ },

get rgba() {
return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`;
return `rgba(${round(color[0] * 255, 0)}, ${round(color[1] * 255, 0)}, ${round(color[2] * 255, 0)}, ${round(color[3], P)})`;
},

@@ -64,3 +64,3 @@ rgbaVal: color,

const [h, s, l, a] = rgbToHSL(color);
return `hsl(${h}, ${strip(times(s, 100))}%, ${strip(times(l, 100))}%, ${round(a, P)})`;
return `hsl(${h}, ${strip(s * 100)}%, ${strip(l * 100)}%, ${round(a, P)})`;
},

@@ -72,3 +72,3 @@ get hslVal() {

const [r, g, b, a] = color;
return `color(display-p3 ${round(divide(r, 255), P)} ${round(divide(g, 255), P)} ${round(divide(b, 255), P)}${a < 1 ? `/${round(a, P)}` : ''})`;
return `color(display-p3 ${round(r, P)} ${round(g, P)} ${round(b, P)}${a < 1 ? `/${round(a, P)}` : ''})`;
},

@@ -78,15 +78,32 @@ };

/**
* Mix colors using more-correct equation
* Mix colors with gamma correction
* @param {Color} color1
* @param {Color} color2
* @param {number} weight
* Explanation: https://www.youtube.com/watch?v=LKnqECcg6Gw
* @param {number} gamma {default: 2.2}
* https://observablehq.com/@sebastien/srgb-rgb-gamma
*/
export function mix(color1, color2, weight = 0.5) {
export function mix(color1, color2, weight = 0.5, γ = 2.2) {
const w = clamp(weight, 0, 1);
const w1 = minus(1, w);
const w1 = 1 - w;
const w2 = w;
const [r1, g1, b1, a1] = from(color1).rgbVal;
const [r2, g2, b2, a2] = from(color2).rgbVal;
return [round(plus(times(r1 ** 2, w1), times(r2 ** 2, w2)), 0), round(plus(times(g1 ** 2, w1), times(g2 ** 2, w2)), 0), round(plus(times(b1 ** 2, w1), times(b2 ** 2, w2)), 0), round(plus(times(a1, w1), times(a2, w2)), 0)];
const tf = 1 / γ; // transfer function (https://en.wikipedia.org/wiki/SRGB#Transfer_function_(%22gamma%22))
const itf = γ; // inverse transfer function
// expanded gamma
const c1 = from(color1).rgbVal;
const c2 = from(color2).rgbVal;
const r1 = c1[0] ** itf;
const g1 = c1[1] ** itf;
const b1 = c1[2] ** itf;
const a1 = c1[3];
const r2 = c2[0] ** itf;
const g2 = c2[1] ** itf;
const b2 = c2[2] ** itf;
const a2 = c2[3];
return from([
clamp((r1 ** itf * w1 + r2 ** itf * w2) ** tf, 0, 1),
clamp((g1 ** itf * w1 + g2 ** itf * w2) ** tf, 0, 1),
clamp((b1 ** itf * w1 + b2 ** itf * w2) ** tf, 0, 1),
a1 * w1 + a2 * w2, // a
]);
}

@@ -99,6 +116,6 @@ /** Convert any number of user inputs into RGBA array */

return [
parseInt(hexStr.substring(0, 2), 16),
parseInt(hexStr.substring(2, 4), 16),
parseInt(hexStr.substring(4, 6), 16),
round(divide(parseInt(hexStr.substring(6, 8) || 'ff', 16), 255), P), // a
parseInt(hexStr.substring(0, 2), 16) / 255,
parseInt(hexStr.substring(2, 4), 16) / 255,
parseInt(hexStr.substring(4, 6), 16) / 255,
parseInt(hexStr.substring(6, 8) || 'ff', 16) / 255, // a
];

@@ -112,14 +129,7 @@ }

throw new Error(`Expected [R, G, B, A?], received ${rawColor}`);
// create new array & copy values
const color = [0, 0, 0, 1]; // note: alpha defaults to 1
for (let n = 0; n < rawColor.length; n++) {
const v = rawColor[n];
color[n] = clamp(v, 0, n == 3 ? 1 : 255);
}
return color;
return rawColor.map((v) => clamp(v, 0, 1));
}
// 0xff0000 (number)
if (typeof rawColor == 'number') {
const color = parseHexVal(rawColor);
return color;
return parseHexVal(rawColor);
}

@@ -132,5 +142,4 @@ // '#ff0000' / 'red' / 'rgb(255, 0, 0)' / 'hsl(0, 1, 1)'

// named color
if (cssNames[strVal]) {
const color = parseHexVal(cssNames[strVal]);
return color;
if (typeof cssNames[strVal.toLowerCase()] === 'number') {
return parseHexVal(cssNames[strVal]);
}

@@ -146,4 +155,3 @@ // hex

: hex, 16);
const color = parseHexVal(hexNum);
return color;
return parseHexVal(hexNum);
}

@@ -153,22 +161,28 @@ // rgb

const v = RGB_RE.exec(strVal).groups || {};
const color = [clamp(parseFloat(v.R), 0, 255), clamp(parseFloat(v.G), 0, 255), clamp(parseFloat(v.B), 0, 255), v.A ? clamp(parseFloat(v.A), 0, 1) : 1];
return color;
if (![v.R, v.G, v.B].every((c) => c.includes('%') || !c.includes('%')))
throw new Error('Mix of integers and %');
let r = parseFloat(v.R) / (v.R.includes('%') ? 100 : 255);
let g = parseFloat(v.G) / (v.G.includes('%') ? 100 : 255);
let b = parseFloat(v.B) / (v.B.includes('%') ? 100 : 255);
let a = 1;
if (v.A) {
a = parseFloat(v.A);
if (v.A.includes('%'))
a /= 100;
}
return [clamp(r, 0, 1), clamp(g, 0, 1), clamp(b, 0, 1), clamp(a, 0, 1)];
}
// hsl
if (HSL_RE.test(strVal)) {
const hsl = [0, 0, 0, 1];
const v = HSL_RE.exec(strVal).groups || {};
hsl[0] = parseFloat(v.H);
const isPerc = strVal.includes('%');
let sVal = parseFloat(v.S);
let lVal = parseFloat(v.L);
if (isPerc)
sVal /= 100;
if (isPerc)
lVal /= 100;
hsl[1] = clamp(sVal, 0, 1);
hsl[2] = clamp(lVal, 0, 1);
hsl[3] = v.A ? parseFloat(v.A) : 1;
const color = hslToRGB(hsl);
return color;
let h = parseFloat(v.H);
let s = parseFloat(v.S) / 100;
let l = parseFloat(v.L) / 100;
let a = 1;
if (v.A) {
a = parseFloat(v.A);
if (v.A.includes('%'))
a /= 100;
}
return hslToRGB([h, clamp(s, 0, 1), clamp(l, 0, 1), clamp(a, 0, 1)]);
}

@@ -178,3 +192,18 @@ // p3

const v = P3_RE.exec(strVal).groups || {};
return [times(clamp(parseFloat(v.R), 0, 1), 255), times(clamp(parseFloat(v.G), 0, 1), 255), times(clamp(parseFloat(v.B), 0, 1), 255), v.A ? clamp(parseFloat(v.A), 0, 1) : 1];
let r = parseFloat(v.R);
if (v.R.includes('%'))
r /= 100;
let g = parseFloat(v.G);
if (v.G.includes('%'))
g /= 100;
let b = parseFloat(v.B);
if (v.B.includes('%'))
b /= 100;
let a = 1;
if (v.A) {
a = parseFloat(v.A);
if (v.A.includes('%'))
a /= 100;
}
return [clamp(r, 0, 1), clamp(g, 0, 1), clamp(b, 0, 1), clamp(a, 0, 1)];
}

@@ -191,6 +220,4 @@ }

export function alpha(rawColor, value) {
if (!(value >= 0 && value <= 1))
throw new Error(`Alpha must be between 0 and 1, received ${alpha}`);
const c = parse(rawColor);
return [c[0], c[1], c[2], value];
return from([c[0], c[1], c[2], clamp(value, 0, 1)]);
}

@@ -205,9 +232,8 @@ /**

export function darken(color, value) {
if (!(value >= -1 && value <= 1))
throw new Error(`Value must be between -1 and 1, received ${value}`);
if (value >= 0) {
return mix(color, [0, 0, 0, 1], value);
const amt = clamp(value, -1, 1);
if (amt >= 0) {
return mix(color, [0, 0, 0, 1], amt);
}
else {
return lighten(color, -value);
return lighten(color, -amt);
}

@@ -223,9 +249,8 @@ }

export function lighten(color, value) {
if (!(value >= -1 && value <= 1))
throw new Error(`Value must be between -1 and 1, received ${value}`);
if (value >= 0) {
return mix(color, [255, 255, 255, 1], value);
const amt = clamp(value, -1, 1);
if (amt >= 0) {
return mix(color, [1, 1, 1, 1], amt);
}
else {
return darken(color, -value);
return darken(color, -amt);
}

@@ -240,4 +265,4 @@ }

H = Math.abs(H % 360); // allow < 0 and > 360
const C = times(S, minus(1, Math.abs(minus(times(2, L), 1))));
const X = times(C, minus(1, Math.abs(minus(divide(H, 60) % 2, 1))));
const C = S * (1 - Math.abs(2 * L - 1));
const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
let R = 0;

@@ -270,4 +295,4 @@ let G = 0;

}
const m = minus(L, divide(C, 2));
return [round(times(plus(R, m), 255), P), round(times(plus(G, m), 255), P), round(times(plus(B, m), 255), P), round(A, P)];
const m = L - C / 2;
return [round(R + m, P), round(G + m, P), round(B + m, P), round(A, P)];
}

@@ -285,8 +310,8 @@ /**

let S = 0;
let L = divide(plus(M, m), 2); // default: standard HSL (“fill”) calculation
let L = (M + m) / 2; // default: standard HSL (“fill”) calculation
// if white/black/gray, exit early
if (M == m)
return [H, S, NP.round(divide(L, 255), 4), A];
return [H, S, NP.round(L, 4), A];
// if any other color, calculate hue & saturation
const C = minus(M, m);
const C = M - m;
// Hue

@@ -296,9 +321,9 @@ if (C != 0) {

case R:
H = divide(times(60, minus(G, B)), C);
H = (60 * (G - B)) / C;
break;
case G:
H = times(60, plus(2, divide(minus(B, R), C)));
H = 60 * (2 + (B - R) / C);
break;
case B:
H = times(60, plus(4, divide(minus(R, G), C)));
H = 60 * (4 + (R - G) / C);
break;

@@ -312,5 +337,5 @@ }

if (L != 0 && L != 1) {
S = divide(minus(M, L), Math.min(L, minus(255, L)));
S = (M - L) / Math.min(L, 1 - L);
}
return [round(H, P - 2), round(S, P), round(divide(L, 255), P), A];
return [round(H, P - 2), round(S, P), round(L, P), A];
}

@@ -317,0 +342,0 @@ export default {

{
"name": "better-color-tools",
"description": "Better color manipulation for Sass and JavaScript / TypeScript.",
"version": "0.1.0",
"version": "0.2.0",
"author": {

@@ -26,6 +26,7 @@ "name": "Drew Powers",

"sass": "./index.scss",
"main": "./dist/index.js",
"main": "./dist/index.min.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "rm -rf dist && tsc",
"build": "rm -rf dist && tsc && npm run bundle",
"bundle": "esbuild dist/index.js --bundle --minify --outfile=dist/index.min.js --format=esm",
"changeset": "changeset",

@@ -35,8 +36,5 @@ "dev": "tsc -w",

"prepublishOnly": "npm run build",
"pretest": "npm run build",
"start": "npm run build && cp ./dist/index.min.js example && npx serve example",
"test": "mocha --parallel"
},
"dependencies": {
"number-precision": "^1.5.1"
},
"devDependencies": {

@@ -48,2 +46,3 @@ "@changesets/cli": "^2.19.0",

"chai": "^4.3.4",
"esbuild": "^0.14.11",
"eslint": "^8.6.0",

@@ -54,5 +53,7 @@ "eslint-config-prettier": "^8.3.0",

"npm-run-all": "^4.1.5",
"number-precision": "^1.5.1",
"prettier": "^2.5.1",
"sass": "^1.47.0",
"typescript": "^4.5.4"
}
}
# better-color-tools
Better color manipulation for Sass and JavaScript/TypeScript.
Better color manipulation for Sass and JavaScript/TypeScript. Fast (`75,000` ops/s) and lightweight (`3.7 kB` gzip).

@@ -17,48 +17,21 @@ Supports:

## Sass
## Mix
Sass has built-in [color][sass-color] functions, but they aren’t as usable as they could be. Here’s why this library exists as an alternative.
Not all mixing algorithms are created equal. A proper color mixer requires [gamma correction][gamma], something most libraries omit (even including Sass, CSS, and SVG). Compare this library’s gamma-corrected results (top) with most libraries’ default mix
function:
### Mix
![](./.github/images/r-g.png)
Let’s compare this library’s mix function to Sass’ (Sass on top; better-color-tools on bottom):
![](./.github/images/g-b.png)
<table>
<thead>
<th>Blend</th>
<th>Comparison</th>
</thead>
<tbody>
<tr>
<td>red–lime</td>
<td><img src="./.github/images/red-lime-sass.png"><img src="./.github/images/red-lime-better.png"></td>
</tr>
<tr>
<td>red–yellow</td>
<td><img src="./.github/images/red-yellow-sass.png"><img src="./.github/images/red-yellow-better.png"></td>
</tr>
<tr>
<td>blue–yellow</td>
<td><img src="./.github/images/blue-yellow-sass.png"><img src="./.github/images/blue-yellow-better.png"></td>
</tr>
<tr>
<td>blue–fuchsia</td>
<td><img src="./.github/images/blue-fuchsia-sass.png"><img src="./.github/images/blue-fuchsia-better.png"></td>
</tr>
<tr>
<td>blue–lime</td>
<td><img src="./.github/images/blue-lime-sass.png"><img src="./.github/images/blue-lime-better.png"></td>
</tr>
</tbody>
</table>
![](./.github/images/b-y.png)
It may be hard to tell a difference at first, but upon closer inspection you’ll see better results with the bottom colors in each row:
![](./.github/images/k-c.png)
- better-color-utils produces brighter, more vibrant colors when mixing complementary colors, while Sass results in [dark, muddy colors][computer-color] (compare mid tones in all examples)
- better-color-utils gives better spacing between colors while Sass inconsistently clumps certain hues together (compare blues in all examples)
- better-color-utils produces more expected colors than Sass (compare how better-color-tools passes through teal in **blue–lime** while Sass doesn’t)
![](./.github/images/k-w.png)
#### Usage
Notice all the bottom gradients have muddy/grayed-out colors in the middle as well as clumping (colors bunch up around certain shades or hues). But fear not! better-color-utils will always give you those beautiful, perfect color transitions you deserve.
```scss
// Sass
@use 'better-color-tools' as color;

@@ -73,8 +46,38 @@

### Lighten / Darken
```ts
// JavaScript / TypeScript
import color from 'better-color-tools';
⚠️ 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 🙂.
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%
```
_Note: `0xcf222e` in JS is just another way of writing `'#cf222e'` (replacing the `#` with `0x`). Either are valid; use whichever you prefer!_
### Advanced: gamma adjustment
To change the gamma adjustment, you can pass in an optional 4th parameter. The default gamma is `2.2`, but you may adjust it to achieve different results (if unsure, best to always omit this option).
```scss
// Sass
$gamma: 2.2; // default
$mix: color.mix(#1a7f37, #cf222e, 0, $gamma);
```
```ts
// JavaScript / TypeScript
const gamma = 2.2; // default
const mix = color.mix(0x1a7f37, 0xcf222e, 0, gamma);
```
## Lighten / Darken
The lighten and darken methods also use [gamma correction][gamma] for improved results (also better than Sass’ `color.lighten()` and `color.darken()`). This method is _relative_, so no matter what color you start with, `darken(…, 0.5)` will always be
halfway to black, and `lighten(…, 0.5)` will always be halfway to white.
```scss
// Sass
@use 'better-color-tools' as color;

@@ -91,27 +94,6 @@

## JavaScript / TypeScript
### Mix
[View comparison](#mix) (Sass’ mix function is a generic implementation of mixing you’ll find with other libraries in JavaScript)
_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
// JavaScript / TypeScript
import color from 'better-color-tools';
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%
```
### Lighten / Darken
⚠️ In development ([see note](#lighten--darken))
```ts
import color from 'better-color-tools';
color.lighten(0xcf222e, 0); // 0% lighter (original color)

@@ -126,74 +108,39 @@ color.lighten(0xcf222e, 0.25); // 25% lighter

### Conversion
## Convert (JS/TS only)
Color conversion between RGB and hexadecimal is a trivial 1:1 conversion, so this library isn’t better than any other in that regard.
Color conversion between RGB and hexadecimal is a trivial 1:1 conversion, so this library isn’t better than any other in that regard. However, it should be noted that [HSL is lossy when rounding to integers][hsl-rgb], so by default this library persists
HSL decimals to prevent rounding errors when converting back-and-forth.
It’s in HSL handling where approaches differ. Few realize that when using whole numbers in HSL, [it only has 14.5% the colors of RGB][hsl-rgb]. In order to recreate the full RGB spectrum you need **at least 1 decimal place in all H, S, and L values.** For
this reason, any library that uses whole numbers in HSL out-of-the-box will result in distorted colors and quality loss (compare this library to [color-convert] converting from RGB -> HSL and back again):
`color.from()` takes any valid CSS string, hex number, or RGBA array (values normalized to `1`) as an input, and can generate any desired output as a result:
```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
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%)
```
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.
#### Usage
`color.from()` takes any valid CSS string, hex number, or RGBA array as an input, and can generate any desired output as a result:
```ts
import color from 'better-color-tools';
// convert color to hex
color.from('rgb(196, 67, 43)').hex; // '#c4432b'
color.from([196, 67, 43]).hex; // '#c4432b'
color.from('rgb(196, 67, 43)').hexVal; // 0xc4432b
color.from('rebeccapurple').hsl; // 'hsl(270, 50%, 40%)'
```
// convert hex to p3
color.from('rgb(196, 67, 43, 0.8)').p3; // 'color(display-p3 0.76863 0.26275 0.16863/0.8)'
| Output | Type | Example |
| :------- | :--------: | :-------------------------- |
| `hex` | `string` | `"#ffffff"` |
| `hexVal` | `number` | `0xffffff` |
| `rgb` | `string` | `"rgb(255, 255, 255)"` |
| `rgbVal` | `number[]` | `[1, 1, 1, 1]` |
| `rgba` | `string` | `"rgba(255, 255, 255, 1)"` |
| `hsl` | `string` | `"hsl(360, 0%, 100%)"` |
| `hslVal` | `number[]` | `[360, 0, 1, 1]"` |
| `p3` | `string` | `"color(display-p3 1 1 1)"` |
// convert from p3 to hex
color.from('color(display-p3 0.23 0.872 0.918)').hex; // #3bdeea
_Note: although this library can convert FROM a CSS color name, there is no method to convert INTO one (as over 99% of colors have no standardized name). However, you may import `better-color-tools/dist/css-names.js` for an easy-to-use map for your
purposes._
// convert color to rgb
color.from('#C4432B').rgb; // 'rgb(196, 67, 43)'
color.from(0xc4432b).rgb; // 'rgb(196, 67, 43)'
color.from('#C4432B').rgbVal; // [196, 67, 43, 1]
// convert hex to rgba
color.from('#C4432B').rgba; // 'rgba(196, 67, 43, 1)'
color.from(0xc4432b).rgba; // 'rgba(196, 67, 43, 1)'
color.from('#C4432B80').rgbaVal; // [196, 67, 43, 0.5]
// convert color to hsl
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]
// convert hsl to rgb
color.from('hsl(328, 100%, 54%)').rgb; // 'rgb(255, 20, 146)'
// convert color names to hex
color.from('rebeccapurple').hex; // '#663399'
```
#### A note on P3
The [P3 colorspace][p3] is larger than RGB. As a result, many tools apply [gamut matrices](http://endavid.com/index.php?entry=79) to convert RGB to P3 and vice-versa. While that is needed when dealing with image editing software and white-balancing, it’s
unnecessary for the web. Since browsers are better at this conversion (and may improve it over time), this library takes an intentional “hands-off” approach where P3 is equated with **Ideal RGB**, e.g.:
This library converts sRGB to P3 “lazily,” meaning every channel is converted 1:1. This differs from some conversions which attempt to simulate hardware differences. Compare this library to colorjs.io:
| P3 Color | Ideal RGB | colorjs.io |
| :------: | :----------: | :----------: |
| `1 0 0` | ✅ `255 0 0` | ❌ `250 0 0` |
| P3 Color | better-color-tools | colorjs.io |
| :------: | :----------------: | :--------: |
| `1 0 0` | `255 0 0` | `250 0 0` |
This approach produces better color conversion across-the-board by letting the browser make conversions rather than the library, which is also the approach
[recommended by Apple for CSS](https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/). TL;DR this library’s P3 conversion is optimized for web.
For the most part, this approach makes P3 much more usable for web and is even [recommended by Apple for Safari](https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/).

@@ -203,7 +150,8 @@ ## TODO / Roadmap

- **Planned**: 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
- **Planned**: Generate nice, gamma-corrected CSS gradients (with P3 enhancements for Safari)
[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
[hsl-rgb]: https://pow.rs/blog/dont-use-hsl-for-anything/
[gamma]: https://observablehq.com/@sebastien/srgb-rgb-gamma
[number-precision]: https://github.com/nefe/number-precision

@@ -210,0 +158,0 @@ [p3]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color()

@@ -9,3 +9,3 @@ import NP from 'number-precision';

export type P3 = [number, number, number, number];
export type Color = string | number | RGB | RGBA;
export type Color = string | number | RGB | RGBA | HSL | P3;
export type ColorOutput = {

@@ -29,6 +29,6 @@ hex: string;

export const HEX_RE = /^#?[0-9a-f]{3,8}$/i;
export const RGB_RE = new RegExp(`^rgba?\\(\\s*(?<R>${FLOAT})${COMMA}(?<G>${FLOAT})${COMMA}(?<B>${FLOAT})(${COMMA}(?<A>${FLOAT}))?\\s*\\)$`, 'i');
export const HSL_RE = new RegExp(`^hsla?\\(\\s*(?<H>${FLOAT})${COMMA}(?<S>${FLOAT})%${COMMA}(?<L>${FLOAT})%(${COMMA}(?<A>${FLOAT}))?\\s*\\)$`, 'i');
export const P3_RE = new RegExp(`^color\\(\\s*display-p3\\s+(?<R>${FLOAT})\\s+(?<G>${FLOAT})\\s+(?<B>${FLOAT})(\\s*\\/\\s*(?<A>${FLOAT}))?\\s*\\)$`, 'i');
const { plus, minus, times, divide, round, strip } = NP;
export const RGB_RE = new RegExp(['^rgba?\\(\\s*', `(?<R>${FLOAT}%?)`, COMMA, `(?<G>${FLOAT}%?)`, COMMA, `(?<B>${FLOAT}%?)`, `(${COMMA}(?<A>${FLOAT}%?))?`, '\\s*\\)$'].join(''), 'i');
export const HSL_RE = new RegExp(['^hsla?\\(\\s*', `(?<H>${FLOAT})`, COMMA, `(?<S>${FLOAT})%`, COMMA, `(?<L>${FLOAT})%`, `(${COMMA}(?<A>${FLOAT})%?)?`, '\\s*\\)$'].join(''), 'i');
export const P3_RE = new RegExp(['^color\\(\\s*display-p3\\s+', `(?<R>${FLOAT}%?)`, '\\s+', `(?<G>${FLOAT}%?)`, '\\s+', `(?<B>${FLOAT}%?)`, `(\\s*\\/\\s*(?<A>${FLOAT}%?))?`, '\\s*\\)$'].join(''), 'i');
const { round, strip } = NP;

@@ -53,4 +53,4 @@ /**

.map((v, n) => {
if (n < 3) return leftPad(round(v, 0).toString(16), 2);
return v < 1 ? round(times(255, v), 0).toString(16) : '';
if (n < 3) return leftPad(round(v * 255, 0).toString(16), 2);
return v < 1 ? round(v * 255, 0).toString(16) : '';
})

@@ -61,4 +61,4 @@ .join('')}`;

const hex = color.map((v, n) => {
if (n < 3) return leftPad(round(v, 0).toString(16), 2);
return v < 1 ? leftPad(times(256, v).toString(16), 2) : '';
if (n < 3) return leftPad(round(v * 255, 0).toString(16), 2);
return v < 1 ? leftPad((v * 256).toString(16), 2) : '';
});

@@ -69,5 +69,5 @@ return parseInt(`0x${hex.join('')}`, 16);

if (color[3] == 1) {
return `rgb(${round(color[0], 0)}, ${round(color[1], 0)}, ${round(color[2], 0)})`;
return `rgb(${round(color[0] * 255, 0)}, ${round(color[1] * 255, 0)}, ${round(color[2] * 255, 0)})`;
} else {
return `rgba(${round(color[0], 0)}, ${round(color[1], 0)}, ${round(color[2], 0)}, ${round(color[3], P)})`;
return `rgba(${round(color[0] * 255, 0)}, ${round(color[1] * 255, 0)}, ${round(color[2] * 255, 0)}, ${round(color[3], P)})`;
}

@@ -77,3 +77,3 @@ },

get rgba(): string {
return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`;
return `rgba(${round(color[0] * 255, 0)}, ${round(color[1] * 255, 0)}, ${round(color[2] * 255, 0)}, ${round(color[3], P)})`;
},

@@ -83,3 +83,3 @@ rgbaVal: color,

const [h, s, l, a] = rgbToHSL(color);
return `hsl(${h}, ${strip(times(s, 100))}%, ${strip(times(l, 100))}%, ${round(a, P)})`;
return `hsl(${h}, ${strip(s * 100)}%, ${strip(l * 100)}%, ${round(a, P)})`;
},

@@ -91,3 +91,3 @@ get hslVal(): RGBA {

const [r, g, b, a] = color;
return `color(display-p3 ${round(divide(r, 255), P)} ${round(divide(g, 255), P)} ${round(divide(b, 255), P)}${a < 1 ? `/${round(a, P)}` : ''})`;
return `color(display-p3 ${round(r, P)} ${round(g, P)} ${round(b, P)}${a < 1 ? `/${round(a, P)}` : ''})`;
},

@@ -98,15 +98,34 @@ };

/**
* Mix colors using more-correct equation
* Mix colors with gamma correction
* @param {Color} color1
* @param {Color} color2
* @param {number} weight
* Explanation: https://www.youtube.com/watch?v=LKnqECcg6Gw
* @param {number} gamma {default: 2.2}
* https://observablehq.com/@sebastien/srgb-rgb-gamma
*/
export function mix(color1: Color, color2: Color, weight = 0.5): RGBA {
export function mix(color1: Color, color2: Color, weight = 0.5, γ = 2.2): ColorOutput {
const w = clamp(weight, 0, 1);
const w1 = minus(1, w);
const w1 = 1 - w;
const w2 = w;
const [r1, g1, b1, a1] = from(color1).rgbVal;
const [r2, g2, b2, a2] = from(color2).rgbVal;
return [round(plus(times(r1 ** 2, w1), times(r2 ** 2, w2)), 0), round(plus(times(g1 ** 2, w1), times(g2 ** 2, w2)), 0), round(plus(times(b1 ** 2, w1), times(b2 ** 2, w2)), 0), round(plus(times(a1, w1), times(a2, w2)), 0)];
const tf = 1 / γ; // transfer function (https://en.wikipedia.org/wiki/SRGB#Transfer_function_(%22gamma%22))
const itf = γ; // inverse transfer function
// expanded gamma
const c1 = from(color1).rgbVal;
const c2 = from(color2).rgbVal;
const r1 = c1[0] ** itf;
const g1 = c1[1] ** itf;
const b1 = c1[2] ** itf;
const a1 = c1[3];
const r2 = c2[0] ** itf;
const g2 = c2[1] ** itf;
const b2 = c2[2] ** itf;
const a2 = c2[3];
return from([
clamp((r1 ** itf * w1 + r2 ** itf * w2) ** tf, 0, 1), // r
clamp((g1 ** itf * w1 + g2 ** itf * w2) ** tf, 0, 1), // g
clamp((b1 ** itf * w1 + b2 ** itf * w2) ** tf, 0, 1), // b
a1 * w1 + a2 * w2, // a
]);
}

@@ -120,6 +139,6 @@

return [
parseInt(hexStr.substring(0, 2), 16), // r
parseInt(hexStr.substring(2, 4), 16), // g
parseInt(hexStr.substring(4, 6), 16), // b
round(divide(parseInt(hexStr.substring(6, 8) || 'ff', 16), 255), P), // a
parseInt(hexStr.substring(0, 2), 16) / 255, // r
parseInt(hexStr.substring(2, 4), 16) / 255, // g
parseInt(hexStr.substring(4, 6), 16) / 255, // b
parseInt(hexStr.substring(6, 8) || 'ff', 16) / 255, // a
];

@@ -132,9 +151,3 @@ }

if (rawColor.length < 3 || rawColor.length > 4) throw new Error(`Expected [R, G, B, A?], received ${rawColor}`);
// create new array & copy values
const color: RGBA = [0, 0, 0, 1]; // note: alpha defaults to 1
for (let n = 0; n < rawColor.length; n++) {
const v = rawColor[n];
color[n] = clamp(v, 0, n == 3 ? 1 : 255);
}
return color;
return rawColor.map((v) => clamp(v, 0, 1)) as RGBA;
}

@@ -144,4 +157,3 @@

if (typeof rawColor == 'number') {
const color = parseHexVal(rawColor);
return color;
return parseHexVal(rawColor);
}

@@ -155,5 +167,4 @@

// named color
if (cssNames[strVal as keyof typeof cssNames]) {
const color = parseHexVal(cssNames[strVal as keyof typeof cssNames] as number);
return color;
if (typeof cssNames[strVal.toLowerCase() as keyof typeof cssNames] === 'number') {
return parseHexVal(cssNames[strVal as keyof typeof cssNames] as number);
}

@@ -172,4 +183,3 @@ // hex

);
const color = parseHexVal(hexNum);
return color;
return parseHexVal(hexNum);
}

@@ -179,20 +189,25 @@ // rgb

const v: Record<string, string> = (RGB_RE.exec(strVal) as any).groups || {};
const color: RGBA = [clamp(parseFloat(v.R), 0, 255), clamp(parseFloat(v.G), 0, 255), clamp(parseFloat(v.B), 0, 255), v.A ? clamp(parseFloat(v.A), 0, 1) : 1];
return color;
if (![v.R, v.G, v.B].every((c) => c.includes('%') || !c.includes('%'))) throw new Error('Mix of integers and %');
let r = parseFloat(v.R) / (v.R.includes('%') ? 100 : 255);
let g = parseFloat(v.G) / (v.G.includes('%') ? 100 : 255);
let b = parseFloat(v.B) / (v.B.includes('%') ? 100 : 255);
let a = 1;
if (v.A) {
a = parseFloat(v.A);
if (v.A.includes('%')) a /= 100;
}
return [clamp(r, 0, 1), clamp(g, 0, 1), clamp(b, 0, 1), clamp(a, 0, 1)];
}
// hsl
if (HSL_RE.test(strVal)) {
const hsl: HSL = [0, 0, 0, 1];
const v: Record<string, string> = (HSL_RE.exec(strVal) as any).groups || {};
hsl[0] = parseFloat(v.H);
const isPerc = strVal.includes('%');
let sVal = parseFloat(v.S);
let lVal = parseFloat(v.L);
if (isPerc) sVal /= 100;
if (isPerc) lVal /= 100;
hsl[1] = clamp(sVal, 0, 1);
hsl[2] = clamp(lVal, 0, 1);
hsl[3] = v.A ? parseFloat(v.A) : 1;
const color = hslToRGB(hsl);
return color;
let h = parseFloat(v.H);
let s = parseFloat(v.S) / 100;
let l = parseFloat(v.L) / 100;
let a = 1;
if (v.A) {
a = parseFloat(v.A);
if (v.A.includes('%')) a /= 100;
}
return hslToRGB([h, clamp(s, 0, 1), clamp(l, 0, 1), clamp(a, 0, 1)]);
}

@@ -202,3 +217,14 @@ // p3

const v: Record<string, string> = (P3_RE.exec(strVal) as any).groups || {};
return [times(clamp(parseFloat(v.R), 0, 1), 255), times(clamp(parseFloat(v.G), 0, 1), 255), times(clamp(parseFloat(v.B), 0, 1), 255), v.A ? clamp(parseFloat(v.A), 0, 1) : 1];
let r = parseFloat(v.R);
if (v.R.includes('%')) r /= 100;
let g = parseFloat(v.G);
if (v.G.includes('%')) g /= 100;
let b = parseFloat(v.B);
if (v.B.includes('%')) b /= 100;
let a = 1;
if (v.A) {
a = parseFloat(v.A);
if (v.A.includes('%')) a /= 100;
}
return [clamp(r, 0, 1), clamp(g, 0, 1), clamp(b, 0, 1), clamp(a, 0, 1)];
}

@@ -216,6 +242,5 @@ }

*/
export function alpha(rawColor: Color, value: number): RGBA {
if (!(value >= 0 && value <= 1)) throw new Error(`Alpha must be between 0 and 1, received ${alpha}`);
export function alpha(rawColor: Color, value: number): ColorOutput {
const c = parse(rawColor);
return [c[0], c[1], c[2], value];
return from([c[0], c[1], c[2], clamp(value, 0, 1)]);
}

@@ -230,8 +255,8 @@

*/
export function darken(color: Color, value: number): RGBA {
if (!(value >= -1 && value <= 1)) throw new Error(`Value must be between -1 and 1, received ${value}`);
if (value >= 0) {
return mix(color, [0, 0, 0, 1], value);
export function darken(color: Color, value: number): ColorOutput {
const amt = clamp(value, -1, 1);
if (amt >= 0) {
return mix(color, [0, 0, 0, 1], amt);
} else {
return lighten(color, -value);
return lighten(color, -amt);
}

@@ -247,8 +272,8 @@ }

*/
export function lighten(color: Color, value: number): RGBA {
if (!(value >= -1 && value <= 1)) throw new Error(`Value must be between -1 and 1, received ${value}`);
if (value >= 0) {
return mix(color, [255, 255, 255, 1], value);
export function lighten(color: Color, value: number): ColorOutput {
const amt = clamp(value, -1, 1);
if (amt >= 0) {
return mix(color, [1, 1, 1, 1], amt);
} else {
return darken(color, -value);
return darken(color, -amt);
}

@@ -265,4 +290,4 @@ }

const C = times(S, minus(1, Math.abs(minus(times(2, L), 1))));
const X = times(C, minus(1, Math.abs(minus(divide(H, 60) % 2, 1))));
const C = S * (1 - Math.abs(2 * L - 1));
const X = C * (1 - Math.abs(((H / 60) % 2) - 1));

@@ -292,5 +317,5 @@ let R = 0;

}
const m = minus(L, divide(C, 2));
const m = L - C / 2;
return [round(times(plus(R, m), 255), P), round(times(plus(G, m), 255), P), round(times(plus(B, m), 255), P), round(A, P)];
return [round(R + m, P), round(G + m, P), round(B + m, P), round(A, P)];
}

@@ -311,9 +336,9 @@

let S = 0;
let L = divide(plus(M, m), 2); // default: standard HSL (“fill”) calculation
let L = (M + m) / 2; // default: standard HSL (“fill”) calculation
// if white/black/gray, exit early
if (M == m) return [H, S, NP.round(divide(L, 255), 4), A];
if (M == m) return [H, S, NP.round(L, 4), A];
// if any other color, calculate hue & saturation
const C = minus(M, m);
const C = M - m;

@@ -324,9 +349,9 @@ // Hue

case R:
H = divide(times(60, minus(G, B)), C);
H = (60 * (G - B)) / C;
break;
case G:
H = times(60, plus(2, divide(minus(B, R), C)));
H = 60 * (2 + (B - R) / C);
break;
case B:
H = times(60, plus(4, divide(minus(R, G), C)));
H = 60 * (4 + (R - G) / C);
break;

@@ -341,6 +366,6 @@ }

if (L != 0 && L != 1) {
S = divide(minus(M, L), Math.min(L, minus(255, L)));
S = (M - L) / Math.min(L, 1 - L);
}
return [round(H, P - 2), round(S, P), round(divide(L, 255), P), A];
return [round(H, P - 2), round(S, P), round(L, P), A];
}

@@ -347,0 +372,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc