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.4.0 to 0.5.0

6

CHANGELOG.md
# better-color-tools
## 0.5.0
### Minor Changes
- ad44c9b: Add fallback Sass function
## 0.4.0

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

21

dist/index.d.ts

@@ -17,2 +17,6 @@ export declare type RGB = [number, number, number];

};
export declare type GradientStop = {
color: RGBA;
position: number;
};
export declare const HEX_RE: RegExp;

@@ -25,3 +29,2 @@ export declare const RGB_RE: RegExp;

export declare const CON_GRAD_RE: RegExp;
export declare const STOP_POS_RE: RegExp;
/**

@@ -77,3 +80,3 @@ * Parse any valid CSS color color string and convert to:

*/
export declare function luminance(color: Color): number;
export declare function luminance(color: Color, γ?: number): number;
/**

@@ -95,8 +98,10 @@ * Lightness

/**
* Gamma Gradient
* Take any CSS gradient and correct gamma
* Parse CSS gradient
* Parse any valid CSS gradient and convert to stops
*/
declare function gradient(input: string, p3?: boolean): string;
/** @deprecated (use gradient instead) */
export declare const gammaGradient: typeof gradient;
export declare function parseGradient(input: string): {
type: 'linear-gradient' | 'radial-gradient' | 'conic-gradient';
position?: string;
stops: GradientStop[];
};
declare const _default: {

@@ -106,4 +111,2 @@ alpha: typeof alpha;

from: typeof from;
gammaGradient: typeof gradient;
gradient: typeof gradient;
hslToRGB: typeof hslToRGB;

@@ -110,0 +113,0 @@ lighten: typeof lighten;

import NP from 'number-precision';
import cssNames from './css-names.js';
import { clamp, leftPad, splitDistance } from './utils.js';
import { clamp, leftPad } from './utils.js';
NP.enableBoundaryChecking(false); // don’t throw error on inaccurate calculation

@@ -12,6 +12,5 @@ const P = 5; // standard precision: 16-bit

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');
export const LIN_GRAD_RE = /^linear-gradient\((.*)\);?$/;
export const RAD_GRAD_RE = /^radial-gradient\((.*)\);?$/;
export const CON_GRAD_RE = /^conic-gradient\((.*)\);?$/;
export const STOP_POS_RE = /\s[^\s]+$/;
export const LIN_GRAD_RE = /^linear-gradient\s*\((.*)\);?$/;
export const RAD_GRAD_RE = /^radial-gradient\s*\((.*)\);?$/;
export const CON_GRAD_RE = /^conic-gradient\s*\((.*)\);?$/;
const { round, strip } = NP;

@@ -251,5 +250,5 @@ /**

*/
export function luminance(color) {
export function luminance(color, γ = 2.2) {
const [r, g, b] = parse(color);
return NP.round(0.2126 * Math.pow(r, 2.2) + 0.7152 * Math.pow(g, 2.2) + 0.0722 * Math.pow(b, 2.2), P);
return NP.round(0.2126 * Math.pow(r, γ) + 0.7152 * Math.pow(g, γ) + 0.0722 * Math.pow(b, γ), P);
}

@@ -344,14 +343,14 @@ /**

/**
* Gamma Gradient
* Take any CSS gradient and correct gamma
* Parse CSS gradient
* Parse any valid CSS gradient and convert to stops
*/
function gradient(input, p3 = false) {
export function parseGradient(input) {
const gradString = input.trim();
let gradType = 'linear-gradient';
let position;
let stops = [];
let rawStops = [];
if (LIN_GRAD_RE.test(gradString)) {
stops = gradString.match(LIN_GRAD_RE)[1].split(',').map((p) => p.trim());
if (stops[0].includes('deg') || stops[0].includes('turn') || stops[0].includes('to ')) {
position = stops.shift();
rawStops = gradString.match(LIN_GRAD_RE)[1].split(',').map((p) => p.trim());
if (rawStops[0].includes('deg') || rawStops[0].includes('turn') || rawStops[0].includes('to ')) {
position = rawStops.shift();
}

@@ -361,5 +360,5 @@ }

gradType = 'radial-gradient';
stops = gradString.match(RAD_GRAD_RE)[1].split(',').map((p) => p.trim());
if (stops[0].includes('circle') || stops[0].includes('ellipse') || stops[0].includes('closest-') || stops[0].includes('farthest-')) {
position = stops.shift();
rawStops = gradString.match(RAD_GRAD_RE)[1].split(',').map((p) => p.trim());
if (rawStops[0].includes('circle') || rawStops[0].includes('ellipse') || rawStops[0].includes('closest-') || rawStops[0].includes('farthest-')) {
position = rawStops.shift();
}

@@ -369,5 +368,5 @@ }

gradType = 'conic-gradient';
stops = gradString.match(CON_GRAD_RE)[1].split(',').map((p) => p.trim());
if (stops[0].includes('from')) {
position = stops.shift();
rawStops = gradString.match(CON_GRAD_RE)[1].split(',').map((p) => p.trim());
if (rawStops[0].includes('from')) {
position = rawStops.shift();
}

@@ -377,31 +376,42 @@ }

throw new Error(`Unable to parse gradient "${input}"`);
const newGradient = [];
for (const stop of stops) {
let pos2 = '';
let color2 = stop;
const posMatch = stop.match(STOP_POS_RE);
if (posMatch) {
pos2 = posMatch[0].trim();
color2 = stop.replace(pos2, '').trim();
if (rawStops.length < 2) {
throw new Error('Gradient must have at least 2 stops');
}
let unitCount = 0;
for (const unit of ['%', 'px', 'em', 'rem', 'pt', 'ch', 'cm', 'in', 'mm', 'vh', 'vm']) {
if (gradString.includes(unit))
unitCount += 1;
}
if (unitCount > 1)
throw new Error(`Can’t normalize gradients with mixed units`);
const stops = [];
const rawPositions = rawStops.map((s) => parseFloat(s.substring(s.indexOf(' '))) || -Infinity);
const max = Math.max(...rawPositions);
for (let n = 0; n < rawStops.length; n++) {
// color is easy
stops[n].color = from(rawStops[n].split(' ')[0]).rgbVal;
// position is not (if omitted, we’ll have to figure out averages)
if (rawPositions[n] >= 0) {
stops[n].position = Math.min(rawPositions[n] / max, 1);
continue;
}
if (newGradient.length) {
const prevItem = newGradient[newGradient.length - 1];
const { pos: pos1, color: color1 } = prevItem;
const skipTransitions = splitDistance(pos1, pos2) <= 0 || from(color1).hex === from(color2).hex; // stops are on top of each other; skip
// TODO: increase/decrease stops
if (!skipTransitions) {
for (let i = 1; i <= 3; i++) {
const p = 0.25 * i;
let c = mix(color1, color2, p);
newGradient.push({ color: p3 ? c.p3 : c.hex, pos: splitDistance(pos1 || 0, pos2, p) });
}
}
else if (n === 0) {
stops[n].position = 0;
continue;
}
const c = from(color2);
newGradient.push({ color: p3 ? c.p3 : c.hex, pos: pos2 });
else if (n === rawStops.length - 1) {
stops[n].position = 1;
}
let startPosition = stops[n - 1].position;
let endPosition = rawStops.findIndex((s) => parseFloat(s.substring(s.indexOf(' '))) > 0);
if (endPosition === -1)
endPosition = rawStops.length - 1;
stops[n].position = (endPosition - startPosition) / endPosition;
}
return `${gradType}(${[...(position ? [position] : []), ...newGradient.map(({ color, pos }) => `${color}${pos ? ` ${pos}` : ''}`)].join(',')})`;
return {
type: gradType,
position,
stops,
};
}
/** @deprecated (use gradient instead) */
export const gammaGradient = gradient;
export default {

@@ -411,4 +421,2 @@ alpha,

from,
gammaGradient,
gradient,
hslToRGB,

@@ -415,0 +423,0 @@ lighten,

@@ -1,1 +0,1 @@

function B(t,e){return e===void 0&&(e=15),+parseFloat(Number(t).toPrecision(e))}function p(t){var e=t.toString().split(/[eE]/),n=(e[0].split(".")[1]||"").length-+(e[1]||0);return n>0?n:0}function v(t){if(t.toString().indexOf("e")===-1)return Number(t.toString().replace(".",""));var e=p(t);return e>0?B(Number(t)*Math.pow(10,e)):Number(t)}function I(t){D&&(t>Number.MAX_SAFE_INTEGER||t<Number.MIN_SAFE_INTEGER)&&console.warn(t+" is beyond boundary when transfer to integer, the results may not be accurate")}function S(t,e){var n=t[0],r=t[1],f=t.slice(2),a=e(n,r);return f.forEach(function(i){a=e(a,i)}),a}function m(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return S(t,m);var n=t[0],r=t[1],f=v(n),a=v(r),i=p(n)+p(r),o=f*a;return I(o),o/Math.pow(10,i)}function j(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return S(t,j);var n=t[0],r=t[1],f=Math.pow(10,Math.max(p(n),p(r)));return(m(n,f)+m(r,f))/f}function H(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return S(t,H);var n=t[0],r=t[1],f=Math.pow(10,Math.max(p(n),p(r)));return(m(n,f)-m(r,f))/f}function L(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return S(t,L);var n=t[0],r=t[1],f=v(n),a=v(r);return I(f),I(a),m(f/a,B(Math.pow(10,p(r)-p(n))))}function ee(t,e){var n=Math.pow(10,e),r=L(Math.round(Math.abs(m(t,n))),n);return t<0&&r!==0&&(r=m(r,-1)),r}var D=!0;function te(t){t===void 0&&(t=!0),D=t}var re={strip:B,plus:j,minus:H,times:m,divide:L,round:ee,digitLength:p,float2Fixed:v,enableBoundaryChecking:te};var h=re;var ne={black:0,silver:12632256,gray:8421504,white:16777215,maroon:8388608,red:16711680,purple:8388736,fuchsia:16711935,green:32768,lime:65280,olive:8421376,yellow:16776960,navy:128,blue:255,teal:32896,aqua:65535,orange:16753920,aliceblue:15792383,antiquewhite:16444375,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,blanchedalmond:16772045,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchard:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:3100495,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavendar:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,limegreen:3329330,linen:16445670,magenta:16711935,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,oldlace:16643558,olivedrab:7048739,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,whitesmoke:16119285,yellowgreen:10145074,rebeccapurple:6697881},P=ne;function k(t,e=2){let n=t;for(;n.length<e;)n=`0${n}`;return n}function u(t,e,n){return Math.min(Math.max(t,e),n)}function T(t,e,n=.5){if(!t||!t&&!e)return;let r=typeof t=="number"?t:0,f;typeof t=="string"&&(r=parseFloat(t),f=t.replace(r.toString(),""));let a=typeof e=="number"?e:0,i;if(typeof e=="string"&&(a=parseFloat(e),i=e.replace(a.toString(),"")),f&&i&&f!==i)return;let o=r*(1-n)+a*n;return f||i?`${o}${f||i}`:o}h.enableBoundaryChecking(!1);var x=5,y="(\\s*,\\s*|\\s+)",g="-?[0-9]+(\\.[0-9]+)?",ae=/^#?[0-9a-f]{3,8}$/i,O=new RegExp(["^rgba?\\(\\s*",`(?<R>${g}%?)`,y,`(?<G>${g}%?)`,y,`(?<B>${g}%?)`,`(${y}(?<A>${g}%?))?`,"\\s*\\)$"].join(""),"i"),_=new RegExp(["^hsla?\\(\\s*",`(?<H>${g})`,y,`(?<S>${g})%`,y,`(?<L>${g})%`,`(${y}(?<A>${g})%?)?`,"\\s*\\)$"].join(""),"i"),U=new RegExp(["^color\\(\\s*display-p3\\s+",`(?<R>${g}%?)`,"\\s+",`(?<G>${g}%?)`,"\\s+",`(?<B>${g}%?)`,`(\\s*\\/\\s*(?<A>${g}%?))?`,"\\s*\\)$"].join(""),"i"),C=/^linear-gradient\((.*)\);?$/,X=/^radial-gradient\((.*)\);?$/,z=/^conic-gradient\((.*)\);?$/,fe=/\s[^\s]+$/,{round:l,strip:J}=h;function b(t){let e=N(t);return{get hex(){return`#${e.map((n,r)=>r<3?k(l(n*255,0).toString(16),2):n<1?l(n*255,0).toString(16):"").join("")}`},get hexVal(){let n=e.map((r,f)=>f<3?k(l(r*255,0).toString(16),2):r<1?k((r*256).toString(16),2):"");return parseInt(`0x${n.join("")}`,16)},get rgb(){return e[3]==1?`rgb(${l(e[0]*255,0)}, ${l(e[1]*255,0)}, ${l(e[2]*255,0)})`:`rgba(${l(e[0]*255,0)}, ${l(e[1]*255,0)}, ${l(e[2]*255,0)}, ${l(e[3],x)})`},rgbVal:e,get rgba(){return`rgba(${l(e[0]*255,0)}, ${l(e[1]*255,0)}, ${l(e[2]*255,0)}, ${l(e[3],x)})`},rgbaVal:e,get hsl(){let[n,r,f,a]=q(e);return`hsl(${n}, ${J(r*100)}%, ${J(f*100)}%, ${l(a,x)})`},get hslVal(){return q(e)},get p3(){let[n,r,f,a]=e;return`color(display-p3 ${l(n,x)} ${l(r,x)} ${l(f,x)}${a<1?`/${l(a,x)}`:""})`}}}function F(t,e,n=.5,r=2.2){let f=u(n,0,1),a=1-f,i=f,o=1/r,s=r,c=b(t).rgbVal,d=b(e).rgbVal,w=c[0]**s,G=c[1]**s,M=c[2]**s,R=c[3],V=d[0]**s,$=d[1]**s,E=d[2]**s,A=d[3];return b([u((w**s*a+V**s*i)**o,0,1),u((G**s*a+$**s*i)**o,0,1),u((M**s*a+E**s*i)**o,0,1),R*a+A*i])}function N(t){function e(n){let r=k(u(n,0,4294967295).toString(16),6);return[parseInt(r.substring(0,2),16)/255,parseInt(r.substring(2,4),16)/255,parseInt(r.substring(4,6),16)/255,parseInt(r.substring(6,8)||"ff",16)/255]}if(Array.isArray(t)){if(t.some(n=>typeof n!="number"))throw new Error(`Color array must be numbers, received ${t}`);if(t.length<3||t.length>4)throw new Error(`Expected [R, G, B, A?], received ${t}`);return t.map(n=>u(n,0,1))}if(typeof t=="number")return e(t);if(typeof t=="string"){let n=t.trim();if(!n)throw new Error("Expected color, received empty string");if(typeof P[n.toLowerCase()]=="number")return e(P[n]);if(ae.test(n)){let r=n.replace("#",""),f=parseInt(r.length<6?r.split("").map(a=>`${a}${a}`).join(""):r,16);return e(f)}if(O.test(n)){let r=O.exec(n).groups||{};if(![r.R,r.G,r.B].every(s=>s.includes("%")||!s.includes("%")))throw new Error("Mix of integers and %");let f=parseFloat(r.R)/(r.R.includes("%")?100:255),a=parseFloat(r.G)/(r.G.includes("%")?100:255),i=parseFloat(r.B)/(r.B.includes("%")?100:255),o=1;return r.A&&(o=parseFloat(r.A),r.A.includes("%")&&(o/=100)),[u(f,0,1),u(a,0,1),u(i,0,1),u(o,0,1)]}if(_.test(n)){let r=_.exec(n).groups||{},f=parseFloat(r.H),a=parseFloat(r.S)/100,i=parseFloat(r.L)/100,o=1;return r.A&&(o=parseFloat(r.A),r.A.includes("%")&&(o/=100)),Y([f,u(a,0,1),u(i,0,1),u(o,0,1)])}if(U.test(n)){let r=U.exec(n).groups||{},f=parseFloat(r.R);r.R.includes("%")&&(f/=100);let a=parseFloat(r.G);r.G.includes("%")&&(a/=100);let i=parseFloat(r.B);r.B.includes("%")&&(i/=100);let o=1;return r.A&&(o=parseFloat(r.A),r.A.includes("%")&&(o/=100)),[u(f,0,1),u(a,0,1),u(i,0,1),u(o,0,1)]}}throw new Error(`Unable to parse color "${t}"`)}function oe(t,e){let n=N(t);return b([n[0],n[1],n[2],u(e,0,1)])}function K(t,e){let n=u(e,-1,1);return n>=0?F(t,[0,0,0,1],n):Q(t,-n)}function Q(t,e){let n=u(e,-1,1);return n>=0?F(t,[1,1,1,1],n):K(t,-n)}function W(t){let[e,n,r]=N(t);return h.round(.2126*Math.pow(e,2.2)+.7152*Math.pow(n,2.2)+.0722*Math.pow(r,2.2),x)}function ie(t){let e=W(t);return h.round((e<=216/24389?e*(24389/27):Math.pow(e,1/3)*116-16)/100,x)}function Y(t){let[e,n,r,f]=t;e=Math.abs(e%360);let a=n*(1-Math.abs(2*r-1)),i=a*(1-Math.abs(e/60%2-1)),o=0,s=0,c=0;0<=e&&e<60?(o=a,s=i):60<=e&&e<120?(o=i,s=a):120<=e&&e<180?(s=a,c=i):180<=e&&e<240?(s=i,c=a):240<=e&&e<300?(o=i,c=a):300<=e&&e<360&&(o=a,c=i);let d=r-a/2;return[l(o+d,x),l(s+d,x),l(c+d,x),l(f,x)]}function q(t){let[e,n,r,f]=t,a=Math.max(e,n,r),i=Math.min(e,n,r),o=0,s=0,c=(a+i)/2;if(a==i)return[o,s,h.round(c,4),f];let d=a-i;if(d!=0){switch(a){case e:o=60*(n-r)/d;break;case n:o=60*(2+(r-e)/d);break;case r:o=60*(4+(e-n)/d);break}for(;o<0;)o+=360}return c!=0&&c!=1&&(s=(a-c)/Math.min(c,1-c)),[l(o,x-2),l(s,x),l(c,x),f]}function Z(t,e=!1){let n=t.trim(),r="linear-gradient",f,a=[];if(C.test(n))a=n.match(C)[1].split(",").map(o=>o.trim()),(a[0].includes("deg")||a[0].includes("turn")||a[0].includes("to "))&&(f=a.shift());else if(X.test(n))r="radial-gradient",a=n.match(X)[1].split(",").map(o=>o.trim()),(a[0].includes("circle")||a[0].includes("ellipse")||a[0].includes("closest-")||a[0].includes("farthest-"))&&(f=a.shift());else if(z.test(n))r="conic-gradient",a=n.match(z)[1].split(",").map(o=>o.trim()),a[0].includes("from")&&(f=a.shift());else throw new Error(`Unable to parse gradient "${t}"`);let i=[];for(let o of a){let s="",c=o,d=o.match(fe);if(d&&(s=d[0].trim(),c=o.replace(s,"").trim()),i.length){let G=i[i.length-1],{pos:M,color:R}=G;if(!(T(M,s)<=0||b(R).hex===b(c).hex))for(let $=1;$<=3;$++){let E=.25*$,A=F(R,c,E);i.push({color:e?A.p3:A.hex,pos:T(M||0,s,E)})}}let w=b(c);i.push({color:e?w.p3:w.hex,pos:s})}return`${r}(${[...f?[f]:[],...i.map(({color:o,pos:s})=>`${o}${s?` ${s}`:""}`)].join(",")})`}var se=Z,me={alpha:oe,darken:K,from:b,gammaGradient:se,gradient:Z,hslToRGB:Y,lighten:Q,lightness:ie,luminance:W,mix:F,parse:N,rgbToHSL:q};export{z as CON_GRAD_RE,ae as HEX_RE,_ as HSL_RE,C as LIN_GRAD_RE,U as P3_RE,X as RAD_GRAD_RE,O as RGB_RE,fe as STOP_POS_RE,oe as alpha,K as darken,me as default,b as from,se as gammaGradient,Y as hslToRGB,Q as lighten,ie as lightness,W as luminance,F as mix,N as parse,q as rgbToHSL};
function A(t,e){return e===void 0&&(e=15),+parseFloat(Number(t).toPrecision(e))}function p(t){var e=t.toString().split(/[eE]/),n=(e[0].split(".")[1]||"").length-+(e[1]||0);return n>0?n:0}function $(t){if(t.toString().indexOf("e")===-1)return Number(t.toString().replace(".",""));var e=p(t);return e>0?A(Number(t)*Math.pow(10,e)):Number(t)}function R(t){L&&(t>Number.MAX_SAFE_INTEGER||t<Number.MIN_SAFE_INTEGER)&&console.warn(t+" is beyond boundary when transfer to integer, the results may not be accurate")}function M(t,e){var n=t[0],r=t[1],a=t.slice(2),f=e(n,r);return a.forEach(function(s){f=e(f,s)}),f}function m(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return M(t,m);var n=t[0],r=t[1],a=$(n),f=$(r),s=p(n)+p(r),o=a*f;return R(o),o/Math.pow(10,s)}function B(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return M(t,B);var n=t[0],r=t[1],a=Math.pow(10,Math.max(p(n),p(r)));return(m(n,a)+m(r,a))/a}function I(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return M(t,I);var n=t[0],r=t[1],a=Math.pow(10,Math.max(p(n),p(r)));return(m(n,a)-m(r,a))/a}function S(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];if(t.length>2)return M(t,S);var n=t[0],r=t[1],a=$(n),f=$(r);return R(a),R(f),m(a/f,A(Math.pow(10,p(r)-p(n))))}function Y(t,e){var n=Math.pow(10,e),r=S(Math.round(Math.abs(m(t,n))),n);return t<0&&r!==0&&(r=m(r,-1)),r}var L=!0;function Z(t){t===void 0&&(t=!0),L=t}var ee={strip:A,plus:B,minus:I,times:m,divide:S,round:Y,digitLength:p,float2Fixed:$,enableBoundaryChecking:Z};var b=ee;var te={black:0,silver:12632256,gray:8421504,white:16777215,maroon:8388608,red:16711680,purple:8388736,fuchsia:16711935,green:32768,lime:65280,olive:8421376,yellow:16776960,navy:128,blue:255,teal:32896,aqua:65535,orange:16753920,aliceblue:15792383,antiquewhite:16444375,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,blanchedalmond:16772045,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchard:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:3100495,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavendar:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,limegreen:3329330,linen:16445670,magenta:16711935,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,oldlace:16643558,olivedrab:7048739,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,whitesmoke:16119285,yellowgreen:10145074,rebeccapurple:6697881},F=te;function w(t,e=2){let n=t;for(;n.length<e;)n=`0${n}`;return n}function c(t,e,n){return Math.min(Math.max(t,e),n)}b.enableBoundaryChecking(!1);var d=5,y="(\\s*,\\s*|\\s+)",g="-?[0-9]+(\\.[0-9]+)?",re=/^#?[0-9a-f]{3,8}$/i,P=new RegExp(["^rgba?\\(\\s*",`(?<R>${g}%?)`,y,`(?<G>${g}%?)`,y,`(?<B>${g}%?)`,`(${y}(?<A>${g}%?))?`,"\\s*\\)$"].join(""),"i"),V=new RegExp(["^hsla?\\(\\s*",`(?<H>${g})`,y,`(?<S>${g})%`,y,`(?<L>${g})%`,`(${y}(?<A>${g})%?)?`,"\\s*\\)$"].join(""),"i"),q=new RegExp(["^color\\(\\s*display-p3\\s+",`(?<R>${g}%?)`,"\\s+",`(?<G>${g}%?)`,"\\s+",`(?<B>${g}%?)`,`(\\s*\\/\\s*(?<A>${g}%?))?`,"\\s*\\)$"].join(""),"i"),H=/^linear-gradient\s*\((.*)\);?$/,T=/^radial-gradient\s*\((.*)\);?$/,j=/^conic-gradient\s*\((.*)\);?$/,{round:u,strip:O}=b;function v(t){let e=E(t);return{get hex(){return`#${e.map((n,r)=>r<3?w(u(n*255,0).toString(16),2):n<1?u(n*255,0).toString(16):"").join("")}`},get hexVal(){let n=e.map((r,a)=>a<3?w(u(r*255,0).toString(16),2):r<1?w((r*256).toString(16),2):"");return parseInt(`0x${n.join("")}`,16)},get rgb(){return e[3]==1?`rgb(${u(e[0]*255,0)}, ${u(e[1]*255,0)}, ${u(e[2]*255,0)})`:`rgba(${u(e[0]*255,0)}, ${u(e[1]*255,0)}, ${u(e[2]*255,0)}, ${u(e[3],d)})`},rgbVal:e,get rgba(){return`rgba(${u(e[0]*255,0)}, ${u(e[1]*255,0)}, ${u(e[2]*255,0)}, ${u(e[3],d)})`},rgbaVal:e,get hsl(){let[n,r,a,f]=N(e);return`hsl(${n}, ${O(r*100)}%, ${O(a*100)}%, ${u(f,d)})`},get hslVal(){return N(e)},get p3(){let[n,r,a,f]=e;return`color(display-p3 ${u(n,d)} ${u(r,d)} ${u(a,d)}${f<1?`/${u(f,d)}`:""})`}}}function G(t,e,n=.5,r=2.2){let a=c(n,0,1),f=1-a,s=a,o=1/r,l=r,i=v(t).rgbVal,x=v(e).rgbVal,h=i[0]**l,k=i[1]**l,_=i[2]**l,z=i[3],J=x[0]**l,K=x[1]**l,Q=x[2]**l,W=x[3];return v([c((h**l*f+J**l*s)**o,0,1),c((k**l*f+K**l*s)**o,0,1),c((_**l*f+Q**l*s)**o,0,1),z*f+W*s])}function E(t){function e(n){let r=w(c(n,0,4294967295).toString(16),6);return[parseInt(r.substring(0,2),16)/255,parseInt(r.substring(2,4),16)/255,parseInt(r.substring(4,6),16)/255,parseInt(r.substring(6,8)||"ff",16)/255]}if(Array.isArray(t)){if(t.some(n=>typeof n!="number"))throw new Error(`Color array must be numbers, received ${t}`);if(t.length<3||t.length>4)throw new Error(`Expected [R, G, B, A?], received ${t}`);return t.map(n=>c(n,0,1))}if(typeof t=="number")return e(t);if(typeof t=="string"){let n=t.trim();if(!n)throw new Error("Expected color, received empty string");if(typeof F[n.toLowerCase()]=="number")return e(F[n]);if(re.test(n)){let r=n.replace("#",""),a=parseInt(r.length<6?r.split("").map(f=>`${f}${f}`).join(""):r,16);return e(a)}if(P.test(n)){let r=P.exec(n).groups||{};if(![r.R,r.G,r.B].every(l=>l.includes("%")||!l.includes("%")))throw new Error("Mix of integers and %");let a=parseFloat(r.R)/(r.R.includes("%")?100:255),f=parseFloat(r.G)/(r.G.includes("%")?100:255),s=parseFloat(r.B)/(r.B.includes("%")?100:255),o=1;return r.A&&(o=parseFloat(r.A),r.A.includes("%")&&(o/=100)),[c(a,0,1),c(f,0,1),c(s,0,1),c(o,0,1)]}if(V.test(n)){let r=V.exec(n).groups||{},a=parseFloat(r.H),f=parseFloat(r.S)/100,s=parseFloat(r.L)/100,o=1;return r.A&&(o=parseFloat(r.A),r.A.includes("%")&&(o/=100)),X([a,c(f,0,1),c(s,0,1),c(o,0,1)])}if(q.test(n)){let r=q.exec(n).groups||{},a=parseFloat(r.R);r.R.includes("%")&&(a/=100);let f=parseFloat(r.G);r.G.includes("%")&&(f/=100);let s=parseFloat(r.B);r.B.includes("%")&&(s/=100);let o=1;return r.A&&(o=parseFloat(r.A),r.A.includes("%")&&(o/=100)),[c(a,0,1),c(f,0,1),c(s,0,1),c(o,0,1)]}}throw new Error(`Unable to parse color "${t}"`)}function ne(t,e){let n=E(t);return v([n[0],n[1],n[2],c(e,0,1)])}function D(t,e){let n=c(e,-1,1);return n>=0?G(t,[0,0,0,1],n):C(t,-n)}function C(t,e){let n=c(e,-1,1);return n>=0?G(t,[1,1,1,1],n):D(t,-n)}function U(t,e=2.2){let[n,r,a]=E(t);return b.round(.2126*Math.pow(n,e)+.7152*Math.pow(r,e)+.0722*Math.pow(a,e),d)}function ae(t){let e=U(t);return b.round((e<=216/24389?e*(24389/27):Math.pow(e,1/3)*116-16)/100,d)}function X(t){let[e,n,r,a]=t;e=Math.abs(e%360);let f=n*(1-Math.abs(2*r-1)),s=f*(1-Math.abs(e/60%2-1)),o=0,l=0,i=0;0<=e&&e<60?(o=f,l=s):60<=e&&e<120?(o=s,l=f):120<=e&&e<180?(l=f,i=s):180<=e&&e<240?(l=s,i=f):240<=e&&e<300?(o=s,i=f):300<=e&&e<360&&(o=f,i=s);let x=r-f/2;return[u(o+x,d),u(l+x,d),u(i+x,d),u(a,d)]}function N(t){let[e,n,r,a]=t,f=Math.max(e,n,r),s=Math.min(e,n,r),o=0,l=0,i=(f+s)/2;if(f==s)return[o,l,b.round(i,4),a];let x=f-s;if(x!=0){switch(f){case e:o=60*(n-r)/x;break;case n:o=60*(2+(r-e)/x);break;case r:o=60*(4+(e-n)/x);break}for(;o<0;)o+=360}return i!=0&&i!=1&&(l=(f-i)/Math.min(i,1-i)),[u(o,d-2),u(l,d),u(i,d),a]}function de(t){let e=t.trim(),n="linear-gradient",r,a=[];if(H.test(e))a=e.match(H)[1].split(",").map(i=>i.trim()),(a[0].includes("deg")||a[0].includes("turn")||a[0].includes("to "))&&(r=a.shift());else if(T.test(e))n="radial-gradient",a=e.match(T)[1].split(",").map(i=>i.trim()),(a[0].includes("circle")||a[0].includes("ellipse")||a[0].includes("closest-")||a[0].includes("farthest-"))&&(r=a.shift());else if(j.test(e))n="conic-gradient",a=e.match(j)[1].split(",").map(i=>i.trim()),a[0].includes("from")&&(r=a.shift());else throw new Error(`Unable to parse gradient "${t}"`);if(a.length<2)throw new Error("Gradient must have at least 2 stops");let f=0;for(let i of["%","px","em","rem","pt","ch","cm","in","mm","vh","vm"])e.includes(i)&&(f+=1);if(f>1)throw new Error("Can\u2019t normalize gradients with mixed units");let s=[],o=a.map(i=>parseFloat(i.substring(i.indexOf(" ")))||-1/0),l=Math.max(...o);for(let i=0;i<a.length;i++){if(s[i].color=v(a[i].split(" ")[0]).rgbVal,o[i]>=0){s[i].position=Math.min(o[i]/l,1);continue}else if(i===0){s[i].position=0;continue}else i===a.length-1&&(s[i].position=1);let x=s[i-1].position,h=a.findIndex(k=>parseFloat(k.substring(k.indexOf(" ")))>0);h===-1&&(h=a.length-1),s[i].position=(h-x)/h}return{type:n,position:r,stops:s}}var xe={alpha:ne,darken:D,from:v,hslToRGB:X,lighten:C,lightness:ae,luminance:U,mix:G,parse:E,rgbToHSL:N};export{j as CON_GRAD_RE,re as HEX_RE,V as HSL_RE,H as LIN_GRAD_RE,q as P3_RE,T as RAD_GRAD_RE,P as RGB_RE,ne as alpha,D as darken,xe as default,v as from,X as hslToRGB,C as lighten,ae as lightness,U as luminance,G as mix,E as parse,de as parseGradient,N as rgbToHSL};
{
"name": "better-color-tools",
"description": "Better color manipulation for Sass and JavaScript / TypeScript.",
"version": "0.4.0",
"version": "0.5.0",
"author": {

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

"types": "./dist/index.d.ts",
"devDependencies": {
"@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",
"esbuild": "^0.14.11",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^9.1.3",
"npm-run-all": "^4.1.5",
"number-precision": "^1.5.1",
"prettier": "^2.5.1",
"sass": "^1.47.0",
"typescript": "^4.5.4"
},
"scripts": {

@@ -52,6 +35,23 @@ "build": "rm -rf dist && tsc && npm run bundle && cp ./dist/index.min.js example",

"lint": "eslint \"**/*.{js,ts}\"",
"prepublishOnly": "npm run build",
"start": "npm run build && npx serve example",
"test": "mocha --parallel"
},
"readme": "# better-color-tools\n\nBetter color manipulation for Sass and JavaScript/TypeScript. Fast (`75,000` ops/s) and lightweight (`3.7 kB` gzip).\n\nSupports:\n\n- ✅ RGB / Hex\n- ✅ HSL\n- ✅ [P3]\n\n👉 **Playground**: https://better-color-tools.pages.dev/\n\n## Installing\n\n```\nnpm install better-color-tools\n```\n\n## Mix\n\nNot 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\nfunction:\n\n![](./.github/images/r-g.png)\n\n![](./.github/images/g-b.png)\n\n![](./.github/images/b-y.png)\n\n![](./.github/images/k-c.png)\n\n![](./.github/images/k-w.png)\n\nNotice 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.\n\n```scss\n// Sass\n@use 'better-color-tools' as better;\n\n$mix: better.mix(#1a7f37, #cf222e, 0); // 100% color 1, 0% color 2\n$mix: better.mix(#1a7f37, #cf222e, 0.25); // 75%, 25%\n$mix: better.mix(#1a7f37, #cf222e, 0.5); // 50%, 50%\n$mix: better.mix(#1a7f37, #cf222e, 0.75); // 25%, 75%\n$mix: better.mix(#1a7f37, #cf222e, 1); // 0%, 100%\n```\n\n```ts\n// JavaScript / TypeScript\nimport better from 'better-color-tools';\n\nconst mix = better.mix(0x1a7f37, 0xcf222e, 0); // 100% color 1, 0% color 2\nconst mix = better.mix(0x1a7f37, 0xcf222e, 0.25); // 75%, 25%\nconst mix = better.mix(0x1a7f37, 0xcf222e, 0.5); // 50%, 50%\nconst mix = better.mix(0x1a7f37, 0xcf222e, 0.75); // 25%, 75%\nconst mix = better.mix(0x1a7f37, 0xcf222e, 1); // 0%, 100%\n```\n\n_Note: `0xcf222e` in JS is just another way of writing `'#cf222e'` (replacing the `#` with `0x`). Either are valid; use whichever you prefer!_\n\n### Advanced: gamma adjustment\n\nTo 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).\n\n```scss\n// Sass\n$gamma: 2.2; // default\n$mix: better.mix(#1a7f37, #cf222e, 0, $gamma);\n```\n\n```ts\n// JavaScript / TypeScript\nconst gamma = 2.2; // default\nconst mix = better.mix(0x1a7f37, 0xcf222e, 0, gamma);\n```\n\n## Lighten / Darken\n\n![](./.github/images/k-c.png)\n\n_Top: better-color-utils / Bottom: RGB averaging_\n\nThe 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\nhalfway to black, and `lighten(…, 0.5)` will always be halfway to white.\n\n```scss\n// Sass\n@use 'better-color-tools' as better;\n\n$lighter: better.lighten(#cf222e, 0); // 0% lighter (original color)\n$lighter: better.lighten(#cf222e, 0.25); // 25% lighter\n$lighter: better.lighten(#cf222e, 1); // 100% lighter (pure white)\n\n$darker: better.darken(#cf222e, 0); // 0% darker (original color)\n$darker: better.darken(#cf222e, 0.25); // 25% darker\n$darker: better.darken(#cf222e, 1); // 100% darker (pure black)\n```\n\n```ts\n// JavaScript / TypeScript\nimport better from 'better-color-tools';\n\nbetter.lighten(0xcf222e, 0); // 0% lighter (original color)\nbetter.lighten(0xcf222e, 0.25); // 25% lighter\nbetter.lighten(0xcf222e, 1); // 100% lighter (pure white)\n\nbetter.darken(0xcf222e, 0); // 0% darker (original color)\nbetter.darken(0xcf222e, 0.25); // 25% darker\nbetter.darken(0xcf222e, 1); // 100% darker (pure black)\n```\n\n## Gradient\n\n![](./.github/images/b-g-gradient.png)\n\n_Top: better-color-utils / Bottom: standard CSS gradient_\n\nCSS gradients and SVG gradients are, sadly, not gamma-optimized. But you can fix that with `better.gradient()`. While there’s no _perfect_ fix for this, this solution drastically improves gradients without bloating filesize.\n\n```ts\n// JavaScript/TypeScript\nimport better from 'better-color-tools';\n\nconst badGradient = 'linear-gradient(90deg, red, lime)';\nconst awesomeGradient = better.gradient(badGradient); // linear-gradient(90deg,#ff0000,#e08800,#baba00,#88e000,#00ff00)\nconst awesomeP3Gradient = better.gradient(badGradient, true); // linear-gradient(90deg,color(display-p3 0 0 1), … )\n```\n\n`better.gradient()` takes any valid CSS gradient as its first parameter. Also specify `true` as the 2nd parameter to generate a P3 gradient instead of hex.\n\n⚠️ Note: unfortunately there’s not a generator function for Sass (and may not ever be) as it’s quite hard to manipulate strings. Please try [the sandbox](https://better-color-tools.pages.dev) to generate a gradient and copy/paste into Sass for now (which\nwill also let you modify it / improve it).\n\n## Perceived Lightness\n\nHSL’s lightness is basically worthless as it’s distorted by the RGB colorspace and has no bearing in actual color brightness or human perception. **Don’t use HSL for lightness!** Instead, use the following:\n\n```js\nimport better from 'better-color-tools;\n\nconst DARK_PURPLE = '#542be9'\n\n// lightness: get human-perceived brightness of a color (blues will appear darker than reds and yellows, e.g.)\nconst lightness = better.lightness(DARK_PURPLE); // 0.3635 (~36% lightness)\n\n// luminance: get absolute brightness of a color (this may not be what you want!)\nconst luminance = better.luminance(DARK_PURPLE); // 0.0919 (~9% luminance)\n\n// HSL (for comparison)\nconst hsl = color.from(DARK_PURPLE).hslVal[3]; // 0.5412 (54%!? there’s no way this dark purple is that bright!)\n```\n\n## Conversion\n\n### Sass\n\nSass already has many [built-in converters][sass-convert], so this library only extends what’s there. Here are a few helpers:\n\n#### P3\n\nThe `p3()` function can convert any Sass-readable color into [P3][p3]:\n\n```scss\n$green: #00ff00;\n$blue: #0000ff;\n\ncolor: $green; // #00ff00\ncolor: better.p3($green); // color(display-p3 0 1 0)\n\nbackground: linear-gradient(135deg, $green, $blue); // linear-gradient(135deg, #00ff00, #0000ff)\nbackground: linear-gradient(135deg, better.p3($green), better.p3($blue)); // linear-gradient(135deg, color(display-p3 0 1 0), color(dipslay-p3 0 0 1)))\n```\n\n⚠️ Be sure to always include fallback colors when using P3\n\n### JavaScript / TypeScript\n\n`better.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:\n\n```ts\nimport better from 'better-color-tools';\n\nbetter.from('rgb(196, 67, 43)').hex; // '#c4432b'\nbetter.from('rebeccapurple').hsl; // 'hsl(270, 50%, 40%)'\n```\n\n| Output | Type | Example |\n| :------- | :--------: | :-------------------------- |\n| `hex` | `string` | `\"#ffffff\"` |\n| `hexVal` | `number` | `0xffffff` |\n| `rgb` | `string` | `\"rgb(255, 255, 255)\"` |\n| `rgbVal` | `number[]` | `[1, 1, 1, 1]` |\n| `rgba` | `string` | `\"rgba(255, 255, 255, 1)\"` |\n| `hsl` | `string` | `\"hsl(360, 0%, 100%)\"` |\n| `hslVal` | `number[]` | `[360, 0, 1, 1]\"` |\n| `p3` | `string` | `\"color(display-p3 1 1 1)\"` |\n\n#### A note on HSL\n\n[HSL is lossy when rounding to integers][hsl-rgb], so better-color-tools will yield better results than any library that rounds HSL, or rounds HSL by default.\n\n#### A note on CSS color names\n\nThis library can convert _FROM_ a CSS color name, but can’t 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.\\_\n\n#### A note on P3\n\nWhen converting to or from P3, this library converts “lazily,” meaning the R/G/B channels are converted 1:1. This differs from some conversions which attempt to simulate hardware differences. Compare this library to colorjs.io:\n\n| P3 Color | better-color-tools | colorjs.io |\n| :------: | :----------------: | :--------: |\n| `1 0 0` | `255 0 0` | `250 0 0` |\n\nFor 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/).\n\n## TODO / Roadmap\n\n- **Planned**: LAB conversion\n- **Planned**: Sass function for Gamma-corrected gradients\n\n[color-convert]: https://github.com/Qix-/color-convert\n[hsl]: https://en.wikipedia.org/wiki/HSL_and_HSV#Disadvantages\n[hsl-rgb]: https://pow.rs/blog/dont-use-hsl-for-anything/\n[gamma]: https://observablehq.com/@sebastien/srgb-rgb-gamma\n[number-precision]: https://github.com/nefe/number-precision\n[p3]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color()\n[sass-color]: https://sass-lang.com/documentation/modules/color\n[sass-color-scale]: https://sass-lang.com/documentation/modules/color#scale\n[sass-convert]: https://sass-lang.com/documentation/values/colors\n"
}
"devDependencies": {
"@changesets/cli": "^2.20.0",
"@types/node": "^16.11.24",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0",
"chai": "^4.3.6",
"esbuild": "^0.14.21",
"eslint": "^8.8.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^9.2.0",
"npm-run-all": "^4.1.5",
"number-precision": "^1.5.1",
"prettier": "^2.5.1",
"sass": "^1.49.7",
"typescript": "^4.5.5"
}
}
# better-color-tools
Better color manipulation for Sass and JavaScript/TypeScript. Fast (`75,000` ops/s) and lightweight (`3.7 kB` gzip).
Better color manipulation for Sass and JavaScript/TypeScript. Fast (`80,000` ops/s) and lightweight (no dependencies, `4.4 kB` gzip).

@@ -40,7 +40,7 @@ Supports:

$mix: better.mix(#1a7f37, #cf222e, 0); // 100% color 1, 0% color 2
$mix: better.mix(#1a7f37, #cf222e, 0); // 100% color 1, 0% color 2
$mix: better.mix(#1a7f37, #cf222e, 0.25); // 75%, 25%
$mix: better.mix(#1a7f37, #cf222e, 0.5); // 50%, 50%
$mix: better.mix(#1a7f37, #cf222e, 0.5); // 50%, 50%
$mix: better.mix(#1a7f37, #cf222e, 0.75); // 25%, 75%
$mix: better.mix(#1a7f37, #cf222e, 1); // 0%, 100%
$mix: better.mix(#1a7f37, #cf222e, 1); // 0%, 100%
```

@@ -52,7 +52,7 @@

const mix = better.mix(0x1a7f37, 0xcf222e, 0); // 100% color 1, 0% color 2
const mix = better.mix(0x1a7f37, 0xcf222e, 0); // 100% color 1, 0% color 2
const mix = better.mix(0x1a7f37, 0xcf222e, 0.25); // 75%, 25%
const mix = better.mix(0x1a7f37, 0xcf222e, 0.5); // 50%, 50%
const mix = better.mix(0x1a7f37, 0xcf222e, 0.5); // 50%, 50%
const mix = better.mix(0x1a7f37, 0xcf222e, 0.75); // 25%, 75%
const mix = better.mix(0x1a7f37, 0xcf222e, 1); // 0%, 100%
const mix = better.mix(0x1a7f37, 0xcf222e, 1); // 0%, 100%
```

@@ -62,3 +62,3 @@

### Advanced: gamma adjustment
#### Advanced: gamma adjustment

@@ -92,9 +92,9 @@ 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).

$lighter: better.lighten(#cf222e, 0); // 0% lighter (original color)
$lighter: better.lighten(#cf222e, 0); // 0% lighter (original color)
$lighter: better.lighten(#cf222e, 0.25); // 25% lighter
$lighter: better.lighten(#cf222e, 1); // 100% lighter (pure white)
$lighter: better.lighten(#cf222e, 1); // 100% lighter (pure white)
$darker: better.darken(#cf222e, 0); // 0% darker (original color)
$darker: better.darken(#cf222e, 0.25); // 25% darker
$darker: better.darken(#cf222e, 1); // 100% darker (pure black)
$darker: better.darken(#cf222e, 0); // 0% darker (original color)
$darker: better.darken(#cf222e, 0.25); // 25% darker
$darker: better.darken(#cf222e, 1); // 100% darker (pure black)
```

@@ -106,9 +106,9 @@

better.lighten(0xcf222e, 0); // 0% lighter (original color)
better.lighten(0xcf222e, 0); // 0% lighter (original color)
better.lighten(0xcf222e, 0.25); // 25% lighter
better.lighten(0xcf222e, 1); // 100% lighter (pure white)
better.lighten(0xcf222e, 1); // 100% lighter (pure white)
better.darken(0xcf222e, 0); // 0% darker (original color)
better.darken(0xcf222e, 0.25); // 25% darker
better.darken(0xcf222e, 1); // 100% darker (pure black)
better.darken(0xcf222e, 0); // 0% darker (original color)
better.darken(0xcf222e, 0.25); // 25% darker
better.darken(0xcf222e, 1); // 100% darker (pure black)
```

@@ -118,47 +118,46 @@

![](./.github/images/b-g-gradient.png)
⚠️ **Temporarily removed**
_Top: better-color-utils / Bottom: standard CSS gradient_
This utility transformed normal, “incorrect” gradients into gamma-corrected gradients in a cross-browser-compatible way (at least, as close as one can without browser support). With the upcoming
[RGB colorspace proposal](https://twitter.com/argyleink/status/1490376117064065025?s=20&t=-MnzzUXCUmyeATVYXB4WbA), however, this tool has been temporarily removed so that it can be reworked into a sort of polyfill for that. Look for it in an upcoming
release!
CSS gradients and SVG gradients are, sadly, not gamma-optimized. But you can fix that with `better.gradient()`. While there’s no _perfect_ fix for this, this solution drastically improves gradients without bloating filesize.
## Lightness
```ts
// JavaScript/TypeScript
import better from 'better-color-tools';
[Don’t use HSL for lightness](https://twitter.com/sitnikcode/status/1470755010464161794?s=20&t=-MnzzUXCUmyeATVYXB4WbA); use this!
const badGradient = 'linear-gradient(90deg, red, lime)';
const awesomeGradient = better.gradient(badGradient); // linear-gradient(90deg,#ff0000,#e08800,#baba00,#88e000,#00ff00)
const awesomeP3Gradient = better.gradient(badGradient, true); // linear-gradient(90deg,color(display-p3 0 0 1), … )
```
```scss
@use 'better-color-tools' as better;
`better.gradient()` takes any valid CSS gradient as its first parameter. Also specify `true` as the 2nd parameter to generate a P3 gradient instead of hex.
$background: #174fd2;
$is-dark: better.lightness($background) < 0.5;
⚠️ Note: unfortunately there’s not a generator function for Sass (and may not ever be) as it’s quite hard to manipulate strings. Please try [the sandbox](https://better-color-tools.pages.dev) to generate a gradient and copy/paste into Sass for now (which
will also let you modify it / improve it).
.card {
@if $is-dark {
color: white; // white text over dark color
} @else {
color: black; // black text over light color
}
}
```
## Perceived Lightness
HSL’s lightness is basically worthless as it’s distorted by the RGB colorspace and has no bearing in actual color brightness or human perception. **Don’t use HSL for lightness!** Instead, use the following:
```js
import better from 'better-color-tools;
const DARK_PURPLE = '#542be9'
const DARK_PURPLE = '#542be9';
// lightness: get human-perceived brightness of a color (blues will appear darker than reds and yellows, e.g.)
const lightness = better.lightness(DARK_PURPLE); // 0.3635 (~36% lightness)
better.lightness(DARK_PURPLE); // 0.3635 (~36% lightness)
// luminance: get absolute brightness of a color (this may not be what you want!)
const luminance = better.luminance(DARK_PURPLE); // 0.0919 (~9% luminance)
better.luminance(DARK_PURPLE); // 0.0919 (~9% luminance)
// HSL (for comparison)
const hsl = color.from(DARK_PURPLE).hslVal[3]; // 0.5412 (54%!? there’s no way this dark purple is that bright!)
better.from(DARK_PURPLE).hslVal[3]; // 0.5412 (54%!? there’s no way this dark purple is that bright!)
```
## Conversion
## Other Tools
### Sass
Sass already has many [built-in converters][sass-convert], so this library only extends what’s there. Here are a few helpers:
#### P3

@@ -169,14 +168,28 @@

```scss
@use 'better-color-tools' as better;
$green: #00ff00;
$blue: #0000ff;
color: $green; // #00ff00
color: better.p3($green); // color(display-p3 0 1 0)
background: linear-gradient(135deg, $green, $blue); // linear-gradient(135deg, #00ff00, #0000ff)
background: linear-gradient(135deg, better.p3($green), better.p3($blue)); // linear-gradient(135deg, color(display-p3 0 1 0), color(dipslay-p3 0 0 1)))
```
⚠️ Be sure to always include fallback colors when using P3
#### Fallback Mixin
The fallback mixin can be used to easily support advanced color modes:
```scss
@use 'better-color-tools' as better;
.button {
@include better.fallback(background, better.p3(#174fd2), #174fd2);
}
// .button {
// background: #174fd2;
// background: color(display-p3 0.090196 0.3098 0.823529);
// }
```
### JavaScript / TypeScript

@@ -193,37 +206,31 @@

| 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)"` |
| Code | Type | Example |
| :---------------------- | :--------: | :-------------------------- |
| `better.from(…).hex` | `string` | `"#ffffff"` |
| `better.from(…).hexVal` | `number` | `0xffffff` |
| `better.from(…).rgb` | `string` | `"rgb(255, 255, 255)"` |
| `better.from(…).rgbVal` | `number[]` | `[1, 1, 1, 1]` |
| `better.from(…).rgba` | `string` | `"rgba(255, 255, 255, 1)"` |
| `better.from(…).hsl` | `string` | `"hsl(360, 0%, 100%)"` |
| `better.from(…).hslVal` | `number[]` | `[360, 0, 1, 1]"` |
| `better.from(…).p3` | `string` | `"color(display-p3 1 1 1)"` |
#### A note on HSL
[HSL is lossy when rounding to integers][hsl-rgb], so better-color-tools will yield better results than any library that rounds HSL, or rounds HSL by default.
[HSL is lossy when rounding to integers][hsl-rgb], so for that reason this library will always preserve decimals in HSL, and there’s no way to disable this.
#### A note on CSS color names
This library can convert _FROM_ a CSS color name, but can’t 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.\_
This library can convert _FROM_ a CSS color name, but can’t 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.
#### A note on P3
When converting to or from P3, this library converts “lazily,” meaning the R/G/B channels are converted 1:1. This differs from some conversions which attempt to simulate hardware differences. Compare this library to colorjs.io:
When converting to or from P3, this library converts “lazily,” meaning the R/G/B channels are converted 1:1. This differs from some conversions which attempt to simulate hardware differences (such as colorjs.io). For the most part, the “lazy” approach
makes P3 much more usable for general purpose and for that reason is the approach [recommended by Apple for Safari](https://webkit.org/blog/10042/wide-gamut-color-in-css-with-display-p3/).
| P3 Color | better-color-tools | colorjs.io |
| :------: | :----------------: | :--------: |
| `1 0 0` | `255 0 0` | `250 0 0` |
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/).
## TODO / Roadmap
- **Planned**: LAB conversion
- **Planned**: Sass function for Gamma-corrected gradients
- **Planned**: [Web Colorspace](https://twitter.com/argyleink/status/1490376117064065025?s=20&t=-MnzzUXCUmyeATVYXB4WbA) gradient polyfill (Sass & TS)
[color-convert]: https://github.com/Qix-/color-convert
[hsl]: https://en.wikipedia.org/wiki/HSL_and_HSV#Disadvantages

@@ -236,2 +243,1 @@ [hsl-rgb]: https://pow.rs/blog/dont-use-hsl-for-anything/

[sass-color-scale]: https://sass-lang.com/documentation/modules/color#scale
[sass-convert]: https://sass-lang.com/documentation/values/colors
import NP from 'number-precision';
import cssNames from './css-names.js';
import { clamp, leftPad, splitDistance } from './utils.js';
import { clamp, leftPad } from './utils.js';

@@ -21,2 +21,3 @@ export type RGB = [number, number, number];

};
export type GradientStop = { color: RGBA; position: number };

@@ -32,6 +33,5 @@ NP.enableBoundaryChecking(false); // don’t throw error on inaccurate calculation

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');
export const LIN_GRAD_RE = /^linear-gradient\((.*)\);?$/;
export const RAD_GRAD_RE = /^radial-gradient\((.*)\);?$/;
export const CON_GRAD_RE = /^conic-gradient\((.*)\);?$/;
export const STOP_POS_RE = /\s[^\s]+$/;
export const LIN_GRAD_RE = /^linear-gradient\s*\((.*)\);?$/;
export const RAD_GRAD_RE = /^radial-gradient\s*\((.*)\);?$/;
export const CON_GRAD_RE = /^conic-gradient\s*\((.*)\);?$/;
const { round, strip } = NP;

@@ -274,5 +274,5 @@

*/
export function luminance(color: Color): number {
export function luminance(color: Color, γ = 2.2): number {
const [r, g, b] = parse(color);
return NP.round(0.2126 * Math.pow(r, 2.2) + 0.7152 * Math.pow(g, 2.2) + 0.0722 * Math.pow(b, 2.2), P);
return NP.round(0.2126 * Math.pow(r, γ) + 0.7152 * Math.pow(g, γ) + 0.0722 * Math.pow(b, γ), P);
}

@@ -376,61 +376,68 @@

/**
* Gamma Gradient
* Take any CSS gradient and correct gamma
* Parse CSS gradient
* Parse any valid CSS gradient and convert to stops
*/
function gradient(input: string, p3 = false): string {
export function parseGradient(input: string): { type: 'linear-gradient' | 'radial-gradient' | 'conic-gradient'; position?: string; stops: GradientStop[] } {
const gradString = input.trim();
let gradType: 'linear-gradient' | 'radial-gradient' | 'conic-gradient' = 'linear-gradient';
let position: string | undefined;
let stops: string[] = [];
let rawStops: string[] = [];
if (LIN_GRAD_RE.test(gradString)) {
stops = (gradString.match(LIN_GRAD_RE) as RegExpMatchArray)[1].split(',').map((p) => p.trim());
if (stops[0].includes('deg') || stops[0].includes('turn') || stops[0].includes('to ')) {
position = stops.shift();
rawStops = (gradString.match(LIN_GRAD_RE) as RegExpMatchArray)[1].split(',').map((p) => p.trim());
if (rawStops[0].includes('deg') || rawStops[0].includes('turn') || rawStops[0].includes('to ')) {
position = rawStops.shift();
}
} else if (RAD_GRAD_RE.test(gradString)) {
gradType = 'radial-gradient';
stops = (gradString.match(RAD_GRAD_RE) as RegExpMatchArray)[1].split(',').map((p) => p.trim());
if (stops[0].includes('circle') || stops[0].includes('ellipse') || stops[0].includes('closest-') || stops[0].includes('farthest-')) {
position = stops.shift();
rawStops = (gradString.match(RAD_GRAD_RE) as RegExpMatchArray)[1].split(',').map((p) => p.trim());
if (rawStops[0].includes('circle') || rawStops[0].includes('ellipse') || rawStops[0].includes('closest-') || rawStops[0].includes('farthest-')) {
position = rawStops.shift();
}
} else if (CON_GRAD_RE.test(gradString)) {
gradType = 'conic-gradient';
stops = (gradString.match(CON_GRAD_RE) as RegExpMatchArray)[1].split(',').map((p) => p.trim());
if (stops[0].includes('from')) {
position = stops.shift();
rawStops = (gradString.match(CON_GRAD_RE) as RegExpMatchArray)[1].split(',').map((p) => p.trim());
if (rawStops[0].includes('from')) {
position = rawStops.shift();
}
} else throw new Error(`Unable to parse gradient "${input}"`);
if (rawStops.length < 2) {
throw new Error('Gradient must have at least 2 stops');
}
let unitCount = 0;
for (const unit of ['%', 'px', 'em', 'rem', 'pt', 'ch', 'cm', 'in', 'mm', 'vh', 'vm']) {
if (gradString.includes(unit)) unitCount += 1;
}
if (unitCount > 1) throw new Error(`Can’t normalize gradients with mixed units`);
const newGradient: { color: string; pos?: string | number }[] = [];
const stops: GradientStop[] = [];
for (const stop of stops) {
let pos2 = '';
let color2 = stop;
const posMatch = stop.match(STOP_POS_RE);
if (posMatch) {
pos2 = posMatch[0].trim();
color2 = stop.replace(pos2, '').trim();
}
if (newGradient.length) {
const prevItem = newGradient[newGradient.length - 1];
const { pos: pos1, color: color1 } = prevItem;
const skipTransitions = (splitDistance(pos1, pos2) as number) <= 0 || from(color1).hex === from(color2).hex; // stops are on top of each other; skip
const rawPositions = rawStops.map((s) => parseFloat(s.substring(s.indexOf(' '))) || -Infinity);
const max = Math.max(...rawPositions);
// TODO: increase/decrease stops
if (!skipTransitions) {
for (let i = 1; i <= 3; i++) {
const p = 0.25 * i;
let c = mix(color1, color2, p);
newGradient.push({ color: p3 ? c.p3 : c.hex, pos: splitDistance(pos1 || 0, pos2, p) });
}
}
for (let n = 0; n < rawStops.length; n++) {
// color is easy
stops[n].color = from(rawStops[n].split(' ')[0]).rgbVal;
// position is not (if omitted, we’ll have to figure out averages)
if (rawPositions[n] >= 0) {
stops[n].position = Math.min(rawPositions[n] / max, 1);
continue;
} else if (n === 0) {
stops[n].position = 0;
continue;
} else if (n === rawStops.length - 1) {
stops[n].position = 1;
}
const c = from(color2);
newGradient.push({ color: p3 ? c.p3 : c.hex, pos: pos2 });
let startPosition = stops[n - 1].position;
let endPosition = rawStops.findIndex((s) => parseFloat(s.substring(s.indexOf(' '))) > 0);
if (endPosition === -1) endPosition = rawStops.length - 1;
stops[n].position = (endPosition - startPosition) / endPosition;
}
return `${gradType}(${[...(position ? [position] : []), ...newGradient.map(({ color, pos }) => `${color}${pos ? ` ${pos}` : ''}`)].join(',')})`;
return {
type: gradType,
position,
stops,
};
}
/** @deprecated (use gradient instead) */
export const gammaGradient = gradient;

@@ -441,4 +448,2 @@ export default {

from,
gammaGradient,
gradient,
hslToRGB,

@@ -445,0 +450,0 @@ lighten,

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