|
// deno-lint-ignore-file no-namespace ban-types no-explicit-any |
|
import { |
|
clearCache, |
|
hasOwn, |
|
inspect, |
|
type InspectOptionsStylized, |
|
memoize, |
|
nonEnumerableProperties, |
|
type Reshape, |
|
round, |
|
roundFloat, |
|
tpl, |
|
type UnionToTuple, |
|
} from "./helpers.ts"; |
|
import { type ColorNames, names2colors } from "./names.ts"; |
|
import * as Illuminants from "./illuminants.ts"; |
|
import type Illuminant from "./illuminants.ts"; |
|
|
|
// #region Common |
|
const schemas = { |
|
ANSI: { |
|
name: "ANSI 4-bit (16 Color)", |
|
schema: { |
|
value: [0, 15], |
|
}, |
|
}, |
|
ANSI256: { |
|
name: "ANSI 8-bit (256 High-Color)", |
|
schema: { |
|
value: [0, 255], |
|
}, |
|
}, |
|
ANSI16M: { |
|
name: "ANSI 24-bit (16M TruColor)", |
|
schema: { |
|
r: [0, 255], |
|
g: [0, 255], |
|
b: [0, 255], |
|
}, |
|
}, |
|
APPLE: { |
|
name: "Apple (48-bit RGB)", |
|
schema: { |
|
r16: [0, 65535], |
|
g16: [0, 65535], |
|
b16: [0, 65535], |
|
}, |
|
}, |
|
CMYK: { |
|
name: "CMYK", |
|
schema: { |
|
c: [0, 1], |
|
m: [0, 1], |
|
y: [0, 1], |
|
k: [0, 1], |
|
a: [0, 1, true], |
|
}, |
|
}, |
|
GRAY: { |
|
name: "Grayscale", |
|
schema: { |
|
g: [0, 100], |
|
a: [0, 1, true], |
|
}, |
|
}, |
|
HCG: { |
|
name: "HCG", |
|
schema: { |
|
h: [0, 360], |
|
c: [0, 100], |
|
g: [0, 100], |
|
}, |
|
}, |
|
HEX: { |
|
name: "HEX", |
|
schema: { |
|
value: [0, 0xFFFFFFFF], |
|
}, |
|
}, |
|
HSL: { |
|
name: "HSL", |
|
schema: { |
|
h: [0, 360], |
|
s: [0, 1], |
|
l: [0, 1], |
|
a: [0, 1, true], |
|
}, |
|
}, |
|
HSV: { |
|
name: "HSV", |
|
schema: { |
|
h: [0, 360], |
|
s: [0, 1], |
|
v: [0, 1], |
|
a: [0, 1, true], |
|
}, |
|
}, |
|
HWB: { |
|
name: "HWB", |
|
schema: { |
|
h: [0, 360], |
|
w: [0, 100], |
|
b: [0, 100], |
|
a: [0, 1, true], |
|
}, |
|
}, |
|
LAB: { |
|
name: "LAB", |
|
schema: { |
|
l: [0, 100], |
|
a: [-128, 127], |
|
b: [-128, 127], |
|
alpha: [0, 1, true], |
|
}, |
|
}, |
|
LCH: { |
|
name: "LCH", |
|
schema: { |
|
l: [0, 100], |
|
c: [0, 100], |
|
h: [0, 360], |
|
alpha: [0, 1, true], |
|
}, |
|
}, |
|
KEYWORD: { |
|
name: "Keyword", |
|
schema: { |
|
value: Object.keys(names2colors) as UnionToTuple<ColorNames>, |
|
}, |
|
}, |
|
OKLAB: { |
|
name: "OKLAB", |
|
schema: { |
|
l: [0, 1], |
|
a: [-0.5, 0.5], |
|
b: [-0.5, 0.5], |
|
alpha: [0, 1, true], |
|
}, |
|
}, |
|
OKLCH: { |
|
name: "OKLCH", |
|
schema: { |
|
l: [0, 1], |
|
c: [0, 1], |
|
h: [0, 360], |
|
alpha: [0, 1, true], |
|
}, |
|
}, |
|
RGB: { |
|
name: "RGB", |
|
schema: { |
|
r: [0, 255], |
|
g: [0, 255], |
|
b: [0, 255], |
|
a: [0, 1, true], |
|
}, |
|
}, |
|
XYZ: { |
|
name: "XYZ", |
|
schema: { |
|
x: [0, 1], |
|
y: [0, 1], |
|
z: [0, 1], |
|
a: [0, 1, true], |
|
}, |
|
}, |
|
} as const; |
|
type schemas = typeof schemas; |
|
|
|
function schema<T extends keyof schemas>(name: T): schemas[T]["schema"] { |
|
if (!(name in schemas) || !schemas[name].schema) { |
|
throw new TypeError("Invalid Color Schema: " + name); |
|
} |
|
return schemas[name].schema; |
|
} |
|
|
|
const _brand: unique symbol = Symbol.for("Color.#brand"); |
|
type _brand = typeof _brand; |
|
|
|
const _schema: unique symbol = Symbol("Color.#schema"); |
|
type _schema = typeof _schema; |
|
|
|
const _keys: unique symbol = Symbol("Color.#keys"); |
|
type _keys = typeof _keys; |
|
|
|
const _name: unique symbol = Symbol("Color.#name"); |
|
type _name = typeof _name; |
|
|
|
const _type: unique symbol = Symbol("Color.#type"); |
|
type _type = typeof _type; |
|
|
|
const _extend: unique symbol = Symbol("Color.#extend"); |
|
type _extend = typeof _extend; |
|
|
|
type Schema = { |
|
readonly [component: string]: |
|
| readonly string[] |
|
| readonly [min: number, max: number] |
|
| readonly [min: number, max: number, optional?: boolean]; |
|
}; |
|
|
|
type ValueType<T extends Schema[string]> = T[0] extends number ? number |
|
: T[0] extends bigint ? bigint |
|
: T[number] & {}; |
|
|
|
type Optional<T extends Schema[string], True = true, False = false> = T extends |
|
{ 2: true } ? True |
|
: T extends { 2: infer O } ? [O & {}] extends [never] ? True |
|
: False |
|
: False; |
|
|
|
type FormatSchema<T extends Schema> = Reshape< |
|
& { readonly [K in keyof T as Optional<T[K], never, K>]-?: ValueType<T[K]> } |
|
& { readonly [K in keyof T as Optional<T[K], K, never>]+?: ValueType<T[K]> } |
|
>; |
|
type FindSchemaKey<T extends Schema> = keyof { |
|
[K in keyof schemas as [schemas[K]["schema"]] extends [T] ? K : never]: 0; |
|
} extends infer K ? K extends keyof schemas ? K : never : never; |
|
|
|
type Printable = string | number | bigint | boolean | null | undefined; |
|
|
|
interface Base { |
|
readonly constructor: typeof Base; |
|
} |
|
|
|
abstract class Base<const T extends Schema = any> { |
|
#ctor: typeof Base = null!; |
|
#name: string = null!; |
|
#schema: T = null!; |
|
#keys: (string & keyof T)[] = []; |
|
|
|
constructor(schema: T) { |
|
const ctor = new.target; |
|
if (ctor === Base) throw new TypeError("Illegal constructor"); |
|
Object.setPrototypeOf(schema, null); |
|
const key = ctor.name as keyof schemas; |
|
if (!(key in schemas)) { |
|
throw new TypeError(`Invalid Color Space: ${key}`); |
|
} |
|
const { name } = schemas[key]; |
|
extendBase.call(this, ctor, key, schema); |
|
this.#ctor ??= ctor; |
|
this.#name ??= name; |
|
this.#schema ??= schema; |
|
this.#keys ??= Reflect.ownKeys(schema) |
|
.filter((k): k is string => typeof k === "string"); |
|
// @ts-ignore reassigning readonly property |
|
this.#ctor[_schema] ??= schema; |
|
// @ts-ignore reassigning readonly property |
|
this.#ctor[_type] ??= ctor; |
|
// @ts-ignore reassigning readonly property |
|
this.#ctor[_name] ??= name; |
|
// @ts-ignore reassigning readonly property |
|
this.#ctor[_keys] ??= this.#keys; |
|
|
|
Object.setPrototypeOf(this, ctor.prototype); |
|
} |
|
|
|
get [_type](): typeof Base { |
|
return this.#ctor; |
|
} |
|
|
|
get [_name](): string { |
|
return this.#name; |
|
} |
|
|
|
get [_schema](): T { |
|
return this.#schema; |
|
} |
|
|
|
get [_keys](): (string & keyof T)[] { |
|
return this.#keys; |
|
} |
|
|
|
/** Convert this color to the {@link ANSI} color space. */ |
|
toANSI(): ANSI { |
|
return ANSI.fromRGB(this.toRGB()); |
|
} |
|
|
|
toAnsi(): ANSI { |
|
return this.toANSI(); |
|
} |
|
|
|
/** Convert this color to the {@link ANSI256} color space. */ |
|
toANSI256(): ANSI256 { |
|
return ANSI256.fromRGB(this.toRGB()); |
|
} |
|
|
|
toAnsi256(): ANSI256 { |
|
return this.toANSI256(); |
|
} |
|
|
|
/** Convert this color to the {@link ANSI16M} color space. */ |
|
toANSI16M(): ANSI16M { |
|
return ANSI16M.fromRGB(this.toRGB()); |
|
} |
|
|
|
toAnsi16M(): ANSI16M { |
|
return this.toANSI16M(); |
|
} |
|
|
|
/** Convert this color to the {@link APPLE} color space. */ |
|
toAPPLE(): APPLE { |
|
return APPLE.fromRGB(this.toRGB()); |
|
} |
|
|
|
toApple(): APPLE { |
|
return this.toAPPLE(); |
|
} |
|
|
|
/** Convert this color to the {@link CMYK} color space. */ |
|
toCMYK(): CMYK { |
|
return RGB.toCMYK(this.toRGB()); |
|
} |
|
|
|
/** Convert this color to a {@link CSS} color string. */ |
|
toCSS(): string { |
|
return this.toString(); |
|
} |
|
|
|
/** Convert this color to a {@link GRAY} color string. */ |
|
toGRAY(): GRAY { |
|
return GRAY.fromRGB(this.toRGB()); |
|
} |
|
|
|
toGray(): GRAY { |
|
return this.toGRAY(); |
|
} |
|
|
|
/** Convert this color to the {@link HEX} color space. */ |
|
toHEX(): HEX { |
|
return new HEX(RGB.toHexString(this.toRGB())); |
|
} |
|
|
|
/** Convert this color to the {@link HEX3} color space. */ |
|
toHEX3(): HEX3 { |
|
return new HEX3(RGB.toHexString(this.toRGB())); |
|
} |
|
|
|
/** Convert this color to the {@link HEX4} color space. */ |
|
toHEX4(): HEX4 { |
|
return new HEX4(RGB.toHexString(this.toRGB())); |
|
} |
|
|
|
/** Convert this color to the {@link HEX6} color space. */ |
|
toHEX6(): HEX6 { |
|
return new HEX6(RGB.toHexString(this.toRGB())); |
|
} |
|
|
|
/** Convert this color to the {@link HEX8} color space. */ |
|
toHEX8(): HEX8 { |
|
return new HEX8(RGB.toHexString(this.toRGB())); |
|
} |
|
|
|
/** Convert this color to the {@link HEX} color space. */ |
|
toHex(): HEX { |
|
return this.toHEX(); |
|
} |
|
|
|
/** Convert this color to the {@link HEX3} color space. */ |
|
toHex3(): HEX3 { |
|
return this.toHEX3(); |
|
} |
|
|
|
/** Convert this color to the {@link HEX4} color space. */ |
|
toHex4(): HEX4 { |
|
return this.toHEX4(); |
|
} |
|
|
|
/** Convert this color to the {@link HEX6} color space. */ |
|
toHex6(): HEX6 { |
|
return this.toHEX6(); |
|
} |
|
|
|
/** Convert this color to the {@link HEX8} color space. */ |
|
toHex8(): HEX8 { |
|
return this.toHEX8(); |
|
} |
|
|
|
/** Convert this color to a Hexadecimal string. */ |
|
toHexString(): string { |
|
return RGB.toHexString(this.toRGB()); |
|
} |
|
|
|
/** Convert this color to the {@link HCG} color space. */ |
|
toHCG(): HCG { |
|
return HCG.fromRGB(this.toRGB()); |
|
} |
|
|
|
/** Convert this color to the {@link HSL} color space. */ |
|
toHSL(): HSL { |
|
return RGB.toHSL(this.toRGB()); |
|
} |
|
|
|
/** Convert this color to the {@link HSV} color space. */ |
|
toHSV(): HSV { |
|
return RGB.toHSV(this.toRGB()); |
|
} |
|
|
|
/** Convert this color to the {@link HWB} color space. */ |
|
toHWB(): HWB { |
|
return HWB.fromRGB(this.toRGB()); |
|
} |
|
|
|
/** Convert this color to the {@link KEYWORD} color space. */ |
|
toKEYWORD(): KEYWORD { |
|
return KEYWORD.fromRGB(this.toRGB()); |
|
} |
|
|
|
/** Convert this color to the {@link KEYWORD} color space. */ |
|
toKeyword(): KEYWORD { |
|
return this.toKEYWORD(); |
|
} |
|
|
|
/** Convert this color to the {@link KEYWORD} color space. */ |
|
toNAME(): KEYWORD { |
|
return this.toKEYWORD(); |
|
} |
|
|
|
/** Convert this color to the {@link KEYWORD} color space. */ |
|
toName(): KEYWORD { |
|
return this.toKEYWORD(); |
|
} |
|
|
|
/** Convert this color to the {@link LAB} color space. */ |
|
toLAB(illuminant?: Color.Illuminant): LAB { |
|
return LAB.fromXYZ(this.toXYZ(), illuminant ?? Color.illuminant); |
|
} |
|
|
|
/** Convert this color to the {@link LCH} color space. */ |
|
toLCH(illuminant?: Color.Illuminant): LCH { |
|
return LCH.fromXYZ(this.toXYZ(), illuminant ?? Color.illuminant); |
|
} |
|
|
|
/** Convert this color to the {@link LAB} color space. */ |
|
toOKLAB(illuminant?: Color.Illuminant): OKLAB { |
|
return OKLAB.fromLAB(this.toLAB(illuminant)); |
|
} |
|
|
|
/** Convert this color to the {@link LCH} color space. */ |
|
toOKLCH(illuminant?: Color.Illuminant): OKLCH { |
|
return OKLCH.fromLCH(this.toLCH(illuminant)); |
|
} |
|
|
|
/** Convert this color to the {@link RGB} color space. */ |
|
toRGB(): RGB { |
|
return this[_type].toRGB(this); |
|
} |
|
|
|
/** Convert this color to the {@link XYZ} color space. */ |
|
toXYZ(): XYZ { |
|
return RGB.toXYZ(this.toRGB()); |
|
} |
|
|
|
/** Returns a JSON representation of this color. */ |
|
toJSON(): FormatSchema<T> { |
|
return this[_keys].reduce((acc, k) => { |
|
acc[k] = (this as any)[k]; |
|
return acc; |
|
}, Object.create(null)); |
|
} |
|
|
|
/** Returns a string representation of this color. */ |
|
toString(): string { |
|
return Color.format( |
|
Color.is(this) ? new Color(this) : Color.fromHex(this.toHexString()), |
|
this[_type].name, |
|
); |
|
} |
|
|
|
valueOf(): number { |
|
return this.toRGB().toNumber(); |
|
} |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
toAnsi: { value: this.prototype.toANSI }, |
|
toAnsi256: { value: this.prototype.toANSI256 }, |
|
toAnsi16M: { value: this.prototype.toANSI16M }, |
|
toApple: { value: this.prototype.toAPPLE }, |
|
toGray: { value: this.prototype.toGRAY }, |
|
toHex: { value: this.prototype.toHEX }, |
|
toHex3: { value: this.prototype.toHEX3 }, |
|
toHex4: { value: this.prototype.toHEX4 }, |
|
toHex6: { value: this.prototype.toHEX6 }, |
|
toHex8: { value: this.prototype.toHEX8 }, |
|
toKeyword: { value: this.prototype.toKEYWORD }, |
|
toName: { value: this.prototype.toKEYWORD }, |
|
toNAME: { value: this.prototype.toKEYWORD }, |
|
}); |
|
} |
|
|
|
*[Symbol.iterator](): IterableIterator<ValueType<T[keyof T]>> { |
|
for (const k of this[_keys]) yield (this as any)[k]; |
|
} |
|
|
|
get [Symbol.toStringTag](): string { |
|
return this[_name]; |
|
} |
|
|
|
[Symbol.toPrimitive](hint: "number" | "string" | "default"): string | number { |
|
if (hint === "number") { |
|
return this.toRGB().toNumber(); |
|
} else { |
|
return this.toString(); |
|
} |
|
} |
|
|
|
[inspect.custom]( |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
const { stylize, ...opts } = options; |
|
const base: typeof Base = this[_type] ?? this.constructor as typeof Base; |
|
const name = this[_name] ?? base.name; |
|
const rgba = base.toRGB(this); |
|
const ansi = new ANSI16M(rgba.r, rgba.g, rgba.b, { |
|
reset: true, |
|
bold: true, |
|
}); |
|
const reset = `\x1b[0m`; |
|
const hex = Color.Format.Hex({ rgba } as unknown as Color).toUpperCase(); |
|
const tag = opts.colors |
|
? `${stylize(`[${name}: `, "special")}${ansi}${hex}${reset}${ |
|
stylize("]", "special") |
|
}` |
|
: `[${name}: ${hex}]`; |
|
|
|
if (depth && depth < 3) return tag; |
|
const schema = this[_schema]; |
|
const obj = {} as Record<string, unknown>; |
|
for (const k in schema) { |
|
const values = schema[k as keyof T] as Printable[]; |
|
const v = (this as any)[k]; |
|
if ( |
|
values.length <= 3 && |
|
typeof values[0] === "number" && |
|
typeof values[1] === "number" |
|
) { |
|
if (v === undefined) continue; |
|
obj[k] = Number(v); |
|
} else { |
|
obj[k] = v; |
|
} |
|
} |
|
const swatch = opts.colors ? ` ${ansi}▩${reset}` : ""; |
|
return `${tag}${swatch} ${inspect(obj, opts)}`; |
|
} |
|
|
|
static fromRGB(_rgba: RGB): Base { |
|
throw new ReferenceError( |
|
`ColorSpace '${this[_name]}' does not implement 'fromRGB'`, |
|
); |
|
} |
|
|
|
static toRGB(_color: Base): RGB { |
|
throw new ReferenceError( |
|
`ColorSpace '${this[_name]}' does not implement 'toRGB'`, |
|
); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): Base { |
|
XYZ.assert(xyz); |
|
return this.fromRGB(XYZ.toRGB(xyz)); |
|
} |
|
|
|
static toXYZ(color: Base): XYZ { |
|
if (XYZ.is(color)) return color; |
|
return XYZ.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromHSL(hsl: HSL): Base { |
|
HSL.assert(hsl); |
|
return this.fromRGB(HSL.toRGB(hsl)); |
|
} |
|
|
|
static toHSL(color: Base): HSL { |
|
if (HSL.is(color)) return color; |
|
return HSL.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromHSV(hsv: HSV): Base { |
|
HSV.assert(hsv); |
|
return this.fromRGB(HSV.toRGB(hsv)); |
|
} |
|
|
|
static toHSV(color: Base): HSV { |
|
if (HSV.is(color)) return color; |
|
return HSV.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromLAB(lab: LAB): Base { |
|
LAB.assert(lab); |
|
return this.fromXYZ(LAB.toXYZ(lab)); |
|
} |
|
|
|
static toLAB(color: Base): LAB { |
|
if (LAB.is(color)) return color; |
|
return LAB.fromXYZ(this.toXYZ(color)); |
|
} |
|
|
|
static fromLCH(lch: LCH): Base { |
|
LCH.assert(lch); |
|
return this.fromLAB(LCH.toLAB(lch)); |
|
} |
|
|
|
static toLCH(color: Base): LCH { |
|
if (LCH.is(color)) return color; |
|
return LCH.fromLAB(this.toLAB(color)); |
|
} |
|
|
|
static fromOKLAB(oklab: OKLAB): Base { |
|
OKLAB.assert(oklab); |
|
return this.fromLAB(OKLAB.toLAB(oklab)); |
|
} |
|
|
|
static toOKLAB(color: Base): OKLAB { |
|
if (OKLAB.is(color)) return color; |
|
return OKLAB.fromLAB(this.toLAB(color)); |
|
} |
|
|
|
static fromOKLCH(oklch: OKLCH): Base { |
|
OKLCH.assert(oklch); |
|
return this.fromLAB(OKLCH.toLAB(oklch)); |
|
} |
|
|
|
static toOKLCH(color: Base): OKLCH { |
|
if (OKLCH.is(color)) return color; |
|
return OKLCH.fromLAB(this.toLAB(color)); |
|
} |
|
|
|
static fromHWB(hwb: HWB): Base { |
|
HWB.assert(hwb); |
|
return this.fromRGB(HWB.toRGB(hwb)); |
|
} |
|
|
|
static toHWB(color: Base): HWB { |
|
if (HWB.is(color)) return color; |
|
return HWB.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromAPPLE(apple: APPLE): Base { |
|
APPLE.assert(apple); |
|
return this.fromRGB(APPLE.toRGB(apple)); |
|
} |
|
|
|
static toAPPLE(color: Base): APPLE { |
|
if (APPLE.is(color)) return color; |
|
return APPLE.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromANSI(ansi: ANSI): Base { |
|
ANSI.assert(ansi); |
|
return this.fromRGB(ANSI.toRGB(ansi)); |
|
} |
|
|
|
static toANSI(color: Base): ANSI { |
|
if (ANSI.is(color)) return color; |
|
return ANSI.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromANSI256(ansi256: ANSI256): Base { |
|
ANSI256.assert(ansi256); |
|
return this.fromRGB(ANSI256.toRGB(ansi256)); |
|
} |
|
|
|
static toANSI256(color: Base): ANSI256 { |
|
if (ANSI256.is(color)) return color; |
|
return ANSI256.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromANSI16M(ansi16m: ANSI16M): Base { |
|
ANSI16M.assert(ansi16m); |
|
return this.fromRGB(ANSI16M.toRGB(ansi16m)); |
|
} |
|
|
|
static toANSI16M(color: Base): ANSI16M { |
|
if (ANSI16M.is(color)) return color; |
|
return ANSI16M.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromHCG(hcg: HCG): Base { |
|
HCG.assert(hcg); |
|
return this.fromRGB(HCG.toRGB(hcg)); |
|
} |
|
|
|
static toHCG(color: Base): HCG { |
|
if (HCG.is(color)) return color; |
|
return HCG.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromGRAY(gray: GRAY): Base { |
|
GRAY.assert(gray); |
|
return this.fromRGB(GRAY.toRGB(gray)); |
|
} |
|
|
|
static toGRAY(color: Base): GRAY { |
|
if (GRAY.is(color)) return color; |
|
return GRAY.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromKEYWORD(keyword: ColorNames | KEYWORD): Base { |
|
if (typeof keyword === "string") keyword = new KEYWORD(keyword); |
|
KEYWORD.assert(keyword); |
|
return this.fromRGB(KEYWORD.toRGB(keyword)); |
|
} |
|
|
|
static toKEYWORD(color: Base): KEYWORD { |
|
if (KEYWORD.is(color)) return color; |
|
return KEYWORD.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static fromName(name: ColorNames | KEYWORD): Base { |
|
return this.fromKEYWORD(name); |
|
} |
|
|
|
static toName(color: Base): KEYWORD { |
|
return this.toKEYWORD(color); |
|
} |
|
|
|
static fromNAME(name: ColorNames | KEYWORD): Base { |
|
return this.fromKEYWORD(name); |
|
} |
|
|
|
static toNAME(color: Base): KEYWORD { |
|
return this.toKEYWORD(color); |
|
} |
|
|
|
static fromKeyword(keyword: ColorNames | KEYWORD): Base { |
|
return this.fromKEYWORD(keyword); |
|
} |
|
|
|
static toKeyword(color: Base): KEYWORD { |
|
return this.toKEYWORD(color); |
|
} |
|
|
|
static { |
|
Object.defineProperties(this, { |
|
fromAnsi: { value: this.fromANSI }, |
|
fromAnsi256: { value: this.fromANSI256 }, |
|
fromAnsi16M: { value: this.fromANSI16M }, |
|
fromApple: { value: this.fromAPPLE }, |
|
fromGray: { value: this.fromGRAY }, |
|
fromName: { value: this.fromKEYWORD }, |
|
fromKeyword: { value: this.fromKEYWORD }, |
|
fromNAME: { value: this.fromKEYWORD }, |
|
toAnsi: { value: this.toANSI }, |
|
toAnsi256: { value: this.toANSI256 }, |
|
toAnsi16M: { value: this.toANSI16M }, |
|
toApple: { value: this.toAPPLE }, |
|
toGray: { value: this.toGRAY }, |
|
toName: { value: this.toKEYWORD }, |
|
toKeyword: { value: this.toKEYWORD }, |
|
toNAME: { value: this.toKEYWORD }, |
|
}); |
|
} |
|
|
|
static fromCMYK(cmyk: CMYK): Base { |
|
CMYK.assert(cmyk); |
|
return this.fromRGB(CMYK.toRGB(cmyk)); |
|
} |
|
|
|
static toCMYK(color: Base): CMYK { |
|
if (CMYK.is(color)) return color; |
|
return CMYK.fromRGB(this.toRGB(color)); |
|
} |
|
|
|
static toHEX3(color: Base): HEX3 { |
|
return new HEX3(this.toRGB(color).toString()); |
|
} |
|
|
|
static toHEX4(color: Base): HEX4 { |
|
return new HEX4(this.toRGB(color).toString()); |
|
} |
|
|
|
static toHEX6(color: Base): HEX6 { |
|
return new HEX6(this.toRGB(color).toString()); |
|
} |
|
|
|
static toHEX8(color: Base): HEX8 { |
|
return new HEX8(this.toRGB(color).toString()); |
|
} |
|
|
|
static fromHex(hex: HEX | string): Base { |
|
if (typeof hex === "string") hex = new HEX(hex); |
|
HEX.assert(hex); |
|
return this.fromHexString(hex.toString()); |
|
} |
|
|
|
static toHex(color: Base): HEX { |
|
return new HEX(this.toRGB(color).toString()); |
|
} |
|
|
|
static fromHexString(hex: string): Base { |
|
HEX.assert(hex); |
|
let match = hex.match(Color.RegExp.HEX8); |
|
let isHex3 = false; |
|
if (!match) { |
|
match = hex.match(Color.RegExp.HEX4); |
|
isHex3 = true; |
|
} |
|
if (!match) return this.fromHexString("#000000"); |
|
const { r, g, b, a } = match.groups as unknown as FormatSchema<$RGB>; |
|
|
|
const red = isHex3 ? `${r}${r}` : r + ""; |
|
const green = isHex3 ? `${g}${g}` : g + ""; |
|
const blue = isHex3 ? `${b}${b}` : b + ""; |
|
const alpha = a ? isHex3 ? `${a}${a}` : a + "" : "FF"; |
|
|
|
return this.fromRGB( |
|
new RGB( |
|
parseInt(red, 16), |
|
parseInt(green, 16), |
|
parseInt(blue, 16), |
|
parseInt(alpha, 16) / 255, |
|
), |
|
); |
|
} |
|
|
|
static toHexString(color: Base): string { |
|
return this.toHex(color).toString(); |
|
} |
|
|
|
static is(it: unknown): it is Base { |
|
return it instanceof (this[_type] ?? this); |
|
} |
|
|
|
static assert( |
|
it: unknown, |
|
message?: string, |
|
): asserts it is Base { |
|
if (!this.is?.(it)) { |
|
const inspected = inspect(it, { |
|
colors: true, |
|
depth: 1, |
|
getters: true, |
|
compact: true, |
|
}); |
|
message ??= tpl("{0} color expected. Received '{1}' ({2})", { |
|
0: this[_name] ?? this.name, |
|
1: inspected, |
|
2: typeof it as string, |
|
}); |
|
const error = new TypeError(message); |
|
Error.captureStackTrace?.(error); |
|
throw error; |
|
} |
|
} |
|
|
|
static equals(a: Base, b: Base): boolean { |
|
const schema = this[_schema]; |
|
if (!this.is?.(a) || !this.is?.(b)) return false; |
|
return this[_keys].every((k) => { |
|
const optional = schema[k].length === 3 && schema[k][2] === true || |
|
schema[2] == null; |
|
const vA = (a as any)[k], vB = (b as any)[k]; |
|
return optional || (vA != null && vB != null && Object.is(vA, vB)); |
|
}); |
|
} |
|
|
|
static [inspect.custom]( |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
options.maxArrayLength = 5; |
|
options.numericSeparator = true; |
|
options.getters = true; |
|
const name = this[_name] ?? this.name; |
|
if (depth && depth < 0) { |
|
return options.stylize(`[Color: ${name}]`, "special"); |
|
} else { |
|
const schema = this[_schema]; |
|
const obj = { name } as Record<string, unknown>; |
|
for (const k of Array.from(this[_keys])) { |
|
const values = schema[k] as Printable[]; |
|
if ( |
|
values.length <= 3 && typeof values[0] === "number" && |
|
typeof values[1] === "number" |
|
) { |
|
let [min, max, optional] = values; |
|
if (optional === undefined) optional = values.length === 3; |
|
min = Number(min), max = Number(max); |
|
obj[k] = { min, max, ...optional ? { optional } : {} }; |
|
} else { |
|
obj[k] = values; |
|
} |
|
} |
|
return `${options.stylize(`${name}`, "special")} ${ |
|
inspect(obj, options) |
|
}`; |
|
} |
|
} |
|
|
|
static readonly [_type]: AbstractConstructor<Base>; |
|
static readonly [_name]: string; |
|
static readonly [_schema]: Schema; |
|
static readonly [_keys]: string[]; |
|
static [Symbol.species]: AbstractConstructor<Base> = Base; |
|
|
|
static [Symbol.hasInstance](it: unknown) { |
|
if (typeof it !== "object" || it == null || Array.isArray(it)) { |
|
return false; |
|
} |
|
if (!(_name in it) || it[_name] !== this[_name]) return false; |
|
if ( |
|
!(_schema in it && typeof it[_schema] === "object" && |
|
it[_schema] != null && !Array.isArray(it[_schema])) || |
|
!(_type in it && typeof it[_type] === "function") || !(_keys in it) |
|
) return false; |
|
const ctor = it[_type]; |
|
if (Function[Symbol.hasInstance].call(ctor, it)) return true; |
|
const schema = it[_schema]; |
|
const keys = it[_keys] ?? Reflect.ownKeys(schema) |
|
.filter((k): k is string => typeof k === "string"); |
|
for (const k of keys as (keyof typeof schema)[]) { |
|
if (!(k in it)) return false; |
|
const values = schema[k] as Printable[]; |
|
const value = it[k as keyof typeof it]; |
|
if ( |
|
[2, 3].includes(values.length) && |
|
typeof values[0] === "number" && |
|
typeof values[1] === "number" |
|
) { |
|
const [min, max, optional] = values; |
|
if (values.length !== 3 || optional === false || value !== null) { |
|
if (typeof value !== "number" || value < min || value > max) { |
|
return false; |
|
} else { |
|
continue; |
|
} |
|
} |
|
} else if ( |
|
!["string", "number", "bigint", "boolean", "undefined"].includes( |
|
typeof value, |
|
) || !values.includes(value as Printable) |
|
) return false; |
|
} |
|
return true; |
|
} |
|
|
|
static [_extend]< |
|
const T extends AbstractConstructor<P>, |
|
P extends Base<S>, |
|
K extends string & keyof schemas, |
|
const S extends Schema, |
|
>( |
|
this: Base | typeof Base | void, |
|
ctor: T, |
|
name: K, |
|
schema: S, |
|
): asserts ctor is T & ColorSpace<T, P, K, S> { |
|
Object.setPrototypeOf(schema, null); |
|
|
|
if (this instanceof Base) { |
|
const base = ctor as unknown as typeof Base; |
|
if (#ctor in this) { |
|
this.#ctor = base; |
|
// @ts-ignore reassigning readonly property |
|
this[_type][_type] ??= base; |
|
this[_type][Symbol.species] ??= base; |
|
// @ts-ignore reassigning readonly property |
|
if (#name in this) this.#name = this[_type][_name] = schemas[name].name; |
|
if (#schema in this) { |
|
// @ts-ignore reassigning readonly property |
|
this.#schema = this[_type][_schema] ??= schema; |
|
} |
|
if (#keys in this) { |
|
// @ts-ignore reassigning readonly property |
|
this.#keys = this[_type][_keys] ??= Reflect.ownKeys(schema) |
|
.filter((k): k is string => typeof k === "string"); |
|
} |
|
} |
|
} |
|
|
|
Object.defineProperties(ctor, { |
|
[Symbol.hasInstance]: { |
|
value: hasInstance, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
[Symbol.species]: { |
|
value: ctor, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
[_name]: { |
|
value: name, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
[_schema]: { |
|
value: schema, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
[_keys]: { |
|
value: Reflect.ownKeys(schema).filter((k) => typeof k === "string"), |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
}); |
|
|
|
function hasInstance(it: unknown): it is InstanceType<typeof ctor> { |
|
if (Function[Symbol.hasInstance].call(ctor, it)) return true; |
|
if (typeof it === "object" && it != null && !Array.isArray(it)) { |
|
if (_name in it && it[_name] === name) return true; |
|
const keys = Reflect.ownKeys(schema).filter((k): k is string => |
|
typeof k === "string" |
|
); |
|
for (const k of keys as (keyof typeof schema)[]) { |
|
if (!(k in it)) return false; |
|
const values = schema[k] as Printable[]; |
|
const value = it[k as keyof typeof it]; |
|
if ( |
|
[2, 3].includes(values.length) && |
|
typeof values[0] === "number" && |
|
typeof values[1] === "number" |
|
) { |
|
const [min, max, optional] = values; |
|
if (values.length === 3 && optional !== false && value == null) { |
|
continue; |
|
} |
|
if (typeof value !== "number" || value < min || value > max) { |
|
return false; |
|
} |
|
} else { |
|
const value = it[k as keyof typeof it]; |
|
if (!values.includes(value)) return false; |
|
} |
|
} |
|
return true; |
|
} |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
const extendBase: typeof Base[_extend] = Base[_extend]; |
|
|
|
type AbstractConstructor<T = any, A extends readonly unknown[] = any> = |
|
& (abstract new (...args: A) => T) |
|
& { readonly prototype: T }; |
|
|
|
type ColorSpacePrototype< |
|
Constructor extends AbstractConstructor, |
|
Prototype extends Base<S>, |
|
S extends Schema, |
|
> = Prototype & { readonly constructor: Constructor }; |
|
|
|
type BaseConstructor< |
|
S extends Schema, |
|
P extends Base<S> = Base<S>, |
|
C extends AbstractConstructor = typeof Base<S>, |
|
> = AbstractConstructor<P, readonly [schema: S] | ConstructorParameters<C>> & C; |
|
|
|
interface ColorSpace< |
|
T extends AbstractConstructor, |
|
P extends Base<S>, |
|
K extends string & keyof schemas, |
|
S extends Schema, |
|
> extends BaseConstructor<S> { |
|
readonly name: K; |
|
readonly prototype: ColorSpacePrototype<this, P, S>; |
|
|
|
[Symbol.hasInstance](it: unknown): it is P; |
|
[Symbol.species]: T; |
|
[_name]: schemas[K]["name"]; |
|
[_schema]: S; |
|
[_keys]: (string & keyof S)[]; |
|
[_type]: T; |
|
|
|
is(it: unknown): it is P; |
|
assert(it: unknown, message?: string): asserts it is P; |
|
equals(a: P, b: P): boolean; |
|
toRGB(color: P): RGB; |
|
fromRGB(rgb: RGB): P; |
|
toHCG(color: P): HCG; |
|
fromHCG(hcg: HCG): P; |
|
toAPPLE(color: P): APPLE; |
|
fromAPPLE(apple: APPLE): P; |
|
toANSI(color: P): ANSI; |
|
fromANSI(ansi: ANSI): P; |
|
toANSI256(color: P): ANSI256; |
|
fromANSI256(ansi256: ANSI256): P; |
|
toANSI16M(color: P): ANSI16M; |
|
fromANSI16M(ansi16m: ANSI16M): P; |
|
toGRAY(color: P): GRAY; |
|
fromGRAY(gray: GRAY): P; |
|
toHSL(color: P): HSL; |
|
fromHSL(hsl: HSL): P; |
|
toHSV(color: P): HSV; |
|
fromHSV(hsv: HSV): P; |
|
toXYZ(color: P): XYZ; |
|
fromXYZ(xyz: XYZ): P; |
|
toLAB(color: P): LAB; |
|
fromLAB(lab: LAB): P; |
|
toLCH(color: P): LCH; |
|
fromLCH(lch: LCH): P; |
|
toCMYK(color: P): CMYK; |
|
fromCMYK(cmyk: CMYK): P; |
|
toHWB(color: P): HWB; |
|
fromHWB(hwb: HWB): P; |
|
toOKLAB(color: P): OKLAB; |
|
fromOKLAB(oklab: OKLAB): P; |
|
toOKLCH(color: P): OKLCH; |
|
fromOKLCH(oklch: OKLCH): P; |
|
toHEX(color: P): HEX; |
|
fromHEX(hex: HEX | string): P; |
|
toHex(color: P): HEX; |
|
fromHex(hex: HEX | string): P; |
|
toHexString(color: P): string; |
|
fromHexString(hex: string): P; |
|
} |
|
// #endregion Common |
|
|
|
// #region ANSI |
|
const $ANSI = schema("ANSI"); |
|
type $ANSI = typeof $ANSI; |
|
|
|
export class ANSI extends Base<$ANSI> { |
|
constructor( |
|
/** ANSI 4-bit color code. */ |
|
public value: number, |
|
) { |
|
super($ANSI); |
|
ANSI.assert(this); |
|
Object.setPrototypeOf(this, ANSI.prototype); |
|
} |
|
|
|
override toString(): string { |
|
ANSI.assert(this); |
|
return `\x1b[${this.value}m`; |
|
} |
|
|
|
override valueOf(): number { |
|
ANSI.assert(this); |
|
return this.value; |
|
} |
|
|
|
declare static is: (it: unknown) => it is ANSI; |
|
declare static assert: (it: unknown, message?: string) => asserts it is ANSI; |
|
|
|
static fromRGB(rgb: RGB, saturation?: number): ANSI { |
|
RGB.assert(rgb); |
|
const { r, g, b } = rgb; |
|
let value = saturation ?? RGB.toHSV(rgb).s; |
|
value = (value / 50) | 0; |
|
let ansi = 30; |
|
if (value > 0) { |
|
ansi += Math.round(b / 255) << 2 | |
|
Math.round(g / 255) << 1 | |
|
Math.round(r / 255); |
|
if (value === 2) ansi += 60; |
|
} |
|
return new ANSI(ansi & 0xFF); |
|
} |
|
|
|
static toRGB(ansi: ANSI): RGB { |
|
if (RGB.is(ansi)) return ansi; |
|
ANSI.assert(ansi); |
|
const { value } = ansi; |
|
|
|
let c = value % 10; |
|
if (c === 0 || c === 7) { |
|
if (value > 50) c += 3.5; |
|
c /= 10.5, c *= 255; |
|
return new RGB(c, c, c, 1); |
|
} |
|
const m = (~~(value > 50) + 1) * 127.5; |
|
const r = (c >> 0 & 1) * m, g = (c >> 1 & 1) * m, b = (c >> 2 & 1) * m; |
|
return new RGB(r & 0xFF, g & 0xFF, b & 0xFF, 1); |
|
} |
|
} |
|
|
|
extendBase(ANSI, "ANSI", $ANSI); |
|
// #endregion ANSI |
|
|
|
// #region ANSI256 |
|
const $ANSI256 = schema("ANSI256"); |
|
type $ANSI256 = typeof $ANSI256; |
|
|
|
export class ANSI256 extends Base<$ANSI256> { |
|
constructor( |
|
/** ANSI 8-bit color code (`0 - 255`). */ |
|
public value: number, |
|
) { |
|
super($ANSI256); |
|
ANSI256.assert(this); |
|
Object.setPrototypeOf(this, ANSI256.prototype); |
|
} |
|
|
|
override toString(): string { |
|
ANSI256.assert(this); |
|
return `\x1b[38;5;${this.value}m`; |
|
} |
|
|
|
override valueOf(): number { |
|
ANSI256.assert(this); |
|
return this.value; |
|
} |
|
|
|
declare static is: (it: unknown) => it is ANSI256; |
|
declare static assert: ( |
|
it: unknown, |
|
message?: string, |
|
) => asserts it is ANSI256; |
|
|
|
static fromRGB(rgb: RGB): ANSI256 { |
|
RGB.assert(rgb); |
|
const { r, g, b } = rgb; |
|
let ansi = 16; |
|
if (r === g && r === b) { |
|
const value = Math.round( |
|
(r * 299 + g * 587 + b * 114) / 1000, |
|
); |
|
ansi = value < 8 ? 16 : value > 248 ? 231 : 232 + Math.round( |
|
(value - 8) / 247 * 24, |
|
); |
|
} else { |
|
ansi += Math.round(r / 255 * 5) * 36; |
|
ansi += Math.round(g / 255 * 5) * 6; |
|
ansi += Math.round(b / 255 * 5); |
|
} |
|
return new ANSI256(ansi); |
|
} |
|
|
|
static toRGB(ansi: ANSI256): RGB { |
|
if (RGB.is(ansi)) return ansi; |
|
ANSI256.assert(ansi); |
|
let { value } = ansi; |
|
let c = 0; |
|
if (value < 16) { |
|
if (value < 8) c = (value - 8) * 10 + 8; |
|
return new RGB(c, c, c, 1); |
|
} else if (value > 231) { |
|
c = (value - 232) * 10 + 8; |
|
return new RGB(c, c, c, 1); |
|
} |
|
value -= 16; |
|
const green = value % 36; |
|
const r = Math.floor(value / 36) / 5 * 255; |
|
const g = Math.floor(green / 6) / 5 * 255; |
|
const b = green % 6 / 5 * 255; |
|
return new RGB(r, g, b, 1); |
|
} |
|
} |
|
|
|
extendBase(ANSI256, "ANSI256", $ANSI256); |
|
// #endregion ANSI256 |
|
|
|
// #region ANSI16M |
|
|
|
const $ANSI16M = schema("ANSI16M"); |
|
type $ANSI16M = typeof $ANSI16M; |
|
|
|
export class ANSI16M extends Base<$ANSI16M> { |
|
constructor( |
|
/** Red: integer between `0` and `255`. */ |
|
public r: number, |
|
/** Green: integer between `0` and `255`. */ |
|
public g: number, |
|
/** Blue: integer between `0` and `255`. */ |
|
public b: number, |
|
/** Options for configuring how an {@link ANSI16M} color is rendered. */ |
|
public options: ANSI16M.Options = {}, |
|
) { |
|
super($ANSI16M); |
|
ANSI16M.assert(this); |
|
Object.setPrototypeOf(this, ANSI16M.prototype); |
|
|
|
this.options = { |
|
background: false, |
|
bold: false, |
|
dim: false, |
|
italic: false, |
|
underline: false, |
|
strikethrough: false, |
|
inverse: false, |
|
blink: false, |
|
hidden: false, |
|
reset: false, |
|
framed: false, |
|
overline: false, |
|
doubleunderline: false, |
|
...options, |
|
}; |
|
|
|
this.r = Math.max(0, Math.min(255, r >>> 0)); |
|
this.g = Math.max(0, Math.min(255, g >>> 0)); |
|
this.b = Math.max(0, Math.min(255, b >>> 0)); |
|
} |
|
|
|
override toString(overrides?: ANSI16M.Options): string { |
|
ANSI16M.assert(this); |
|
const { r, g, b } = this; |
|
let code = ""; |
|
const { |
|
background, |
|
bold, |
|
dim, |
|
italic, |
|
underline, |
|
strikethrough, |
|
blink, |
|
doubleunderline, |
|
overline, |
|
framed, |
|
inverse, |
|
hidden, |
|
reset, |
|
} = { ...this.options, ...overrides ?? {} }; |
|
|
|
const modifiers = [ |
|
reset && "0", |
|
bold && "1", |
|
dim && "2", |
|
italic && "3", |
|
underline && "4", |
|
blink && "5", |
|
inverse && "7", |
|
hidden && "8", |
|
strikethrough && "9", |
|
doubleunderline && "21", |
|
framed && "51", |
|
overline && "53", |
|
].filter(Boolean); |
|
if (modifiers.length) code += `\x1b[${modifiers.join(";")}m`; |
|
code += `\x1b[${background ? 48 : 38};2;${r};${g};${b}m`; |
|
return code; |
|
} |
|
|
|
override valueOf(): number { |
|
ANSI16M.assert(this); |
|
return this.r << 16 | this.g << 8 | this.b; |
|
} |
|
|
|
declare static is: (it: unknown) => it is ANSI16M; |
|
declare static assert: ( |
|
it: unknown, |
|
message?: string, |
|
) => asserts it is ANSI16M; |
|
|
|
static override fromRGB(rgb: RGB): ANSI16M { |
|
RGB.assert(rgb); |
|
return new ANSI16M(rgb.r, rgb.g, rgb.b); |
|
} |
|
|
|
static override toRGB(ansi: ANSI16M): RGB { |
|
ANSI16M.assert(ansi); |
|
return new RGB(ansi.r, ansi.g, ansi.b, 1); |
|
} |
|
} |
|
|
|
export declare namespace ANSI16M { |
|
/** Options for configuring how an {@link ANSI16M} color is rendered. */ |
|
export interface Options { |
|
/** If `true`, renders as an ANSI background color. */ |
|
background?: boolean; |
|
/** If `true`, prepends a **bold** escape sequence. */ |
|
bold?: boolean; |
|
/** If `true`, prepends a **dim** escape sequence. */ |
|
dim?: boolean; |
|
/** If `true`, prepends an **italics** escape sequence. */ |
|
italic?: boolean; |
|
/** If `true`, prepends an **underline** escape sequence. */ |
|
underline?: boolean; |
|
/** If `true`, prepends a **strikethrough** escape sequence. */ |
|
strikethrough?: boolean; |
|
/** If `true`, prepends a **flash** / **blink** escape sequence. */ |
|
blink?: boolean; |
|
/** If `true`, prepends an **inversion** escape sequence. */ |
|
inverse?: boolean; |
|
/** If `true`, prepends an escape sequence to render text as hidden. */ |
|
hidden?: boolean; |
|
/** If `true`, prepends an **overline** escape sequence. */ |
|
overline?: boolean; |
|
/** If `true`, prepends an **underline** escape sequence. */ |
|
doubleunderline?: boolean; |
|
/** If `true`, prepends a **framed** escape sequence. */ |
|
framed?: boolean; |
|
/** If `true`, prepends an escape sequence to reset previous styles. */ |
|
reset?: boolean; |
|
} |
|
} |
|
|
|
extendBase(ANSI16M, "ANSI16M", $ANSI16M); |
|
|
|
// #endregion ANSI16M |
|
|
|
// #region APPLE |
|
const $APPLE = schema("APPLE"); |
|
type $APPLE = typeof $APPLE; |
|
|
|
export class APPLE extends Base<$APPLE> { |
|
constructor( |
|
/** Red: integer between `0` and `65535`. */ |
|
public r16: number, |
|
/** Green: integer between `0` and `65535`. */ |
|
public g16: number, |
|
/** Blue: integer between `0` and `65535`. */ |
|
public b16: number, |
|
) { |
|
super($APPLE); |
|
APPLE.assert(this); |
|
Object.setPrototypeOf(this, APPLE.prototype); |
|
} |
|
|
|
override toString(): string { |
|
APPLE.assert(this); |
|
const { r16, g16, b16 } = this; |
|
const r = (r16 / 65535 * 255) | 0; |
|
const g = (g16 / 65535 * 255) | 0; |
|
const b = (b16 / 65535 * 255) | 0; |
|
return Color.Format.RGB(new RGB(r, g, b, 1)); |
|
} |
|
|
|
override valueOf(): number { |
|
APPLE.assert(this); |
|
return this.r16 << 32 | this.g16 << 16 | this.b16; |
|
} |
|
|
|
declare static is: (it: unknown) => it is APPLE; |
|
declare static assert: (it: unknown, message?: string) => asserts it is APPLE; |
|
|
|
static override fromRGB(rgb: RGB): APPLE { |
|
RGB.assert(rgb); |
|
const { r, g, b } = rgb; |
|
const r16 = (r / 255 * 65535) | 0; |
|
const g16 = (g / 255 * 65535) | 0; |
|
const b16 = (b / 255 * 65535) | 0; |
|
return new APPLE(r16, g16, b16); |
|
} |
|
|
|
static override toRGB(apple: APPLE): RGB { |
|
APPLE.assert(apple); |
|
const { r16, g16, b16 } = apple; |
|
const r = (r16 / 65535 * 255) | 0; |
|
const g = (g16 / 65535 * 255) | 0; |
|
const b = (b16 / 65535 * 255) | 0; |
|
return new RGB(r, g, b, 1); |
|
} |
|
} |
|
|
|
extendBase(APPLE, "APPLE", $APPLE); |
|
|
|
// #endregion APPLE |
|
|
|
// #region CMYK |
|
const $CMYK = schema("CMYK"); |
|
type $CMYK = typeof $CMYK; |
|
|
|
export class CMYK extends Base<$CMYK> { |
|
constructor( |
|
/** **C**yan: float between `0` and `1`. */ |
|
public c: number, |
|
/** **M**agenta: float between `0` and `1`. */ |
|
public m: number, |
|
/** **Y**ellow: float between `0` and `1`. */ |
|
public y: number, |
|
/** blac**K**: float between `0` and `1`. */ |
|
public k: number, |
|
/** Alpha: float between `0` and `1`. */ |
|
public a = 1, |
|
) { |
|
super($CMYK); |
|
CMYK.assert(this); |
|
Object.setPrototypeOf(this, CMYK.prototype); |
|
} |
|
|
|
toString() { |
|
return Color.Format.CMYK(this); |
|
} |
|
|
|
declare static is: (it: unknown) => it is CMYK; |
|
declare static assert: (it: unknown, message?: string) => asserts it is CMYK; |
|
|
|
static toRGB(color: CMYK): RGB { |
|
if (RGB.is(color)) return color; |
|
if (CMYK.is(color)) { |
|
return XYZ.toRGB(CMYK.toXYZ(color)); |
|
} else { |
|
throw new TypeError("Cannot convert to RGB"); |
|
} |
|
} |
|
|
|
static fromRGB(rgb: RGB): CMYK { |
|
RGB.assert(rgb); |
|
const xyz = XYZ.fromRGB(rgb); |
|
return CMYK.fromXYZ(xyz); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): CMYK { |
|
XYZ.assert(xyz); |
|
const { x, y, z, a } = xyz; |
|
const k = 1 - Math.max(x, y, z); |
|
const c = (1 - x - k) / (1 - k); |
|
const m = (1 - y - k) / (1 - k); |
|
const y2 = (1 - z - k) / (1 - k); |
|
return new CMYK(c, m, y2, k, a); |
|
} |
|
|
|
static toXYZ(cmyk: CMYK): XYZ { |
|
CMYK.assert(cmyk); |
|
const { c, m, y, k, a } = cmyk; |
|
const x = 1 - Math.min(1, c * (1 - k) + k); |
|
const y2 = 1 - Math.min(1, m * (1 - k) + k); |
|
const z = 1 - Math.min(1, y * (1 - k) + k); |
|
return new XYZ(x, y2, z, a); |
|
} |
|
} |
|
// #endregion CMYK |
|
|
|
// #region GRAY |
|
const $GRAY = schema("GRAY"); |
|
type $GRAY = typeof $GRAY; |
|
|
|
export class GRAY extends Base<$GRAY> { |
|
constructor( |
|
/** Grayness: integer between `0` and `255`. */ |
|
public g: number, |
|
/** Alpha: float between `0` and `1`. */ |
|
public a = 1, |
|
) { |
|
super($GRAY); |
|
GRAY.assert(this); |
|
Object.setPrototypeOf(this, GRAY.prototype); |
|
} |
|
|
|
declare static is: (it: unknown) => it is GRAY; |
|
declare static assert: (it: unknown, message?: string) => asserts it is GRAY; |
|
|
|
static toRGB(color: GRAY): RGB { |
|
if (RGB.is(color)) return color; |
|
GRAY.assert(color); |
|
const { g, a = 1 } = color; |
|
return new RGB((g / 100) * 255, (g / 100) * 255, (g / 100) * 255, a); |
|
} |
|
|
|
static fromRGB(rgb: RGB): GRAY { |
|
RGB.assert(rgb); |
|
const { r, g, b, a = 1 } = rgb; |
|
const gg = ((r + g + b) / 3) / 255 * 100; |
|
return new GRAY(gg, a); |
|
} |
|
|
|
static toLAB(color: GRAY): LAB { |
|
if (LAB.is(color)) return color; |
|
GRAY.assert(color); |
|
const { g, a = 1 } = color; |
|
return new LAB(g, 0, 0, a); |
|
} |
|
|
|
static fromLAB(lab: LAB): GRAY { |
|
LAB.assert(lab); |
|
const { l, alpha = 1 } = lab; |
|
return new GRAY(l, alpha); |
|
} |
|
|
|
static toLCH(color: GRAY): LCH { |
|
if (LCH.is(color)) return color; |
|
GRAY.assert(color); |
|
const { g, a = 1 } = color; |
|
return new LCH(g, 0, 0, a); |
|
} |
|
|
|
static fromLCH(lch: LCH): GRAY { |
|
LCH.assert(lch); |
|
const { l, alpha = 1 } = lch; |
|
return new GRAY(l, alpha); |
|
} |
|
|
|
static toCMYK(color: GRAY): CMYK { |
|
if (CMYK.is(color)) return color; |
|
GRAY.assert(color); |
|
const { g, a = 1 } = color; |
|
return new CMYK(0, 0, 0, g / 100, a); |
|
} |
|
|
|
static fromCMYK(cmyk: CMYK): GRAY { |
|
CMYK.assert(cmyk); |
|
const { k, a = 1 } = cmyk; |
|
return new GRAY(k * 100, a); |
|
} |
|
|
|
static toHEX(color: GRAY): HEX { |
|
if (HEX.is(color)) return color; |
|
GRAY.assert(color); |
|
const { g, a = 1 } = color; |
|
const v = Math.round(g / 100 * 0xFF) & 0xFF; |
|
const i = v << 16 | v << 8 | v; |
|
let s = "#"; |
|
s += i.toString(16).padStart(6, "0"); |
|
s += (a * 255 | 0).toString(16).padStart(2, "0"); |
|
return new HEX(s.padEnd(9, "F")); |
|
} |
|
} |
|
// #endregion GRAY |
|
|
|
// #region HCG |
|
const $HCG = schema("HCG"); |
|
type $HCG = typeof $HCG; |
|
|
|
export class HCG extends Base<$HCG> { |
|
constructor( |
|
/** **H**ue: float between `0` and `360`. */ |
|
public h: number, |
|
/** **C**hroma: float between `0` and `100`. */ |
|
public c: number, |
|
/** **G**rayness: float between `0` and `100`. */ |
|
public g: number, |
|
/** Alpha: float between `0` and `1`. */ |
|
public a = 1, |
|
) { |
|
super($HCG); |
|
HCG.assert(this); |
|
Object.setPrototypeOf(this, HCG.prototype); |
|
} |
|
|
|
declare static is: (it: unknown) => it is HCG; |
|
declare static assert: (it: unknown, message?: string) => asserts it is HCG; |
|
|
|
static toRGB(color: HCG): RGB { |
|
HCG.assert(color); |
|
const { h, c, g: gg, a = 1 } = color; |
|
if (c === 0) { |
|
return new RGB(gg * 255, gg * 255, gg * 255, a); |
|
} |
|
let r = 0, g = 0, b = 0; |
|
const hi = h % 1 * 6; |
|
const v = hi % 1; |
|
const w = 1 - v; |
|
// deno-fmt-ignore |
|
switch (Math.floor(hi)) { |
|
case 0: [r, g, b] = [1, v, 0]; break; |
|
case 1: [r, g, b] = [w, 1, 0]; break; |
|
case 2: [r, g, b] = [0, 1, v]; break; |
|
case 3: [r, g, b] = [0, w, 1]; break; |
|
case 4: [r, g, b] = [v, 0, 1]; break; |
|
default: [r, g, b] = [1, 0, w]; |
|
} |
|
const mg = (1 - c) * gg; |
|
return new RGB((c * r + mg) * 255, (c * g + mg) * 255, (c * b + mg) * 255); |
|
} |
|
|
|
static fromRGB(rgb: RGB): HCG { |
|
RGB.assert(rgb); |
|
let { r, g, b } = rgb; |
|
r /= 255, g /= 255, b /= 255; |
|
const max = Math.max(r, g, b), min = Math.min(r, g, b); |
|
const chroma = max - min; |
|
let grayscale = 0, hue = 0; |
|
if (chroma < 1) grayscale = min / (1 - chroma); |
|
if (chroma <= 0) { |
|
hue = 0; |
|
} else if (max === r) { |
|
hue = (g - b) / chroma % 6; |
|
} else if (max === g) { |
|
hue = 2 + (b - r) / chroma; |
|
} else { |
|
hue = 4 + (r - g) / chroma; |
|
} |
|
hue /= 6, hue %= 1, hue *= 360; |
|
return new HCG(hue, chroma * 100, grayscale * 100); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): HCG { |
|
XYZ.assert(xyz); |
|
const { x, y, z, a } = xyz; |
|
const c = Math.sqrt(x * x + y * y + z * z); |
|
const g = Math.atan2(y, x) * 180 / Math.PI; |
|
const h = g < 0 ? g + 360 : g; |
|
return new HCG(h, c, z, a); |
|
} |
|
|
|
static toXYZ(hcg: HCG): XYZ { |
|
HCG.assert(hcg); |
|
const { h, c, g, a } = hcg; |
|
const rad = h * Math.PI / 180; |
|
const x = c * Math.cos(rad); |
|
const y = c * Math.sin(rad); |
|
return new XYZ(x, y, g, a); |
|
} |
|
} |
|
// #endregion HCG |
|
|
|
// #region HEX |
|
const $HEX = schema("HEX"); |
|
type $HEX = typeof $HEX; |
|
|
|
export class HEX extends String { |
|
#value: string; |
|
#length = 0; |
|
|
|
constructor(hex: HEX | HEX3 | HEX4 | HEX6 | HEX8); |
|
constructor(hexLike: FormatSchema<$HEX>); |
|
constructor(hexString: `${"0x" | "0X" | "#"}${string}`); |
|
constructor(hexString: string); |
|
constructor(hexNumber: number); |
|
constructor(value: string | number | HEX | FormatSchema<$HEX>); |
|
constructor(value: string | number | HEX | FormatSchema<$HEX>) { |
|
if (typeof value === "object" && value != null) { |
|
if ("value" in value && typeof value.value === "number") { |
|
value = value.value; |
|
} else { |
|
value = value.toString(); |
|
} |
|
} |
|
if (typeof value === "number") { |
|
value = value.toString(16).toUpperCase().padStart(6, "0").padEnd(8, "F"); |
|
} |
|
HEX.assert(value); |
|
const HEX_RE = /^(?:0[Xx]|#)?([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i; |
|
const hex = value.toString().replace(HEX_RE, "$1").toUpperCase(); |
|
if (![3, 4, 6, 8].includes(hex.length)) { |
|
throw new TypeError( |
|
tpl("Invalid HEX color value '{hex}'.", { hex }), |
|
); |
|
} |
|
if (!Color.RegExp.HEX8.test(hex) && !Color.RegExp.HEX4.test(hex)) { |
|
throw new TypeError( |
|
tpl("Invalid HEX color value '{hex}'.", { hex }), |
|
); |
|
} |
|
|
|
super(hex); |
|
this.#value = hex; |
|
this.#length = hex.length; |
|
} |
|
|
|
toJSON(): string { |
|
return this.toString(); |
|
} |
|
|
|
toString(): string { |
|
const hex = this.valueOf(); |
|
return `#${hex}`; |
|
} |
|
|
|
toHex3String(): string { |
|
return this.toHex4String().slice(0, 4); |
|
} |
|
|
|
toHex4String(): string { |
|
let hex = super.valueOf(); |
|
hex = hex.replace(/^#/, ""); |
|
if (hex.length === 3) hex = hex.replace(/(.)/g, "$1$1"); |
|
if (hex.length === 6) hex += "FF"; |
|
return `#${hex.toUpperCase()}`; |
|
} |
|
|
|
toHex6String(): string { |
|
return this.toHex8String().slice(0, 7); |
|
} |
|
|
|
toHex8String(): string { |
|
let hex = super.valueOf(); |
|
hex = hex.replace(/^#/, ""); |
|
if (hex.length === 3) hex = hex.replace(/(.)/g, "$1$1"); |
|
if (hex.length === 4) hex = hex.replace(/(.)/g, "$1$1"); |
|
if (hex.length === 6) hex += "FF"; |
|
return `#${hex.toUpperCase()}`; |
|
} |
|
|
|
toHex3(): HEX3 { |
|
return new HEX3(this.toHex3String()); |
|
} |
|
|
|
toHex4(): HEX4 { |
|
return new HEX4(this.toHex4String()); |
|
} |
|
|
|
toHex6(): HEX6 { |
|
return new HEX6(this.toHex6String()); |
|
} |
|
|
|
toHex8(): HEX8 { |
|
return new HEX8(this.toHex8String()); |
|
} |
|
|
|
*[Symbol.iterator](): IterableIterator<string> { |
|
let hex = this.toString().replace(/^#/, ""); |
|
if (hex.length === 3 || hex.length === 4) hex = hex.replace(/(.)/g, "$1$1"); |
|
for (let i = 0; i < hex.length; i += 2) yield hex.slice(i, 2); |
|
} |
|
|
|
static fromRGB(rgb: RGB): HEX { |
|
RGB.assert(rgb); |
|
return new HEX(RGB.toHexString(rgb)); |
|
} |
|
|
|
static toRGB(hex: string | HEX): RGB { |
|
HEX.assert(hex); |
|
return RGB.fromHexString(hex.toString()); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): HEX { |
|
XYZ.assert(xyz); |
|
return HEX.fromRGB(XYZ.toRGB(xyz)); |
|
} |
|
|
|
static toXYZ(hex: string | HEX): XYZ { |
|
HEX.assert(hex); |
|
return XYZ.fromRGB(HEX.toRGB(hex)); |
|
} |
|
|
|
static is(it: unknown): it is HEX { |
|
return it instanceof HEX; |
|
} |
|
|
|
static assert(it: unknown, message?: string): asserts it is HEX { |
|
if (!this.is(it)) { |
|
const inspected = inspect(it, { |
|
colors: true, |
|
depth: 1, |
|
getters: true, |
|
compact: true, |
|
}); |
|
message = tpl( |
|
message ?? "{kind:HEX} color expected. Received '{it}' ({typeof})", |
|
{ |
|
0: inspected, |
|
1: typeof it as string, |
|
kind: this.name, |
|
it: inspected, |
|
typeof: typeof it as string, |
|
}, |
|
); |
|
const error = new TypeError(message); |
|
Error.captureStackTrace?.(error); |
|
throw error; |
|
} |
|
} |
|
|
|
static equals(a: HEX, b: HEX): boolean { |
|
if (!HEX.is(a) || !HEX.is(b)) return false; |
|
return a.toLowerCase() === b.toLowerCase(); |
|
} |
|
|
|
static [Symbol.hasInstance](it: unknown): it is HEX { |
|
if (Function[Symbol.hasInstance].call(HEX, it)) return true; |
|
return it != null && (typeof it === "string" || it instanceof String) && ( |
|
Color.RegExp.HEX4.test(it.toString().replace(/^#?/, "#")) || |
|
Color.RegExp.HEX8.test(it.toString().replace(/^#?/, "#")) |
|
); |
|
} |
|
|
|
declare readonly [_brand]: "HEX"; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[_brand]: { |
|
get: () => "HEX", |
|
enumerable: false, |
|
configurable: false, |
|
}, |
|
[Symbol.toStringTag]: { |
|
value: "Color.HEX", |
|
enumerable: false, |
|
configurable: true, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
const SIZE: unique symbol = Symbol("SIZE"); |
|
|
|
// #region HEX3 |
|
export class HEX3 extends HEX { |
|
readonly [SIZE]: 3 = 3; |
|
|
|
declare readonly length: 4; |
|
|
|
constructor(v: string | HEX) { |
|
v = Color.Format.parseHex(v.toString())?.toHexString(3) ?? v; |
|
super(v); |
|
HEX3.assert(this); |
|
Object.setPrototypeOf(this, HEX3.prototype); |
|
} |
|
|
|
static is(it: unknown): it is HEX3 { |
|
return it instanceof HEX3 || |
|
(typeof it === "string" && (it = it.replace("#", "")).length === 3 && |
|
!isNaN(parseInt(it + "", 16))); |
|
} |
|
|
|
declare static assert: (it: unknown, message?: string) => asserts it is HEX3; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[_brand]: { |
|
get: () => "HEX3", |
|
enumerable: false, |
|
configurable: false, |
|
}, |
|
[Symbol.toStringTag]: { |
|
value: "Color.HEX3", |
|
enumerable: false, |
|
configurable: true, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
// #endregion HEX3 |
|
|
|
// #region HEX4 |
|
export class HEX4 extends HEX { |
|
readonly [SIZE]: 4 = 4; |
|
|
|
declare readonly length: 5; |
|
|
|
constructor(v: string | HEX) { |
|
v = Color.Format.parseHex(v.toString())?.toHexString(4) ?? v; |
|
super(v); |
|
HEX4.assert(this); |
|
Object.setPrototypeOf(this, HEX4.prototype); |
|
} |
|
|
|
static is(it: unknown): it is HEX4 { |
|
return it instanceof HEX4 || |
|
(typeof it === "string" && (it = it.replace("#", "")).length === 4 && |
|
!isNaN(parseInt(it + "", 16))); |
|
} |
|
|
|
declare static assert: (it: unknown, message?: string) => asserts it is HEX4; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[_brand]: { |
|
get: () => "HEX4", |
|
enumerable: false, |
|
configurable: false, |
|
}, |
|
[Symbol.toStringTag]: { |
|
value: "Color.HEX4", |
|
enumerable: false, |
|
configurable: true, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
// #endregion HEX4 |
|
|
|
// #region HEX6 |
|
export class HEX6 extends HEX { |
|
readonly [SIZE]: 6 = 6; |
|
|
|
declare readonly length: 7; |
|
|
|
constructor(v: string | HEX) { |
|
v = Color.Format.parseHex(v.toString())?.toHexString(6) ?? v; |
|
super(v); |
|
HEX6.assert(this); |
|
Object.setPrototypeOf(this, HEX6.prototype); |
|
} |
|
|
|
static is(it: unknown): it is HEX6 { |
|
return it instanceof HEX6 || |
|
(typeof it === "string" && (it = it.replace("#", "")).length === 6 && |
|
!isNaN(parseInt(it + "", 16))); |
|
} |
|
|
|
declare static assert: (it: unknown, message?: string) => asserts it is HEX6; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[_brand]: { |
|
get: () => "HEX6", |
|
enumerable: false, |
|
configurable: false, |
|
}, |
|
[Symbol.toStringTag]: { |
|
value: "Color.HEX6", |
|
enumerable: false, |
|
configurable: true, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
// #endregion HEX6 |
|
|
|
// #region HEX8 |
|
export class HEX8 extends HEX { |
|
readonly [SIZE]: 8 = 8; |
|
|
|
declare readonly length: 9; |
|
|
|
constructor(v: string | HEX) { |
|
v = Color.Format.parseHex(v.toString())?.toHexString(8) ?? v; |
|
super(v); |
|
HEX8.assert(this); |
|
Object.setPrototypeOf(this, HEX8.prototype); |
|
} |
|
|
|
static is(it: unknown): it is HEX8 { |
|
return it instanceof HEX8 || |
|
(typeof it === "string" && (it = it.replace("#", "")).length === 8 && |
|
!isNaN(parseInt(it + "", 16))); |
|
} |
|
|
|
declare static assert: (it: unknown, message?: string) => asserts it is HEX8; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[_brand]: { |
|
get: () => "HEX8", |
|
enumerable: false, |
|
configurable: false, |
|
}, |
|
[Symbol.toStringTag]: { |
|
value: "Color.HEX8", |
|
enumerable: false, |
|
configurable: true, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
// #endregion HEX8 |
|
|
|
// #endregion HEX |
|
|
|
// #region HSL |
|
const $HSL = schema("HSL"); |
|
type $HSL = typeof $HSL; |
|
|
|
export class HSL extends Base<$HSL> { |
|
constructor( |
|
public h: number, |
|
public s: number, |
|
public l: number, |
|
public a = 1, |
|
) { |
|
super($HSL); |
|
HSL.assert(this); |
|
Object.setPrototypeOf(this, HSL.prototype); |
|
} |
|
|
|
toString() { |
|
return Color.Format.HSL(this); |
|
} |
|
|
|
declare static is: (it: unknown) => it is HSL; |
|
declare static assert: (it: unknown, message?: string) => asserts it is HSL; |
|
|
|
static toRGB(color: HSL): RGB { |
|
HSL.assert(color); |
|
const { h, s, l, a } = color; |
|
const c = (1 - Math.abs(2 * l - 1)) * s; |
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); |
|
const m = l - c / 2; |
|
let r = 0, g = 0, b = 0; |
|
if (h < 60) [r, g, b] = [c, x, 0]; |
|
if (h >= 60 && h < 120) [r, g, b] = [x, c, 0]; |
|
if (h >= 120 && h < 180) [r, g, b] = [0, c, x]; |
|
if (h >= 180 && h < 240) [r, g, b] = [0, x, c]; |
|
if (h >= 240 && h < 300) [r, g, b] = [x, 0, c]; |
|
if (h >= 300 && h < 360) [r, g, b] = [c, 0, x]; |
|
return new RGB( |
|
Math.round((r + m) & 0xFF), |
|
Math.round((g + m) & 0xFF), |
|
Math.round((b + m) & 0xFF), |
|
a, |
|
); |
|
} |
|
|
|
static fromRGB(rgb: RGB): HSL { |
|
RGB.assert(rgb); |
|
const { r, g, b, a } = rgb; |
|
const max = Math.max(r, g, b); |
|
const min = Math.min(r, g, b); |
|
const l = (min + max) / 2; |
|
const chroma = max - min; |
|
let h = 0, s = 0; |
|
if (chroma > 0) { |
|
s = Math.min(l <= 0.5 ? chroma / (2 * l) : chroma / (2 - (2 * l)), 1); |
|
// deno-fmt-ignore |
|
switch (max) { |
|
case r: h = (g - b) / chroma + (g < b ? 6 : 0); break; |
|
case g: h = (b - r) / chroma + 2; break; |
|
case b: h = (r - g) / chroma + 4; break; |
|
} |
|
h = round(h * 60); |
|
} |
|
|
|
return new HSL(h, s, l, a); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): HSL { |
|
XYZ.assert(xyz); |
|
return HSL.fromRGB(XYZ.toRGB(xyz)); |
|
} |
|
|
|
static toXYZ(hsl: HSL): XYZ { |
|
HSL.assert(hsl); |
|
return XYZ.fromRGB(HSL.toRGB(hsl)); |
|
} |
|
|
|
static toHSV(hsl: HSL): HSV { |
|
HSL.assert(hsl); |
|
return HSV.fromHSL(hsl); |
|
} |
|
|
|
static fromHSV(hsv: HSV): HSL { |
|
HSV.assert(hsv); |
|
return HSV.toHSL(hsv); |
|
} |
|
} |
|
// #endregion HSL |
|
|
|
// #region HSV |
|
const $HSV = schema("HSV"); |
|
type $HSV = typeof $HSV; |
|
|
|
export class HSV extends Base<$HSV> { |
|
constructor( |
|
public h: number, |
|
public s: number, |
|
public v: number, |
|
public a = 1, |
|
) { |
|
super($HSV); |
|
HSV.assert(this); |
|
Object.setPrototypeOf(this, HSV.prototype); |
|
} |
|
|
|
toRGB(): RGB { |
|
return HSV.toRGB(this); |
|
} |
|
|
|
toHSL(): HSL { |
|
return HSV.toHSL(this); |
|
} |
|
|
|
toXYZ(): XYZ { |
|
return HSV.toXYZ(this); |
|
} |
|
|
|
toLAB(): LAB { |
|
return HSV.toLAB(this); |
|
} |
|
|
|
toLCH(): LCH { |
|
return HSV.toLCH(this); |
|
} |
|
|
|
toOKLAB(): OKLAB { |
|
return HSV.toOKLAB(this); |
|
} |
|
|
|
toOKLCH(): OKLCH { |
|
return HSV.toOKLCH(this); |
|
} |
|
|
|
toString(): `hsv(${number} ${number} ${number} / ${number})` { |
|
return `hsv(${this.h} ${this.s} ${this.v} / ${this.a})`; |
|
} |
|
|
|
declare static is: (it: unknown) => it is HSV; |
|
declare static assert: (it: unknown, message?: string) => asserts it is HSV; |
|
|
|
static toRGB<T extends Base>(color: T): RGB { |
|
if (RGB.is(color)) return color; |
|
if (HSV.is(color)) { |
|
const { h, s, v, a } = color; |
|
const c = v * s; |
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); |
|
const m = v - c; |
|
let r = 0, g = 0, b = 0; |
|
if (h < 60) [r, g, b] = [c, x, 0]; |
|
if (h >= 60 && h < 120) [r, g, b] = [x, c, 0]; |
|
if (h >= 120 && h < 180) [r, g, b] = [0, c, x]; |
|
if (h >= 180 && h < 240) [r, g, b] = [0, x, c]; |
|
if (h >= 240 && h < 300) [r, g, b] = [x, 0, c]; |
|
if (h >= 300 && h < 360) [r, g, b] = [c, 0, x]; |
|
return new RGB( |
|
Math.round((r + m) & 0xFF), |
|
Math.round((g + m) & 0xFF), |
|
Math.round((b + m) & 0xFF), |
|
a, |
|
); |
|
} else { |
|
try { |
|
return color.toRGB(); |
|
} catch { |
|
throw new TypeError("Cannot convert to RGB"); |
|
} |
|
} |
|
} |
|
|
|
static fromRGB(rgb: RGB): HSV { |
|
RGB.assert(rgb); |
|
const { r, g, b, a } = rgb; |
|
const max = Math.max(r, g, b); |
|
const min = Math.min(r, g, b); |
|
const v = max; |
|
const chroma = max - min; |
|
const s = max === 0 ? 0 : chroma / max; |
|
let h = 0; |
|
if (chroma > 0) { |
|
// deno-fmt-ignore |
|
switch (max) { |
|
case r: h = (g - b) / chroma + (g < b ? 6 : 0); break; |
|
case g: h = (b - r) / chroma + 2; break; |
|
case b: h = (r - g) / chroma + 4; break; |
|
} |
|
h = round(h * 60); |
|
} |
|
|
|
return new HSV(h, s, v, a); |
|
} |
|
|
|
static fromHSL(hsl: HSL): HSV { |
|
HSL.assert(hsl); |
|
const { h, s, l, a } = hsl; |
|
const v = l + s * Math.min(l, 1 - l); |
|
const s2 = v === 0 ? 0 : 2 * (1 - l / v); |
|
return new HSV(h, s2, v, a); |
|
} |
|
|
|
static toHSL(hsv: HSV): HSL { |
|
HSV.assert(hsv); |
|
const { h, s, v, a } = hsv; |
|
const l = v - v * s / 2; |
|
const s2 = l === 0 || l === 1 ? 0 : (v - l) / Math.min(l, 1 - l); |
|
return new HSL(h, s2, l, a); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): HSV { |
|
XYZ.assert(xyz); |
|
return HSV.fromRGB(XYZ.toRGB(xyz)); |
|
} |
|
|
|
static toXYZ(hsv: HSV): XYZ { |
|
HSV.assert(hsv); |
|
return XYZ.fromRGB(HSV.toRGB(hsv)); |
|
} |
|
} |
|
// #endregion HSV |
|
|
|
// #region HWB |
|
const $HWB = schema("HWB"); |
|
type $HWB = typeof $HWB; |
|
|
|
export class HWB extends Base<$HWB> { |
|
constructor( |
|
public h: number, |
|
public w: number, |
|
public b: number, |
|
public a = 1, |
|
) { |
|
super($HWB); |
|
HWB.assert(this); |
|
Object.setPrototypeOf(this, HWB.prototype); |
|
} |
|
|
|
toString(): `hwb(${number} ${number} ${number} / ${number})` { |
|
return `hwb(${this.h} ${this.w} ${this.b} / ${this.a})`; |
|
} |
|
|
|
declare static is: (it: unknown) => it is HWB; |
|
declare static assert: (it: unknown, message?: string) => asserts it is HWB; |
|
|
|
static toRGB(color: HWB): RGB { |
|
if (RGB.is(color)) return color; |
|
HWB.assert(color); |
|
let { h, w: white, b: black, a = 1 } = color; |
|
h %= 360, h /= 360, white /= 100, black /= 100; |
|
const ratio = white + black; |
|
// normalize greys |
|
if (ratio > 1) white /= ratio, black /= ratio; |
|
const hue = h * 6; |
|
const value = 1 - black; |
|
const intensity = Math.floor(value); |
|
let factor = hue - intensity; |
|
if ((intensity & 1) !== 0) factor = 1 - factor; |
|
const n = white + factor * (value - white); |
|
|
|
let r = 0, g = 0, b = 0; |
|
// deno-fmt-ignore |
|
switch (intensity) { |
|
case +5: [r, g, b] = [value, white, n]; break; |
|
case +4: [r, g, b] = [n, white, value]; break; |
|
case +3: [r, g, b] = [white, n, value]; break; |
|
case +2: [r, g, b] = [white, value, n]; break; |
|
case +1: [r, g, b] = [n, value, white]; break; |
|
default: [r, g, b] = [value, n, white]; break; |
|
} |
|
return new RGB(r * 255, g * 255, b * 255, a); |
|
} |
|
|
|
static fromRGB(rgb: RGB): HWB { |
|
if (HWB.is(rgb)) return rgb; |
|
RGB.assert(rgb); |
|
const { r, g, a = 1 } = rgb; |
|
let { b } = rgb; |
|
const { h } = RGB.toHSL(rgb); |
|
const w = 1 / 255 * Math.min(r, g, b); |
|
b = 1 - 1 / 255 * Math.max(r, g, b); |
|
return new HWB(h, w * 100, b * 100, a); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): HWB { |
|
if (HWB.is(xyz)) return xyz; |
|
XYZ.assert(xyz); |
|
return HWB.fromRGB(XYZ.toRGB(xyz)); |
|
} |
|
|
|
static toXYZ(hwb: HWB): XYZ { |
|
if (XYZ.is(hwb)) return hwb; |
|
HWB.assert(hwb); |
|
return XYZ.fromRGB(HWB.toRGB(hwb)); |
|
} |
|
} |
|
// #endregion HWB |
|
|
|
// #region KEYWORD |
|
const $KEYWORD = schema("KEYWORD"); |
|
type $KEYWORD = typeof $KEYWORD; |
|
|
|
export type Keywords = keyof typeof KEYWORD.names; |
|
|
|
export class KEYWORD extends Base<$KEYWORD> { |
|
constructor(public value: Keywords) { |
|
super($KEYWORD); |
|
KEYWORD.assert(this); |
|
Object.setPrototypeOf(this, KEYWORD.prototype); |
|
} |
|
|
|
toRGB(): RGB { |
|
return KEYWORD.toRGB(this); |
|
} |
|
|
|
toString(): Keywords { |
|
return this.value; |
|
} |
|
|
|
declare static is: (it: unknown) => it is KEYWORD; |
|
declare static assert: ( |
|
it: unknown, |
|
message?: string, |
|
) => asserts it is KEYWORD; |
|
|
|
static get names(): typeof Color.names { |
|
return Color.names; |
|
} |
|
|
|
static find(color: Colors): KEYWORD { |
|
const _color = color; |
|
color = Color.from(color); |
|
const hex = color.toHexString(6); |
|
const names = Object.keys(Color.names) as ColorNames[]; |
|
const exactMatch = names.find((k) => |
|
k === _color || |
|
names2colors[k].replace(/^#/, "").toUpperCase() === |
|
hex.replace(/^#/, "").toUpperCase() |
|
); |
|
if (exactMatch) return new KEYWORD(exactMatch as ColorNames); |
|
const closestName = names.toSorted((a, b) => { |
|
const d1 = Color.distance(color, Color.names[a]); |
|
const d2 = Color.distance(color, Color.names[b]); |
|
return d1 - d2; |
|
}).shift(); |
|
if (!closestName) { |
|
throw new TypeError(`Unresolved keyword for color '${hex}'`); |
|
} |
|
return new KEYWORD(closestName); |
|
} |
|
|
|
static toRGB(color: KEYWORD): RGB { |
|
KEYWORD.assert(color); |
|
const { value } = color; |
|
if (value === "transparent") return new RGB(0, 0, 0, 0); |
|
const rgb = Color.names(value)?.rgb; |
|
if (!rgb) throw new TypeError(`Unknown keyword '${value}'`); |
|
return new RGB(rgb.r, rgb.g, rgb.b, rgb.a ?? 1); |
|
} |
|
|
|
static fromRGB(rgb: RGB): KEYWORD { |
|
RGB.assert(rgb); |
|
return KEYWORD.find(rgb); |
|
} |
|
} |
|
// #endregion KEYWORD |
|
|
|
// #region LAB |
|
const $LAB = schema("LAB"); |
|
type $LAB = typeof $LAB; |
|
|
|
export class LAB extends Base<$LAB> { |
|
constructor( |
|
public l: number, |
|
public a: number, |
|
public b: number, |
|
public alpha = 1, |
|
) { |
|
super($LAB); |
|
LAB.assert(this); |
|
Object.setPrototypeOf(this, LAB.prototype); |
|
const { |
|
l: [l_min, l_max], |
|
a: [a_min, a_max], |
|
b: [b_min, b_max], |
|
alpha: [alpha_min, alpha_max], |
|
} = $LAB; |
|
this.l = Color.clamp(l, l_min, l_max); |
|
this.a = Color.clamp(a, a_min, a_max); |
|
this.b = Color.clamp(b, b_min, b_max); |
|
alpha = alpha > 1 && alpha <= 100 |
|
? alpha / 100 |
|
: alpha > 100 && alpha <= 255 |
|
? alpha / 255 |
|
: alpha; |
|
this.alpha = Color.clamp(alpha, alpha_min, alpha_max); |
|
} |
|
|
|
toRGB(): RGB { |
|
return LAB.toRGB(this); |
|
} |
|
|
|
toLCH(): LCH { |
|
return LAB.toLCH(this); |
|
} |
|
|
|
toOKLAB(): OKLAB { |
|
return LAB.toOKLAB(this); |
|
} |
|
|
|
toOKLCH(): OKLCH { |
|
return LAB.toOKLCH(this); |
|
} |
|
|
|
toString(): `lab(${number} ${number} ${number} / ${number})` { |
|
return `lab(${this.l} ${this.a} ${this.b} / ${this.alpha})`; |
|
} |
|
|
|
declare static is: (it: unknown) => it is LAB; |
|
declare static assert: (it: unknown, message?: string) => asserts it is LAB; |
|
|
|
static toRGB<T extends Base>(color: T): RGB { |
|
if (RGB.is(color)) return color; |
|
if (LAB.is(color)) { |
|
return XYZ.toRGB(LAB.toXYZ(color)); |
|
} else { |
|
try { |
|
return color.toRGB(); |
|
} catch { |
|
throw new TypeError("Cannot convert to RGB"); |
|
} |
|
} |
|
} |
|
|
|
static fromRGB(rgb: RGB): LAB { |
|
RGB.assert(rgb); |
|
return LAB.fromXYZ(XYZ.fromRGB(rgb)); |
|
} |
|
|
|
static toXYZ(lab: LAB, illuminant?: Illuminant): XYZ { |
|
LAB.assert(lab); |
|
const { l, a, b, alpha = 1 } = lab; |
|
const y = (l + 16) / 116; |
|
const x = a / 500 + y; |
|
const z = y - b / 200; |
|
const gamma = (c: number, c3 = c ** 3) => |
|
c3 > 0.008856 ? c3 : (c - 16 / 116) / 7.787; |
|
illuminant ??= Color.illuminant; |
|
let x2 = gamma(x) * illuminant.x; |
|
let y2 = gamma(y) * illuminant.y; |
|
let z2 = gamma(z) * illuminant.z; |
|
x2 /= 100, y2 /= 100, z2 /= 100; |
|
return new XYZ(x2, y2, z2, alpha); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ, illuminant?: Illuminant): LAB { |
|
XYZ.assert(xyz); |
|
let { x, y, z, a: alpha = 1 } = xyz; |
|
x *= 100, y *= 100, z *= 100; |
|
const gamma = (c: number) => |
|
c > 0.008856 ? c ** (1 / 3) : 7.787 * c + 16 / 116; |
|
illuminant ??= Color.illuminant; |
|
const x2 = gamma(x / illuminant.x); |
|
const y2 = gamma(y / illuminant.y); |
|
const z2 = gamma(z / illuminant.z); |
|
const l = 116 * y2 - 16; |
|
const a = 500 * (x2 - y2); |
|
const b = 200 * (y2 - z2); |
|
return new LAB(l, a, b, alpha); |
|
} |
|
|
|
static toLCH(lab: LAB): LCH { |
|
LAB.assert(lab); |
|
return LCH.fromLAB(lab); |
|
} |
|
|
|
static fromLCH(lch: LCH): LAB { |
|
LCH.assert(lch); |
|
return LCH.toLAB(lch); |
|
} |
|
|
|
static fromOKLAB(oklab: OKLAB): LAB { |
|
OKLAB.assert(oklab); |
|
return OKLAB.toLAB(oklab); |
|
} |
|
|
|
static toOKLAB(lab: LAB): OKLAB { |
|
LAB.assert(lab); |
|
return OKLAB.fromLAB(lab); |
|
} |
|
|
|
static fromOKLCH(oklch: OKLCH): LAB { |
|
OKLCH.assert(oklch); |
|
return OKLCH.toLAB(oklch); |
|
} |
|
|
|
static toOKLCH(lab: LAB): OKLCH { |
|
LAB.assert(lab); |
|
return OKLCH.fromLAB(lab); |
|
} |
|
} |
|
// #endregion LAB |
|
|
|
// #region LCH |
|
const $LCH = schema("LCH"); |
|
type $LCH = typeof $LCH; |
|
|
|
export class LCH extends Base<$LCH> { |
|
constructor( |
|
public l: number, |
|
public c: number, |
|
public h: number, |
|
public alpha = 1, |
|
) { |
|
super($LCH); |
|
LCH.assert(this); |
|
Object.setPrototypeOf(this, LCH.prototype); |
|
const { |
|
l: [l_min, l_max], |
|
c: [c_min, c_max], |
|
h: [h_min, h_max], |
|
alpha: [alpha_min, alpha_max], |
|
} = $LCH; |
|
|
|
this.l = Color.clamp(l, l_min, l_max); |
|
this.c = Color.clamp(c, c_min, c_max); |
|
this.h = Color.clamp(h, h_min, h_max); |
|
alpha = alpha > 1 && alpha <= 100 |
|
? alpha / 100 |
|
: alpha > 100 && alpha <= 255 |
|
? alpha / 255 |
|
: alpha; |
|
this.alpha = Color.clamp(alpha, alpha_min, alpha_max); |
|
} |
|
|
|
toLAB(): LAB { |
|
return LCH.toLAB(this); |
|
} |
|
|
|
toString(): `lch(${number} ${number} ${number} / ${number})` { |
|
return `lch(${this.l} ${this.c} ${this.h} / ${this.alpha})`; |
|
} |
|
|
|
declare static is: (it: unknown) => it is LCH; |
|
declare static assert: (it: unknown, message?: string) => asserts it is LCH; |
|
|
|
static toRGB(color: LCH): RGB { |
|
if (RGB.is(color)) return color; |
|
LCH.assert(color); |
|
return LAB.toRGB(LCH.toLAB(color)); |
|
} |
|
|
|
static fromRGB(rgb: RGB): LCH { |
|
return LCH.fromXYZ(RGB.toXYZ(rgb)); |
|
} |
|
|
|
/** |
|
* Convert a color from {@link LCH} to {@link XYZ}. By implementing this and |
|
* the `fromXYZ` methods, you can convert between any two color spaces that |
|
* also implement `toXYZ` and `fromXYZ`. |
|
*/ |
|
static toXYZ(lch: LCH, illuminant?: Illuminant): XYZ { |
|
LCH.assert(lch); |
|
return LAB.toXYZ(LCH.toLAB(lch), illuminant); |
|
} |
|
|
|
/** Convert a color from {@link XYZ} to {@link LCH}, via {@link LAB}. */ |
|
static fromXYZ(xyz: XYZ, illuminant?: Illuminant): LCH { |
|
XYZ.assert(xyz); |
|
return LCH.fromLAB(LAB.fromXYZ(xyz, illuminant)); |
|
} |
|
|
|
/** Convert a color from {@link LAB} to {@link LCH}. */ |
|
static fromLAB(lab: LAB): LCH { |
|
LAB.assert(lab); |
|
const { l, a, b, alpha = 1 } = lab; |
|
const c = Math.sqrt(a * a + b * b); |
|
const r = Math.atan2(b, a); |
|
let h = r * 360 / 2 / Math.PI; |
|
if (h < 0) h += 360; |
|
return new LCH(l, c, h, alpha); |
|
} |
|
|
|
/** Convert a color from {@link LCH} to {@link LAB}. */ |
|
static toLAB(lch: LCH): LAB { |
|
LCH.assert(lch); |
|
const { l, c, h, alpha = 1 } = lch; |
|
const r = h / 360 * 2 * Math.PI; |
|
const a = c * Math.cos(r), b = c * Math.sin(r); |
|
return new LAB(l, a, b, alpha); |
|
} |
|
} |
|
// #endregion LCH |
|
|
|
// #region OKLAB |
|
const $OKLAB = schema("OKLAB"); |
|
type $OKLAB = typeof $OKLAB; |
|
|
|
export class OKLAB extends Base<$OKLAB> { |
|
constructor( |
|
public l: number, |
|
public a: number, |
|
public b: number, |
|
public alpha: number | undefined = 1, |
|
) { |
|
super($OKLAB); |
|
OKLAB.assert(this); |
|
Object.setPrototypeOf(this, OKLAB.prototype); |
|
} |
|
|
|
toString(): `oklab(${number} ${number} ${number} / ${number})` { |
|
return `oklab(${this.l} ${this.a} ${this.b} / ${this.alpha ?? 1})`; |
|
} |
|
|
|
declare static is: (it: unknown) => it is OKLAB; |
|
declare static assert: (it: unknown, message?: string) => asserts it is OKLAB; |
|
|
|
static toRGB(color: OKLAB): RGB { |
|
if (RGB.is(color)) return color; |
|
OKLAB.assert(color); |
|
return XYZ.toRGB(OKLAB.toXYZ(color)); |
|
} |
|
|
|
static fromRGB(rgb: RGB): OKLAB { |
|
return OKLAB.fromXYZ(RGB.toXYZ(rgb)); |
|
} |
|
|
|
static toXYZ(oklab: OKLAB): XYZ { |
|
OKLAB.assert(oklab); |
|
let l = oklab.l * 0.2104542553 + oklab.a * 1.9779984951 + |
|
oklab.b * 0.0259040371; |
|
let m = oklab.l * 0.7936177850 - oklab.a * 1.9952866074 + |
|
oklab.b * 0.1585786374; |
|
let s = oklab.l * -0.0040720468 + oklab.a * 0.0174348723 - |
|
oklab.b * 1.1842546100; |
|
|
|
l = Math.pow(l, 3), m = Math.pow(m, 3), s = Math.pow(s, 3); |
|
const x = l * 4.0767416621 - m * 3.3077115913 + s * 0.2309699292; |
|
const y = l * -1.2684380046 + m * 2.6097574011 - s * 0.3413193965; |
|
const z = l * -0.0041960863 - m * 0.7034186147 + s * 1.7076147010; |
|
|
|
const X = x * 0.4124564 + y * 0.3575761 + z * 0.1804375; |
|
const Y = x * 0.2126729 + y * 0.7151522 + z * 0.0721750; |
|
const Z = x * 0.0193339 + y * 0.1191920 + z * 0.9503041; |
|
|
|
return new XYZ(X, Y, Z, oklab.alpha ?? 1); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): OKLAB { |
|
XYZ.assert(xyz); |
|
|
|
const { x, y, z, a = 1 } = xyz; |
|
|
|
const r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314; |
|
const g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560; |
|
const b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252; |
|
|
|
let l = r * 0.4122214708 + g * 0.5363325363 + b * 0.0514459929; |
|
let m = r * 0.2119034982 + g * 0.6806995451 + b * 0.1073969566; |
|
let s = r * 0.0883024619 + g * 0.2817188376 + b * 0.6299787005; |
|
|
|
l = Math.cbrt(l), m = Math.cbrt(m), s = Math.cbrt(s); |
|
|
|
const oklabL = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s; |
|
const oklabA = 1.9779984951 * l - 1.9952866074 * m + 0.0174348723 * s; |
|
const oklabB = 0.0259040371 * l + 0.1585786374 * m - 1.1842546100 * s; |
|
|
|
return new OKLAB(oklabL, oklabA, oklabB, a); |
|
} |
|
|
|
static toLAB(oklab: OKLAB): LAB { |
|
OKLAB.assert(oklab); |
|
return LAB.fromXYZ(OKLAB.toXYZ(oklab)); |
|
} |
|
|
|
static fromLAB(lab: LAB): OKLAB { |
|
LAB.assert(lab); |
|
return OKLAB.fromXYZ(LAB.toXYZ(lab)); |
|
} |
|
|
|
static toLCH(oklab: OKLAB): LCH { |
|
OKLAB.assert(oklab); |
|
return LCH.fromXYZ(OKLAB.toXYZ(oklab)); |
|
} |
|
|
|
static fromLCH(lch: LCH): OKLAB { |
|
LCH.assert(lch); |
|
return OKLAB.fromXYZ(LCH.toXYZ(lch)); |
|
} |
|
|
|
static toOKLCH(oklab: OKLAB): OKLCH { |
|
OKLAB.assert(oklab); |
|
return OKLCH.fromOKLAB(oklab); |
|
} |
|
|
|
static fromOKLCH(oklch: OKLCH): OKLAB { |
|
OKLCH.assert(oklch); |
|
return OKLCH.toOKLAB(oklch); |
|
} |
|
} |
|
// #endregion OKLAB |
|
|
|
// #region OKLCH |
|
const $OKLCH = schema("OKLCH"); |
|
type $OKLCH = typeof $OKLCH; |
|
|
|
export class OKLCH extends Base<$OKLCH> { |
|
constructor( |
|
public l: number, |
|
public c: number, |
|
public h: number, |
|
public alpha: number | undefined = 1, |
|
) { |
|
super($OKLCH); |
|
OKLCH.assert(this); |
|
Object.setPrototypeOf(this, OKLCH.prototype); |
|
} |
|
|
|
toString(): `oklch(${number} ${number} ${number} / ${number})` { |
|
return `oklch(${this.l} ${this.c} ${this.h} / ${this.alpha ?? 1})`; |
|
} |
|
|
|
declare static is: (it: unknown) => it is OKLCH; |
|
declare static assert: (it: unknown, message?: string) => asserts it is OKLCH; |
|
|
|
static toRGB(color: OKLCH): RGB { |
|
if (RGB.is(color)) return color; |
|
OKLCH.assert(color); |
|
return LAB.toRGB(OKLCH.toLAB(color)); |
|
} |
|
|
|
static fromRGB(rgb: RGB): OKLCH { |
|
return OKLCH.fromXYZ(RGB.toXYZ(rgb)); |
|
} |
|
|
|
static toXYZ(oklch: OKLCH): XYZ { |
|
OKLCH.assert(oklch); |
|
return OKLAB.toXYZ(OKLCH.toOKLAB(oklch)); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): OKLCH { |
|
XYZ.assert(xyz); |
|
return OKLCH.fromOKLAB(OKLAB.fromXYZ(xyz)); |
|
} |
|
|
|
static toLAB(oklch: OKLCH): LAB { |
|
OKLCH.assert(oklch); |
|
return LAB.fromXYZ(OKLCH.toXYZ(oklch)); |
|
} |
|
|
|
static fromLAB(lab: LAB): OKLCH { |
|
LAB.assert(lab); |
|
return OKLCH.fromXYZ(LAB.toXYZ(lab)); |
|
} |
|
|
|
static toLCH(oklch: OKLCH): LCH { |
|
OKLCH.assert(oklch); |
|
return LCH.fromXYZ(OKLCH.toXYZ(oklch)); |
|
} |
|
|
|
static fromLCH(lch: LCH): OKLCH { |
|
LCH.assert(lch); |
|
return OKLCH.fromXYZ(LCH.toXYZ(lch)); |
|
} |
|
|
|
static toOKLAB(oklch: OKLCH): OKLAB { |
|
OKLCH.assert(oklch); |
|
const oklabL = oklch.l; |
|
const oklabA = oklch.c * Math.cos(oklch.h); |
|
const oklabB = oklch.c * Math.sin(oklch.h); |
|
|
|
return new OKLAB(oklabL, oklabA, oklabB, oklch.alpha ?? 1); |
|
} |
|
|
|
static fromOKLAB(oklab: OKLAB): OKLCH { |
|
OKLAB.assert(oklab); |
|
|
|
const oklchL = oklab.l; |
|
const oklchC = Math.sqrt(oklab.a * oklab.a + oklab.b * oklab.b); |
|
let oklchH = Math.atan2(oklab.b, oklab.a); |
|
|
|
if (oklchH < 0) oklchH += 2 * Math.PI; |
|
|
|
return new OKLCH(oklchL, oklchC, oklchH, oklab.alpha ?? 1); |
|
} |
|
} |
|
// #endregion OKLCH |
|
|
|
// #region RGB |
|
const $RGB = schema("RGB"); |
|
type $RGB = typeof $RGB; |
|
|
|
export class RGB extends Base<$RGB> { |
|
constructor( |
|
public r: number, |
|
public g: number, |
|
public b: number, |
|
public a = 1, |
|
) { |
|
super($RGB); |
|
RGB.assert(this); |
|
Object.setPrototypeOf(this, RGB.prototype); |
|
} |
|
|
|
toNumber(): number { |
|
return (this.r << 24) | (this.g << 16) | (this.b << 8) | this.a; |
|
} |
|
|
|
toRGB(): RGB { |
|
return RGB.toRGB(this); |
|
} |
|
|
|
toString() { |
|
return Color.Format.RGB(this); |
|
} |
|
|
|
declare static is: (it: unknown) => it is RGB; |
|
declare static assert: (it: unknown, message?: string) => asserts it is RGB; |
|
|
|
static toRGB(color: unknown): RGB { |
|
if (RGB.is(color)) return new RGB(color.r, color.g, color.b, color.a); |
|
if (color instanceof Base) return color.toRGB(); |
|
if (typeof color === "number") { |
|
const r = (color >> 24) & 0xff; |
|
const g = (color >> 16) & 0xff; |
|
const b = (color >> 8) & 0xff; |
|
const a = color & 0xff; |
|
return new RGB(r, g, b, a); |
|
} |
|
throw new TypeError("Cannot convert to RGB"); |
|
} |
|
|
|
static fromHex(hex: HEX | string): RGB { |
|
if (typeof hex === "string") hex = new HEX(hex); |
|
HEX.assert(hex); |
|
return RGB.fromHexString(hex.toString()); |
|
} |
|
|
|
static fromHexString(hex: string): RGB { |
|
HEX.assert(hex); |
|
let match = hex.match(Color.RegExp.HEX8); |
|
let isHex3 = false; |
|
if (!match) { |
|
match = hex.match(Color.RegExp.HEX4); |
|
isHex3 = true; |
|
} |
|
if (!match) return RGB.fromHexString("#000000"); |
|
const { r, g, b, a } = match.groups as unknown as FormatSchema<$RGB>; |
|
|
|
const red = isHex3 ? `${r}${r}` : r + ""; |
|
const green = isHex3 ? `${g}${g}` : g + ""; |
|
const blue = isHex3 ? `${b}${b}` : b + ""; |
|
const alpha = a ? isHex3 ? `${a}${a}` : a + "" : "FF"; |
|
|
|
return new RGB( |
|
parseInt(red, 16), |
|
parseInt(green, 16), |
|
parseInt(blue, 16), |
|
parseInt(alpha, 16) / 255, |
|
); |
|
} |
|
|
|
static toHexString(rgba: RGB): string { |
|
const { r, g, b, a = 1 } = rgba; |
|
const red = r.toString(16).padStart(2, "0"); |
|
const green = g.toString(16).padStart(2, "0"); |
|
const blue = b.toString(16).padStart(2, "0"); |
|
const alpha = round(a * 255).toString(16).padStart(2, "0"); |
|
return `#${red}${green}${blue}${a === 1 ? "" : alpha}`; |
|
} |
|
|
|
static fromRGB(rgb: RGB): RGB { |
|
RGB.assert(rgb); |
|
return rgb; |
|
} |
|
|
|
static fromHSL(hsl: HSL): RGB { |
|
HSL.assert(hsl); |
|
return HSL.toRGB(hsl); |
|
} |
|
|
|
static toHSL(rgb: RGB): HSL { |
|
RGB.assert(rgb); |
|
return HSL.fromRGB(rgb); |
|
} |
|
|
|
static fromHSV(hsv: HSV): RGB { |
|
HSV.assert(hsv); |
|
return HSV.toRGB(hsv); |
|
} |
|
|
|
static toHSV(rgb: RGB): HSV { |
|
RGB.assert(rgb); |
|
return HSV.fromRGB(rgb); |
|
} |
|
|
|
static fromXYZ(xyz: XYZ): RGB { |
|
XYZ.assert(xyz); |
|
return XYZ.toRGB(xyz); |
|
} |
|
|
|
static toXYZ(rgb: RGB): XYZ { |
|
RGB.assert(rgb); |
|
return XYZ.fromRGB(rgb); |
|
} |
|
|
|
static fromLAB(lab: LAB): RGB { |
|
LAB.assert(lab); |
|
return LAB.toRGB(lab); |
|
} |
|
|
|
static toLAB(rgb: RGB): LAB { |
|
RGB.assert(rgb); |
|
return LAB.fromRGB(rgb); |
|
} |
|
|
|
static fromLCH(lch: LCH): RGB { |
|
LCH.assert(lch); |
|
return LCH.toRGB(lch); |
|
} |
|
|
|
static toLCH(rgb: RGB): LCH { |
|
RGB.assert(rgb); |
|
return LCH.fromRGB(rgb); |
|
} |
|
} |
|
|
|
extendBase(RGB, "RGB", $RGB); |
|
// #endregion RGB |
|
|
|
// #region XYZ |
|
const $XYZ = schema("XYZ"); |
|
type $XYZ = typeof $XYZ; |
|
|
|
export class XYZ extends Base<$XYZ> { |
|
constructor( |
|
public x: number, |
|
public y: number, |
|
public z: number, |
|
public a = 1, |
|
) { |
|
super($XYZ); |
|
XYZ.assert(this); |
|
Object.setPrototypeOf(this, XYZ.prototype); |
|
} |
|
|
|
toRGB(): RGB { |
|
return XYZ.toRGB(this); |
|
} |
|
|
|
toLAB(): LAB { |
|
return XYZ.toLAB(this); |
|
} |
|
|
|
toLCH(): LCH { |
|
return XYZ.toLCH(this); |
|
} |
|
|
|
toOKLAB(): OKLAB { |
|
return XYZ.toOKLAB(this); |
|
} |
|
|
|
toOKLCH(): OKLCH { |
|
return XYZ.toOKLCH(this); |
|
} |
|
|
|
toHSL(): HSL { |
|
return XYZ.toHSL(this); |
|
} |
|
|
|
toHSV(): HSV { |
|
return XYZ.toHSV(this); |
|
} |
|
|
|
toString(): `xyz(${number} ${number} ${number} / ${number})` { |
|
return `xyz(${this.x} ${this.y} ${this.z} / ${this.a})`; |
|
} |
|
|
|
declare static is: (it: unknown) => it is XYZ; |
|
declare static assert: (it: unknown, message?: string) => asserts it is XYZ; |
|
declare static equals: (a: XYZ, b: XYZ) => boolean; |
|
|
|
static toRGB<T extends XYZ>(color: T): RGB { |
|
if (RGB.is(color)) return color; |
|
XYZ.assert(color); |
|
const { x, y, z, a = 1 } = color; |
|
const r = x * 3.2406 + y * -1.5372 + z * -0.4986; |
|
const g = x * -0.9689 + y * 1.8758 + z * 0.0415; |
|
const b = x * 0.0557 + y * -0.2040 + z * 1.0570; |
|
const gamma = (c: number) => |
|
c > 0.0031308 ? 1.055 * c ** (1 / 2.4) - 0.055 : 12.92 * c; |
|
const clamp = (c: number) => Math.min(Math.max(c, 0), 1); |
|
return new RGB( |
|
Math.ceil(clamp(gamma(r)) * 255), |
|
Math.ceil(clamp(gamma(g)) * 255), |
|
Math.ceil(clamp(gamma(b)) * 255), |
|
a, |
|
); |
|
} |
|
|
|
static fromRGB(rgb: RGB): XYZ { |
|
RGB.assert(rgb); |
|
const { r, g, b, a = 1 } = rgb; |
|
const gamma = (c: number) => |
|
c > 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92; |
|
const clamp = (c: number) => Math.min(Math.max(c, 0), 1); |
|
const r2 = gamma(r / 255), g2 = gamma(g / 255), b2 = gamma(b / 255); |
|
const x = clamp(r2 * 0.4124 + g2 * 0.3576 + b2 * 0.1805); |
|
const y = clamp(r2 * 0.2126 + g2 * 0.7152 + b2 * 0.0722); |
|
const z = clamp(r2 * 0.0193 + g2 * 0.1192 + b2 * 0.9505); |
|
return new XYZ(x, y, z, a); |
|
} |
|
|
|
static fromLAB(lab: LAB, illuminant?: Color.Illuminant): XYZ { |
|
LAB.assert(lab); |
|
return Color.Convert.LABtoXYZ(lab, illuminant); |
|
} |
|
|
|
static toLAB(xyz: XYZ, illuminant?: Color.Illuminant): LAB { |
|
XYZ.assert(xyz); |
|
return Color.Convert.XYZtoLAB(xyz, illuminant); |
|
} |
|
|
|
// static toRGB<T extends XYZ>(color: T): RGB { |
|
// if (RGB.is(color)) return color; |
|
// XYZ.assert(color); |
|
// const { x, y, z, a = 1 } = color; |
|
// let r = x * 3.2406 + y * -1.5372 + z * -0.4986; |
|
// let g = x * -0.9689 + y * 1.8758 + z * 0.0415; |
|
// let b = x * 0.0557 + y * -0.204 + z * 1.057; |
|
// r = r > 31308e-7 ? 1.055 * r ** (1 / 2.4) - 0.055 : r * 12.92; |
|
// g = g > 31308e-7 ? 1.055 * g ** (1 / 2.4) - 0.055 : g * 12.92; |
|
// b = b > 31308e-7 ? 1.055 * b ** (1 / 2.4) - 0.055 : b * 12.92; |
|
// r = Math.min(Math.max(0, r), 1); |
|
// g = Math.min(Math.max(0, g), 1); |
|
// b = Math.min(Math.max(0, b), 1); |
|
// return new RGB(r * 255, g * 255, b * 255, a); |
|
// } |
|
|
|
// static fromRGB(rgb: RGB): XYZ { |
|
// RGB.assert(rgb); |
|
// const { r, g, b, a = 1 } = rgb; |
|
// const gamma = (c: number) => |
|
// c > 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92; |
|
// const clamp = (c: number) => Math.min(Math.max(c, 0), 1); |
|
// const r2 = gamma(r / 255), g2 = gamma(g / 255), b2 = gamma(b / 255); |
|
// const x = clamp(r2 * 0.4124 + g2 * 0.3576 + b2 * 0.1805); |
|
// const y = clamp(r2 * 0.2126 + g2 * 0.7152 + b2 * 0.0722); |
|
// const z = clamp(r2 * 0.0193 + g2 * 0.1192 + b2 * 0.9505); |
|
// return new XYZ(x, y, z, a); |
|
// } |
|
|
|
// static fromLAB(lab: LAB, illuminant?: Color.Illuminant): XYZ { |
|
// LAB.assert(lab); |
|
// illuminant ??= Color.illuminant; |
|
// const { l, a, b, alpha = 1 } = lab; |
|
// let y = (l + 16) / 116; |
|
// let x = a / 500 + y; |
|
// let z = y - b / 200; |
|
// const y2 = y ** 3, x2 = x ** 3, z2 = z ** 3; |
|
// y = y2 > 8856e-6 ? y2 : (y - 16 / 116) / 7.787; |
|
// x = x2 > 8856e-6 ? x2 : (x - 16 / 116) / 7.787; |
|
// z = z2 > 8856e-6 ? z2 : (z - 16 / 116) / 7.787; |
|
// x *= illuminant.x, y *= illuminant.y, z *= illuminant.z; |
|
// x /= 100, y /= 100, z /= 100; |
|
// return new XYZ(x, y, z, alpha); |
|
// } |
|
|
|
// static toLAB(xyz: XYZ, illuminant?: Color.Illuminant): LAB { |
|
// XYZ.assert(xyz); |
|
// illuminant ??= Color.illuminant; |
|
// let { x, y, z, a: alpha = 1 } = xyz; |
|
// x *= 100, y *= 100, z *= 100; |
|
// x /= illuminant.x, y /= illuminant.y, z /= illuminant.z; |
|
// x = x > 8856e-6 ? x ** (1 / 3) : 7.787 * x + 16 / 116; |
|
// y = y > 8856e-6 ? y ** (1 / 3) : 7.787 * y + 16 / 116; |
|
// z = z > 8856e-6 ? z ** (1 / 3) : 7.787 * z + 16 / 116; |
|
// const l = 116 * y - 16, a = 500 * (x - y), b = 200 * (y - z); |
|
// return new LAB(l, a, b, alpha); |
|
// } |
|
} |
|
// #endregion XYZ |
|
|
|
// #region Color |
|
export interface Ansi24bitOptions { |
|
mode?: "foreground" | "background" | "decoration"; |
|
bold?: boolean; |
|
underline?: boolean; |
|
italic?: boolean; |
|
dim?: boolean; |
|
invert?: boolean; |
|
} |
|
|
|
/** |
|
* Represents an undefined or unused argument of a function. Used in partial |
|
* application as an argument placeholder (as {@linkcode Color.undefined}). |
|
*/ |
|
const Undefined: unique symbol = Symbol("undefined"); |
|
type Undefined = typeof Undefined; |
|
|
|
declare namespace spaces { |
|
export { |
|
ANSI, |
|
ANSI16M, |
|
ANSI256, |
|
APPLE, |
|
CMYK, |
|
GRAY, |
|
HCG, |
|
HEX, |
|
HEX3, |
|
HEX4, |
|
HEX6, |
|
HEX8, |
|
HSL, |
|
HSV, |
|
HWB, |
|
KEYWORD, |
|
LAB, |
|
LCH, |
|
OKLAB, |
|
OKLCH, |
|
RGB, |
|
XYZ, |
|
}; |
|
} |
|
namespace spaces { |
|
spaces.ANSI = ANSI; |
|
spaces.ANSI16M = ANSI16M; |
|
spaces.ANSI256 = ANSI256; |
|
spaces.APPLE = APPLE; |
|
spaces.CMYK = CMYK; |
|
spaces.GRAY = GRAY; |
|
spaces.HCG = HCG; |
|
spaces.HEX = HEX; |
|
spaces.HEX3 = HEX3; |
|
spaces.HEX4 = HEX4; |
|
spaces.HEX6 = HEX6; |
|
spaces.HEX8 = HEX8; |
|
spaces.HSL = HSL; |
|
spaces.HSV = HSV; |
|
spaces.HWB = HWB; |
|
spaces.KEYWORD = KEYWORD; |
|
spaces.LAB = LAB; |
|
spaces.LCH = LCH; |
|
spaces.OKLAB = OKLAB; |
|
spaces.OKLCH = OKLCH; |
|
spaces.RGB = RGB; |
|
spaces.XYZ = XYZ; |
|
} |
|
|
|
extendBase(spaces.ANSI, "ANSI", $ANSI); |
|
extendBase(spaces.ANSI16M, "ANSI16M", $ANSI16M); |
|
extendBase(spaces.ANSI256, "ANSI256", $ANSI256); |
|
extendBase(spaces.APPLE, "APPLE", $APPLE); |
|
extendBase(spaces.CMYK, "CMYK", $CMYK); |
|
extendBase(spaces.GRAY, "GRAY", $GRAY); |
|
extendBase(spaces.HCG, "HCG", $HCG); |
|
extendBase(spaces.HSL, "HSL", $HSL); |
|
extendBase(spaces.HSV, "HSV", $HSV); |
|
extendBase(spaces.HWB, "HWB", $HWB); |
|
extendBase(spaces.KEYWORD, "KEYWORD", $KEYWORD); |
|
extendBase(spaces.LAB, "LAB", $LAB); |
|
extendBase(spaces.LCH, "LCH", $LCH); |
|
extendBase(spaces.OKLAB, "OKLAB", $OKLAB); |
|
extendBase(spaces.OKLCH, "OKLCH", $OKLCH); |
|
extendBase(spaces.RGB, "RGB", $RGB); |
|
extendBase(spaces.XYZ, "XYZ", $XYZ); |
|
|
|
type spaces = typeof spaces; |
|
|
|
export class Color { |
|
/** Create a new {@link Color} from a {@link Color.names|CSS color name}. */ |
|
constructor(name: keyof typeof Color.names); |
|
/** Create a new {@link Color} from a {@link HEX} color or hex string. */ |
|
constructor(hex: string | HEX); |
|
/** Create a new {@link Color} from an {@link ANSI} color. */ |
|
constructor(ansi: ANSI); |
|
/** Create a new {@link Color} from an {@link ANSI16M} color. */ |
|
constructor(ansi16m: ANSI16M); |
|
/** Create a new {@link Color} from an {@link ANSI256} color. */ |
|
constructor(ansi256: ANSI256); |
|
/** Create a new {@link Color} from a {@link APPLE} color. */ |
|
constructor(apple: APPLE); |
|
/** Create a new {@link Color} from a {@link CMYK} color. */ |
|
constructor(cmyk: CMYK); |
|
/** Create a new {@link Color} from another {@link Color} instance. */ |
|
constructor(color: Color); |
|
/** Create a new {@link Color} from a {@link GRAY} color. */ |
|
constructor(gray: GRAY); |
|
/** Create a new {@link Color} from an {@link HCG} color. */ |
|
constructor(hcg: HCG); |
|
/** Create a new {@link Color} from an {@link HSL} color. */ |
|
constructor(hsl: HSL); |
|
/** Create a new {@link Color} from an {@link HSV} color. */ |
|
constructor(hsv: HSV); |
|
/** Create a new {@link Color} from an {@link HWB} color. */ |
|
constructor(hwb: HWB); |
|
/** Create a new {@link Color} from a {@link LAB} color. */ |
|
constructor(lab: LAB); |
|
/** Create a new {@link Color} from an {@link LCH} color. */ |
|
constructor(lch: LCH); |
|
/** Create a new {@link Color} from a {@link OKLAB} color. */ |
|
constructor(oklab: OKLAB); |
|
/** Create a new {@link Color} from an {@link OKLCH} color. */ |
|
constructor(oklch: OKLCH); |
|
/** Create a new {@link Color} from an {@link RGB} color. */ |
|
constructor(rgb: RGB); |
|
/** Create a new {@link Color} from an {@link XYZ} color. */ |
|
constructor(xyz: XYZ); |
|
/** Create a new {@link Color} from raw numeric RGB(A) values. */ |
|
constructor(r: number, g: number, b: number, a?: number); |
|
/** Create a new {@link Color}. */ |
|
constructor(color: Colors); |
|
constructor(arg: unknown, g?: number, b?: number, a = 1) { |
|
const expected = |
|
`[Color] constructor received invalid argument type. The single-argument constructor overloads expect either a CSS color name, hexadecimal color string / number, a color space string such as \`rgb(60, 90, 120)\`, an existing instance of the Color class, or a color space object such as Color.RGB.`; |
|
let r = 0; |
|
if (arguments.length === 1) { |
|
if (!arg) { |
|
throw new TypeError(`${expected} Received '${arg}' (${typeof arg}).`); |
|
} else if (arg instanceof Color) { |
|
({ r, g, b, a } = (arg as Color).rgba); |
|
this.rgba = new RGB(r, g, b, a); |
|
} else if (arg instanceof RGB) { |
|
this.rgba = arg; |
|
} else if (arg instanceof ANSI) { |
|
this.rgba = ANSI.toRGB(arg); |
|
} else if (arg instanceof ANSI16M) { |
|
this.rgba = ANSI16M.toRGB(arg); |
|
} else if (arg instanceof ANSI256) { |
|
this.rgba = ANSI256.toRGB(arg); |
|
} else if (arg instanceof APPLE) { |
|
this.rgba = APPLE.toRGB(arg); |
|
} else if (arg instanceof CMYK) { |
|
this.rgba = CMYK.toRGB(arg); |
|
} else if (arg instanceof GRAY) { |
|
this.rgba = GRAY.toRGB(arg); |
|
} else if (arg instanceof HCG) { |
|
this.rgba = HCG.toRGB(arg); |
|
} else if (arg instanceof HEX3) { |
|
this.rgba = RGB.fromHex(arg.toHex6String()); |
|
} else if (arg instanceof HEX4) { |
|
this.rgba = RGB.fromHex(arg.toHex8String()); |
|
} else if (arg instanceof HEX6) { |
|
this.rgba = RGB.fromHex(arg.toString()); |
|
} else if (arg instanceof HEX8) { |
|
this.rgba = RGB.fromHex(arg.toString()); |
|
} else if (arg instanceof HEX) { |
|
this.rgba = RGB.fromHex(arg.toString()); |
|
} else if (arg instanceof HSL) { |
|
this.rgba = HSL.toRGB(arg); |
|
} else if (arg instanceof HSV) { |
|
this.rgba = HSV.toRGB(arg); |
|
} else if (arg instanceof HWB) { |
|
this.rgba = HWB.toRGB(arg); |
|
} else if (arg instanceof LAB) { |
|
this.rgba = LAB.toRGB(arg); |
|
} else if (arg instanceof LCH) { |
|
this.rgba = LCH.toRGB(arg); |
|
} else if (arg instanceof OKLAB) { |
|
this.rgba = OKLAB.toRGB(arg); |
|
} else if (arg instanceof OKLCH) { |
|
this.rgba = OKLCH.toRGB(arg); |
|
} else if (arg instanceof XYZ) { |
|
this.rgba = XYZ.toRGB(arg); |
|
} else if (typeof arg === "string") { |
|
if (arg in Color.names) { |
|
const color = Color.names[arg as keyof typeof Color.names]; |
|
this.rgba = color.rgba; |
|
} else if (HEX.is(arg)) { |
|
this.rgba = RGB.fromHex(arg.toString()); |
|
} else if (arg.startsWith("#")) { |
|
this.rgba = RGB.fromHex(arg); |
|
} else if (Color.RegExp.RGB.test(arg)) { |
|
const { r, g, b, a } = Color.RegExp.RGB.exec(arg)!.groups!; |
|
this.rgba = new RGB(+r, +g, +b, +a); |
|
} else if (Color.RegExp.HSL.test(arg)) { |
|
const { h, s, l, a } = Color.RegExp.HSL.exec(arg)!.groups!; |
|
this.rgba = HSL.toRGB(new HSL(+h, +s, +l, +a)); |
|
} else if (Color.RegExp.HSV.test(arg)) { |
|
const { h, s, v, a } = Color.RegExp.HSV.exec(arg)!.groups!; |
|
this.rgba = HSV.toRGB(new HSV(+h, +s, +v, +a)); |
|
} else if (Color.RegExp.XYZ.test(arg)) { |
|
const { x, y, z, a } = Color.RegExp.XYZ.exec(arg)!.groups!; |
|
this.rgba = XYZ.toRGB(new XYZ(+x, +y, +z, +a)); |
|
} else if (Color.RegExp.LAB.test(arg)) { |
|
const { l, a, b, alpha } = Color.RegExp.LAB.exec(arg)!.groups!; |
|
this.rgba = LAB.toRGB(new LAB(+l, +a, +b, +alpha)); |
|
// } else if (Color.RegExp.LCH.test(arg)) { |
|
// const { l, c, h, alpha } = Color.RegExp.LCH.exec(arg)!.groups!; |
|
// this.rgba = LCH.toRGB(new LCH(+l, +c, +h, +alpha)); |
|
} else if (Color.RegExp.HWB.test(arg)) { |
|
const { h, w, b, a } = Color.RegExp.HWB.exec(arg)!.groups!; |
|
this.rgba = HWB.toRGB(new HWB(+h, +w, +b, +a)); |
|
} else if (Color.RegExp.CMYK.test(arg)) { |
|
const { c, m, y, k } = Color.RegExp.CMYK.exec(arg)!.groups!; |
|
this.rgba = CMYK.toRGB(new CMYK(+c, +m, +y, +k)); |
|
} |
|
} else { |
|
throw new TypeError( |
|
`${expected} Received '${arg}' (${typeof arg}, ${ |
|
arg?.constructor?.name ?? "unknown constructor" |
|
}).`, |
|
); |
|
} |
|
} else if (arguments.length >= 3 && arguments.length <= 4) { |
|
const args = [...arguments].filter((a): a is number => |
|
typeof a === "number" |
|
) as [r: number, g: number, b: number, a?: number]; |
|
if (args.length !== 3 && args.length !== 4) { |
|
throw new Error( |
|
`[Color] constructor received an invalid number of arguments. The available constructor overloads expect either 1, 3, or 4 arguments, but ${ |
|
args.length < 3 ? "only" : "instead" |
|
} received ${args.length}.`, |
|
); |
|
} |
|
[r, g, b, a = 1] = args; |
|
this.rgba = new RGB(r, g, b, a); |
|
} |
|
|
|
if (!this.rgba || !RGB.is(this.rgba)) { |
|
throw new TypeError( |
|
`${expected} Received '${arg}' (${typeof arg}, ${ |
|
(arg as any)?.constructor?.name |
|
}).`, |
|
); |
|
} |
|
} |
|
|
|
#rgba!: RGB; |
|
|
|
/** The {@link RGB} representation of this {@link Color}. */ |
|
get rgba(): RGB { |
|
return this.#rgba; |
|
} |
|
|
|
set rgba(value: RGB | { r: number; g: number; b: number; a?: number }) { |
|
if (!RGB.is(value)) { |
|
throw new TypeError( |
|
`[Color.rgba] expected an RGB or RGBA object, but received '${value}' (${typeof value}, ${ |
|
(value as any).constructor.name |
|
}).`, |
|
); |
|
} |
|
const current = this.#rgba; |
|
const { r, g, b, a = 1 } = value; |
|
const next = new RGB(r, g, b, a); |
|
if (!current || !RGB.equals(current, next)) { |
|
clearCache(this); |
|
this.#rgba = next; |
|
} |
|
} |
|
|
|
/** The {@link RGB} representation of this {@link Color}. */ |
|
get rgb(): RGB { |
|
return this.rgba; |
|
} |
|
|
|
set rgb(rgb: RGB | { r: number; g: number; b: number; a?: number }) { |
|
this.rgba = rgb; |
|
} |
|
|
|
/** The {@link APPLE} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get apple(): APPLE { |
|
return APPLE.fromRGB(this.rgba); |
|
} |
|
|
|
set apple(apple: APPLE | { r16: number; g16: number; b16: number }) { |
|
this.rgba = APPLE.toRGB(new APPLE(apple.r16, apple.g16, apple.b16)); |
|
} |
|
|
|
/** The {@link ANSI} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get ansi(): ANSI { |
|
return this.toANSI(); |
|
} |
|
|
|
set ansi(ansi: ANSI | { value: number }) { |
|
this.rgba = ANSI.toRGB(new ANSI(ansi.value)); |
|
} |
|
|
|
/** The {@link ANSI16M} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get ansi16m(): ANSI16M { |
|
return ANSI16M.fromRGB(this.rgba); |
|
} |
|
|
|
set ansi16m(ansi16m: ANSI16M | { r: number; g: number; b: number }) { |
|
this.rgba = ANSI16M.toRGB( |
|
new ANSI16M(ansi16m.r, ansi16m.g, ansi16m.b), |
|
); |
|
} |
|
|
|
/** The {@link ANSI256} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get ansi256(): ANSI256 { |
|
return ANSI256.fromRGB(this.rgba); |
|
} |
|
|
|
set ansi256(ansi256: ANSI256 | { value: number }) { |
|
this.rgba = ANSI256.toRGB(new ANSI256(ansi256.value)); |
|
} |
|
|
|
/** The {@link GRAY} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get gray(): GRAY { |
|
return GRAY.fromRGB(this.rgba); |
|
} |
|
|
|
set gray(gray: GRAY | { g: number; a?: number }) { |
|
this.rgba = GRAY.toRGB(new GRAY(gray.g, gray.a ?? 1)); |
|
} |
|
|
|
/** The {@link HSL} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get hsla(): HSL { |
|
return HSL.fromRGB(this.rgba); |
|
} |
|
|
|
set hsla(hsla: HSL | { h: number; s: number; l: number; a?: number }) { |
|
this.rgba = HSL.toRGB(new HSL(hsla.h, hsla.s, hsla.l, hsla.a ?? 1)); |
|
} |
|
|
|
/** The {@link HSL} representation of this {@link Color}. */ |
|
get hsl(): HSL { |
|
return this.hsla; |
|
} |
|
|
|
set hsl(hsl: HSL | { h: number; s: number; l: number; a?: number }) { |
|
this.hsla = hsl; |
|
} |
|
|
|
/** |
|
* The {@link HSV} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get hsva(): HSV { |
|
return HSV.fromRGB(this.rgba); |
|
} |
|
|
|
set hsva(hsva: HSV | { h: number; s: number; v: number; a?: number }) { |
|
this.rgba = HSV.toRGB(new HSV(hsva.h, hsva.s, hsva.v, hsva.a ?? 1)); |
|
} |
|
|
|
/** |
|
* The {@link HSV} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
get hsv(): HSV { |
|
return this.hsva; |
|
} |
|
|
|
set hsv(hsv: HSV | { h: number; s: number; v: number; a?: number }) { |
|
this.hsva = hsv; |
|
} |
|
|
|
/** |
|
* The {@link XYZ} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get xyz(): XYZ { |
|
return XYZ.fromRGB(this.rgba); |
|
} |
|
|
|
set xyz(xyz: XYZ | { x: number; y: number; z: number; a?: number }) { |
|
this.rgba = XYZ.toRGB(new XYZ(xyz.x, xyz.y, xyz.z, xyz.a ?? 1)); |
|
} |
|
|
|
/** |
|
* The {@link LAB} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get lab(): LAB { |
|
return LAB.fromRGB(this.rgba); |
|
} |
|
|
|
set lab(lab: LAB | { l: number; a: number; b: number; alpha?: number }) { |
|
this.rgba = LAB.toRGB(new LAB(lab.l, lab.a, lab.b, lab.alpha ?? 1)); |
|
} |
|
|
|
/** |
|
* The {@link LCH} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get lch(): LCH { |
|
return LCH.fromRGB(this.rgba); |
|
} |
|
|
|
set lch(lch: LCH | { l: number; c: number; h: number; alpha?: number }) { |
|
this.rgba = LCH.toRGB(new LCH(lch.l, lch.c, lch.h, lch.alpha ?? 1)); |
|
} |
|
|
|
/** |
|
* The {@link HCG} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get hcg(): HCG { |
|
return HCG.fromRGB(this.rgba); |
|
} |
|
|
|
set hcg(hcg: HCG | { h: number; c: number; g: number; a?: number }) { |
|
this.rgba = HCG.toRGB(new HCG(hcg.h, hcg.c, hcg.g, hcg.a ?? 1)); |
|
} |
|
|
|
/** |
|
* The {@link HWB} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get hwb(): HWB { |
|
return HWB.fromRGB(this.rgba); |
|
} |
|
|
|
set hwb(hwb: HWB | { h: number; w: number; b: number; a?: number }) { |
|
this.rgba = HWB.toRGB(new HWB(hwb.h, hwb.w, hwb.b, hwb.a ?? 1)); |
|
} |
|
|
|
/** |
|
* The {@link CMYK} representation of this {@link Color}. This value is |
|
* cached based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get cmyk(): CMYK { |
|
return CMYK.fromRGB(this.rgba); |
|
} |
|
|
|
set cmyk(cmyk: CMYK | { c: number; m: number; y: number; k: number }) { |
|
this.rgba = CMYK.toRGB(new CMYK(cmyk.c, cmyk.m, cmyk.y, cmyk.k)); |
|
} |
|
|
|
/** |
|
* The {@link HEX} representation of this {@link Color}. This value is cached |
|
* based on the current value of the {@link Color.rgba} property. */ |
|
@memoize("rgba") |
|
get hex(): HEX { |
|
return this.rgba.toHEX(); |
|
} |
|
|
|
set hex(hex: HEX | string) { |
|
this.rgba = RGB.fromHex(hex); |
|
} |
|
|
|
/** The {@link HEX3} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get hex3(): HEX3 { |
|
return this.rgba.toHEX3(); |
|
} |
|
|
|
set hex3(hex3: HEX3 | string) { |
|
this.rgba = RGB.fromHex(hex3); |
|
} |
|
|
|
/** The {@link HEX4} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get hex4(): HEX4 { |
|
return this.rgba.toHEX4(); |
|
} |
|
|
|
set hex4(hex4: HEX4 | string) { |
|
this.rgba = RGB.fromHex(hex4); |
|
} |
|
|
|
/** The {@link HEX6} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get hex6(): HEX6 { |
|
return this.rgba.toHEX6(); |
|
} |
|
|
|
set hex6(hex6: HEX6 | string) { |
|
this.rgba = RGB.fromHex(hex6); |
|
} |
|
|
|
/** The {@link HEX8} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get hex8(): HEX8 { |
|
return this.rgba.toHEX8(); |
|
} |
|
|
|
set hex8(hex8: HEX8 | string) { |
|
this.rgba = RGB.fromHex(hex8); |
|
} |
|
|
|
/** The {@link OKLAB} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get oklab(): OKLAB { |
|
return OKLAB.fromRGB(this.rgba); |
|
} |
|
|
|
set oklab( |
|
oklab: OKLAB | { l: number; a: number; b: number; alpha?: number }, |
|
) { |
|
this.rgba = OKLAB.toRGB( |
|
new OKLAB(oklab.l, oklab.a, oklab.b, oklab.alpha ?? 1), |
|
); |
|
} |
|
|
|
/** The {@link OKLCH} representation of this {@link Color}. */ |
|
@memoize("rgba") |
|
get oklch(): OKLCH { |
|
return OKLCH.fromRGB(this.rgba); |
|
} |
|
|
|
set oklch( |
|
oklch: OKLCH | { l: number; c: number; h: number; alpha?: number }, |
|
) { |
|
this.rgba = OKLCH.toRGB( |
|
new OKLCH(oklch.l, oklch.c, oklch.h, oklch.alpha ?? 1), |
|
); |
|
} |
|
|
|
/** @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef */ |
|
get yiq(): number { |
|
const { r, g, b } = this.rgba; |
|
return (r * 299 + g * 587 + b * 114) / 1e3; |
|
} |
|
|
|
/** |
|
* Returns the name from the {@link Color.names} list that closest |
|
* represents this Color. Unless the {@linkcode hex} value of this Color is |
|
* an exact match for a name in the list, the name returned will be an |
|
* approximation based on its distance from the closest named color. */ |
|
get name(): ColorNames { |
|
return KEYWORD.find(this).toString(); |
|
} |
|
|
|
/** Returns `true` if this {@link Color} is equal to {@linkcode that}. */ |
|
equals(that: Color | null): boolean { |
|
return !!that && Color.is(that) && |
|
RGB.equals(this.rgba, new Color(that).rgba); |
|
} |
|
|
|
clone(): Color { |
|
return new Color(this.rgba); |
|
} |
|
|
|
/** |
|
* http://www.w3.org/TR/WCAG20/#relativeluminancedef |
|
* Returns a number in the range of [0, 1]. |
|
* `O` is the darkest (100% black), `1` is the lightest (100% white). |
|
*/ |
|
luminance(): number { |
|
const R = Color.#componentLuminance(this.rgba.r); |
|
const G = Color.#componentLuminance(this.rgba.g); |
|
const B = Color.#componentLuminance(this.rgba.b); |
|
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B; |
|
|
|
return roundFloat(luminance, 4); |
|
} |
|
|
|
/** |
|
* http://www.w3.org/TR/WCAG20/#contrast-ratiodef |
|
* Returns the contrast ration number in the set [1, 21]. |
|
*/ |
|
getContrastRatio(that: Color): number { |
|
const L1 = this.luminance(), L2 = that.luminance(); |
|
return L1 > L2 ? (L1 + 0.05) / (L2 + 0.05) : (L2 + 0.05) / (L1 + 0.05); |
|
} |
|
|
|
/** |
|
* http://24ways.org/2010/calculating-color-contrast |
|
* Return 'true' if darker color otherwise 'false' |
|
*/ |
|
isDark(): this is Color.Dark { |
|
return this.yiq < 128; |
|
} |
|
|
|
/** |
|
* http://24ways.org/2010/calculating-color-contrast |
|
* Return 'true' if lighter color otherwise 'false' |
|
*/ |
|
isLight(): this is Color.Light { |
|
return !this.isDark(); |
|
} |
|
|
|
/** |
|
* Returns `true` if this is **lighter** than {@linkcode that} color. Uses |
|
* the {@linkcode luminance} value to determine which color is lighter. |
|
*/ |
|
isLighterThan(that: Color): boolean { |
|
return this.luminance() > that.luminance(); |
|
} |
|
|
|
/** |
|
* Returns `true` if this is **darker** than {@linkcode that} color. Uses |
|
* the {@linkcode luminance} value to determine which color is darker. |
|
* |
|
* @param {Colors} that - The color to compare this color to. |
|
* @returns `true` if this is darker than `that`, otherwise `false`. |
|
*/ |
|
isDarkerThan(that: Colors): boolean { |
|
return this.luminance() < Color.from(that).luminance(); |
|
} |
|
|
|
/** Lighten this color by the given {@link factor}. */ |
|
lighten(factor: number): this { |
|
this.hsla = new HSL( |
|
this.hsla.h, |
|
this.hsla.s, |
|
this.hsla.l + this.hsla.l * factor, |
|
this.hsla.a, |
|
); |
|
return this; |
|
} |
|
|
|
/** Darken this color by the given {@link factor}. */ |
|
darken(factor: number): this { |
|
return this.lighten(-factor); |
|
} |
|
|
|
/** Saturate this color by the given {@link factor}. */ |
|
saturate(factor: number): this { |
|
this.hsla = new HSL( |
|
this.hsla.h, |
|
this.hsla.s + this.hsla.s * factor, |
|
this.hsla.l, |
|
this.hsla.a, |
|
); |
|
return this; |
|
} |
|
|
|
/** Desaturate this color by the given {@link factor}. */ |
|
desaturate(factor: number): this { |
|
return this.saturate(-factor); |
|
} |
|
|
|
/** Fade this color by the given {@link factor}. */ |
|
alpha(alpha: number, relative?: boolean): this { |
|
let negative = false; |
|
if (alpha < 0) negative = true, alpha = -alpha; |
|
if (alpha > 1 && alpha <= 100) alpha /= 100; |
|
if (alpha > 100 && alpha <= 255) alpha /= 255; |
|
if (alpha > 255) alpha = 1; |
|
if (negative) alpha = -alpha; |
|
if (relative) alpha += this.rgba.a; |
|
this.rgba = new RGB( |
|
this.rgba.r, |
|
this.rgba.g, |
|
this.rgba.b, |
|
Color.clamp(alpha, 0, 1), |
|
); |
|
return this; |
|
} |
|
|
|
/** Fade this color by the given {@link factor}. */ |
|
fade(factor: number): this { |
|
return this.alpha(-factor, true); |
|
} |
|
|
|
/** Fade this color in (make it more opaque) by the given {@link factor}. */ |
|
transparency(factor: number): this { |
|
return this.alpha(factor, true); |
|
} |
|
|
|
/** Returns `true` if this color is transparent (alpha === 0). */ |
|
isTransparent(): boolean { |
|
return this.rgba.a === 0; |
|
} |
|
|
|
/** Returns `true` if this color is opaque (alpha === 1). */ |
|
isOpaque(): boolean { |
|
return this.rgba.a === 1; |
|
} |
|
|
|
/** Returns a new {@link Color} that is the opposite of this color. */ |
|
opposite(): Color { |
|
return new Color( |
|
new RGB( |
|
255 - this.rgba.r, |
|
255 - this.rgba.g, |
|
255 - this.rgba.b, |
|
this.rgba.a, |
|
), |
|
); |
|
} |
|
|
|
/** |
|
* Returns a new {@link Color} that is the result of blending this color onto |
|
* another color. This is similar to {@linkcode makeOpaque}. |
|
*/ |
|
blend(c: Color): Color { |
|
const rgba = c.rgba; |
|
|
|
// Convert to 0..1 opacity |
|
const thisA = this.rgba.a; |
|
const colorA = rgba.a; |
|
|
|
const a = thisA + colorA * (1 - thisA); |
|
if (a < 1e-6) return Color.transparent; |
|
|
|
const r = this.rgba.r * thisA / a + rgba.r * colorA * (1 - thisA) / a; |
|
const g = this.rgba.g * thisA / a + rgba.g * colorA * (1 - thisA) / a; |
|
const b = this.rgba.b * thisA / a + rgba.b * colorA * (1 - thisA) / a; |
|
|
|
return new Color(new RGB(r, g, b, a)); |
|
} |
|
|
|
/** |
|
* Returns a new {@link Color} that is the result of blending this color onto |
|
* a background color. This is similar to {@linkcode blend}, but will result |
|
* in a different color if the background color is not opaque. |
|
*/ |
|
makeOpaque(opaqueBackground: Color): Color { |
|
const { r: r1, g: g1, b: b1, a: a1 } = opaqueBackground.rgba; |
|
// only allow to blend onto a non-opaque color onto a opaque color |
|
if (this.isOpaque() || a1 !== 1) return this; |
|
const { r: r2, g: g2, b: b2, a: a2 } = this.rgba; |
|
// https://stackoverflow.com/questions/12228548/finding-equivalent-color-with-opacity |
|
const r3 = r1 - a2 * (r1 - r2); |
|
const g3 = g1 - a2 * (g1 - g2); |
|
const b3 = b1 - a2 * (b1 - b2); |
|
return new Color(new RGB(r3, g3, b3, 1)); |
|
} |
|
|
|
/** Flatten this color onto a background color. */ |
|
flatten(...backgrounds: Color[]): Color { |
|
const bg = backgrounds.reduceRight((acc, c) => Color.#flatten(c, acc)); |
|
return Color.#flatten(this, bg); |
|
} |
|
|
|
/** Returns this color in the {@linkcode ANSI} color space. */ |
|
toANSI(): ANSI { |
|
return ANSI.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode ANSI16M} color space. */ |
|
toANSI16M(): ANSI16M { |
|
return ANSI16M.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode ANSI256} color space. */ |
|
toANSI256(): ANSI256 { |
|
return ANSI256.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode APPLE} color space. */ |
|
toAPPLE(): APPLE { |
|
return APPLE.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode APPLE} color space. */ |
|
toApple(): APPLE { |
|
return APPLE.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode CMYK} color space. */ |
|
toCMYK(): CMYK { |
|
return CMYK.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode GRAY} color space. */ |
|
toGRAY(): GRAY { |
|
return GRAY.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode GRAY} color space. */ |
|
toGray(): GRAY { |
|
return GRAY.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode HCG} color space. */ |
|
toHCG(): HCG { |
|
return HCG.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode HSL} color space. */ |
|
toHSL(): HSL { |
|
return HSL.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode HSV} color space. */ |
|
toHSV(): HSV { |
|
return HSV.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode HWB} color space. */ |
|
toHWB(): HWB { |
|
return HWB.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode KEYWORD} color space. */ |
|
toKeyword(): KEYWORD { |
|
return KEYWORD.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode KEYWORD} color space. */ |
|
toKEYWORD(): KEYWORD { |
|
return this.toKeyword(); |
|
} |
|
|
|
/** Returns this color in the {@linkcode KEYWORD} color space. */ |
|
toName(): KEYWORD { |
|
return this.toKeyword(); |
|
} |
|
|
|
/** Returns this color as a {@linkcode KEYWORD} color string. */ |
|
toKeywordString(): string { |
|
return this.toKeyword().toString(); |
|
} |
|
|
|
/** Returns this color as a {@linkcode KEYWORD} color string. */ |
|
toNameString(): string { |
|
return this.toKeywordString(); |
|
} |
|
|
|
/** Returns this color in the {@linkcode LAB} color space. */ |
|
toLAB(): LAB { |
|
return LAB.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode LCH} color space. */ |
|
toLCH(): LCH { |
|
return LCH.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode OKLAB} color space. */ |
|
toOKLAB(): OKLAB { |
|
return OKLAB.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode OKLCH} color space. */ |
|
toOKLCH(): OKLCH { |
|
return OKLCH.fromRGB(this.rgba); |
|
} |
|
|
|
/** Returns this color in the {@linkcode RGB} color space. */ |
|
toRGB(): RGB { |
|
return this.rgba; |
|
} |
|
|
|
/** Returns this color in the {@linkcode XYZ} color space. */ |
|
toXYZ(): XYZ { |
|
return XYZ.fromRGB(this.rgba); |
|
} |
|
|
|
toJSON() { |
|
const ansi = this.ansi.toString().replace(/\\/g, "\\\\"); |
|
const ansi16m = this.ansi16m.toString().replace(/\\/g, "\\\\"); |
|
const ansi256 = this.ansi256.toString().replace(/\\/g, "\\\\"); |
|
const apple = this.apple.toJSON(); |
|
const cmyk = this.cmyk.toJSON(); |
|
const gray = this.gray.toJSON(); |
|
const hcg = this.hcg.toJSON(); |
|
const hex = this.hex.toString(); |
|
const hsl = this.hsla.toJSON(); |
|
const hsv = this.hsva.toJSON(); |
|
const hwb = this.hwb.toJSON(); |
|
const lab = this.lab.toJSON(); |
|
const lch = this.lch.toJSON(); |
|
const name = this.toKeywordString(); |
|
const oklab = this.oklab.toJSON(); |
|
const oklch = this.oklch.toJSON(); |
|
const rgb = this.rgba.toJSON(); |
|
const xyz = this.xyz.toJSON(); |
|
|
|
return { |
|
ansi, |
|
ansi256, |
|
ansi16m, |
|
apple, |
|
cmyk, |
|
gray, |
|
hcg, |
|
hex, |
|
hsl, |
|
hsv, |
|
hwb, |
|
lab, |
|
lch, |
|
name, |
|
oklab, |
|
oklch, |
|
rgb, |
|
xyz, |
|
} as const; |
|
} |
|
|
|
/** Returns the color as a formatted CSS-compatible string. */ |
|
toString(): string { |
|
return Color.format(this); |
|
} |
|
|
|
/** Convert to ANSI 8-bit color (256 colors) */ |
|
toAnsi8(): string { |
|
let ansi = 0; |
|
|
|
if (this.rgba.r === this.rgba.g && this.rgba.g === this.rgba.b) { |
|
if (this.rgba.r < 8) { |
|
ansi = 16; |
|
} else if (this.rgba.r > 248) { |
|
ansi = 231; |
|
} else { |
|
ansi = Math.round(((this.rgba.r - 8) / 247) * 24) + 232; |
|
} |
|
} else { |
|
ansi = 16 + |
|
(36 * Math.round((this.rgba.r / 255) * 5)) + |
|
(6 * Math.round((this.rgba.g / 255) * 5)) + |
|
Math.round((this.rgba.b / 255) * 5); |
|
} |
|
|
|
return `\u001b[${ansi & 0xFF}m`; |
|
} |
|
|
|
/** Convert to ANSI 16-bit color (High color) */ |
|
toAnsi16(): string { |
|
// Simple 16 color scheme |
|
const r = this.rgba.r > 127 ? 1 : 0; |
|
const g = this.rgba.g > 127 ? 1 : 0; |
|
const b = this.rgba.b > 127 ? 1 : 0; |
|
const ansi = 30 + r * 4 + g * 2 + b; |
|
|
|
return `\u001b[38;5;${ansi}m`; |
|
} |
|
|
|
/** Convert to ANSI 24-bit TrueColor */ |
|
toAnsi24(options?: Ansi24bitOptions): string { |
|
const { mode = "foreground", ...opts } = options ?? {}; |
|
let modifiers = ""; |
|
if (mode === "foreground") modifiers = "38;2"; |
|
if (mode === "background") modifiers = "48;2"; |
|
if (mode === "decoration") modifiers = "58;2"; |
|
const styles = [ |
|
opts.bold ? "1" : "", |
|
opts.dim ? "2" : "", |
|
opts.italic ? "3" : "", |
|
opts.underline ? "4" : "", |
|
opts.invert ? "7" : "", |
|
].filter(Boolean); |
|
const { r, g, b } = this.rgba; |
|
modifiers = [...styles, modifiers, r, g, b].join(";"); |
|
return `\u001b[${modifiers}m`; |
|
} |
|
|
|
toHexString(length?: 3 | 4 | 6 | 8, prefix = "#"): string { |
|
const { r, g, b, a = 1 } = this.rgba; // Default alpha to 1 (fully opaque) |
|
const a255 = Math.round(a * 255); // Convert 0-1 alpha value to 0-255 |
|
|
|
const toHex = (value: number, length: 1 | 2 = 2): string => { |
|
if (length === 2) { |
|
return value.toString(16).padStart(2, "0").toUpperCase(); |
|
} else { |
|
const rounded = Math.round(value / 17) * 17; |
|
return (rounded / 17).toString(16).toUpperCase(); |
|
} |
|
}; |
|
|
|
let newR: string, newG: string, newB: string, newA: string | null; |
|
|
|
length ??= a === 1 ? 6 : 8; |
|
if (length === 3 || length === 4) { |
|
newR = toHex(r, 1), newG = toHex(g, 1), newB = toHex(b, 1); |
|
newA = length === 4 ? toHex(a255, 1) : null; |
|
} else { |
|
newR = toHex(r, 2), newG = toHex(g, 2), newB = toHex(b, 2); |
|
newA = length === 8 ? toHex(a255, 2) : null; |
|
} |
|
|
|
return `${prefix ?? ""}${newR}${newG}${newB}${newA ?? ""}`; |
|
} |
|
|
|
[inspect.custom]( |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
const { stylize, ...opts } = { |
|
...options, |
|
colors: true, |
|
getters: true, |
|
compact: 4, |
|
}; |
|
const ansi = this.toAnsi24({ mode: "foreground" }); |
|
const hex = this.toHexString(6); |
|
const tag = `${ |
|
stylize("[Color: ", "special") |
|
}${ansi}◪ \x1b[1m${hex}\x1b[0m${stylize("]", "special")}`; |
|
depth ??= options.depth ?? 2; |
|
if (depth < 0) return stylize(`[Color: ${hex}]`, "special"); |
|
if (depth < 1) return tag; |
|
const { hsl, rgb, xyz, name } = this; |
|
return `${tag} ${inspect({ name, hsl, rgb, xyz }, opts)}`; |
|
} |
|
|
|
static readonly white: Color = new Color(new RGB(255, 255, 255, 1)); |
|
static readonly black: Color = new Color(new RGB(0, 0, 0, 1)); |
|
static readonly red: Color = new Color(new RGB(255, 0, 0, 1)); |
|
static readonly blue: Color = new Color(new RGB(0, 0, 255, 1)); |
|
static readonly green: Color = new Color(new RGB(0, 255, 0, 1)); |
|
static readonly cyan: Color = new Color(new RGB(0, 255, 255, 1)); |
|
static readonly lightgrey: Color = new Color(new RGB(211, 211, 211, 1)); |
|
static readonly transparent: Color = new Color(new RGB(0, 0, 0, 0)); |
|
|
|
static #illuminant?: Illuminant; |
|
|
|
static get illuminant(): Illuminant { |
|
return Color.#illuminant ??= Illuminants.D65; |
|
} |
|
|
|
static set illuminant( |
|
illuminant: Illuminant | (string & keyof typeof Illuminants), |
|
) { |
|
if (typeof illuminant === "string") { |
|
if ( |
|
illuminant === "default" || illuminant === "D65" || |
|
illuminant === "Illuminant" |
|
) { |
|
illuminant = Illuminants.D65; |
|
} else if (illuminant in Illuminants) { |
|
illuminant = Illuminants[illuminant]; |
|
} else { |
|
throw new TypeError( |
|
`[Color.illuminant] expected an illuminant object or a valid illuminant name from Color.Illuminant, but received '${illuminant}' (${typeof illuminant}).`, |
|
); |
|
} |
|
} |
|
|
|
Color.#illuminant = illuminant; |
|
} |
|
|
|
static get Illuminant(): typeof Illuminants { |
|
return Illuminants; |
|
} |
|
|
|
static get spaces(): spaces { |
|
return spaces; |
|
} |
|
|
|
static get schemas(): schemas { |
|
return schemas; |
|
} |
|
|
|
static from(input: Colors): Color { |
|
return new Color(input); |
|
} |
|
|
|
static fromHex(hex: string | HEX): Color { |
|
if (typeof hex === "string") hex = new HEX(hex); |
|
HEX.assert(hex); |
|
return Color.Format.parseHex(hex.toString()) || Color.black; |
|
} |
|
|
|
/** |
|
* If {@linkcode of} is lighter than the {@linkcode relative} color, then it |
|
* will be returned as-is. Otherwise, a new {@link Color} will be created by |
|
* lightening it by a given {@link factor}, proportional to the difference in |
|
* luminance between {@linkcode of} and {@linkcode relative}. If no factor is |
|
* specified, it will default to `0.5`. |
|
*/ |
|
static getLighterColor(of: Colors, relative: Colors, factor?: number): Color { |
|
of = new Color(of); |
|
relative = new Color(relative); |
|
if (of.isLighterThan(relative)) return of; |
|
factor = factor ? factor : 0.5; |
|
const L1 = of.luminance(); |
|
const L2 = relative.luminance(); |
|
factor = factor * (L2 - L1) / L2; |
|
return of.lighten(factor); |
|
} |
|
|
|
/** |
|
* If {@linkcode of} is darker than the {@linkcode relative} color, then it |
|
* will be returned as-is. Otherwise, a new {@link Color} will be created by |
|
* darkening it by a given {@link factor}, proportional to the difference in |
|
* luminance between {@linkcode of} and {@linkcode relative}. If no factor is |
|
* specified, it will default to `0.5`. |
|
*/ |
|
static getDarkerColor(of: Colors, relative: Colors, factor?: number): Color { |
|
of = new Color(of); |
|
relative = new Color(relative); |
|
if (of.isDarkerThan(relative)) return of; |
|
factor = factor ? factor : 0.5; |
|
const L1 = of.luminance(); |
|
const L2 = relative.luminance(); |
|
factor = factor * (L1 - L2) / L1; |
|
return of.darken(factor); |
|
} |
|
|
|
/** Returns a new {@link Color} that is the result of blending 2 colors. */ |
|
static mix(color1: Colors, color2: Colors, amount: number | undefined = 50) { |
|
amount = Color.clamp(amount ??= 50, 0, 100); |
|
|
|
const { r: r1, g: g1, b: b1, a: a1 } = new Color(color1).rgba; |
|
const { r: r2, g: g2, b: b2, a: a2 } = new Color(color2).rgba; |
|
|
|
const p = amount / 100; |
|
|
|
const r = (r2 - r1) * (p + r1); |
|
const g = (g2 - g1) * (p + g1); |
|
const b = (b2 - b1) * (p + b1); |
|
const a = (a2 - a1) * (p + a1); |
|
|
|
return new Color(r, g, b, a); |
|
} |
|
|
|
/** Returns a random {@link Color} instance. */ |
|
static random(opaque = true): Color { |
|
const x = Math.random(); |
|
const y = Math.random(); |
|
const z = Math.random(); |
|
const a = opaque ? 1 : Math.max(Math.random(), 0.2); |
|
|
|
return new Color(new XYZ(x, y, z, a)); |
|
} |
|
|
|
/** |
|
* Readability Functions |
|
* @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef (WCAG Version 2) */ |
|
|
|
/** Analyze the 2 colors and returns the color contrast defined by (WCAG Version 2) */ |
|
static readability(color1: Colors, color2: Colors) { |
|
const c1 = new Color(color1), l1 = c1.luminance(); |
|
const c2 = new Color(color2), l2 = c2.luminance(); |
|
|
|
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); |
|
} |
|
|
|
/** Rotate the hue of a {@link color} by a given number of {@link degrees}. */ |
|
static rotate(color: Colors, degrees = 10): Color { |
|
color = new Color(color); |
|
const hsl = color.hsla; |
|
hsl.h = (hsl.h + degrees) % 360; |
|
return new Color(hsl); |
|
} |
|
|
|
static complement(color: Colors): Color { |
|
color = new Color(color); |
|
return Color.rotate(color, 180); |
|
} |
|
|
|
static triad(color: Colors): [a: Color, b: Color, c: Color] { |
|
color = new Color(color); |
|
return [color, Color.rotate(color, 120), Color.rotate(color, 240)]; |
|
} |
|
|
|
static tetrad(color: Colors): [a: Color, b: Color, c: Color, d: Color] { |
|
color = new Color(color); |
|
return [ |
|
color, |
|
Color.rotate(color, 90), |
|
Color.rotate(color, 180), |
|
Color.rotate(color, 270), |
|
]; |
|
} |
|
|
|
static splitComplement(color: Colors): [a: Color, b: Color, c: Color] { |
|
color = new Color(color); |
|
return [color, Color.rotate(color, 150), Color.rotate(color, 210)]; |
|
} |
|
|
|
static analogous(color: Colors, results = 6, slices = 30): Color[] { |
|
color = new Color(color); |
|
const hsl = color.hsla; |
|
const part = 360 / slices; |
|
const result: Color[] = []; |
|
const max = results - 1; |
|
|
|
for (let i = 0; i < slices; i++) { |
|
const h = (i * part + hsl.h) % 360; |
|
const s = i / max * hsl.s; |
|
const l = hsl.l; |
|
result.push(new Color(new HSL(h, s, l))); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
static monochromatic(color: Color, steps = 6): Color[] { |
|
const hsl = color.hsla; |
|
const result: Color[] = []; |
|
const step = 30; |
|
for (let i = 0; i < steps; i++) { |
|
result.push(new Color(hsl)); |
|
hsl.l += step; |
|
hsl.l %= 100; |
|
} |
|
return result; |
|
} |
|
|
|
static brighten(color: Colors, amount: number | undefined = 10): Color { |
|
amount = amount === 0 ? 0 : amount || 10; |
|
color = new Color(color); |
|
const rgb = color.rgba; |
|
rgb.r = Color.clamp(rgb.r - Math.round(255 * -(amount / 100)), 0, 255); |
|
rgb.g = Color.clamp(rgb.g - Math.round(255 * -(amount / 100)), 0, 255); |
|
rgb.b = Color.clamp(rgb.b - Math.round(255 * -(amount / 100)), 0, 255); |
|
// alpha channel is from 0-1.0 |
|
rgb.a = Color.clamp(rgb.a - Math.round(1 * -(amount / 100)), 0, 1); |
|
return new Color(rgb); |
|
} |
|
|
|
static darken(color: Colors, amount: number | undefined = 10): Color { |
|
return Color.brighten(color, -amount); |
|
} |
|
|
|
static saturate(color: Colors, amount: number | undefined = 10): Color { |
|
amount = amount === 0 ? 0 : amount || 10; |
|
color = new Color(color); |
|
const hsl = color.hsla; |
|
hsl.s = Color.clamp(hsl.s - Math.round(100 * -(amount / 100)), 0, 100); |
|
return new Color(hsl); |
|
} |
|
|
|
static desaturate(color: Colors, amount: number | undefined = 10): Color { |
|
amount = -amount % 100; |
|
return Color.saturate(color, amount); |
|
} |
|
|
|
static grayscale(color: Colors): Color { |
|
return Color.desaturate(color, 100); |
|
} |
|
|
|
static greyscale(color: Colors): Color { |
|
return Color.grayscale(color); |
|
} |
|
|
|
static tint(color: Colors, amount: number | undefined = 10): Color { |
|
amount = amount === 0 ? 0 : amount || 10; |
|
color = new Color(color); |
|
const hsl = color.hsla; |
|
hsl.l = Color.clamp(hsl.l + Math.round(100 * (amount / 100)), 0, 100); |
|
return new Color(hsl); |
|
} |
|
|
|
static curves( |
|
color: Colors, |
|
rgb: readonly (readonly [x: number, y: number])[] = [[0, 0], [255, 255]], |
|
): Color { |
|
color = new Color(color); |
|
const { r, g, b, a = 1 } = color.rgba; |
|
|
|
const [[x0, y0], [x1, y1]] = rgb; |
|
|
|
const x = Color.clamp(r, x0, x1), y = Color.clamp(g, y0, y1); |
|
|
|
const [xMin, xMax] = [Math.min(x0, x1), Math.max(x0, x1)]; |
|
const [yMin, yMax] = [Math.min(y0, y1), Math.max(y0, y1)]; |
|
|
|
const [rMin, rMax] = [Math.min(xMin, yMin), Math.max(xMin, yMin)]; |
|
const [gMin, gMax] = [Math.min(xMax, yMax), Math.max(xMax, yMax)]; |
|
|
|
const r1 = Math.round((x - rMin) / (rMax - rMin) * 255); |
|
const g1 = Math.round((y - gMin) / (gMax - gMin) * 255); |
|
const b1 = Math.round((b - 0) / (255 - 0) * 255); |
|
|
|
return new Color(new RGB(r1, g1, b1, a)); |
|
} |
|
|
|
static contrast(color: Colors): number; |
|
static contrast(color: Colors, adjustmentFactor: number): Color; |
|
static contrast( |
|
color: Colors, |
|
amount: number | Color.undefined = Color.undefined, |
|
): number | Color { |
|
const rgb = new Color(color).rgba; |
|
const _isSingleArgument = amount === Color.undefined; |
|
amount = amount === Color.undefined ? 10 : +amount; |
|
const contrast = amount ?? 0; |
|
const lum = Color.#componentLuminance; |
|
const adjust = (comp: number) => { |
|
const c = comp / 255, a = 0.5; |
|
const d = c < 0.5 ? contrast : -contrast, b = (c + d) / 1 + a; |
|
return Math.round(255 * lum(b)); |
|
}; |
|
const [r, g, b] = [...rgb].map(adjust), { a = 1 } = rgb; |
|
return new Color(new RGB(r, g, b, a)); |
|
} |
|
|
|
static sepia(color: Colors, amount: number | undefined = 10): Color { |
|
amount = amount === 0 ? 0 : amount || 10; |
|
const { r, g, b, a = 1 } = new Color(color).rgba; |
|
const [r1, g1, b1] = [ |
|
r * (1 - 0.607 * amount), |
|
g * (1 - 0.769 * amount), |
|
b * (1 - 0.189 * amount), |
|
]; |
|
return new Color(new RGB(r1, g1, b1, a)); |
|
} |
|
|
|
static alpha(color: Colors): number; |
|
static alpha(color: Colors, amount: number): Color; |
|
static alpha( |
|
color: Colors, |
|
amount: number | Color.undefined = Color.undefined, |
|
): Color | number { |
|
const rgba = new Color(color).rgba; |
|
if (amount === Color.undefined) return rgba.a; |
|
rgba.a = amount; |
|
return new Color(rgba); |
|
} |
|
|
|
/** Returns the distance between 2 colors using the {@link deltaE00} formula. */ |
|
static distance(color1: Colors, color2: Colors): number { |
|
const c1 = new Color(color1), c2 = new Color(color2); |
|
return Color.deltaE00(c1.lab, c2.lab); |
|
} |
|
|
|
/** Returns the distance between 2 colors using the {@link deltaE00} formula. */ |
|
static deltaE00(lab1: LAB, lab2: LAB): number { |
|
const deltaL = lab1.l - lab2.l; |
|
const deltaA = lab1.a - lab2.a; |
|
const deltaB = lab1.b - lab2.b; |
|
const c1 = Math.sqrt(lab1.a * lab1.a + lab1.b * lab1.b); |
|
const c2 = Math.sqrt(lab2.a * lab2.a + lab2.b * lab2.b); |
|
const deltaC = c1 - c2; |
|
const deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC; |
|
const sc = 1.0 + 0.045 * c1; |
|
const sh = 1.0 + 0.015 * c1; |
|
const deltaLKlsl = deltaL / (1.0); |
|
const deltaCkcsc = deltaC / sc; |
|
const deltaHkhsh = deltaH / sh; |
|
const i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + |
|
deltaHkhsh * deltaHkhsh; |
|
return i < 0 ? 0 : Math.sqrt(i); |
|
} |
|
|
|
/** |
|
* Creates a comparator function, used to sort collections of colors. The |
|
* resulting function accepts two colors, {@linkcode a} and {@linkcode b}, |
|
* and returns a number between `-1` and `1` indicating whether {@linkcode a} |
|
* should come before {@linkcode b} (`-1`), after (`1`), or if they're equal |
|
* (`0`). The strategy used for the color comparison can be specified via the |
|
* {@linkcode strategy} parameter, which can be one of the following: |
|
* - `"luminance"`: Sorts by their relative luminance value. |
|
* - `"contrast"`: Sorts by {@linkcode yiq|YIQ value}. |
|
* - `"hue"`: Sorts by their hue value (`h` of {@link HSL}). |
|
* - `"saturation"`: Sorts by saturation value (`s` of {@link HSL}). |
|
* - `"lightness"`: Sorts by lightness value (`l` of {@link HSL}). |
|
* - `"chroma"`: Sorts by chroma value. |
|
* - `"name"`: Sorts alphabetically by their closest matching {@link name}. |
|
* - `"distance"`: Sorts by the distance using the {@link deltaE00} formula. |
|
*/ |
|
static comparator( |
|
strategy: Color.Strategy = Color.Strategy.default, |
|
order: Color.Order = Color.Order.ASC, |
|
): Color.Comparator { |
|
if (!(strategy in Color.strategies)) { |
|
throw new TypeError( |
|
`[Color.comparator] expected a valid strategy name from Color.Strategy, but received '${strategy}' (${typeof strategy}).`, |
|
); |
|
} |
|
if (order !== -1 && order !== 1) order = 1; |
|
const fn = Color.strategies[strategy]; |
|
if (typeof fn !== "function") { |
|
throw new TypeError( |
|
`[Color.comparator] invalid strategy provided: '${strategy}'`, |
|
); |
|
} |
|
return (a, b) => order * fn(new Color(a), new Color(b)); |
|
} |
|
|
|
static sort< |
|
const A extends readonly Colors[], |
|
S extends Color.Strategy = Color.Strategy.default, |
|
O extends Color.Order = Color.Order.ASC, |
|
>(colors: A, strategy?: S, order?: O): readonly [...A]; |
|
static sort< |
|
const A extends readonly Colors[], |
|
S extends keyof typeof Color.Strategy = "default", |
|
O extends Color.Order = Color.Order.ASC, |
|
>(colors: A, strategy: S, order?: O): readonly [...A]; |
|
static sort<const A extends readonly Colors[]>( |
|
colors: A, |
|
strategy?: Color.Strategy | keyof typeof Color.Strategy, |
|
order?: Color.Order, |
|
) { |
|
strategy ??= Color.Strategy.default; |
|
if (strategy && strategy in Color.Strategy) { |
|
strategy = Color.Strategy[strategy as keyof typeof Color.Strategy]; |
|
} |
|
const compareFn = Color.comparator(strategy as Color.Strategy, order); |
|
return colors.toSorted(compareFn); |
|
} |
|
|
|
static get strategies(): Record<Color.Strategy, Color.Comparator<Color>> { |
|
return { |
|
[Color.Strategy.Luminance](a, b) { |
|
const l1 = a.luminance(), l2 = b.luminance(); |
|
return l1 < l2 ? -1 : l1 > l2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Contrast](a, b) { |
|
const y1 = a.yiq, y2 = b.yiq; |
|
return y1 < y2 ? -1 : y1 > y2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.HSL](a, b) { |
|
const h1 = a.hsl.h, h2 = b.hsl.h; |
|
const s1 = a.hsl.s, s2 = b.hsl.s; |
|
const l1 = a.hsl.l, l2 = b.hsl.l; |
|
return h1 < h2 |
|
? -1 |
|
: h1 > h2 |
|
? 1 |
|
: s1 < s2 |
|
? -1 |
|
: s1 > s2 |
|
? 1 |
|
: l1 < l2 |
|
? -1 |
|
: l1 > l2 |
|
? 1 |
|
: 0; |
|
}, |
|
[Color.Strategy.Hue](a, b) { |
|
const h1 = a.hsl.h, h2 = b.hsl.h; |
|
return h1 < h2 ? -1 : h1 > h2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Saturation](a, b) { |
|
const s1 = a.hsl.s, s2 = b.hsl.s; |
|
return s1 < s2 ? -1 : s1 > s2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Lightness](a, b) { |
|
const l1 = a.hsl.l, l2 = b.hsl.l; |
|
return l1 < l2 ? -1 : l1 > l2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Chroma](a, b) { |
|
const c1 = a.lch.c, c2 = b.lch.c; |
|
return c1 < c2 ? -1 : c1 > c2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Name](a, b) { |
|
const n1 = a.name, n2 = b.name; |
|
return n1 < n2 ? -1 : n1 > n2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Distance](a, b) { |
|
const d1 = Color.distance(a, b), d2 = Color.distance(b, a); |
|
return d1 < d2 ? -1 : d1 > d2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Brightness](a, b) { |
|
const b1 = a.hsv.v, b2 = b.hsv.v; |
|
return b1 < b2 ? -1 : b1 > b2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Red](a, b) { |
|
const r1 = a.rgba.r, r2 = b.rgba.r; |
|
return r1 < r2 ? -1 : r1 > r2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Green](a, b) { |
|
const g1 = a.rgba.g, g2 = b.rgba.g; |
|
return g1 < g2 ? -1 : g1 > g2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Blue](a, b) { |
|
const b1 = a.rgba.b, b2 = b.rgba.b; |
|
return b1 < b2 ? -1 : b1 > b2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Alpha](a, b) { |
|
const a1 = a.rgba.a, a2 = b.rgba.a; |
|
return a1 < a2 ? -1 : a1 > a2 ? 1 : 0; |
|
}, |
|
[Color.Strategy.Grayscale](a, b) { |
|
const g1 = a.gray.g, g2 = b.gray.g; |
|
return g1 < g2 ? -1 : g1 > g2 ? 1 : 0; |
|
}, |
|
}; |
|
} |
|
|
|
static { |
|
// simplify stacktraces by de-aliasing all aliased methods |
|
Object.defineProperties(this, { |
|
greyscale: { value: this.grayscale }, |
|
}); |
|
} |
|
|
|
/** |
|
* @example |
|
* ```ts |
|
* let pstr = Color.createPalette(Color.colors.plum_100, 6, 6).flat().map( |
|
* (v,i) => inspect(v, { colors: true, getters: true, depth: 0 }) + |
|
* ((i+1) % 6 === 0 ? "\n" : "\t") |
|
* ).join(""); |
|
* console.log(pstr); |
|
* |
|
* // Output: |
|
* #FFBBFF #E5A1E5 #CC88CC #B26EB2 #995599 #7F3B7F |
|
* #FFBBBB #E5A1A1 #CC8888 #B26E6E #995555 #7F3B3B |
|
* #FFFFBB #E5E5A1 #CCCC88 #B2B26E #999955 #7F7F3B |
|
* #BBFFBB #A1E5A1 #88CC88 #6EB26E #559955 #3B7F3B |
|
* #BBFFFF #A1E5E5 #88CCCC #6EB2B2 #559999 #3B7F7F |
|
* #BBBBFF #A1A1E5 #8888CC #6E6EB2 #555599 #3B3B7F |
|
* ``` |
|
*/ |
|
static palette( |
|
color: Colors, |
|
{ |
|
hues = 6, |
|
shades = 10, |
|
factor = 10, |
|
op = "brighten", |
|
}: Color.PaletteOptions, |
|
): [...shades: Color[]][] { |
|
const fn = Color[op]; |
|
color = new Color(color); |
|
const palette: Color[][] = []; |
|
for (let i = 0; i < hues; i++) { |
|
const hue = Color.rotate(color, i * 360 / hues); |
|
const fam: Color[] = []; |
|
palette.push(fam); |
|
for (let j = 0; j < shades; j++) { |
|
fam.push(fn(hue, factor * (hue.isDark() ? j : -j) / shades)); |
|
} |
|
} |
|
return palette; |
|
} |
|
|
|
static clamp(value: number, min = 0, max = 1): number { |
|
return Math.min(Math.max(value, min), max); |
|
} |
|
|
|
static equals(a: Color | null, b: Color | null): boolean { |
|
if (!a && !b) return true; |
|
if (!a || !b) return false; |
|
if (!Color.is(a) || !Color.is(b)) return false; |
|
return a.equals(b); |
|
} |
|
|
|
static isSpace(it: unknown): it is spaces[keyof spaces] { |
|
return Object.values(Color.spaces).some((C) => C.is(it)); |
|
} |
|
|
|
static is(it: unknown): it is Color { |
|
if (!it || typeof it !== "object" || Array.isArray(it)) return false; |
|
if (Function[Symbol.hasInstance].call(Color, it)) return true; |
|
const proto: Color = it.constructor.prototype; |
|
if (!proto || typeof proto !== "object" || Array.isArray(proto)) { |
|
return false; |
|
} |
|
if (_brand in proto && proto[_brand] !== "Color") return false; |
|
const keys = [ |
|
"rgb", |
|
"hsl", |
|
"hsv", |
|
"lab", |
|
"lch", |
|
"hcg", |
|
"oklab", |
|
"oklch", |
|
"xyz", |
|
"rgba", |
|
"hsla", |
|
"hsva", |
|
] as const; |
|
return Object.is(proto, Color.prototype) || |
|
Object.prototype.isPrototypeOf.call(Color.prototype, proto) || |
|
keys.every((k): k is Extract<typeof k, keyof Color> => |
|
(k in it) && (k in proto) && hasOwn(proto, k) && |
|
Color.isSpace(it[k as keyof typeof it]) |
|
); |
|
} |
|
|
|
static assert(it: unknown, message?: string): asserts it is Color { |
|
if (!this.is(it)) { |
|
const inspected = inspect(it, { |
|
colors: true, |
|
depth: 1, |
|
getters: true, |
|
compact: true, |
|
}); |
|
const msg = tpl("Color expected. Received '{0}' ({typeof})", { |
|
0: inspected, |
|
1: typeof it as string, |
|
it: inspected, |
|
typeof: typeof it, |
|
}); |
|
const error = new TypeError(message ?? msg); |
|
Error.captureStackTrace?.(error); |
|
throw error; |
|
} |
|
} |
|
|
|
static [Symbol.hasInstance](it: unknown): it is Color { |
|
return Color.is(it); |
|
} |
|
|
|
static #flatten(foreground: Color, background: Color) { |
|
const backgroundAlpha = 1 - foreground.rgba.a; |
|
return new Color( |
|
new RGB( |
|
backgroundAlpha * background.rgba.r + |
|
foreground.rgba.a * foreground.rgba.r, |
|
backgroundAlpha * background.rgba.g + |
|
foreground.rgba.a * foreground.rgba.g, |
|
backgroundAlpha * background.rgba.b + |
|
foreground.rgba.a * foreground.rgba.b, |
|
), |
|
); |
|
} |
|
|
|
static #componentLuminance(color: number): number { |
|
const c = color / 255; |
|
return (c <= 0.03928) ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); |
|
} |
|
} |
|
|
|
export declare namespace Color { |
|
export { Undefined as undefined }; |
|
import ANSI = spaces.ANSI; |
|
import ANSI16M = spaces.ANSI16M; |
|
import ANSI256 = spaces.ANSI256; |
|
import APPLE = spaces.APPLE; |
|
import CMYK = spaces.CMYK; |
|
import GRAY = spaces.GRAY; |
|
import HCG = spaces.HCG; |
|
import HEX = spaces.HEX; |
|
import HEX3 = spaces.HEX3; |
|
import HEX4 = spaces.HEX4; |
|
import HEX6 = spaces.HEX6; |
|
import HEX8 = spaces.HEX8; |
|
import HSL = spaces.HSL; |
|
import HSV = spaces.HSV; |
|
import HWB = spaces.HWB; |
|
import KEYWORD = spaces.KEYWORD; |
|
import LAB = spaces.LAB; |
|
import LCH = spaces.LCH; |
|
import OKLAB = spaces.OKLAB; |
|
import OKLCH = spaces.OKLCH; |
|
import RGB = spaces.RGB; |
|
import XYZ = spaces.XYZ; |
|
|
|
export { |
|
ANSI, |
|
ANSI16M, |
|
ANSI256, |
|
APPLE, |
|
CMYK, |
|
GRAY, |
|
HCG, |
|
HEX, |
|
HEX3, |
|
HEX4, |
|
HEX6, |
|
HEX8, |
|
HSL, |
|
HSV, |
|
HWB, |
|
KEYWORD, |
|
LAB, |
|
LCH, |
|
OKLAB, |
|
OKLCH, |
|
RGB, |
|
XYZ, |
|
}; |
|
|
|
export type Illuminant = Illuminants.Illuminant; |
|
|
|
const IsDark: unique symbol; |
|
const IsLight: unique symbol; |
|
|
|
export interface Dark extends Color { |
|
readonly [IsDark]: true; |
|
readonly [IsLight]: false; |
|
} |
|
|
|
export interface Light extends Color { |
|
readonly [IsDark]: false; |
|
readonly [IsLight]: true; |
|
} |
|
|
|
export interface PaletteOptions { |
|
hues?: number; |
|
shades?: number; |
|
factor?: number; |
|
op?: "brighten" | "darken" | "saturate" | "rotate"; |
|
} |
|
} |
|
|
|
export namespace Color { |
|
Color.undefined = Undefined; |
|
|
|
Color.ANSI = spaces.ANSI; |
|
Color.ANSI16M = spaces.ANSI16M; |
|
Color.ANSI256 = spaces.ANSI256; |
|
Color.APPLE = spaces.APPLE; |
|
Color.CMYK = spaces.CMYK; |
|
Color.GRAY = spaces.GRAY; |
|
Color.HCG = spaces.HCG; |
|
Color.HEX = spaces.HEX; |
|
Color.HEX3 = spaces.HEX3; |
|
Color.HEX4 = spaces.HEX4; |
|
Color.HEX6 = spaces.HEX6; |
|
Color.HEX8 = spaces.HEX8; |
|
Color.HSL = spaces.HSL; |
|
Color.HSV = spaces.HSV; |
|
Color.HWB = spaces.HWB; |
|
Color.KEYWORD = spaces.KEYWORD; |
|
Color.LAB = spaces.LAB; |
|
Color.LCH = spaces.LCH; |
|
Color.OKLAB = spaces.OKLAB; |
|
Color.OKLCH = spaces.OKLCH; |
|
Color.RGB = spaces.RGB; |
|
Color.XYZ = spaces.XYZ; |
|
|
|
export enum Order { |
|
ASC = 1, |
|
DESC = -1, |
|
} |
|
|
|
export type Comparator<T extends Colors = Colors> = (a: T, b: T) => number; |
|
|
|
export enum Strategy { |
|
/** Sorts by the colors' chroma value (`c` of `LCH`). */ |
|
default = "hsl", |
|
/** Sorts by the colors' relative contrast values (YIQ). */ |
|
Contrast = "yiq", |
|
/** Sorts by the colors' distance using the {@link deltaE00} formula. */ |
|
Distance = "distance", |
|
/** Sorts by the colors' relative luminance value. */ |
|
Luminance = "luminance", |
|
/** Sorts colors alphabetically by their closest matching {@link name}. */ |
|
Name = "name", |
|
/** Sorts by the colors' chroma value (`c` of `LCH`). */ |
|
Chroma = "lch.c", |
|
/** Sorts by the colors' grayscale value (`g` of `gray`). */ |
|
Grayscale = "gray.g", |
|
/** Sorts by the colors' hue value (`h` of {@link HSL}). */ |
|
Hue = "hsl.h", |
|
/** Sorts by the colors' saturation value (`s` of {@link HSL}). */ |
|
Saturation = "hsl.s", |
|
/** Sorts by the colors' lightness value (`l` of {@link HSL}). */ |
|
Lightness = "hsl.l", |
|
/** Sorts by the colors' relative brightness value (`v` of {@link HSV}). */ |
|
Brightness = "hsv.v", |
|
/** Sorts by the colors' hue, then saturation, then lightness values. */ |
|
HSL = "hsl", |
|
/** Sorts by the colors' red component value (`r` of {@link RGB}). */ |
|
Red = "rgb.r", |
|
/** Sorts by the colors' green component value (`g` of {@link RGB}). */ |
|
Green = "rgb.g", |
|
/** Sorts by the colors' blue component value (`b` of {@link RGB}). */ |
|
Blue = "rgb.b", |
|
/** Sorts by the colors' alpha component value (`a` of {@link RGBA}). */ |
|
Alpha = "rgb.a", |
|
} |
|
|
|
const nonEnumerableSchemas = nonEnumerableProperties(Color.schemas); |
|
Object.defineProperties(Color.schemas, nonEnumerableSchemas); |
|
|
|
type UnionToIntersection<Union> = |
|
(Union extends any ? (argument: Union) => void : never) extends |
|
(argument: infer Intersection) => void ? Intersection : never; |
|
|
|
for (const k of Object.keys(schemas) as (keyof typeof schemas)[]) { |
|
const { schema } = schemas[k]; |
|
const space = spaces[k]; |
|
type space = UnionToIntersection<typeof Color[typeof k]>; |
|
Color[k] = space as unknown as space; |
|
if (k !== "HEX") extendBase(Color[k] as any, k, schema); |
|
} |
|
|
|
Object.defineProperties(Color, { |
|
Strategy: { enumerable: false, configurable: true }, |
|
Order: { enumerable: false, configurable: true }, |
|
undefined: { |
|
enumerable: false, |
|
configurable: false, |
|
writable: false, |
|
value: Undefined, |
|
}, |
|
}); |
|
|
|
export interface CIELAB { |
|
l: number; |
|
a: number; |
|
b: number; |
|
alpha: number; |
|
} |
|
|
|
export namespace Convert { |
|
export function XYZtoLAB(xyz: Color.XYZ, ref?: Illuminant): Color.LAB { |
|
ref ??= Color.illuminant; |
|
let X = xyz.x * 100, Y = xyz.y * 100, Z = xyz.z * 100; |
|
X /= ref.x, Y /= ref.y, Z /= ref.z; |
|
const f = (v = 0) => v > 8.856e-3 ? v ** (1 / 3) : 7.787 * v + 16 / 116; |
|
X = f(X), Y = f(Y), Z = f(Z); |
|
let l = 116 * Y - 16, a = 500 * (X - Y), b = 200 * (Y - Z); |
|
const { |
|
l: [l_min, l_max], |
|
a: [a_min, a_max], |
|
b: [b_min, b_max], |
|
} = schema("LAB"); |
|
l = Color.clamp(l, l_min, l_max); |
|
a = Color.clamp(a, a_min, a_max); |
|
b = Color.clamp(b, b_min, b_max); |
|
return new Color.LAB(l, a, b, xyz.a ?? 1); |
|
} |
|
|
|
export function LABtoXYZ(cie: CIELAB, ref?: Illuminant): Color.XYZ { |
|
ref ??= Color.illuminant; |
|
let Y = (cie.l + 16) / 116, X = cie.a / 500 + Y, Z = Y - cie.b / 200; |
|
const f = (v = 0, v2 = v ** 3) => |
|
v2 > 8.856e-3 ? v2 : (v - 16 / 116) / 7.787; |
|
X = ref.x * f(X), Y = ref.y * f(Y), Z = ref.z * f(Z); |
|
|
|
return new Color.XYZ(X / 100, Y / 100, Z / 100, cie.alpha ?? 1); |
|
} |
|
} |
|
|
|
export namespace Format { |
|
export function RGB(color: Color.RGB | Color): string { |
|
if (color instanceof Color) { |
|
color = color.rgba; |
|
} else if (!(color instanceof Color.RGB)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.RGB for 'color'`, |
|
); |
|
} |
|
const { r, g, b, a } = color; |
|
if (a === 1) return `rgb(${r & 0xFF}, ${g & 0xFF}, ${b & 0xFF})`; |
|
return Format.RGBA(color); |
|
} |
|
|
|
export function RGBA(color: Color.RGB | Color): string { |
|
if (color instanceof Color) { |
|
color = color.rgba; |
|
} else if (!(color instanceof Color.RGB)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.RGB for 'color'`, |
|
); |
|
} |
|
const { r, g, b, a } = color; |
|
return `rgba(${r & 0xFF}, ${g & 0xFF}, ${b & 0xFF}, ${+a.toFixed(2)})`; |
|
} |
|
|
|
export function HSL(color: Color.HSL | Color): string { |
|
if (color instanceof Color) { |
|
color = color.hsla; |
|
} else if (!(color instanceof Color.HSL)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.HSL for 'color'`, |
|
); |
|
} |
|
const { h, s, l, a } = color; |
|
const H = h & 360, S = s * 100, L = l * 100; |
|
if (a === 1) { |
|
return `hsl(${H.toFixed(2)}, ${S.toFixed(2)}%, ${L.toFixed(2)}%)`; |
|
} |
|
return Format.HSLA(color); |
|
} |
|
|
|
export function HSLA(color: Color.HSL | Color): string { |
|
if (color instanceof Color) { |
|
color = color.hsla; |
|
} else if (!(color instanceof Color.HSL)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.HSL for 'color'`, |
|
); |
|
} |
|
const { h, s, l, a } = color; |
|
const H = h & 360, S = s * 100, L = l * 100, A = a.toFixed(2); |
|
return `hsla(${H.toFixed(2)}, ${S.toFixed(2)}%, ${L.toFixed(2)}%, ${A})`; |
|
} |
|
|
|
export function HSV(color: Color.HSV | Color): string { |
|
if (color instanceof Color) { |
|
color = color.hsva; |
|
} else if (!(color instanceof Color.HSV)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.HSV for 'color'`, |
|
); |
|
} |
|
const { h, s, v, a } = color; |
|
const H = h & 360, S = s * 100, V = v * 100; |
|
if (a === 1) { |
|
return `hsv(${H.toFixed(2)}, ${S.toFixed(2)}%, ${V.toFixed(2)}%)`; |
|
} |
|
return Format.HSVA(color); |
|
} |
|
|
|
export function HSVA(color: Color.HSV | Color): string { |
|
if (color instanceof Color) { |
|
color = color.hsva; |
|
} else if (!(color instanceof Color.HSV)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.HSV for 'color'`, |
|
); |
|
} |
|
const { h, s, v, a } = color; |
|
const H = (h & 360).toFixed(2), |
|
S = (s & 100).toFixed(2), |
|
V = (v & 100).toFixed(2), |
|
A = a.toFixed(2); |
|
return `hsva(${H}, ${S}%, ${V}%, ${A})`; |
|
} |
|
|
|
export function XYZ(color: Color.XYZ | Color): string { |
|
if (color instanceof Color) { |
|
color = color.xyz; |
|
} else if (!(color instanceof Color.XYZ)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.XYZ for 'color'`, |
|
); |
|
} |
|
const { x, y, z, a } = color; |
|
if (a === 1) { |
|
return `xyz(${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)})`; |
|
} |
|
return Format.XYZA(color); |
|
} |
|
|
|
export function XYZA(color: Color.XYZ | Color): string { |
|
if (color instanceof Color) { |
|
color = color.xyz; |
|
} else if (!(color instanceof Color.XYZ)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.XYZ for 'color'`, |
|
); |
|
} |
|
const { x, y, z, a } = color; |
|
return `xyza(${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)}, ${ |
|
a.toFixed(2) |
|
})`; |
|
} |
|
|
|
export function LAB(color: Color.LAB | Color): string { |
|
if (color instanceof Color) { |
|
color = color.lab; |
|
} else if (!(color instanceof Color.LAB)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.LAB for 'color'`, |
|
); |
|
} |
|
const { l, a, b, alpha } = color; |
|
if (alpha === 1) { |
|
return `lab(${l.toFixed(2)}, ${a.toFixed(2)}, ${b.toFixed(2)})`; |
|
} |
|
return Format.LABA(color); |
|
} |
|
|
|
export function LABA(color: Color.LAB | Color): string { |
|
if (color instanceof Color) { |
|
color = color.lab; |
|
} else if (!(color instanceof Color.LAB)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.LAB for 'color'`, |
|
); |
|
} |
|
const { l, a, b, alpha } = color; |
|
return `laba(${l.toFixed(2)}, ${a.toFixed(2)}, ${b.toFixed(2)}, ${ |
|
alpha.toFixed(2) |
|
})`; |
|
} |
|
|
|
export function LCH(color: Color.LCH | Color): string { |
|
if (color instanceof Color) { |
|
color = color.lch; |
|
} else if (!(color instanceof Color.LCH)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.LCH for 'color'`, |
|
); |
|
} |
|
const { l, c, h, alpha } = color; |
|
if (alpha === 1) { |
|
return `lch(${l.toFixed(2)}, ${c.toFixed(2)}, ${h.toFixed(2)})`; |
|
} |
|
return Format.LCHA(color); |
|
} |
|
|
|
export function LCHA(color: Color.LCH | Color): string { |
|
if (color instanceof Color) { |
|
color = color.lch; |
|
} else if (!(color instanceof Color.LCH)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.LCH for 'color'`, |
|
); |
|
} |
|
const { l, c, h, alpha } = color; |
|
return `lcha(${l.toFixed(2)}, ${c.toFixed(2)}, ${h.toFixed(2)}, ${ |
|
alpha.toFixed(2) |
|
})`; |
|
} |
|
|
|
export function HWB(color: Color.HWB | Color): string { |
|
if (color instanceof Color) { |
|
color = color.hwb; |
|
} else if (!(color instanceof Color.HWB)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.HWB for 'color'`, |
|
); |
|
} |
|
const { h, w, b, a } = color; |
|
if (a === 1) { |
|
return `hwb(${h.toFixed(2)}, ${w.toFixed(2)}, ${b.toFixed(2)})`; |
|
} |
|
return Format.HWBA(color); |
|
} |
|
|
|
export function HWBA(color: Color.HWB | Color): string { |
|
if (color instanceof Color) { |
|
color = color.hwb; |
|
} else if (!(color instanceof Color.HWB)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.HWB for 'color'`, |
|
); |
|
} |
|
const { h, w, b, a } = color; |
|
return `hwba(${h.toFixed(2)}, ${w.toFixed(2)}, ${b.toFixed(2)}, ${ |
|
a.toFixed(2) |
|
})`; |
|
} |
|
|
|
export function CMYK(color: Color.CMYK | Color): string { |
|
if (color instanceof Color) { |
|
color = color.cmyk; |
|
} else if (!(color instanceof Color.CMYK)) { |
|
throw new TypeError( |
|
`Expected an instance of Color or Color.CMYK for 'color'`, |
|
); |
|
} |
|
const { c, m, y, k } = color; |
|
return `cmyk(${c.toFixed(2)}, ${m.toFixed(2)}, ${y.toFixed(2)}, ${ |
|
k.toFixed(2) |
|
})`; |
|
} |
|
|
|
function formatByte(n: number): string { |
|
n = (+n >>> 0) & 0xFF; // coerce to unsigned 8-bit integer |
|
return n.toString(16).padStart(2, "0"); |
|
} |
|
|
|
/** |
|
* Formats the color as #RRGGBB |
|
*/ |
|
export function Hex(color: Color): string { |
|
const { r, g, b, a } = color.rgba; |
|
return `#${formatByte(r)}${formatByte(g)}${formatByte(b)}${ |
|
a === 1 ? "" : formatByte(round(a * 255)) |
|
}`; |
|
} |
|
|
|
/** |
|
* Formats the color as #RRGGBBAA |
|
* If 'compact' is set, colors without transparancy will be printed as #RRGGBB |
|
*/ |
|
export function HexA(color: Color, compact = false): string { |
|
const { r, g, b, a } = color.rgba; |
|
if (compact && a === 1) return Color.Format.Hex(color); |
|
|
|
return `#${formatByte(r)}${formatByte(g)}${formatByte(b)}${ |
|
formatByte(round(a * 255)) |
|
}`; |
|
} |
|
|
|
/** |
|
* Converts an Hex color value to a Color. |
|
* @param hex string (#RGB, #RGB, #RRGGBB or #RRGGBBAA). |
|
* @returns `r`, `g`, `b` in range `[0, 255]`, `a` in range `[0, 1]`. |
|
* @returns `null` if the hex value is invalid. |
|
*/ |
|
export function parseHex(hex: string): Color | null { |
|
const { length } = hex; |
|
|
|
if (length < 3) return null; |
|
// does not begin with a #. be nice and add one. |
|
if (hex.charCodeAt(0) !== 35) return parseHex(`#${hex}`); |
|
|
|
switch (length) { |
|
// #RGB format |
|
case 4: { |
|
const r = parseHexDigit(hex.charCodeAt(1)); |
|
const g = parseHexDigit(hex.charCodeAt(2)); |
|
const b = parseHexDigit(hex.charCodeAt(3)); |
|
return new Color( |
|
new spaces.RGB(16 * r * 2, 16 * g * 2, 16 * b * 2, 1), |
|
); |
|
} |
|
// #RGB format |
|
case 5: { |
|
const r = parseHexDigit(hex.charCodeAt(1)); |
|
const g = parseHexDigit(hex.charCodeAt(2)); |
|
const b = parseHexDigit(hex.charCodeAt(3)); |
|
const a = parseHexDigit(hex.charCodeAt(4)); |
|
return new Color( |
|
new spaces.RGB( |
|
16 * r * 2, |
|
16 * g * 2, |
|
16 * b * 2, |
|
(16 * a * 2) / 255, |
|
), |
|
); |
|
} |
|
// #RRGGBB format |
|
case 7: { |
|
const r = 16 * parseHexDigit(hex.charCodeAt(1)) + |
|
parseHexDigit(hex.charCodeAt(2)); |
|
const g = 16 * parseHexDigit(hex.charCodeAt(3)) + |
|
parseHexDigit(hex.charCodeAt(4)); |
|
const b = 16 * parseHexDigit(hex.charCodeAt(5)) + |
|
parseHexDigit(hex.charCodeAt(6)); |
|
return new Color(new spaces.RGB(r, g, b, 1)); |
|
} |
|
// #RRGGBBAA format |
|
case 9: { |
|
const r = 16 * parseHexDigit(hex.charCodeAt(1)) + |
|
parseHexDigit(hex.charCodeAt(2)); |
|
const g = 16 * parseHexDigit(hex.charCodeAt(3)) + |
|
parseHexDigit(hex.charCodeAt(4)); |
|
const b = 16 * parseHexDigit(hex.charCodeAt(5)) + |
|
parseHexDigit(hex.charCodeAt(6)); |
|
const a = 16 * parseHexDigit(hex.charCodeAt(7)) + |
|
parseHexDigit(hex.charCodeAt(8)); |
|
return new Color(new spaces.RGB(r, g, b, a / 255)); |
|
} |
|
default: |
|
return null; |
|
} |
|
} |
|
|
|
export function parseHexDigit(ch: number): number { |
|
if (ch >= 48 && ch <= 57) return ch - 48; |
|
if (ch >= 97 && ch <= 102) ch -= 32; // A-F -> a-f |
|
if (ch >= 65 && ch <= 70) return ch - (65 - 10); |
|
return 0; // for invalid characters, return 0 |
|
} |
|
} |
|
|
|
type strings = string & {}; |
|
|
|
export type FormatType = strings | keyof spaces | Lowercase<keyof spaces>; |
|
|
|
/** The default format will use HEX if opaque and RGB otherwise. */ |
|
export function format( |
|
color: Color, |
|
type: FormatType = "hex8", |
|
): string { |
|
type = type.toUpperCase() as Uppercase<FormatType>; |
|
switch (type) { |
|
case "HEX": |
|
return Format.Hex(color); |
|
case "HEX3": |
|
return Format.HexA(color, true); |
|
case "HEX4": |
|
return Format.HexA(color); |
|
case "HEX6": |
|
return Format.Hex(color); |
|
case "HEX8": |
|
return Format.HexA(color); |
|
case "RGB": |
|
return Format.RGB(color); |
|
case "RGBA": |
|
return Format.RGBA(color); |
|
case "HSL": |
|
return Format.HSL(color); |
|
case "HSLA": |
|
return Format.HSLA(color); |
|
case "HSV": |
|
return Format.HSV(color); |
|
case "HSVA": |
|
return Format.HSVA(color); |
|
case "HWB": |
|
return Format.HWB(color); |
|
case "HCG": |
|
return HCG.fromRGB(color.rgb).toString(); |
|
case "CMYK": |
|
return Format.CMYK(color); |
|
case "LAB": |
|
return Format.LAB(color); |
|
case "LCH": |
|
return Format.LCH(color); |
|
case "XYZ": |
|
return Format.XYZ(color); |
|
case "OKLAB": |
|
return OKLAB.fromLAB(color.lab).toString(); |
|
case "OKLCH": |
|
return OKLCH.fromLCH(color.lch).toString(); |
|
case "ANSI": |
|
return ANSI.fromRGB(color.rgb).toString(); |
|
case "ANSI16M": |
|
return ANSI16M.fromRGB(color.rgb).toString(); |
|
case "ANSI256": |
|
return ANSI256.fromRGB(color.rgb).toString(); |
|
case "APPLE": |
|
return APPLE.fromRGB(color.rgb).toString(); |
|
case "GRAY": |
|
return GRAY.fromRGB(color.rgb).toString(); |
|
default: { |
|
if (color.isOpaque()) return Format.HexA(color, true); |
|
return Format.RGBA(color); |
|
} |
|
} |
|
} |
|
|
|
export namespace RegExp { |
|
export const RGB = |
|
/^(?<type>rgba?)\(\s*(?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)\s*(?:,\s*(?<a>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const HSL = |
|
/^(?<type>hsla?)\(\s*(?<h>\d+)\s*,\s*(?<s>\d*(?:\.\d+)?)%\s*,\s*(?<l>\d*(?:\.\d+)?)%\s*(?:,\s*(?<a>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const HSV = |
|
/^(?<type>hsva?)\(\s*(?<h>\d+)\s*,\s*(?<s>\d*(?:\.\d+)?)%\s*,\s*(?<v>\d*(?:\.\d+)?)%\s*(?:,\s*(?<a>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const XYZ = |
|
/^(?<type>xyz)\(\s*(?<x>\d*(?:\.\d+)?)\s*,\s*(?<y>\d*(?:\.\d+)?)\s*,\s*(?<z>\d*(?:\.\d+)?)\s*(?:,\s*(?<a>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const LAB = |
|
/^(?<type>lab)\(\s*(?<l>\d*(?:\.\d+)?)\s*,\s*(?<a>\d*(?:\.\d+)?)\s*,\s*(?<b>\d*(?:\.\d+)?)\s*(?:,\s*(?<alpha>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const LCH = |
|
/^(?<type>lch)\(\s*(?<l>\d*(?:\.\d+)?)\s*,\s*(?<c>\d*(?:\.\d+)?)\s*,\s*(?<h>\d*(?:\.\d+)?)\s*(?:,\s*(?<alpha>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const OKLAB = |
|
/^(?<type>oklab)\(\s*(?<l>\d*(?:\.\d+)?)\s*,\s*(?<a>\d*(?:\.\d+)?)\s*,\s*(?<b>\d*(?:\.\d+)?)\s*(?:,\s*(?<alpha>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const OKLCH = |
|
/^(?<type>oklch)\(\s*(?<l>\d*(?:\.\d+)?)\s*,\s*(?<c>\d*(?:\.\d+)?)\s*,\s*(?<h>\d*(?:\.\d+)?)\s*(?:,\s*(?<alpha>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const HWB = |
|
/^(?<type>hwb)\(\s*(?<h>\d+)\s*,\s*(?<w>\d*(?:\.\d+)?)%\s*,\s*(?<b>\d*(?:\.\d+)?)%\s*(?:,\s*(?<a>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const CMYK = |
|
/^(?<type>(?:device-)?cmyk)\(\s*(?<c>\d*(?:\.\d+)?)\s*,\s*(?<m>\d*(?:\.\d+)?)\s*,\s*(?<y>\d*(?:\.\d+)?)\s*,\s*(?<k>\d*(?:\.\d+)?)\s*(?:,\s*(?<a>\d*(?:\.\d+)?))?\s*\)$/i; |
|
export const HEX4 = |
|
/^#?(?<r>[0-9A-F])(?<g>[0-9A-F])(?<b>[0-9A-F])(?<a>[0-9A-F])?$/i; |
|
export const HEX8 = |
|
/^#?(?<r>[0-9A-F]{2})(?<g>[0-9A-F]{2})(?<b>[0-9A-F]{2})(?<a>[0-9A-F]{2})?$/i; |
|
export const ANSI = |
|
// deno-lint-ignore no-control-regex |
|
/(?<=\x1b\[(?:\d+;)*?)(?<ansi>(?:[349]|10)[0-7]|)(?=(?:;\d+)*m\b)/g; |
|
export const NAME = new globalThis.RegExp( |
|
`^(?<name>transparent|${Object.keys(names2colors).join("|")})$`, |
|
"i", |
|
); |
|
|
|
export const RGBA = RGB, HSLA = HSL, HSVA = HSV, XYZA = XYZ, LABA = LAB; |
|
export const LCHA = LCH, HWBA = HWB, CMYKA = CMYK; |
|
|
|
Object.defineProperties( |
|
RegExp, |
|
[ |
|
"RGB", |
|
"HSL", |
|
"HSV", |
|
"XYZ", |
|
"LAB", |
|
"LCH", |
|
"HWB", |
|
"OKLAB", |
|
"OKLCH", |
|
"HEX4", |
|
"HEX8", |
|
"NAME", |
|
"ANSI", |
|
"CMYK", |
|
"RGBA", |
|
"HSLA", |
|
"HSVA", |
|
"XYZA", |
|
"LABA", |
|
"LCHA", |
|
"HWBA", |
|
"CMYKA", |
|
].reduce( |
|
(o, k) => ({ |
|
...o, |
|
[k]: { enumerable: false, configurable: true, writable: false }, |
|
}), |
|
Object.create(null), |
|
), |
|
); |
|
} |
|
|
|
export const names = Object.defineProperties( |
|
<K extends ColorNames>(name: K): Color => { |
|
if ( |
|
name in Color.names && |
|
Color.names[name as ColorNames] != null |
|
) { |
|
return Color.names[name]; |
|
} else { |
|
throw new Error(`Unknown color name: ${name}`); |
|
} |
|
}, |
|
Object.entries(names2colors).reduce( |
|
(o, [k, v]) => { |
|
o[k as keyof typeof o] = { |
|
value: Color.fromHex(v), |
|
enumerable: true, |
|
configurable: true, |
|
writable: false, |
|
}; |
|
return o; |
|
}, |
|
{ |
|
[inspect.custom]: { |
|
value: function ( |
|
this: { cache: WeakMap<typeof Color, typeof Color.names> }, |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
if (depth === null || depth < 0) { |
|
return options.stylize(`[Color.names]`, "special"); |
|
} |
|
const obj = this.cache.get(Color) ?? this.cache |
|
.set(Color, { ...Color.names } as typeof Color.names) |
|
.get(Color)!; |
|
Reflect.deleteProperty(obj, inspect.custom); // avoid recursion |
|
return `${options.stylize("Color.names", "special")} ${ |
|
inspect(obj, { |
|
...options, |
|
depth: 1, |
|
colors: true, |
|
compact: 3, |
|
getters: true, |
|
}) |
|
}`; |
|
}.bind({ cache: new WeakMap() }), |
|
}, |
|
} as unknown as Record<ColorNames, PropertyDescriptor>, |
|
), |
|
) as unknown as ( |
|
& { <K extends ColorNames>(name: K): Color } |
|
& { readonly [K in ColorNames]: Color } |
|
); |
|
} |
|
// #endregion Color |
|
|
|
export type Colors = |
|
| Color |
|
| ANSI |
|
| ANSI16M |
|
| ANSI256 |
|
| APPLE |
|
| CMYK |
|
| GRAY |
|
| HCG |
|
| HEX |
|
| HEX3 |
|
| HEX4 |
|
| HEX6 |
|
| HEX8 |
|
| HSV |
|
| HSL |
|
| HWB |
|
| LAB |
|
| LCH |
|
| OKLAB |
|
| OKLCH |
|
| RGB |
|
| XYZ |
|
| string; |