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.0.2 to 0.1.0

6

CHANGELOG.md
# better-color-tools
## 0.1.0
### Minor Changes
- Add P3 support
## 0.0.2

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

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

@@ -14,3 +15,8 @@ export declare type ColorOutput = {

hslVal: RGBA;
p3: string;
};
export declare const HEX_RE: RegExp;
export declare const RGB_RE: RegExp;
export declare const HSL_RE: RegExp;
export declare const P3_RE: RegExp;
/**

@@ -25,2 +31,3 @@ * Parse any valid CSS color color string and convert to:

* - hslVal: [0, 1, 1, 1] // 0% = 0, 100% = 1
* - p3: 'color(display-p3 1 0 0)'
*/

@@ -27,0 +34,0 @@ export declare function from(rawColor: Color): ColorOutput;

169

dist/index.js
import NP from 'number-precision';
import cssNames from './css-names.js';
import { leftPad } from './utils.js';
import { clamp, leftPad } from './utils.js';
NP.enableBoundaryChecking(false); // don’t throw error on inaccurate calculation
const P = 5; // standard precision: 16-bit
const COMMA = '(\\s*,\\s*|\\s+)';
const FLOAT = '-?[0-9]+(\\.[0-9]+)?';
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;
/**

@@ -14,2 +22,3 @@ * Parse any valid CSS color color string and convert to:

* - hslVal: [0, 1, 1, 1] // 0% = 0, 100% = 1
* - p3: 'color(display-p3 1 0 0)'
*/

@@ -23,4 +32,4 @@ export function from(rawColor) {

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

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

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

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

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

@@ -54,3 +63,3 @@ },

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

@@ -60,2 +69,6 @@ get hslVal() {

},
get p3() {
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)}` : ''})`;
},
};

@@ -71,9 +84,8 @@ }

export function mix(color1, color2, weight = 0.5) {
if (!(weight >= 0 && weight <= 1))
throw new Error(`Weight must be between 0 and 1, received ${weight}`);
const w1 = 1 - weight;
const w2 = weight;
const w = clamp(weight, 0, 1);
const w1 = minus(1, w);
const w2 = w;
const [r1, g1, b1, a1] = from(color1).rgbVal;
const [r2, g2, b2, a2] = from(color2).rgbVal;
return [Math.round(r1 ** 2 * w1 + r2 ** 2 * w2), Math.round(g1 ** 2 * w1 + g2 ** 2 * w2), Math.round(b1 ** 2 * w1 + b2 ** 2 * w2), Math.round(a1 * w1 + a2 * w2)];
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)];
}

@@ -84,5 +96,3 @@ /** Convert any number of user inputs into RGBA array */

function parseHexVal(hexVal) {
if (hexVal < 0 || hexVal > 0xffffffff)
throw new Error(`Expected color in hex range (0xff0000), received ${hexVal.toString(16) || hexVal}`);
const hexStr = leftPad(hexVal.toString(16), 6); // note: 0x000001 will convert to '1'
const hexStr = leftPad(clamp(hexVal, 0, 0xffffffff).toString(16), 6); // note: 0x000001 will convert to '1'
return [

@@ -92,17 +102,5 @@ parseInt(hexStr.substring(0, 2), 16),

parseInt(hexStr.substring(4, 6), 16),
parseInt(hexStr.substring(6, 8) || 'ff', 16) / 255, // a
round(divide(parseInt(hexStr.substring(6, 8) || 'ff', 16), 255), P), // a
];
}
/** Validate RGBA array */
function validate(c) {
if (!(c[0] >= 0 && c[0] <= 255))
throw new Error(`${rawColor} Expected r to be between 0 and 255, received ${c[0]}`);
if (!(c[1] >= 0 && c[1] <= 255))
throw new Error(`${rawColor} Expected g to be between 0 and 255, received ${c[1]}`);
if (!(c[2] >= 0 && c[2] <= 255))
throw new Error(`${rawColor} Expected b to be between 0 and 255, received ${c[2]}`);
if (!(c[3] >= 0 && c[3] <= 1))
throw new Error(`${rawColor} Expected alpha to be between 0 and 1, received ${c[3]}`);
return c;
}
// [R, G, B] or [R, G, B, A]

@@ -118,9 +116,5 @@ if (Array.isArray(rawColor)) {

const v = rawColor[n];
if (n == 3 && (v < 0 || v > 1))
throw new Error(`Alpha must be between 0 and 1, received ${v}`);
else if (v < 0 || v > 255)
throw new Error(`Color channel must be between 0 and 255, received ${v}`);
color[n] = rawColor[n];
color[n] = clamp(v, 0, n == 3 ? 1 : 255);
}
return validate(color);
return color;
}

@@ -130,3 +124,3 @@ // 0xff0000 (number)

const color = parseHexVal(rawColor);
return validate(color);
return color;
}

@@ -141,52 +135,45 @@ // '#ff0000' / 'red' / 'rgb(255, 0, 0)' / 'hsl(0, 1, 1)'

const color = parseHexVal(cssNames[strVal]);
return validate(color);
return color;
}
// hex
const maybeHex = parseInt(strVal.replace(/^#/, ''), 16);
if (!Number.isNaN(maybeHex)) {
const color = parseHexVal(maybeHex);
return validate(color);
if (HEX_RE.test(strVal)) {
const hex = strVal.replace('#', '');
const hexNum = parseInt(hex.length < 6
? hex
.split('')
.map((d) => `${d}${d}`)
.join('') // handle shortcut (#fff)
: hex, 16);
const color = parseHexVal(hexNum);
return color;
}
// rgb
if (RGB_RE.test(strVal)) {
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;
}
// hsl
if (strVal.toLocaleLowerCase().startsWith('hsl')) {
if (HSL_RE.test(strVal)) {
const hsl = [0, 0, 0, 1];
let [h, s, l, a] = strVal
.replace(/hsla?\s*\(/i, '')
.replace(/\)\s*$/, '')
.split(',');
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 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 validate(color);
return color;
}
// rgb (and fallbacks)
const rawStr = strVal.replace(/rgba?\s*\(/i, '').replace(/\)\s*$/, '');
const values = rawStr.includes(',') ? rawStr.split(',').filter((v) => !!v.trim()) : rawStr.split(' ').filter((v) => !!v.trim());
if (values.length != 3 && values.length != 4)
throw new Error(`Unable to parse color "${rawColor}"`);
const [r, g, b, a] = values;
const color = [parseInt(r, 10), parseInt(g, 10), parseInt(b, 10), parseFloat((a || '1').trim())];
return validate(color);
// p3
if (P3_RE.test(strVal)) {
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];
}
}

@@ -248,4 +235,4 @@ throw new Error(`Unable to parse color "${rawColor}"`);

H = Math.abs(H % 360); // allow < 0 and > 360
const C = (1 - Math.abs(2 * L - 1)) * S;
const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
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))));
let R = 0;

@@ -278,4 +265,4 @@ let G = 0;

}
const m = L - C / 2;
return [Math.round((R + m) * 255), Math.round((G + m) * 255), Math.round((B + m) * 255), A];
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)];
}

@@ -293,8 +280,8 @@ /**

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

@@ -304,9 +291,9 @@ if (C != 0) {

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

@@ -320,5 +307,5 @@ }

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

@@ -325,0 +312,0 @@ export default {

@@ -5,1 +5,2 @@ /** you know it, you love it */

export declare function radToDeg(radians: number): number;
export declare function clamp(input: number, min: number, max: number): number;

@@ -16,1 +16,4 @@ import NP from 'number-precision';

}
export function clamp(input, min, max) {
return Math.min(Math.max(input, min), max);
}
{
"name": "better-color-tools",
"description": "Better color manipulation for Sass and JavaScript / TypeScript.",
"version": "0.0.2",
"version": "0.1.0",
"author": {

@@ -28,2 +28,11 @@ "name": "Drew Powers",

"types": "./dist/index.d.ts",
"scripts": {
"build": "rm -rf dist && tsc",
"changeset": "changeset",
"dev": "tsc -w",
"lint": "eslint \"**/*.{js,ts}\"",
"prepublishOnly": "npm run build",
"pretest": "npm run build",
"test": "mocha --parallel"
},
"dependencies": {

@@ -33,8 +42,8 @@ "number-precision": "^1.5.1"

"devDependencies": {
"@changesets/cli": "^2.18.1",
"@types/node": "^16.11.11",
"@typescript-eslint/eslint-plugin": "^5.5.0",
"@typescript-eslint/parser": "^5.5.0",
"@changesets/cli": "^2.19.0",
"@types/node": "^16.11.19",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"@typescript-eslint/parser": "^5.9.0",
"chai": "^4.3.4",
"eslint": "^8.4.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",

@@ -45,12 +54,4 @@ "eslint-plugin-prettier": "^4.0.0",

"prettier": "^2.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"
"typescript": "^4.5.4"
}
}
}

@@ -5,2 +5,8 @@ # better-color-tools

Supports:
- ✅ RGB / Hex
- ✅ HSL
- ✅ [P3]
## Installing

@@ -122,4 +128,4 @@

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):
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):

@@ -142,2 +148,4 @@ ```ts

`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

@@ -151,2 +159,8 @@ import color from 'better-color-tools';

// convert hex to p3
color.from('rgb(196, 67, 43, 0.8)').p3; // 'color(display-p3 0.76863 0.26275 0.16863/0.8)'
// convert from p3 to hex
color.from('color(display-p3 0.23 0.872 0.918)').hex; // #3bdeea
// convert color to rgb

@@ -157,6 +171,6 @@ color.from('#C4432B').rgb; // 'rgb(196, 67, 43)'

// convert color to rgba
// convert hex to rgba
color.from('#C4432B').rgba; // 'rgba(196, 67, 43, 1)'
color.from(0xc4432b).rgba; // 'rgba(196, 67, 43, 1)'
color.from('#C4432B').rgbaVal; // [196, 67, 43, 1]
color.from('#C4432B80').rgbaVal; // [196, 67, 43, 0.5]

@@ -168,2 +182,5 @@ // convert color to hsl

// convert hsl to rgb
color.from('hsl(328, 100%, 54%)').rgb; // 'rgb(255, 20, 146)'
// convert color names to hex

@@ -173,6 +190,17 @@ 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.:
| P3 Color | Ideal RGB | 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.
## 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!)
- **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

@@ -182,4 +210,6 @@ [color-convert]: https://github.com/Qix-/color-convert

[hsl]: https://en.wikipedia.org/wiki/HSL_and_HSV#Disadvantages
[hsl-rgb]: https://pow.rs/blog/dont-use-hsl-for-anything/
[number-precision]: https://github.com/nefe/number-precision
[p3]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color()
[sass-color]: https://sass-lang.com/documentation/modules/color
[sass-color-scale]: https://sass-lang.com/documentation/modules/color#scale
import NP from 'number-precision';
import cssNames from './css-names.js';
import { leftPad } from './utils.js';
import { clamp, leftPad } from './utils.js';

@@ -8,7 +8,27 @@ export type RGB = [number, number, number];

export type HSL = [number, number, number, number];
export type P3 = [number, number, number, number];
export type Color = string | number | RGB | RGBA;
export type ColorOutput = { hex: string; rgb: string; rgba: string; hexVal: number; rgbVal: RGBA; rgbaVal: RGBA; hsl: string; hslVal: RGBA };
export type ColorOutput = {
hex: string;
rgb: string;
rgba: string;
hexVal: number;
rgbVal: RGBA;
rgbaVal: RGBA;
hsl: string;
hslVal: RGBA;
p3: string;
};
NP.enableBoundaryChecking(false); // don’t throw error on inaccurate calculation
const P = 5; // standard precision: 16-bit
const COMMA = '(\\s*,\\s*|\\s+)';
const FLOAT = '-?[0-9]+(\\.[0-9]+)?';
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;
/**

@@ -23,2 +43,3 @@ * Parse any valid CSS color color string and convert to:

* - hslVal: [0, 1, 1, 1] // 0% = 0, 100% = 1
* - p3: 'color(display-p3 1 0 0)'
*/

@@ -32,4 +53,4 @@ export function from(rawColor: Color): ColorOutput {

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

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

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

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

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

@@ -61,3 +82,3 @@ },

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

@@ -67,2 +88,6 @@ get hslVal(): RGBA {

},
get p3(): string {
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)}` : ''})`;
},
};

@@ -79,8 +104,8 @@ }

export function mix(color1: Color, color2: Color, weight = 0.5): RGBA {
if (!(weight >= 0 && weight <= 1)) throw new Error(`Weight must be between 0 and 1, received ${weight}`);
const w1 = 1 - weight;
const w2 = weight;
const w = clamp(weight, 0, 1);
const w1 = minus(1, w);
const w2 = w;
const [r1, g1, b1, a1] = from(color1).rgbVal;
const [r2, g2, b2, a2] = from(color2).rgbVal;
return [Math.round(r1 ** 2 * w1 + r2 ** 2 * w2), Math.round(g1 ** 2 * w1 + g2 ** 2 * w2), Math.round(b1 ** 2 * w1 + b2 ** 2 * w2), Math.round(a1 * w1 + a2 * w2)];
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)];
}

@@ -92,4 +117,3 @@

