UNPKG

jebcolors

Version:

npm module that contains colors, gradients and a class used to modify the colors or gradients

639 lines (559 loc) 19.3 kB
import {evaluateCode, getFullCode, hex} from './functions' import { rrggbbREGEX } from './regex' import { internalError, layer8Error } from './errors' import type { HexadecimalColor, HEX00FF, RGBAV, RED, GREEN, BLUE, RGB, RGBA, HUE, SATURATION, LIGHTNESS, HSL, LUMINANCE, JEB, ContrastObject, NormalizationType, CodeEvaluation, } from './types' import convert from 'color-convert' import { supergradient, Gradient} from './Gradient' import seedrandom from 'seedrandom' /** * Class with basic functions of a color * (If you want to add the gradient creator use class GradientableColor) */ export class Color { private _code: HexadecimalColor private _rgb: RGB private _hsl: HSL private _luminance: LUMINANCE private _jeb: JEB private _dark: boolean private _lightdarkconst = 1.5 constructor(code:string){ this._code = getFullCode(code) this._rgb = this.getRGB() this._hsl = this.getHSL() this._luminance = this.getLuminance() // Didn't know how to name this number, so I named is 'Color Jeb Constant' to be mamon this._jeb = this.YiqFormula() this._dark = this._jeb < 128 } // Static Methods /** * Tests if a given code is a valid code and returns a CodeEvalutation {type, valid} * @param code The code of the color * @returns a CodeEvaluation object * @example * Color.test('#c00') * // return * {type: 'rgb', valid: true} * * Color.test('#ff0000') * // return * {type: 'rrggbb', valid: true} * * Color.test('red') * // return * {type: 'css', valid:true} * * Color.test('aaa') * // return * {type: 'invalid', valid:false} */ static test(code:string):CodeEvaluation{ return evaluateCode(code) } /** * Gets the contrast values of 2 supercolors * @param color1 the first supercolor (the background supercolor) * @param color2 the second supercolor (the fore color supercolor) * @returns A Contrast Object * @example * Color.contrast("#ffffff","#000000") * * // return * * { * passes: { * normal: { minimal: true, perfect: true }, * bold: { minimal: true, perfect: true } * } * } */ static contrast(color1:Color, color2:Color):ContrastObject{ if(color1 instanceof Color && color2 instanceof Color){ // Formula (Ratio): https://www.w3.org/TR/WCAG20/#contrast-ratiodef // Contrast Conditions: https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast (normal) // Contrast Conditions: https://www.w3.org/TR/WCAG20/#visual-audio-contrast7 (bold) const maxL = Math.max(color1.luminance, color2.luminance) const minL = Math.min(color1.luminance, color2.luminance) const ratio = (maxL + 0.05) / (minL + 0.05) const contrast:ContrastObject = { passes: { normal: { minimal: ratio >= 4.5, perfect: ratio >= 7 }, bold: { minimal: ratio >= 3, perfect: ratio >= 4.5 } } } return contrast } else{ throw layer8Error(`Must provide 2 instances of Color, in contrast() in Color(), providen were: ${typeof color1} and ${typeof color2}`) } } /** * Creates a new Color instance for the given 3 values * @param RGB An array with [red, green, blue] values [0,255] * @returns A supercolor (Color instance) */ static rgb(RGB:RGB):Color{ const [red, green, blue] = RGB.map((rgbav:RGBAV)=>Color.normalize(rgbav,'RGB')) const rrggbb = convert.rgb.hex([red,green,blue]) return supercolor(`#${rrggbb}`) } /** * Creates a new Color instance for the given 3 values * @param HSL An array with [hue, saturation, lightness] values [0,360], [0,100] & [0,100] * @returns A supercolor (Color instance) */ static hsl(HSL:HSL):Color{ let [hue,saturation,lightness] = HSL hue = Color.normalize(hue,'H') saturation = Color.normalize(saturation,'SL') lightness = Color.normalize(lightness,'SL') const rrggbb = convert.hsl.hex([hue,saturation,lightness]) return supercolor(`#${rrggbb}`) } private static normalize(value:number,to:NormalizationType):RGBAV|HUE|SATURATION|LIGHTNESS|JEB{ const normalize = (value:number, min:number, max:number):number =>{ return Math.min(Math.max(value, min), max) } if(to === 'RGB') return ~~(normalize(value,0,255)) as RGBAV else if(to === 'H') { value = value % 360 return (value < 0 ? value + 360 : value) as HUE } else if(to === 'SL') return normalize(value,0,100) as SATURATION | LIGHTNESS else if(to === 'JEB') return normalize(value,0,255) as JEB else throw layer8Error(`to is different from allowed values, given was: ${to}`) } /** * Returns a random supercolor * @returns A random supercolor */ static random():Color{ const ran = () => Math.floor(Math.random() * 255) + 1 const [red, green, blue] = [ran(),ran(),ran()] return Color.rgb([red,green,blue]) } /** * Returns a random supercolor based on a seed * @param seed The seed * @returns A random supercolor */ static seed(seed:string):Color{ const ran = seedrandom(seed) const randoms = [ran(),ran(),ran()] const [red, green, blue] = randoms.map(rannum => Math.floor(rannum * 255) + 1) return Color.rgb([red,green,blue]) } // Private Methods private getRGB():RGB{ const result = rrggbbREGEX.exec(this._code) if(!result) throw internalError('Trying to get RGBA values in Color()') // [_,rr,bb,gg] const red = result[1] as HEX00FF const green = result[2] as HEX00FF const blue = result[3] as HEX00FF return [ hex(red), hex(green), hex(blue), ] } private getHSL():HSL{ const [red, green, blue] = this._rgb const [hue, saturation, lightness] = convert.rgb.hsl(red, green, blue) return [hue, saturation, lightness] } private getLuminance():LUMINANCE{ const rgb = this._rgb const calculated = rgb.map(value => { // Formula: https://www.w3.org/TR/WCAG20/#relativeluminancedef value /= 255 value = value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4 return value }) const luminance = 0.2126 * calculated[0] + 0.7152 * calculated[1] + 0.0722 * calculated[2] return luminance } private YiqFormula():JEB{ // Formula: https://24ways.org/2010/calculating-color-contrast const [red, green, blue] = this._rgb const yiq = ((red * 299) + (green * 587) + (blue * 114)) / 1000 return yiq } // Getters /** * Hexadecimal code of the color */ get code():HexadecimalColor{ return this._code } /** * Red value of the color */ get red():RED{ return this._rgb[0] } /** * Green value of the color */ get green():GREEN{ return this._rgb[1] } /** * Blue value of the color */ get blue():BLUE{ return this._rgb[2] } /** * Array of the 3 colors [red, green, blue] */ get rgb():RGB{ return this._rgb } /** * Hue of the color */ get hue():HUE{ return this._hsl[0] } /** * Saturation of the color */ get saturation():SATURATION{ return this._hsl[1] } /** * Lightness of the color */ get lightness():LIGHTNESS{ return this._hsl[2] } /** * Array of the 3 hsl values [hue, saturation, lightness] */ get hsl():HSL{ return this._hsl } /** * Array with the values of the color [cyan, magenta, yellow, black] */ get cmyk():[number,number,number,number]{ return convert.rgb.cmyk(this._rgb) } /** * Luminance of the color */ get luminance():LUMINANCE{ return this._luminance } /** * A constant of the color based on YIQ used to get if the color is dark */ get jeb():JEB{ return this._jeb } /** * Boolean saying if the color is a dark color */ get isDark():boolean{ return this._dark } /** * Boolean saying if the color is a light color */ get isLight():boolean{ return !this._dark } /** * Returns white o black, depending of the jeb constant of the color * This is to get the best color for a text assuming, that the supercolor is the background color */ get text():HexadecimalColor{ return this.isDark ? '#ffffff' : '#000000' } // Public Methods /** * Makes the color transparent * @param alpha The alpha that the color is going to have, in range [0,100] 0 - trasparent, 100 - opaque * @returns A string representing a rgba color 'rgba(255,0,0,.5)' for instance * @example * htmlElement.style.backgroundColor = supercolor("red").alpha(50) // half-transparent red */ public alpha(alpha:number):RGBA{ const [red, green, blue] = this.rgb alpha /= 100 return `rgba(${red},${green},${blue},${alpha})` } /** * Multiplies the saturation os a color in factor times * @param factor The times you want to multiply the saturation of the color * @returns A supercolor */ public saturate(factor:number):Color{ // eslint-disable-next-line prefer-const let [hue, saturation, lightness] = this.hsl saturation = saturation * factor return Color.hsl([hue, saturation, lightness]) } /** * Divivdes the saturation os a color in factor times * @param factor The times you want to divide the saturation of the color * @returns A supercolor */ public desaturate(factor:number):Color{ // eslint-disable-next-line prefer-const let [hue, saturation, lightness] = this.hsl saturation = saturation / factor return Color.hsl([hue, saturation, lightness]) } /** * Multiplies the lightness os a color in factor times * @param factor The times you want to multiply the lightness of the color * @returns A supercolor */ public lighten(factor:number):Color{ // eslint-disable-next-line prefer-const let [hue, saturation, lightness] = this.hsl lightness = lightness * factor return Color.hsl([hue, saturation, lightness]) } /** * Divides the lightness os a color in factor times * @param factor The times you want to divide the lightness of the color * @returns A supercolor */ public darken(factor:number):Color{ // eslint-disable-next-line prefer-const let [hue, saturation, lightness] = this.hsl lightness = lightness / factor return Color.hsl([hue, saturation, lightness]) } /** * Rotates the hue os a color in degrees degress * @param degrees The times you want to multiply the saturation of the color * @returns A supercolor */ public rotate(degrees:number):Color{ // eslint-disable-next-line prefer-const let [hue, saturation, lightness] = this.hsl hue = Color.normalize(hue + degrees,'H') return Color.hsl([hue, saturation, lightness]) } // Cromatic circle are 360 degrees in 12 steps, so each step is 30 degrees /** * Rotates the hue of a color step steps (1 step = 30 degrees) * @param steps The steps * @returns a supercolor */ public next(steps:number):Color{ return this.rotate(steps * 30) } /** * Rotates (in the opposite direction) the hue of a color step steps (1 step = 30 degrees) * @param steps The steps * @returns a supercolor */ public previous(steps:number):Color{ return this.next(steps * -1) } // Public methods that not require parameters, so they are properties /** * The negative of the color */ get negative():Color{ let [red, green, blue] = this.rgb red = Color.normalize(255 - red,'RGB') green = Color.normalize(255 - green,'RGB') blue = Color.normalize(255 - blue,'RGB') return Color.rgb([red, green, blue]) } /** * The complementary color (hue rotated 180 degrees) */ get complementary():Color{ return this.next(6) } /** * The analogous colors */ get analogous():[Color,Color]{ return [this.previous(1),this.next(1)] } /** * The split complementary colors */ get splitComplementary():[Color,Color]{ return [this.previous(5),this.next(5)] } /** * The triadic colors */ get triadic():[Color,Color]{ return [this.previous(4),this.next(4)] } /** * The color a little bit darker */ get hover():Color{ return this.darken(this._lightdarkconst) } /** * The color a little bit lighter */ get disabled():Color{ return this.lighten(this._lightdarkconst) } /** * The monocromatic colors */ get monocromatic():[Color,Color]{ return [this.disabled,this.hover] } /** * The color converted to a grayscale */ get grayscale():Color{ // Formula: http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale const [red, green, blue] = this.rgb const value = red * 0.3 + green * 0.59 + blue * 0.11; return Color.rgb([value, value, value]); } // Prototype Functions /** * Converts the supercolor to a string * @returns The hexadecimal hexcode */ toString():string{ return this.code } } export class GradientableColor extends Color { private _gradientWithNegative:Gradient private _gradientWithComplementary:Gradient private _gradientWithAnalogous:Gradient private _gradientWithSplitComplementary:Gradient private _gradientWithTriadic:Gradient private _gradientWithLighter:Gradient private _gradientWithDarker:Gradient private _gradientWithMonocromatic:Gradient constructor(code:string){ super(code) this._gradientWithAnalogous = this.gAnalogous() this._gradientWithComplementary = this.gComplementary() this._gradientWithDarker = this.gDarker() this._gradientWithLighter = this.gLighter() this._gradientWithMonocromatic = this.gMonocromatic() this._gradientWithNegative = this.gNegative() this._gradientWithSplitComplementary = this.gSplitComplementary() this._gradientWithTriadic = this.gTriadic() } // Private methods that creates the gradient private gNegative():Gradient{ return supergradient([this, this.negative]) } private gComplementary():Gradient{ return supergradient([this,this.next(3),this.complementary]) } private gAnalogous():Gradient{ const [prethis, postthis] = this.analogous return supergradient([prethis, this, postthis]) } private gSplitComplementary():Gradient{ const [prethis, postthis] = this.splitComplementary return supergradient([prethis, this, postthis]) } private gTriadic():Gradient{ const [prethis, postthis] = this.triadic return supergradient([prethis, this, postthis]) } private gLighter():Gradient{ return supergradient([this, this.disabled]) } private gDarker():Gradient{ return supergradient([this, this.hover]) } private gMonocromatic():Gradient{ return supergradient([this.disabled, this, this.hover]) } // Properties (the gradients) /** * A gradient that contains the color in middle and is surrounded by its analogous */ get gradientWithAnalogous():Gradient{ return this._gradientWithAnalogous } /** * A gradient that starts with the color and ends with its complementary * (There is a middle color in between to avoid crossing the gray) */ get gradientWithComplementary():Gradient{ return this._gradientWithComplementary } /** * A gradient that starts with the color and ends with a darker version of the color */ get gradientWithDarker():Gradient{ return this._gradientWithDarker } /** * A gradient that starts with the color and ends with a lighter version of the color */ get gradientWithLighter():Gradient{ return this._gradientWithLighter } /** * A gradient that contains the color in middle and is surrounded by its lighter and darker versions */ get gradientWithMonocromatic():Gradient{ return this._gradientWithMonocromatic } /** * A gradient that starts with the color and ends with its negative * (There is a middle color in between to avoid crossing the gray) */ get gradientWithNegative():Gradient{ return this._gradientWithNegative } /** * A gradient that contains the color in middle and is surrounded by its split complementaries */ get gradientWithSplitComplementary():Gradient{ return this._gradientWithSplitComplementary } /** * A gradient that contains the color in middle and is surrounded by its triadic */ get gradientWithTriadic():Gradient{ return this._gradientWithTriadic } } export const supercolor = (code:string):Color => new Color(code) export const megacolor = (code:string):GradientableColor => new GradientableColor(code) export const upgradecolor = (codeOrColor: string | Color):Color|GradientableColor => { if(codeOrColor instanceof Color){ return megacolor(codeOrColor.code) as GradientableColor } else if(typeof codeOrColor === 'string'){ return supercolor(codeOrColor) as Color } else{ throw layer8Error(`Argument in upgradecolor() must be a string or a Color instance, the provided was: ${typeof codeOrColor}`) } }