Created
November 14, 2022 23:28
-
-
Save Roy-Ermers/cbe32fdf881325353ddf6109e5e84f50 to your computer and use it in GitHub Desktop.
A generic color class with built in converters between different color formats
This file contains 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
export default class Color { | |
get R() { | |
return Math.round(this.r); | |
} | |
get G() { | |
return Math.round(this.g); | |
} | |
get B() { | |
return Math.round(this.b); | |
} | |
private constructor( | |
private r: number, | |
private g: number, | |
private b: number | |
) {} | |
private static _getLuminance(r: number, g: number, b: number) { | |
let R = r / 255; | |
let G = g / 255; | |
let B = b / 255; | |
if (R < 0.03928) R = R / 12.92; | |
else R = Math.pow((R + 0.055) / 1.055, 2.4); | |
if (G < 0.03928) G = G / 12.92; | |
else G = Math.pow((G + 0.055) / 1.055, 2.4); | |
if (B < 0.03928) B = B / 12.92; | |
else B = Math.pow((B + 0.055) / 1.055, 2.4); | |
return 0.2126 * R + 0.7152 * G + 0.0722 * B; | |
} | |
private _evaluateContrastRatio() { | |
const whiteLuminance = Color._getLuminance(255, 255, 255); | |
const colorLuminance = Color._getLuminance(this.r, this.g, this.b); | |
const contrast = | |
(Math.max(whiteLuminance, colorLuminance) + 0.05) / | |
(Math.min(whiteLuminance, colorLuminance) + 0.05); | |
return contrast; | |
} | |
/** | |
* Returns the correct text color for this color. | |
* | |
* Either `light` or `dark`, based on [WCAG AA](https://www.w3.org/WAI/WCAG2AA-Conformance) standards. | |
*/ | |
get colorPerception(): "light" | "dark" { | |
return this._evaluateContrastRatio() >= 3 ? "dark" : "light"; | |
} | |
/** | |
* Create a color based on hsl values | |
*/ | |
static fromHSL(hue: number, saturation: number, lightness: number) { | |
if (saturation === 0) | |
return new this(lightness * 255, lightness * 255, lightness * 255); | |
let p2; | |
lightness /= 100; | |
saturation /= 100; | |
if (lightness <= 0.5) { | |
p2 = lightness * (1 + saturation); | |
} else { | |
p2 = lightness + saturation - lightness * saturation; | |
} | |
const p1 = 2 * lightness - p2; | |
function findRGB(q1: number, q2: number, hue: number) { | |
if (hue > 360) hue -= 360; | |
if (hue < 0) hue += 360; | |
if (hue < 60) return q1 + ((q2 - q1) * hue) / 60; | |
else if (hue < 180) return q2; | |
else if (hue < 250) return q1 + ((q2 - q1) * (240 - hue)) / 60; | |
return q1; | |
} | |
return new this( | |
findRGB(p1, p2, hue + 120) * 255, | |
findRGB(p1, p2, hue) * 255, | |
findRGB(p1, p2, hue - 120) * 255 | |
); | |
} | |
/** | |
* Creates a color based on rgb values | |
*/ | |
static fromRGB(red: number, green: number, blue: number) { | |
return new this(red, green, blue); | |
} | |
/** | |
* Creates a color based on hex values | |
*/ | |
static fromHex(hex: number | string) { | |
if (typeof hex === "number") hex = hex.toString(16); | |
else if (typeof hex === "string") { | |
hex = hex.slice(1); | |
} | |
if (hex.length === 3) { | |
hex = [...hex].map((x) => x + x).join(""); | |
} | |
return new this( | |
parseInt(hex.substring(0, 2), 16), | |
parseInt(hex.substring(2, 4), 16), | |
parseInt(hex.substring(4, 8), 16) | |
); | |
} | |
/** | |
* Converts the current class to its hex representation. | |
*/ | |
toHex() { | |
return ( | |
"#" + | |
[ | |
this.R.toString(16).padStart(2, "0"), | |
this.G.toString(16).padStart(2, "0"), | |
this.B.toString(16).padStart(2, "0"), | |
].join("") | |
); | |
} | |
/** | |
* Converts the current class to its RGB representation. | |
*/ | |
toRGB() { | |
return `rgb(${this.R}, ${this.G}, ${this.B})`; | |
} | |
/** | |
* Converts the current class to its HSL representation. | |
*/ | |
toHSL() { | |
const r = this.r / 255; | |
const g = this.g / 255; | |
const b = this.b / 255; | |
const max = Math.max(r, g, b); | |
const min = Math.min(r, g, b); | |
const chroma = max - min; | |
const lightness = 0.5 * (max + min); | |
const saturation = | |
chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * lightness - 1)); | |
let hue = 0; | |
if (chroma === 0) { | |
hue = 0; | |
} else if (max === r) { | |
const segment = (g - b) / chroma; | |
const shift = segment < 0 ? 360 / 60 : 0 / 60; | |
hue = segment + shift; | |
} else if (max === g) { | |
const segment = (b - r) / chroma; | |
hue = segment + 120 / 60; | |
} else if (max === b) { | |
const segment = (r - g) / chroma; | |
hue = segment + 240 / 60; | |
} | |
hue *= 60; | |
if (hue < 0) { | |
hue += 360; | |
} | |
return `hsl(${hue}deg, ${Math.round(saturation * 100)}%, ${ | |
lightness * 100 | |
}%)`; | |
} | |
toString() { | |
return this.toHex(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment