Skip to content

Instantly share code, notes, and snippets.

@yuriuliam
Last active March 4, 2024 06:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yuriuliam/ddb09f694a23280ecc4e4c70036dcd51 to your computer and use it in GitHub Desktop.
Save yuriuliam/ddb09f694a23280ecc4e4c70036dcd51 to your computer and use it in GitHub Desktop.
A way to convert colors from one format to another by using Math Algorithms
import { isNumber, isBetween } from './numbers'
type ARGB32 = number
/**
* Represents a red-green-blue key-value object.
* all values are limited from 0 to 255 when used internally.
*/
type RGB = { red: number; green: number; blue: number }
type RGBA = RGB & { alpha: number }
type RGBA32 = number
type Hex = string
type HSL = { hue: number; saturation: number; luminance: number }
type HSLA = HSL & { alpha: number }
type HSV = { hue: number; saturation: number; vibrance: number }
type CMYK = { cyan: number; magenta: number; yellow: number; key: number }
type ColorParams<TParams, TParamsSuperset> = TParams &
Partial<Omit<TParamsSuperset, keyof TParams>>
type RGBAParams = ColorParams<RGB, RGBA>
type HSLAParams = ColorParams<HSL, HSLA>
type SupportedColorFormats =
| 'argb32'
| 'cmyk'
| 'hex'
| 'hsl'
| 'hsla'
| 'hsv'
| 'rgb'
| 'rgba'
| 'rgba32'
type MutateFn<T> = (params: T) => T
type StringifyFn = () => string
const HEX_VALUE_REGEXP = /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/
const ARGB32_SIGNATURE = Symbol.for('__ARGB_32BITS__')
const CMYK_SIGNATURE = Symbol.for('__CMYK__')
const HEX_SIGNATURE = Symbol.for('__HEX__')
const HSL_SIGNATURE = Symbol.for('__HSL__')
const HSLA_SIGNATURE = Symbol.for('__HSLA__')
const HSV_SIGNATURE = Symbol.for('__HSV__')
const RGB_SIGNATURE = Symbol.for('__RGB__')
const RGBA_SIGNATURE = Symbol.for('__RGBA__')
const RGBA32_SIGNATURE = Symbol.for('__RGBA_32BITS__')
const COLOR_SIGNATURES = Object.freeze<Record<SupportedColorFormats, symbol>>({
argb32: ARGB32_SIGNATURE,
cmyk: CMYK_SIGNATURE,
hex: HEX_SIGNATURE,
hsl: HSL_SIGNATURE,
hsla: HSLA_SIGNATURE,
hsv: HSV_SIGNATURE,
rgb: RGB_SIGNATURE,
rgba: RGBA_SIGNATURE,
rgba32: RGBA32_SIGNATURE,
})
const isBetween = (value: number, min: number, max: number) =>
value >= min && value <= max
const isNumber = (value: any): value is number =>
typeof value === 'number' && !Number.isNaN(value)
class Color {
private readonly value: number
private readonly signature: symbol
private constructor(value: number, signature: symbol) {
this.value = value & 0xffffffff
this.signature = signature
}
public static argb32(value: number) {
if (!isNumber(value)) throw Error('Given ARGB32 value is not a number')
return new Color(value, ARGB32_SIGNATURE)
}
public static cmyk({ cyan, magenta, yellow, key }: CMYK) {
if (!Color.isCmykValid({ cyan, magenta, yellow, key })) {
throw new Error(
`Invalid CMYK value(s): (${cyan}, ${magenta}, ${yellow}, ${key})`,
)
}
const rgbValue = this.cmykToRgb({ cyan, magenta, yellow, key })
const argbValue = this.rgbaToArgb32({ ...rgbValue, alpha: 255 })
return new Color(argbValue, CMYK_SIGNATURE)
}
public static hex(value: string) {
if (!Color.isHexValid(value)) throw Error(`Invalid Hex Format: ${value}`)
const rgba32Value = Color.hexToRgba32(value)
const rgbaValue = Color.rgba32ToRgba(rgba32Value)
const argbValue = Color.rgbaToArgb32(rgbaValue)
return new Color(argbValue, HEX_SIGNATURE)
}
public static hsla({ hue, saturation, luminance, alpha = 100 }: HSLAParams) {
if (!Color.isHslaValid({ hue, saturation, luminance, alpha })) {
throw new Error(
`Invalid HSLA value(s): (${hue}, ${saturation}, ${luminance}, ${alpha})`,
)
}
const rgbaValue = Color.hslaToRgba({ hue, saturation, luminance, alpha })
const argbValue = Color.rgbaToArgb32(rgbaValue)
return new Color(argbValue, HSLA_SIGNATURE)
}
public static hsv({ hue, saturation, vibrance }: HSV) {
if (!Color.isHsvValid({ hue, saturation, vibrance })) {
throw new Error(
`Invalid HSV value(s): (${hue}, ${saturation}, ${vibrance})`,
)
}
const rgbValue = Color.hsvToRgb({ hue, saturation, vibrance })
const argbValue = Color.rgbaToArgb32({ ...rgbValue, alpha: 255 })
return new Color(argbValue, HSV_SIGNATURE)
}
public static rgba({ red, green, blue, alpha = 255 }: RGBAParams) {
if (!Color.isRgbaValid({ red, green, blue, alpha })) {
throw new Error(
`Invalid RGB(A) value(s): (${red}, ${green}, ${blue}, ${alpha})`,
)
}
const argbValue = Color.rgbaToArgb32({ red, green, blue, alpha })
return new Color(argbValue, RGBA_SIGNATURE)
}
public static rgba32(value: number) {
if (!isNumber(value)) throw Error('Given RGBA32 value is not a number')
const rgbaValue = Color.rgba32ToRgba(value)
const argbValue = Color.rgbaToArgb32(rgbaValue)
return new Color(argbValue, RGBA32_SIGNATURE)
}
private static argb32ToRgba(value: number) {
const argbValue = value & 0xffffffff
const alpha = (argbValue >> 24) & 0xff
const red = (argbValue >> 16) & 0xff
const green = (argbValue >> 8) & 0xff
const blue = argbValue & 0xff
return { red, green, blue, alpha } satisfies RGBA
}
private static cmykToRgb({ cyan, magenta, yellow, key }: CMYK) {
const f = (n: number) => Math.round(255 * (1 - n / 100) * (1 - key / 100))
const red = f(cyan)
const green = f(magenta)
const blue = f(yellow)
return { red, green, blue } satisfies RGB
}
private static hexToRgba32(value: string) {
return parseInt(value.replace('#', '').padEnd(8, 'f'), 16) & 0xffffffff
}
/** @see https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative */
private static hslaToRgba(hslaValue: HSLA) {
const { hue, saturation, luminance, alpha } = {
...hslaValue,
saturation: hslaValue.saturation / 100,
luminance: hslaValue.luminance / 100,
alpha: hslaValue.alpha / 100,
}
const k = (n: number) => (n + hue / 30) % 12
const a = saturation * Math.min(luminance, 1 - luminance)
const f = (n: number) =>
luminance - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))
const red = Math.round(f(0) * 255)
const green = Math.round(f(8) * 255)
const blue = Math.round(f(4) * 255)
const rgbAlpha = Math.round(alpha * 255)
return { red, green, blue, alpha: rgbAlpha } satisfies RGBA
}
/** @see https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative */
private static hsvToRgb(hsvValue: HSV) {
const { hue, saturation, vibrance } = {
...hsvValue,
saturation: hsvValue.saturation / 100,
vibrance: hsvValue.vibrance / 100,
}
const k = (n: number) => (n + hue / 60) % 6
const f = (n: number) =>
vibrance -
vibrance * saturation * Math.max(0, Math.min(k(n), 4 - k(n), 1))
const red = Math.round(f(5) * 255)
const green = Math.round(f(3) * 255)
const blue = Math.round(f(1) * 255)
return { red, green, blue } satisfies RGB
}
private static rgbToHsv({ red, green, blue }: RGB) {
const r = red / 255
const g = green / 255
const b = blue / 255
const rgbMax = Math.max(r, g, b)
const rgbMin = Math.min(r, g, b)
const rgbDelta = rgbMax - rgbMin
const h =
(rgbDelta !== 0
? rgbMax === r
? ((g - b) / rgbDelta) % 6
: rgbMax === g
? (b - r) / rgbDelta + 2
: (r - g) / rgbDelta + 4
: 0) * 60
const hue = Math.round(h < 0 ? h + 360 : h)
const saturation = Math.round((rgbDelta / rgbMax) * 100)
const vibrance = Math.round(rgbMax * 100)
return { hue, saturation, vibrance } satisfies HSV
}
private static rgbaToArgb32({ red, green, blue, alpha }: RGBA) {
const argbValue = (alpha << 24) | (red << 16) | (green << 8) | blue
return argbValue satisfies ARGB32
}
private static rgbaToHex({ red, green, blue, alpha }: RGBA) {
const r16 = red.toString(16)
const g16 = green.toString(16)
const b16 = blue.toString(16)
const a16 = alpha.toString(16)
return `#${r16}${g16}${b16}${a16}` satisfies Hex
}
private static rgbaToRgba32({ red, green, blue, alpha }: RGBA) {
const rgba32Value = (red << 24) | (green << 16) | (blue << 8) | alpha
return rgba32Value satisfies RGBA32
}
private static rgbaToCmyk({ red, green, blue }: RGB) {
const keyDelta = 1 - Math.max(red, green, blue) / 255
const f = (n: number) =>
Math.round(((1 - n / 255 - keyDelta) / (1 - keyDelta)) * 100)
const cyan = f(red)
const magenta = f(green)
const yellow = f(blue)
const key = Math.round(keyDelta * 100)
return { cyan, magenta, yellow, key } satisfies CMYK
}
private static rgbaToHsla({ red, green, blue, alpha }: RGBA) {
const r = red / 255
const g = green / 255
const b = blue / 255
const rgbMax = Math.max(r, g, b)
const rgbMin = Math.min(r, g, b)
const rgbDelta = rgbMax - rgbMin
const l = (rgbMax + rgbMin) / 2
const s = rgbDelta ? rgbDelta / (1 - Math.abs(2 * l - 1)) : 0
const h =
(rgbDelta !== 0
? rgbMax === r
? ((g - b) / rgbDelta) % 6
: rgbMax === g
? (b - r) / rgbDelta + 2
: (r - g) / rgbDelta + 4
: 0) * 60
const luminance = Math.round(l * 100)
const saturation = Math.round(s * 100)
const hue = Math.round(h < 0 ? h + 360 : h)
const hslAlpha = Math.round((alpha / 255) * 100)
return { hue, saturation, luminance, alpha: hslAlpha } satisfies HSLA
}
private static rgbaToRgb({ red, green, blue, alpha }: RGBA) {
const base = { red: 255, green: 255, blue: 255 } satisfies RGB
const alphaValue = alpha / 255
// Source as S
// Base as B
// f(redS, redB) = ((1 - redS) * redB) + (alphaS * redS)
// f(greenS, greenB) = ((1 - greenS) * greenB) + (alphaS * greenS)
// f(blueS, blueB) = ((1 - blueS) * blueB) + (alphaS * blueS)
const f = (source: number, base: number) =>
Math.round(((1 - alphaValue) * base + alphaValue * (source / 255)) * 255)
const rgbRed = f(red, base.red)
const rgbGreen = f(green, base.green)
const rgbBlue = f(blue, base.blue)
return { red: rgbRed, green: rgbGreen, blue: rgbBlue } satisfies RGB
}
private static rgba32ToRgba(value: number) {
const red = (value >> 24) & 0xff
const green = (value >> 16) & 0xff
const blue = (value >> 8) & 0xff
const alpha = value & 0xff
return { red, green, blue, alpha } satisfies RGBA
}
private static isCmykValid({ cyan, magenta, yellow, key }: CMYK) {
return (
isBetween(cyan, 0, 100) &&
isBetween(magenta, 0, 100) &&
isBetween(yellow, 0, 100) &&
isBetween(key, 0, 100)
)
}
private static isHexValid(value: string) {
return HEX_VALUE_REGEXP.test(value)
}
private static isHslaValid({
hue,
saturation,
luminance,
alpha = 100,
}: HSLAParams) {
return (
isBetween(hue, 0, 360) &&
isBetween(saturation, 0, 100) &&
isBetween(luminance, 0, 100) &&
isBetween(alpha, 0, 100)
)
}
private static isHsvValid({ hue, saturation, vibrance }: HSV) {
return (
isBetween(hue, 0, 360) &&
isBetween(saturation, 0, 100) &&
isBetween(vibrance, 0, 100)
)
}
private static isRgbaValid({ red, green, blue, alpha = 255 }: RGBAParams) {
return (
isBetween(red, 0, 255) &&
isBetween(green, 0, 255) &&
isBetween(blue, 0, 255) &&
isBetween(alpha, 0, 255)
)
}
private static filterColorParams<T extends Record<any, any>>(params: T) {
const paramsClone: Record<any, any> = {}
for (const key in paramsClone) {
if (params[key] === undefined) continue
paramsClone[key] = params[key]
}
return paramsClone as T
}
public cmyk(cmykParams: Partial<CMYK> | MutateFn<Partial<CMYK>>) {
const cmykValue = this.toCmyk()
const params = Color.filterColorParams(
typeof cmykParams === 'function' ? cmykParams(cmykValue) : cmykParams,
)
return Color.cmyk({ ...cmykValue, ...params })
}
public hsla(hslaParams: Partial<HSLA> | MutateFn<Partial<HSLA>>) {
const hslaValue = this.toHsla()
const params = Color.filterColorParams(
typeof hslaParams === 'function' ? hslaParams(hslaValue) : hslaParams,
)
return Color.hsla({ ...hslaValue, ...params })
}
public hsv(hsvParams: Partial<HSV> | MutateFn<Partial<HSV>>) {
const hsvValue = this.toHsv()
const params = Color.filterColorParams(
typeof hsvParams === 'function' ? hsvParams(hsvValue) : hsvParams,
)
return Color.hsv({ ...hsvValue, ...params })
}
public rgba(rgbaParams: Partial<RGBA> | MutateFn<Partial<RGBA>>) {
const rgbaValue = this.toRgba()
const params = Color.filterColorParams(
typeof rgbaParams === 'function' ? rgbaParams(rgbaValue) : rgbaParams,
)
return Color.rgba({ ...rgbaValue, ...params })
}
public toArgb32() {
return this.value
}
public toCmyk() {
const argb = this.toArgb32()
const rgba = Color.argb32ToRgba(argb)
const rgb = Color.rgbaToRgb(rgba)
return Color.rgbaToCmyk(rgb)
}
public toHex() {
const argb = this.toArgb32()
const rgbaValue = Color.argb32ToRgba(argb)
return Color.rgbaToHex(rgbaValue)
}
public toHsl() {
const argb = this.toArgb32()
const rgba = Color.argb32ToRgba(argb)
const rgb = Color.rgbaToRgb(rgba)
const { alpha, ...hslValue } = Color.rgbaToHsla({ ...rgb, alpha: 255 })
return hslValue
}
public toHsla() {
const argb = this.toArgb32()
const rgbaValue = Color.argb32ToRgba(argb)
return Color.rgbaToHsla(rgbaValue)
}
public toHsv() {
const argb = this.toArgb32()
const rgbaValue = Color.argb32ToRgba(argb)
const rgbValue = Color.rgbaToRgb(rgbaValue)
return Color.rgbToHsv(rgbValue)
}
public toRgb() {
const argb = this.toArgb32()
const rgba = Color.argb32ToRgba(argb)
return Color.rgbaToRgb(rgba)
}
public toRgba() {
const argb = this.toArgb32()
return Color.argb32ToRgba(argb)
}
public toRgba32() {
const argb = this.toArgb32()
const rgbaValue = Color.argb32ToRgba(argb)
return Color.rgbaToRgba32(rgbaValue)
}
toString(format?: SupportedColorFormats | undefined) {
if (format && !(format in COLOR_SIGNATURES)) {
throw TypeError(`Invalid format: ${format}`)
}
const formatSignature =
format && format in COLOR_SIGNATURES
? COLOR_SIGNATURES[format]
: this.signature
const stringify = Reflect.get(this, formatSignature) as StringifyFn
return stringify()
}
public valueOf() {
return this.toArgb32()
}
public *[Symbol.iterator]() {
yield this.toArgb32()
}
public [Symbol.toPrimitive]() {
return this.toArgb32()
}
protected [ARGB32_SIGNATURE]() {
return this.toArgb32().toString()
}
protected [CMYK_SIGNATURE]() {
const { cyan, magenta, yellow, key } = this.toCmyk()
return `cmyk(${cyan}, ${magenta}, ${yellow}, ${key})`
}
protected [HEX_SIGNATURE]() {
return this.toHex()
}
protected [HSL_SIGNATURE]() {
const { hue, saturation, luminance } = this.toHsl()
return `hsl(${hue}, ${saturation}, ${luminance})`
}
protected [HSLA_SIGNATURE]() {
const { hue, saturation, luminance, alpha } = this.toHsla()
return `hsla(${hue}, ${saturation}, ${luminance}, ${alpha})`
}
protected [HSV_SIGNATURE]() {
const { hue, saturation, vibrance } = this.toHsv()
return `hsv(${hue}, ${saturation}, ${vibrance})`
}
protected [RGB_SIGNATURE]() {
const { red, green, blue } = this.toRgb()
return `rgb(${red}, ${green}, ${blue})`
}
protected [RGBA_SIGNATURE]() {
const { red, green, blue, alpha } = this.toRgba()
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
}
protected [RGBA32_SIGNATURE]() {
return this.toRgba32().toString()
}
}
const argb32 = Color.argb32.bind(Color)
const cmyk = Color.cmyk.bind(Color)
const hex = Color.hex.bind(Color)
const hsla = Color.hsla.bind(Color)
const hsv = Color.hsv.bind(Color)
const rgba = Color.rgba.bind(Color)
const rgba32 = Color.rgba32.bind(Color)
export { argb32, cmyk, hex, hsla, hsv, rgba, rgba32 }
export type { Color }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment