Skip to content

Instantly share code, notes, and snippets.

@ricokahler
Last active May 23, 2020 22:57
Show Gist options
  • Save ricokahler/de697f10d1d8cc4e89a1f0bb16630072 to your computer and use it in GitHub Desktop.
Save ricokahler/de697f10d1d8cc4e89a1f0bb16630072 to your computer and use it in GitHub Desktop.
color2k in one file
import parseToRgba from '@color2k/parse-to-rgba';
export { default as parseToRgba } from '@color2k/parse-to-rgba';
// taken from:
/**
* Parses a color in hue, saturation, lightness, and the alpha channel.
*
* Hue is a number between 0 and 360, saturation, lightness, and alpha are
* decimal percentages between 0 and 1
*/
function parseToHsla(color) {
const [red, green, blue, alpha] = parseToRgba(color).map((
value,
index // 3rd index is alpha channel which is already normalized
) => (index === 3 ? value : value / 255));
const max = Math.max(red, green, blue);
const min = Math.min(red, green, blue);
const lightness = (max + min) / 2; // achromatic
if (max === min) return [0, 0, lightness, alpha];
const delta = max - min;
const saturation =
lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
const hue =
60 *
(red === max
? (green - blue) / delta + (green < blue ? 6 : 0)
: green === max
? (blue - red) / delta + 2
: (red - green) / delta + 4);
return [hue, saturation, lightness, alpha];
}
/**
* A simple guard function:
*
* ```js
* Math.min(Math.max(low, value), high)
* ```
*/
function guard(low, high, value) {
return Math.min(Math.max(low, value), high);
}
/**
* Takes in hsla parts and constructs an hsla string
*
* @param hue The color circle (from 0 to 360) - 0 (or 360) is red, 120 is green, 240 is blue
* @param saturation Percentage of saturation, given as a decimal between 0 and 1
* @param lightness Percentage of lightness, given as a decimal between 0 and 1
* @param alpha Percentage of opacity, given as a decimal between 0 and 1
*/
function hsla(hue, saturation, lightness, alpha) {
return `hsla(${hue % 360}, ${guard(
0,
100,
saturation * 100
).toFixed()}%, ${guard(0, 100, lightness * 100).toFixed()}%, ${guard(
0,
1,
alpha
)})`;
}
/**
* Adjusts the current hue of the color by the given degrees. Wraps around when
* over 360.
*
* @param color input color
* @param degrees degrees to adjust the input color, accepts degree integers
* (0 - 360) and wraps around on overflow
*/
function adjustHue(color, degrees) {
const [h, s, l, a] = parseToHsla(color);
return hsla(h + degrees, s, l, a);
}
// https://github.com/styled-components/polished/blob/0764c982551b487469043acb56281b0358b3107b/src/color/getLuminance.js
/**
* Returns a number (float) representing the luminance of a color.
*/
function getLuminance(color) {
if (color === 'transparent') return 0;
function f(x) {
const channel = x / 255;
return channel <= 0.03928
? channel / 12.92
: ((channel + 0.055) / 1.055) ** 2.4;
}
const [r, g, b] = parseToRgba(color);
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
}
/**
* Uses Stevens's Power Law to get value for perceived brightness. Returns a
* value between 0 and 1.
*/
function getBrightness(color) {
return Math.pow(getLuminance(color), 0.5);
}
/**
* Darkens using lightness. This is equivalent to subtracting the lightness
* from the L in HSL.
*
* @param amount the amount to darken, given as a decimal between 0 and 1
*/
function lightnessDarken(color, amount) {
const [hue, saturation, lightness, alpha] = parseToHsla(color);
return hsla(hue, saturation, lightness - amount, alpha);
}
const step = 0.01;
/**
* Darkens the input color by the given amount using brightness
*
* @param amount the amount to darken, given as a decimal between 0 and 1
*/
function darken(color, amount) {
if (amount === 0) return color;
const originalBrightness = getBrightness(color);
let currentBrightness = originalBrightness;
let currentColor = color;
while (Math.abs(currentBrightness - originalBrightness) < Math.abs(amount)) {
const direction = amount > 0 ? 1 : -1;
currentColor = lightnessDarken(currentColor, direction * step);
currentBrightness = getBrightness(currentColor);
if (currentBrightness <= 0) return '#000';
if (currentBrightness >= 1) return '#fff';
}
return currentColor;
}
/**
* Desaturates the input color by the given amount via subtracting from the `s`
* in `hsla`.
*
* @param amount amount to desaturate, given as a decimal between 0 and 1
*/
function desaturate(color, amount) {
const [h, s, l, a] = parseToHsla(color);
return hsla(h, s - amount, l, a);
}
// taken from:
/**
* Returns the contrast ratio between two colors based on
* [W3's recommended equation for calculating contrast](http://www.w3.org/TR/WCAG20/#contrast-ratiodef).
*/
function getContrast(color1, color2) {
const luminance1 = getLuminance(color1);
const luminance2 = getLuminance(color2);
return luminance1 > luminance2
? (luminance1 + 0.05) / (luminance2 + 0.05)
: (luminance2 + 0.05) / (luminance1 + 0.05);
}
/**
* Takes in rgba parts and returns an rgba string
*
* @param red The amount of red in the red channel, given in a number between 0 and 255 inclusive
* @param green The amount of green in the red channel, given in a number between 0 and 255 inclusive
* @param blue The amount of blue in the red channel, given in a number between 0 and 255 inclusive
* @param alpha Percentage of opacity, given as a decimal between 0 and 1
*/
function rgba(red, green, blue, alpha) {
return `rgba(${guard(0, 255, red).toFixed()}, ${guard(
0,
255,
green
).toFixed()}, ${guard(0, 255, blue).toFixed()}, ${guard(0, 1, alpha)})`;
}
/**
* Mixes two colors together. Taken from sass's implementation.
*/
function mix(color1, color2, weight) {
const normalize = (
n,
index // 3rd index is alpha channel which is already normalized
) => (index === 3 ? n : n / 255);
const [r1, g1, b1, a1] = parseToRgba(color1).map(normalize);
const [r2, g2, b2, a2] = parseToRgba(color2).map(normalize); // The formula is copied from the original Sass implementation:
// http://sass-lang.com/documentation/Sass/Script/Functions.html#mix-instance_method
const alphaDelta = a2 - a1;
const x = weight * 2 - 1;
const y = x * alphaDelta === -1 ? x : x + alphaDelta;
const z = 1 + x * alphaDelta;
const weight2 = (y / z + 1) / 2.0;
const weight1 = 1 - weight2;
const r = (r1 * weight1 + r2 * weight2) * 255;
const g = (g1 * weight1 + g2 * weight2) * 255;
const b = (b1 * weight1 + b2 * weight2) * 255;
const a = a2 * weight + a1 * (1 - weight);
return rgba(r, g, b, a);
}
/**
* Given a series colors, this function will return a `scale(x)` function that
* accepts a percentage as a decimal between 0 and 1 and returns the color at
* that percentage in the scale.
*
* ```js
* const scale = getScale('red', 'yellow', 'green');
* console.log(scale(0)); // rgba(255, 0, 0, 1)
* console.log(scale(0.5)); // rgba(255, 255, 0, 1)
* console.log(scale(1)); // rgba(0, 128, 0, 1)
* ```
*
* If you'd like to limit the domain and range like chroma-js, we recommend
* wrapping scale again.
*
* ```js
* const _scale = getScale('red', 'yellow', 'green');
* const scale = x => _scale(x / 100);
*
* console.log(scale(0)); // rgba(255, 0, 0, 1)
* console.log(scale(50)); // rgba(255, 255, 0, 1)
* console.log(scale(100)); // rgba(0, 128, 0, 1)
* ```
*/
function getScale(...colors) {
return (n) => {
const lastIndex = colors.length - 1;
const lowIndex = guard(0, lastIndex, Math.floor(n * lastIndex));
const highIndex = guard(0, lastIndex, Math.ceil(n * lastIndex));
const color1 = colors[lowIndex];
const color2 = colors[highIndex];
const unit = 1 / lastIndex;
const weight = (n - unit * lowIndex) / unit;
return mix(color1, color2, weight);
};
}
const guidelines = {
decorative: 1.5,
readable: 3,
aa: 4.5,
aaa: 7,
};
/**
* Returns whether or not a color has bad contrast according to a given standard
*/
function hasBadContrast(color, standard = 'aa') {
return getContrast(color, '#fff') < guidelines[standard];
}
/**
* Lightens a color by a given amount. This is equivalent to
* `darken(color, -amount)`
*
* @param amount the amount to darken, given as a decimal between 0 and 1
*/
function lighten(color, amount) {
return darken(color, -amount);
}
/**
* Takes in a color and makes it more transparent by convert to `rgba` and
* decreasing the amount in the alpha channel.
*
* @param amount the amount to darken, given as a decimal between 0 and 1
*/
function transparentize(color, amount) {
const [r, g, b, a] = parseToRgba(color);
return rgba(r, g, b, a - amount);
}
/**
* Takes a color and un-transparentizes it. Equivalent to
* `transparentize(color, -amount)`
*
* @param amount the amount to darken, given as a decimal between 0 and 1
*/
function opacify(color, amount) {
return transparentize(color, -amount);
}
/**
* An alternative function to `readableColor`. Returns whether or not the
* readable color (i.e. the color to be place on top the input color) should be
* black.
*/
function readableColorIsBlack(color) {
return getLuminance(color) > 0.179;
}
/**
* Returns black or white for best contrast depending on the luminosity of the
* given color.
*/
function readableColor(color) {
return readableColorIsBlack(color) ? '#000' : '#fff';
}
/**
* Saturates a color by converting it to `hsl` and increasing the saturation
* amount. Equivalent to `desaturate(color, -amount)`
*
* @param color the input color
* @param amount the amount to darken, given as a decimal between 0 and 1
*/
function saturate(color, amount) {
return desaturate(color, -amount);
}
const { ColorError } = parseToRgba;
export {
ColorError,
adjustHue,
darken,
desaturate,
getBrightness,
getContrast,
getLuminance,
getScale,
guard,
hasBadContrast,
hsla,
lighten,
lightnessDarken,
mix,
opacify,
parseToHsla,
readableColor,
readableColorIsBlack,
rgba,
saturate,
transparentize,
};
//# sourceMappingURL=index.esm.js.map
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment