Last active
May 23, 2020 22:57
-
-
Save ricokahler/de697f10d1d8cc4e89a1f0bb16630072 to your computer and use it in GitHub Desktop.
color2k in one file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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