function parseHexVal(hexVal: number): RGBA {
if (hexVal < 0 || hexVal > 0xffffffff) throw new Error(`Expected color in hex range (0xff0000), received ${hexVal.toString(16) || hexVal}`);
const hexStr = leftPad(hexVal.toString(16), 6); // note: 0x000001 will convert to '1'
const hexStr = leftPad(clamp(hexVal, 0, 0xffffffff).toString(16), 6); // note: 0x000001 will convert to '1'
return [

@@ -99,15 +123,6 @@ parseInt(hexStr.substring(0, 2), 16), // r

parseInt(hexStr.substring(4, 6), 16), // b
parseInt(hexStr.substring(6, 8) || 'ff', 16) / 255, // a
round(divide(parseInt(hexStr.substring(6, 8) || 'ff', 16), 255), P), // a
];
}
/** Validate RGBA array */
function validate(c: RGBA): RGBA {
if (!(c[0] >= 0 && c[0] <= 255)) throw new Error(`${rawColor} Expected r to be between 0 and 255, received ${c[0]}`);
if (!(c[1] >= 0 && c[1] <= 255)) throw new Error(`${rawColor} Expected g to be between 0 and 255, received ${c[1]}`);
if (!(c[2] >= 0 && c[2] <= 255)) throw new Error(`${rawColor} Expected b to be between 0 and 255, received ${c[2]}`);
if (!(c[3] >= 0 && c[3] <= 1)) throw new Error(`${rawColor} Expected alpha to be between 0 and 1, received ${c[3]}`);
return c;
}
// [R, G, B] or [R, G, B, A]

@@ -121,7 +136,5 @@ if (Array.isArray(rawColor)) {

const v = rawColor[n];
if (n == 3 && (v < 0 || v > 1)) throw new Error(`Alpha must be between 0 and 1, received ${v}`);
else if (v < 0 || v > 255) throw new Error(`Color channel must be between 0 and 255, received ${v}`);
color[n] = rawColor[n];
color[n] = clamp(v, 0, n == 3 ? 1 : 255);
}
return validate(color);
return color;
}

@@ -132,3 +145,3 @@

const color = parseHexVal(rawColor);
return validate(color);
return color;
}

@@ -144,45 +157,46 @@

const color = parseHexVal(cssNames[strVal as keyof typeof cssNames] as number);
return validate(color);
return color;
}
// hex
const maybeHex = parseInt(strVal.replace(/^#/, ''), 16);
if (!Number.isNaN(maybeHex)) {
const color = parseHexVal(maybeHex);
return validate(color);
if (HEX_RE.test(strVal)) {
const hex = strVal.replace('#', '');
const hexNum = parseInt(
hex.length < 6
? hex
.split('')
.map((d) => `${d}${d}`)
.join('') // handle shortcut (#fff)
: hex,
16
);
const color = parseHexVal(hexNum);
return color;
}
// rgb
if (RGB_RE.test(strVal)) {
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;
}
// hsl
if (strVal.toLocaleLowerCase().startsWith('hsl')) {
if (HSL_RE.test(strVal)) {
const hsl: HSL = [0, 0, 0, 1];
let [h, s, l, a] = strVal
.replace(/hsla?\s*\(/i, '')
.replace(/\)\s*$/, '')
.split(',');
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 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 validate(color);
return color;
}
// rgb (and fallbacks)
const rawStr = strVal.replace(/rgba?\s*\(/i, '').replace(/\)\s*$/, '');
const values = rawStr.includes(',') ? rawStr.split(',').filter((v) => !!v.trim()) : rawStr.split(' ').filter((v) => !!v.trim());
if (values.length != 3 && values.length != 4) throw new Error(`Unable to parse color "${rawColor}"`);
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);
// p3
if (P3_RE.test(strVal)) {
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];
}
}

@@ -245,4 +259,4 @@

const C = (1 - Math.abs(2 * L - 1)) * S;
const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
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))));

@@ -272,5 +286,5 @@ let R = 0;

}
const m = L - C / 2;
const m = minus(L, divide(C, 2));
return [Math.round((R + m) * 255), Math.round((G + m) * 255), Math.round((B + m) * 255), A];
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)];
}

@@ -291,9 +305,9 @@

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

@@ -304,9 +318,9 @@ // Hue

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

@@ -321,6 +335,6 @@ }

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

@@ -327,0 +341,0 @@

@@ -19,1 +19,5 @@ import NP from 'number-precision';

}
export function clamp(input: number, min: number, max: number): number {
return Math.min(Math.max(input, min), max);
}

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