better-color-tools
Advanced tools
Comparing version 0.1.0 to 0.2.0
# better-color-tools | ||
## 0.2.0 | ||
### Minor Changes | ||
- 90b85ee: Improve mix(), lighten(), and darken() | ||
## 0.1.0 | ||
@@ -4,0 +10,0 @@ |
@@ -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" | ||
} | ||
} |
192
README.md
# 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() |
183
src/index.ts
@@ -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
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
162420
0
20
1289
15
156
- Removednumber-precision@^1.5.1
- Removednumber-precision@1.6.0(transitive)