Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active January 25, 2024 06:56
Show Gist options
  • Save nberlette/f44ed05a43b18bc58902f614eb3e9bd2 to your computer and use it in GitHub Desktop.
Save nberlette/f44ed05a43b18bc58902f614eb3e9bd2 to your computer and use it in GitHub Desktop.
`Color`: TypeScript Color Conversion and Manipulation Tools

Color Conversion and Manipulation Tools for TypeScript

Usage

If you're using Deno, or Bun with the Node flag for --experimental-network-imports enabled, you can import the source code directly from this gist:

import {
  Color,
  HSV,
  RGB,
  XYZ,
} from "https://gist.githubusercontent.com/nberlette/f44ed05a43b18bc58902f614eb3e9bd2/raw/color.ts";

Examples

const red = new Color("indianred");

// color spaces available as classes, each of which is represented by a corresponding memoized property on every Color instance
let rgb = red.rgba, // RGBA { r: 205, g: 92, b: 92, a: 1 }
  hsv = red.hsva, // HSVA { h: 0, s: 0.53, v: 0.8, a: 1 }
  hsl = red.hsla, // HSLA { h: 0, s: 0.53, l: 0.58, a: 1 }
  xyz = red.xyz, // XYZ { x: 0.412, y: 0.212, z: 0.019 }
  lab = red.lab, // LAB { l: 0.532, a: 0.498, b: 0.366 }
  lch = red.lch, // LCH { l: 0.532, c: 0.635, h: 0.982 }
  hwb = red.hwb, // HWB { h: 0, w: 0.2, b: 0.2 }
  cmyk = red.cmyk; // CMYK { c: 0, m: 0.55, y: 0.55, k: 0.2 }

// color spaces have their own methods for conversion between each other

// and for conversion to strings or json
console.log(hsl.toString()); // "hsl(0, 53.10%, 58.20%)"
console.log(JSON.stringify(hsl)); // '{"h":0,"s":0.531,"l":0.58,"a":1}'

// Color has many static methods for working with colors, like creating palettes:
const palette = Color.palette(red, 5, 5); // 5 hues, 5 shades each

// or for creating random colors:
const random = Color.random(); // random color

// or changing an existing color:
if (red.isDark()) Color.brighten(red, 10); // creates a new color

// red is unchanged by the above method. this one, however...:
if (red.isDark()) red.brighten(10); // changes red's values in-place

// convert to other formats
red.hsl.toHEX(); // "#cd5c5c"

// or from another color space, using a static method:
RGB.fromHEX("#cd5c5c");

// or to ANSI escape codes, for easy terminal usage:
red.toAnsi24(); // "\\u001b[38;2;205;92;92m"

API

Class: Color

Constructor: new Color(color: Color)

Creates a new Color instance from a string or another Color instance.

Static Methods and Properties

Color.palette

Creates a palette of colors from a base color, with a specified number of hues and shades.

Color.palette(color: Color, hues: number, shades: number): Color[][];

Color.random

Creates a random color.

Color.random(): Color;

Color.brighten

Creates a new color that is brighter than the given color.

Color.brighten(color: Color, amount: number): Color;

Color.darken

Creates a new color that is darker than the given color.

Color.darken(color: Color, amount: number): Color;

Color.isDark

Returns true if the given color is dark.

Color.isDark(color: Color): color is Color.Dark;

Color.isLight

Returns true if the given color is light.

Color.isLight(color: Color): color is Color.Light;

Color.rotate

Creates a new color that is rotated around the color wheel by the given amount.

Color.rotate(color: Color, amount: number): Color;

Color.saturate

Creates a new color that is more saturated than the given color.

Color.saturate(color: Color, amount: number): Color;

Color.desaturate

Creates a new color that is less saturated than the given color.

Color.desaturate(color: Color, amount: number): Color;

Color.mix

Creates a new color that is a mix of two colors.

Color.mix(color1: Color, color2: Color, amount: number): Color;

Color.invert

Creates a new color that is the inverse of the given color.

Color.invert(color: Color): Color;

Color.contrast

Creates a new color that has a contrast ratio of at least 4.5:1 with the given color.

Color.contrast(color: Color): Color;

Color.triad

Creates a tuple of three colors based on a given color, with a distance of 120° between each color.

Color.triad(color: Color): [a: Color, b: Color, c: Color];

Color.tetrad

Creates a tuple of four colors based on a given color, with a distance of 90° between each color.

Color.tetrad(color: Color): [a: Color, b: Color, c: Color, d: Color];

Color.complement

Creates a new color that is the complement of the given color.

Color.complement(color: Color): Color;

Color.splitComplement

Creates a tuple of three colors based on a given color, with a distance of 150° between each color.

Color.splitComplement(color: Color): [a: Color, b: Color, c: Color];

Color.analogous

Creates a tuple of three colors based on a given color, with a distance of 30° between each color.

Color.analogous(color: Color, steps?: number, slices?: number): [a: Color, b: Color, c: Color];

Color.monochromatic

Creates a tuple of three colors based on a given color, with a distance of 60° between each color.

Color.monochromatic(color: Color): [a: Color, b: Color, c: Color];

Color.fromHex

Creates a new color from a hex string.

Color.fromHex(hex: string): Color;

Color.names

Maps CSS color names to Color instances, created from their corresponding hex values.

Color.names: {

}

Instance Methods and Properties

get rgba(): RGB

Returns the color as an RGB instance.

const color = Color.names.indianred;
color.rgba; // RGB { r: 205, g: 92, b: 92, a: 1 }

Compatibility

  • Deno
  • Bun
  • Node (w/ compilation)
  • Browser (w/ compilation)
export enum Char {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
/**
* The &nbsp; (no-break space) character.
* Unicode Character 'NO-BREAK SPACE' (U+00A0)
*/
NoBreakSpace = 160,
U_Combining_Grave_Accent = 768,
U_Combining_Acute_Accent = 769,
U_Combining_Circumflex_Accent = 770,
U_Combining_Tilde = 771,
U_Combining_Macron = 772,
U_Combining_Overline = 773,
U_Combining_Breve = 774,
U_Combining_Dot_Above = 775,
U_Combining_Diaeresis = 776,
U_Combining_Hook_Above = 777,
U_Combining_Ring_Above = 778,
U_Combining_Double_Acute_Accent = 779,
U_Combining_Caron = 780,
U_Combining_Vertical_Line_Above = 781,
U_Combining_Double_Vertical_Line_Above = 782,
U_Combining_Double_Grave_Accent = 783,
U_Combining_Candrabindu = 784,
U_Combining_Inverted_Breve = 785,
U_Combining_Turned_Comma_Above = 786,
U_Combining_Comma_Above = 787,
U_Combining_Reversed_Comma_Above = 788,
U_Combining_Comma_Above_Right = 789,
U_Combining_Grave_Accent_Below = 790,
U_Combining_Acute_Accent_Below = 791,
U_Combining_Left_Tack_Below = 792,
U_Combining_Right_Tack_Below = 793,
U_Combining_Left_Angle_Above = 794,
U_Combining_Horn = 795,
U_Combining_Left_Half_Ring_Below = 796,
U_Combining_Up_Tack_Below = 797,
U_Combining_Down_Tack_Below = 798,
U_Combining_Plus_Sign_Below = 799,
U_Combining_Minus_Sign_Below = 800,
U_Combining_Palatalized_Hook_Below = 801,
U_Combining_Retroflex_Hook_Below = 802,
U_Combining_Dot_Below = 803,
U_Combining_Diaeresis_Below = 804,
U_Combining_Ring_Below = 805,
U_Combining_Comma_Below = 806,
U_Combining_Cedilla = 807,
U_Combining_Ogonek = 808,
U_Combining_Vertical_Line_Below = 809,
U_Combining_Bridge_Below = 810,
U_Combining_Inverted_Double_Arch_Below = 811,
U_Combining_Caron_Below = 812,
U_Combining_Circumflex_Accent_Below = 813,
U_Combining_Breve_Below = 814,
U_Combining_Inverted_Breve_Below = 815,
U_Combining_Tilde_Below = 816,
U_Combining_Macron_Below = 817,
U_Combining_Low_Line = 818,
U_Combining_Double_Low_Line = 819,
U_Combining_Tilde_Overlay = 820,
U_Combining_Short_Stroke_Overlay = 821,
U_Combining_Long_Stroke_Overlay = 822,
U_Combining_Short_Solidus_Overlay = 823,
U_Combining_Long_Solidus_Overlay = 824,
U_Combining_Right_Half_Ring_Below = 825,
U_Combining_Inverted_Bridge_Below = 826,
U_Combining_Square_Below = 827,
U_Combining_Seagull_Below = 828,
U_Combining_X_Above = 829,
U_Combining_Vertical_Tilde = 830,
U_Combining_Double_Overline = 831,
U_Combining_Grave_Tone_Mark = 832,
U_Combining_Acute_Tone_Mark = 833,
U_Combining_Greek_Perispomeni = 834,
U_Combining_Greek_Koronis = 835,
U_Combining_Greek_Dialytika_Tonos = 836,
U_Combining_Greek_Ypogegrammeni = 837,
U_Combining_Bridge_Above = 838,
U_Combining_Equals_Sign_Below = 839,
U_Combining_Double_Vertical_Line_Below = 840,
U_Combining_Left_Angle_Below = 841,
U_Combining_Not_Tilde_Above = 842,
U_Combining_Homothetic_Above = 843,
U_Combining_Almost_Equal_To_Above = 844,
U_Combining_Left_Right_Arrow_Below = 845,
U_Combining_Upwards_Arrow_Below = 846,
U_Combining_Grapheme_Joiner = 847,
U_Combining_Right_Arrowhead_Above = 848,
U_Combining_Left_Half_Ring_Above = 849,
U_Combining_Fermata = 850,
U_Combining_X_Below = 851,
U_Combining_Left_Arrowhead_Below = 852,
U_Combining_Right_Arrowhead_Below = 853,
U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 854,
U_Combining_Right_Half_Ring_Above = 855,
U_Combining_Dot_Above_Right = 856,
U_Combining_Asterisk_Below = 857,
U_Combining_Double_Ring_Below = 858,
U_Combining_Zigzag_Above = 859,
U_Combining_Double_Breve_Below = 860,
U_Combining_Double_Breve = 861,
U_Combining_Double_Macron = 862,
U_Combining_Double_Macron_Below = 863,
U_Combining_Double_Tilde = 864,
U_Combining_Double_Inverted_Breve = 865,
U_Combining_Double_Rightwards_Arrow_Below = 866,
U_Combining_Latin_Small_Letter_A = 867,
U_Combining_Latin_Small_Letter_E = 868,
U_Combining_Latin_Small_Letter_I = 869,
U_Combining_Latin_Small_Letter_O = 870,
U_Combining_Latin_Small_Letter_U = 871,
U_Combining_Latin_Small_Letter_C = 872,
U_Combining_Latin_Small_Letter_D = 873,
U_Combining_Latin_Small_Letter_H = 874,
U_Combining_Latin_Small_Letter_M = 875,
U_Combining_Latin_Small_Letter_R = 876,
U_Combining_Latin_Small_Letter_T = 877,
U_Combining_Latin_Small_Letter_V = 878,
U_Combining_Latin_Small_Letter_X = 879,
/**
* Unicode Character 'LINE SEPARATOR' (U+2028)
* http://www.fileformat.info/info/unicode/char/2028/index.htm
*/
LINE_SEPARATOR = 8232,
/**
* Unicode Character 'PARAGRAPH SEPARATOR' (U+2029)
* http://www.fileformat.info/info/unicode/char/2029/index.htm
*/
PARAGRAPH_SEPARATOR = 8233,
/**
* Unicode Character 'NEXT LINE' (U+0085)
* http://www.fileformat.info/info/unicode/char/0085/index.htm
*/
NEXT_LINE = 133,
// http://www.fileformat.info/info/unicode/category/Sk/list.htm
U_CIRCUMFLEX = 94,
U_GRAVE_ACCENT = 96,
U_DIAERESIS = 168,
U_MACRON = 175,
U_ACUTE_ACCENT = 180,
U_CEDILLA = 184,
U_MODIFIER_LETTER_LEFT_ARROWHEAD = 706,
U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 707,
U_MODIFIER_LETTER_UP_ARROWHEAD = 708,
U_MODIFIER_LETTER_DOWN_ARROWHEAD = 709,
U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 722,
U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 723,
U_MODIFIER_LETTER_UP_TACK = 724,
U_MODIFIER_LETTER_DOWN_TACK = 725,
U_MODIFIER_LETTER_PLUS_SIGN = 726,
U_MODIFIER_LETTER_MINUS_SIGN = 727,
U_BREVE = 728,
U_DOT_ABOVE = 729,
U_RING_ABOVE = 730,
U_OGONEK = 731,
U_SMALL_TILDE = 732,
U_DOUBLE_ACUTE_ACCENT = 733,
U_MODIFIER_LETTER_RHOTIC_HOOK = 734,
U_MODIFIER_LETTER_CROSS_ACCENT = 735,
U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 741,
U_MODIFIER_LETTER_HIGH_TONE_BAR = 742,
U_MODIFIER_LETTER_MID_TONE_BAR = 743,
U_MODIFIER_LETTER_LOW_TONE_BAR = 744,
U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 745,
U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 746,
U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 747,
U_MODIFIER_LETTER_UNASPIRATED = 749,
U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 751,
U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 752,
U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 753,
U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 754,
U_MODIFIER_LETTER_LOW_RING = 755,
U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 756,
U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 757,
U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 758,
U_MODIFIER_LETTER_LOW_TILDE = 759,
U_MODIFIER_LETTER_RAISED_COLON = 760,
U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 761,
U_MODIFIER_LETTER_END_HIGH_TONE = 762,
U_MODIFIER_LETTER_BEGIN_LOW_TONE = 763,
U_MODIFIER_LETTER_END_LOW_TONE = 764,
U_MODIFIER_LETTER_SHELF = 765,
U_MODIFIER_LETTER_OPEN_SHELF = 766,
U_MODIFIER_LETTER_LOW_LEFT_ARROW = 767,
U_GREEK_LOWER_NUMERAL_SIGN = 885,
U_GREEK_TONOS = 900,
U_GREEK_DIALYTIKA_TONOS = 901,
U_GREEK_KORONIS = 8125,
U_GREEK_PSILI = 8127,
U_GREEK_PERISPOMENI = 8128,
U_GREEK_DIALYTIKA_AND_PERISPOMENI = 8129,
U_GREEK_PSILI_AND_VARIA = 8141,
U_GREEK_PSILI_AND_OXIA = 8142,
U_GREEK_PSILI_AND_PERISPOMENI = 8143,
U_GREEK_DASIA_AND_VARIA = 8157,
U_GREEK_DASIA_AND_OXIA = 8158,
U_GREEK_DASIA_AND_PERISPOMENI = 8159,
U_GREEK_DIALYTIKA_AND_VARIA = 8173,
U_GREEK_DIALYTIKA_AND_OXIA = 8174,
U_GREEK_VARIA = 8175,
U_GREEK_OXIA = 8189,
U_GREEK_DASIA = 8190,
U_IDEOGRAPHIC_FULL_STOP = 12290,
U_LEFT_CORNER_BRACKET = 12300,
U_RIGHT_CORNER_BRACKET = 12301,
U_LEFT_BLACK_LENTICULAR_BRACKET = 12304,
U_RIGHT_BLACK_LENTICULAR_BRACKET = 12305,
U_OVERLINE = 8254,
/**
* UTF-8 BOM
* Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
* http://www.fileformat.info/info/unicode/char/feff/index.htm
*/
UTF8_BOM = 65279,
U_FULLWIDTH_SEMICOLON = 65307,
U_FULLWIDTH_COMMA = 65292,
}
export default Char;
// 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;
{
"compilerOptions": {
"noErrorTruncation": true
}
}
// deno-lint-ignore-file no-explicit-any
import util, { type InspectOptionsStylized } from "node:util";
export const { inspect } = util;
export type { InspectOptionsStylized };
export { type Template, tpl } from "./tpl.ts";
export type Id<T> = T;
export type EnumKeyFromValue<
E extends Record<string | number, string | number>,
V = E[keyof E],
> = keyof {
[K in keyof E as E[K] extends V ? K : never]: E[K];
};
export type Reshape<T, Deep = false> = T extends object ? {
[K in keyof T]: [Deep] extends [false] ? T[K] : Reshape<T[K]>;
}
: T;
export type UnionToIntersection<U> =
(U extends unknown ? (U: U) => 0 : never) extends (I: infer I) => 0 ? I
: never;
export type LastInUnion<U> =
UnionToIntersection<U extends unknown ? (U: U) => 0 : never> extends
(x: infer L) => 0 ? L : never;
type UTI<T> = (T extends T ? (params: T) => any : never) extends
(params: infer P) => any ? P
: never;
export type UnionToTuple<T, A extends any[] = []> =
UTI<T extends any ? () => T : never> extends () => infer R
? UnionToTuple<Exclude<T, R>, [...A, R]>
: A;
export type HasOwn<O extends object, P extends PropertyKey, T = unknown> =
Reshape<O & Record<P, P extends keyof O ? O[P] : T>>;
/**
* Like {@link Object.hasOwn} but with much stronger types. Internally this
* function uses {@link Object.hasOwn}, unless it's unavailable, in which case
* it falls back to using `Object.prototype.hasOwnProperty`.
*
* @template {object} O The parent object to check for the own property.
* @template {PropertyKey} P The own property key to check the object for.
* @template [T=unknown] The inferred / default type of the property value.
* @param {O} O The parent object to check for the own property.
* @param {P} P The own property key to check the object for.
* @returns {O is HasOwn<O, P, T>} `true` if the object has the own property.
*/
export function hasOwn<
const O extends object,
P extends PropertyKey,
T = unknown,
>(O: O, P: P): O is Reshape<O & Record<P, P extends keyof O ? O[P] : T>> {
if ("hasOwn" in Object && typeof Object.hasOwn === "function") {
return Object.hasOwn(O, P);
} else {
return Object.prototype.hasOwnProperty.call(O, P);
}
}
export const { round } = Math;
export function roundFloat(number: number, decimalPoints: number): number {
const decimal = Math.pow(10, decimalPoints + 1);
return round(number * decimal) / decimal;
}
const MEMOIZE_CACHE = Symbol("MEMOIZE_CACHE");
/**
* Simple decorator for memoizing a method or getter based on a cache key. If
* the target member is a method, the cache key is the first argument it is
* passed, serialized as JSON. If the target member is a getter, the value to
* serialize as its cache key can be specified as the first argument to the
* decorator (calling it as a decorator factory).
*
* @example
* ```ts
* class Color {
* @ memoize()
* equals(that: Color | null): boolean {
* return this === that;
* }
*
* @ memoize(this.rgba)
* get hex(): string {
* return RGB.toHexString(this.rgba);
* }
* }
* ```
*/
export function memoize<T extends object, V = unknown>(
target: T,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<V>,
): TypedPropertyDescriptor<V>;
export function memoize<
T extends object,
K extends keyof T | T[keyof T] = keyof T | T[keyof T],
V = unknown,
>(
cacheKey: K,
): (
target: T,
key: string | symbol,
descriptor: TypedPropertyDescriptor<V>,
) => TypedPropertyDescriptor<V>;
export function memoize<
T extends object,
K = keyof T | T[keyof T],
V = unknown,
>(
target: T | K,
key?: string | symbol,
descriptor?: TypedPropertyDescriptor<V>,
):
| TypedPropertyDescriptor<V>
| ((
target: T,
key: string | symbol,
descriptor: TypedPropertyDescriptor<V>,
) => TypedPropertyDescriptor<V>) {
if (target && descriptor && key) {
const method = descriptor.value;
if (typeof method !== "function") {
throw new TypeError("Memoize decorator can only be applied to methods");
}
const memoized = function (
this: T & { [MEMOIZE_CACHE]?: Map<unknown, unknown> },
...args: unknown[]
) {
const prefix = getCacheKeyPrefix.call(this, target as T, key as never);
const ck = JSON.stringify([prefix, ...args]);
const cache = this[MEMOIZE_CACHE] ??= new Map();
Object.defineProperty(this, MEMOIZE_CACHE, { enumerable: false });
if (!cache.has(ck)) cache.set(ck, method.apply(this, args));
return cache.get(ck);
};
return { ...descriptor, value: memoized as V };
} else {
const key = target;
return function (
target: T,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<V>,
): TypedPropertyDescriptor<V> {
const getter = descriptor.get;
if (typeof getter !== "function") {
throw new TypeError("Memoize decorator can only be applied to getters");
}
const memoized = function (
this: T & { [MEMOIZE_CACHE]?: Map<unknown, unknown> },
) {
const prefix = getCacheKeyPrefix.call(this, target, key as never);
const ck = JSON.stringify([prefix, propertyKey.toString()]);
const cache = this[MEMOIZE_CACHE] ??= new Map();
Object.defineProperty(this, MEMOIZE_CACHE, { enumerable: false });
if (!cache.has(ck)) cache.set(ck, getter.call(this));
return cache.get(ck);
};
return {
...descriptor,
get: memoized as () => V,
};
};
}
}
function getCacheKeyPrefix<
T extends object,
K extends keyof T | T[keyof T],
>(
this: Memoized<T, K> | void,
target: T,
key: K,
): string {
const { get: getter1, value: method1 } = Object.getOwnPropertyDescriptor(
target,
key as keyof T,
) ?? {};
const { get: getter2, value: method2 } = Object.getOwnPropertyDescriptor(
this,
key as keyof T,
) ?? {};
return JSON.stringify(
[getter1 ?? method1, getter2 ?? method2].filter((v) => v).map(
(fn) => typeof fn === "function" ? fn.call(this) : fn,
),
);
}
export type Memoized<T extends object, K extends keyof T | T[keyof T]> = T & {
[MEMOIZE_CACHE]?: Map<K, unknown>;
};
export function clearCache<
const T extends {
readonly [Symbol.metadata]: { [MEMOIZE_CACHE]: WeakMap<object, unknown> };
},
>(obj: T): boolean;
export function clearCache(obj: object): boolean;
export function clearCache(obj: object): boolean {
if (typeof obj === "object" && obj != null && !Array.isArray(obj)) {
try {
if (Symbol.metadata in obj) {
const metadata = obj[Symbol.metadata] as Record<PropertyKey, unknown>;
metadata[MEMOIZE_CACHE] = new WeakMap();
return true;
} else {
return false;
}
} catch {
return false;
}
} else {
return false;
}
}
/**
* Decorator for memoizing a method or getter based on a cache key. If called
* as a decorator factory, the first argument is used to create the cache key:
* if the key is a string and it is the name of an existing property on the
* target object, the value of that property is used as the cache key (so the
* decorated method or getter will return a cached value so long as the value
* of the specified property does not change. if the value changes, the cache
* is cleared and the method or getter will be called and cached again). If
* the key is a function, it is called with the target object as its only
* argument, and the return value of that function is used as the cache key.
*/
export function memo<const T, Key extends keyof T>(
this: T | void,
key: Key,
): {
<This extends T, Value extends (this: This) => unknown>(
target: Value,
context: ClassGetterDecoratorContext<This, Value>,
): Value | void;
<This extends T, Value extends (this: This, ...args: any[]) => any>(
target: Value,
context: ClassMethodDecoratorContext<This, Value>,
): Value | void;
};
export function memo<const T, Key extends string>(
this: T | void,
key: () => Key,
): {
<This extends T, Value extends (this: This) => unknown>(
target: Value,
context: ClassGetterDecoratorContext<This, Value>,
): Value | void;
<This extends T, Value extends (this: This, ...args: any[]) => any>(
target: Value,
context: ClassMethodDecoratorContext<This, Value>,
): Value | void;
};
export function memo<const This, Value extends (this: This) => unknown>(
target: Value,
context: ClassGetterDecoratorContext<This, Value>,
): Value | void;
export function memo<
const This,
Value extends (this: This, ...args: any[]) => any,
>(
target: Value,
context: ClassMethodDecoratorContext<This, Value>,
): Value | void;
export function memo(
target: unknown,
context?: ClassMethodDecoratorContext | ClassGetterDecoratorContext,
): unknown {
if (context === undefined) {
if (typeof target === "function") {
return memoizeDecoratorFactory(target());
} else if (
typeof target === "string" ||
typeof target === "symbol" ||
typeof target === "number"
) {
return memoizeDecoratorFactory(target);
}
} else {
return memoizeDecoratorFactory();
}
}
function memoizeDecoratorFactory<
const K extends PropertyKey | (() => PropertyKey),
Key extends PropertyKey = K extends PropertyKey ? K
: K extends ((...args: any) => infer R extends PropertyKey) ? R
: never,
>(cacheKey?: K): {
<This, Value extends (this: This) => unknown>(
target: Value,
context: ClassGetterDecoratorContext<This, Value>,
): Value | void;
<This, Value extends (this: This, ...args: unknown[]) => unknown>(
target: Value,
context: ClassMethodDecoratorContext<This, Value>,
): Value | void;
} {
return function memoizeDecorator<
This extends object,
Value extends (this: This, ...args: any) => any,
>(
target: Value,
context:
| ClassMethodDecoratorContext<This, Value>
| ClassGetterDecoratorContext<This, Value>,
) {
const { name, kind, metadata } = context;
metadata[MEMOIZE_CACHE] ??= new WeakMap();
const caches = metadata[MEMOIZE_CACHE] as WeakMap<This, Map<Key, unknown>>;
return function memoized(this: This) {
const cache = caches.get(this) ?? caches.set(this, new Map()).get(this)!;
let key: Key = null!;
if (
typeof cacheKey === "string" ||
typeof cacheKey === "number" ||
typeof cacheKey === "symbol"
) {
key = (cacheKey in this) ? (this as any)[cacheKey] : cacheKey;
} else if (typeof cacheKey === "function") {
key = cacheKey.call(this) as Key;
} else {
key = JSON.stringify([name.toString(), ...arguments]) as Key;
}
if (kind === "method") {
if (cache.has(key)) {
return cache.get(key);
} else {
const value = target.apply(this, arguments);
cache.set(key, value);
return value;
}
} else if (kind === "getter") {
if (cache.has(key)) {
return cache.get(key);
} else {
const value = target.call(this);
cache.set(key, value);
return value;
}
}
};
};
}
export function nonEnumerableProperties<
const T extends object,
const P extends readonly (keyof T)[],
>(o: T, ...props: P): {
[K in keyof T as P["length"] extends 0 ? K : K extends P[number] ? K : never]:
TypedPropertyDescriptor<T[K]>;
} {
const keys = Object.getOwnPropertyNames(o).filter((k) =>
props.length === 0 || props.includes(k as any)
);
return keys.reduce((o, k) => ({
...o,
[k]: {
...Object.getOwnPropertyDescriptor(o, k),
enumerable: false,
},
}), Object.create(null));
}
export interface Illuminant {
name: string;
x: number;
y: number;
z: number;
}
/** `A`: Incandescent/Tungsten */
export const A = {
name: "A",
info: "Incandescent/Tungsten",
x: 109.85,
y: 100,
z: 35.585,
} as const;
export type A = typeof A;
/** `B`: Old direct sunlight at noon */
export const B = {
name: "B",
info: "Old direct sunlight at noon",
x: 99.0927,
y: 100,
z: 85.313,
} as const;
export type B = typeof B;
/** `C`: Old daylight */
export const C = {
name: "C",
info: "Old daylight",
x: 98.074,
y: 100,
z: 118.232,
} as const;
export type C = typeof C;
/** `D50`: ICC profile PCS */
export const D50 = {
name: "D50",
info: "ICC profile PCS",
x: 96.422,
y: 100,
z: 82.521,
} as const;
export type D50 = typeof D50;
/** `D55`: Mid-morning daylight */
export const D55 = {
name: "D55",
info: "Mid-morning daylight",
x: 95.682,
y: 100,
z: 92.149,
} as const;
export type D55 = typeof D55;
/** `D65`: Daylight, sRGB, Adobe-RGB */
export const D65 = {
name: "D65",
info: "Daylight, sRGB, Adobe-RGB",
x: 95.047,
y: 100,
z: 108.883,
} as const;
export type D65 = typeof D65;
/** `D75`: North sky daylight */
export const D75 = {
name: "D75",
info: "North sky daylight",
x: 94.972,
y: 100,
z: 122.638,
} as const;
export type D75 = typeof D75;
/** `E`: Equal Energy */
export const E = {
name: "E",
info: "Equal Energy",
x: 100,
y: 100,
z: 100,
} as const;
export type E = typeof E;
/** `F1`: Daylight Fluorescent */
export const F1 = {
name: "F1",
info: "Daylight Fluorescent",
x: 92.834,
y: 100,
z: 103.665,
} as const;
export type F1 = typeof F1;
/** `F2`: Cool Fluorescent */
export const F2 = {
name: "F2",
info: "Cool Fluorescent",
x: 99.187,
y: 100,
z: 67.395,
} as const;
export type F2 = typeof F2;
/** `F3`: White Fluorescent */
export const F3 = {
name: "F3",
info: "White Fluorescent",
x: 103.754,
y: 100,
z: 49.861,
} as const;
export type F3 = typeof F3;
/** `F4`: Warm White Fluorescent */
export const F4 = {
name: "F4",
info: "Warm White Fluorescent",
x: 109.147,
y: 100,
z: 38.813,
} as const;
export type F4 = typeof F4;
/** `F5`: Daylight Fluorescent */
export const F5 = {
name: "F5",
info: "Daylight Fluorescent",
x: 90.872,
y: 100,
z: 98.723,
} as const;
export type F5 = typeof F5;
/** `F6`: Lite White Fluorescent */
export const F6 = {
name: "F6",
info: "Lite White Fluorescent",
x: 97.309,
y: 100,
z: 60.191,
} as const;
export type F6 = typeof F6;
/** `F7`: Daylight fluorescent, {@linkcode D65} simulator */
export const F7 = {
name: "F7",
info: "Daylight fluorescent, D65 simulator",
x: 95.044,
y: 100,
z: 108.755,
} as const;
export type F7 = typeof F7;
/** `F8`: Sylvania F40, {@linkcode D50} simulator */
export const F8 = {
name: "F8",
info: "Sylvania F40, D50 simulator",
x: 96.413,
y: 100,
z: 82.333,
} as const;
export type F8 = typeof F8;
/** `F9`: Cool White Fluorescent */
export const F9 = {
name: "F9",
info: "Cool White Fluorescent",
x: 100.365,
y: 100,
z: 67.868,
} as const;
export type F9 = typeof F9;
/** `F10`: Ultralume 50, Philips TL85 */
export const F10 = {
name: "F10",
info: "Ultralume 50, Philips TL85",
x: 96.174,
y: 100,
z: 81.712,
} as const;
export type F10 = typeof F10;
/** `F11`: Ultralume 40, Philips TL84 */
export const F11 = {
name: "F11",
info: "Ultralume 40, Philips TL84",
x: 100.966,
y: 100,
z: 64.37,
} as const;
export type F11 = typeof F11;
/** `F12`: Ultralume 30, Philips TL83 */
export const F12 = {
name: "F12",
info: "Ultralume 30, Philips TL83",
x: 108.046,
y: 100,
z: 39.228,
} as const;
export type F12 = typeof F12;
export const Illuminant = {
A,
B,
C,
D50,
D55,
D65,
D75,
E,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
} as const;
export default Illuminant;

The MIT License (MIT)

Copyright © 2024 Nicholas Berlette (https://gist.github.com/nberlette). All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

export const names2colors = {
"lavenderblush": "#FFF0F5",
"lavenderblush_100": "#EEE0E5",
"lavenderblush_200": "#CDC1C5",
"lavenderblush_300": "#8B8386",
"pink": "#FFC0CB",
"pink_100": "#FFB5C5",
"pink_200": "#EEA9B8",
"pink_300": "#CD919E",
"pink_400": "#8B636C",
"lightpink": "#FFB6C1",
"lightpink_100": "#FFAEB9",
"lightpink_200": "#EEA2AD",
"lightpink_300": "#CD8C95",
"lightpink_400": "#8B5F65",
"palevioletred": "#FF82AB",
"palevioletred_100": "#EE799F",
"palevioletred_200": "#DB7093",
"palevioletred_300": "#CD6889",
"palevioletred_400": "#8B475D",
"hotpink": "#FF69B4",
"hotpink_100": "#FF6EB4",
"hotpink_200": "#EE6AA7",
"hotpink_300": "#CD6090",
"hotpink_400": "#8B3A62",
"raspberry": "#872657",
"violetred": "#D02090",
"violetred_100": "#FF3E96",
"violetred_200": "#EE3A8C",
"violetred_300": "#CD3278",
"violetred_400": "#8B2252",
"mediumvioletred": "#C71585",
"deeppink": "#FF1493",
"deeppink_100": "#EE1289",
"deeppink_200": "#CD1076",
"deeppink_300": "#8B0A50",
"thistle": "#FFE1FF",
"thistle_100": "#EED2EE",
"thistle_200": "#D8BFD8",
"thistle_300": "#CDB5CD",
"thistle_400": "#8B7B8B",
"plum": "#DDA0DD",
"plum_100": "#FFBBFF",
"plum_200": "#EEAEEE",
"plum_300": "#CD96CD",
"plum_400": "#8B668B",
"orchid": "#DA70D6",
"orchid_100": "#FF83FA",
"orchid_200": "#EE7AE9",
"orchid_300": "#CD69C9",
"orchid_400": "#8B4789",
"purple": "#800080",
"rebeccapurple": "#663399",
"violet": "#EE82EE",
"darkviolet": "#9400D3",
"darkmagenta": "#8B008B",
"fuchsia": "#FF00EE",
"magenta": "#FF00FF",
"magenta_200": "#EE00EE",
"magenta_300": "#CD00CD",
"magenta_400": "#8B008B",
"mediumorchid": "#BA55D3",
"mediumorchid_100": "#E066FF",
"mediumorchid_200": "#D15FEE",
"mediumorchid_300": "#B452CD",
"mediumorchid_400": "#7A378B",
"darkorchid": "#9932CC",
"darkorchid_100": "#BF3EFF",
"darkorchid_200": "#B23AEE",
"darkorchid_300": "#9A32CD",
"darkorchid_400": "#68228B",
"indigo": "#4B0082",
"blueviolet": "#8A2BE2",
"purple_100": "#9B30FF",
"purple_200": "#912CEE",
"purple_300": "#7D26CD",
"purple_400": "#551A8B",
"mediumpurple": "#9370DB",
"mediumpurple_100": "#AB82FF",
"mediumpurple_200": "#9F79EE",
"mediumpurple_300": "#8968CD",
"mediumpurple_400": "#5D478B",
"darkslateblue": "#483D8B",
"lightslateblue": "#8470FF",
"mediumslateblue": "#7B68EE",
"slateblue": "#6A5ACD",
"slateblue_100": "#836FFF",
"slateblue_200": "#7A67EE",
"slateblue_300": "#6959CD",
"slateblue_400": "#473C8B",
"blue": "#0000FF",
"mediumblue": "#0000CD",
"darkblue": "#00008B",
"navy": "#000080",
"midnightblue": "#191970",
"cobalt": "#3D59AB",
"royalblue": "#4169E1",
"royalblue_100": "#4876FF",
"royalblue_200": "#436EEE",
"royalblue_300": "#3A5FCD",
"royalblue_400": "#27408B",
"cornflowerblue": "#6495ED",
"lightsteelblue": "#B0C4DE",
"lightsteelblue_100": "#CAE1FF",
"lightsteelblue_200": "#BCD2EE",
"lightsteelblue_300": "#A2B5CD",
"lightsteelblue_400": "#6E7B8B",
"lightslategray": "#778899",
"slategray": "#708090",
"slategray_100": "#C6E2FF",
"slategray_200": "#B9D3EE",
"slategray_300": "#9FB6CD",
"slategray_400": "#6C7B8B",
"dodgerblue": "#1E90FF",
"dodgerblue_100": "#1C86EE",
"dodgerblue_200": "#1874CD",
"dodgerblue_300": "#104E8B",
"aliceblue": "#F0F8FF",
"steelblue": "#4682B4",
"steelblue_100": "#63B8FF",
"steelblue_200": "#5CACEE",
"steelblue_300": "#4F94CD",
"steelblue_400": "#36648B",
"lightskyblue": "#87CEFA",
"lightskyblue_100": "#B0E2FF",
"lightskyblue_200": "#A4D3EE",
"lightskyblue_300": "#8DB6CD",
"lightskyblue_400": "#607B8B",
"skyblue": "#87CEEB",
"skyblue_100": "#87CEFF",
"skyblue_200": "#7EC0EE",
"skyblue_300": "#6CA6CD",
"skyblue_400": "#4A708B",
"deepskyblue": "#00BFFF",
"deepskyblue_100": "#00B2EE",
"deepskyblue_200": "#009ACD",
"deepskyblue_300": "#00688B",
"peacock": "#33A1C9",
"lightblue": "#ADD8E6",
"lightblue_100": "#BFEFFF",
"lightblue_200": "#B2DFEE",
"lightblue_300": "#9AC0CD",
"lightblue_400": "#68838B",
"powderblue": "#B0E0E6",
"cadetblue": "#5F9EA0",
"cadetblue_100": "#98F5FF",
"cadetblue_200": "#8EE5EE",
"cadetblue_300": "#7AC5CD",
"cadetblue_400": "#53868B",
"turquoise": "#40E0D0",
"turquoise_100": "#00F5FF",
"turquoise_200": "#00E5EE",
"turquoise_300": "#00C5CD",
"turquoise_400": "#00868B",
"azure": "#F0FFFF",
"azure_100": "#E0EEEE",
"azure_200": "#C1CDCD",
"azure_300": "#838B8B",
"lightcyan": "#E0FFFF",
"lightcyan_100": "#D1EEEE",
"lightcyan_200": "#B4CDCD",
"lightcyan_300": "#7A8B8B",
"paleturquoise": "#AEEEEE",
"paleturquoise_100": "#BBFFFF",
"paleturquoise_200": "#96CDCD",
"paleturquoise_300": "#668B8B",
"darkslategray": "#2F4F4F",
"darkslategray_100": "#97FFFF",
"darkslategray_200": "#8DEEEE",
"darkslategray_300": "#79CDCD",
"darkslategray_400": "#528B8B",
"aqua": "#00FFEE",
"cyan": "#00FFFF",
"cyan_200": "#00EEEE",
"cyan_300": "#00CDCD",
"darkcyan": "#008B8B",
"teal": "#008080",
"lightseagreen": "#20B2AA",
"manganeseblue": "#03A89E",
"coldgrey": "#808A87",
"aquamarine": "#7FFFD4",
"aquamarine_100": "#76EEC6",
"aquamarine_200": "#66CDAA",
"aquamarine_400": "#458B74",
"mintcream": "#F5FFFA",
"springgreen": "#00FF7F",
"springgreen_100": "#00EE76",
"springgreen_200": "#00CD66",
"springgreen_300": "#008B45",
"seagreen": "#2E8B57",
"seagreen_100": "#54FF9F",
"seagreen_200": "#4EEE94",
"seagreen_300": "#43CD80",
"emeraldgreen": "#00C957",
"mint": "#BDFCC9",
"cobaltgreen": "#3D9140",
"honeydew": "#F0FFF0",
"honeydew_100": "#E0EEE0",
"honeydew_200": "#C1CDC1",
"honeydew_300": "#838B83",
"turquoiseblue": "#00C78C",
"mediumturquoise": "#48D1CC",
"darkturquoise": "#00CED1",
"mediumseagreen": "#3CB371",
"mediumaquamarine": "#66CDAA",
"mediumspringgreen": "#00FA9A",
"darkseagreen": "#8FBC8F",
"darkseagreen_100": "#C1FFC1",
"darkseagreen_200": "#B4EEB4",
"darkseagreen_300": "#9BCD9B",
"darkseagreen_400": "#698B69",
"palegreen": "#98FB98",
"palegreen_100": "#9AFF9A",
"palegreen_200": "#7CCD7C",
"palegreen_300": "#548B54",
"lightgreen": "#90EE90",
"limegreen": "#32CD32",
"forestgreen": "#228B22",
"lime": "#00FF00",
"green": "#008000",
"green_100": "#00EE00",
"green_200": "#00CD00",
"darkgreen": "#006400",
"sapgreen": "#308014",
"lawngreen": "#7CFC00",
"chartreuse": "#7FFF00",
"chartreuse_100": "#76EE00",
"chartreuse_200": "#66CD00",
"chartreuse_300": "#458B00",
"greenyellow": "#ADFF2F",
"darkolivegreen": "#556B2F",
"darkolivegreen_100": "#CAFF70",
"darkolivegreen_200": "#BCEE68",
"darkolivegreen_300": "#A2CD5A",
"darkolivegreen_400": "#6E8B3D",
"olivedrab": "#6B8E23",
"olivedrab_100": "#C0FF3E",
"olivedrab_200": "#B3EE3A",
"olivedrab_300": "#698B22",
"yellowgreen": "#9ACD32",
"ivory": "#FFFFF0",
"ivory_100": "#EEEEE0",
"ivory_200": "#CDCDC1",
"ivory_300": "#8B8B83",
"beige": "#F5F5DC",
"lightyellow": "#FFFFE0",
"lightyellow_100": "#EEEED1",
"lightyellow_200": "#CDCDB4",
"lightyellow_300": "#8B8B7A",
"lightgoldenrodyellow": "#FAFAD2",
"yellow": "#FFFF00",
"yellow_100": "#EEEE00",
"yellow_200": "#CDCD00",
"yellow_300": "#8B8B00",
"warmgrey": "#808069",
"olive": "#808000",
"khaki": "#F0E68C",
"khaki_100": "#FFF68F",
"khaki_200": "#EEE685",
"khaki_300": "#CDC673",
"khaki_400": "#8B864E",
"darkkhaki": "#BDB76B",
"palegoldenrod": "#EEE8AA",
"lemonchiffon": "#FFFACD",
"lemonchiffon_100": "#EEE9BF",
"lemonchiffon_200": "#CDC9A5",
"lemonchiffon_300": "#8B8970",
"lightgoldenrod_100": "#FFEC8B",
"lightgoldenrod_200": "#EEDC82",
"lightgoldenrod_300": "#CDBE70",
"lightgoldenrod_400": "#8B814C",
"banana": "#E3CF57",
"gold": "#FFD700",
"gold_100": "#EEC900",
"gold_200": "#CDAD00",
"gold_300": "#8B7500",
"cornsilk": "#FFF8DC",
"cornsilk_100": "#EEE8CD",
"cornsilk_200": "#CDC8B1",
"cornsilk_300": "#8B8878",
"goldenrod": "#DAA520",
"goldenrod_100": "#FFC125",
"goldenrod_200": "#EEB422",
"goldenrod_300": "#CD9B1D",
"goldenrod_400": "#8B6914",
"darkgoldenrod": "#B8860B",
"darkgoldenrod_100": "#FFB90F",
"darkgoldenrod_200": "#EEAD0E",
"darkgoldenrod_300": "#CD950C",
"darkgoldenrod_400": "#8B6508",
"orange": "#FF8000",
"orange_100": "#FFA500",
"orange_200": "#EE9A00",
"orange_300": "#CD8500",
"orange_400": "#8B5A00",
"floralwhite": "#FFFAF0",
"oldlace": "#FDF5E6",
"wheat": "#F5DEB3",
"wheat_100": "#FFE7BA",
"wheat_200": "#EED8AE",
"wheat_300": "#CDBA96",
"wheat_400": "#8B7E66",
"moccasin": "#FFE4B5",
"papayawhip": "#FFEFD5",
"blanchedalmond": "#FFEBCD",
"navajowhite": "#FFDEAD",
"navajowhite_100": "#EECFA1",
"navajowhite_200": "#CDB38B",
"navajowhite_300": "#8B795E",
"eggshell": "#FCE6C9",
"tan": "#D2B48C",
"brick": "#9C661F",
"cadmiumyellow": "#FF9912",
"antiquewhite": "#FAEBD7",
"antiquewhite_100": "#FFEFDB",
"antiquewhite_200": "#EEDFCC",
"antiquewhite_300": "#CDC0B0",
"antiquewhite_400": "#8B8378",
"burlywood": "#DEB887",
"burlywood_100": "#FFD39B",
"burlywood_200": "#EEC591",
"burlywood_300": "#CDAA7D",
"burlywood_400": "#8B7355",
"bisque": "#FFE4C4",
"bisque_100": "#EED5B7",
"bisque_200": "#CDB79E",
"bisque_300": "#8B7D6B",
"melon": "#E3A869",
"carrot": "#ED9121",
"darkorange": "#FF8C00",
"darkorange_100": "#FF7F00",
"darkorange_200": "#EE7600",
"darkorange_300": "#CD6600",
"darkorange_400": "#8B4500",
"peru": "#CD853F",
"peru_100": "#FFA54F",
"peru_200": "#EE9A49",
"peru_400": "#8B5A2B",
"linen": "#FAF0E6",
"peachpuff": "#FFDAB9",
"peachpuff_100": "#EECBAD",
"peachpuff_200": "#CDAF95",
"peachpuff_300": "#8B7765",
"seashell": "#FFF5EE",
"seashell_100": "#EEE5DE",
"seashell_200": "#CDC5BF",
"seashell_300": "#8B8682",
"sandybrown": "#F4A460",
"rawsienna": "#C76114",
"chocolate": "#D2691E",
"chocolate_100": "#FF7F24",
"chocolate_200": "#EE7621",
"chocolate_300": "#CD661D",
"saddlebrown": "#8B4513",
"ivoryblack": "#292421",
"flesh": "#FF7D40",
"cadmiumorange": "#FF6103",
"burntsienna": "#8A360F",
"sienna": "#A0522D",
"sienna_100": "#FF8247",
"sienna_200": "#EE7942",
"sienna_300": "#CD6839",
"sienna_400": "#8B4726",
"lightsalmon": "#FFA07A",
"lightsalmon_100": "#EE9572",
"lightsalmon_200": "#CD8162",
"lightsalmon_300": "#8B5742",
"orangered": "#FF4500",
"orangered_100": "#EE4000",
"orangered_200": "#CD3700",
"orangered_300": "#8B2500",
"sepia": "#5E2612",
"darksalmon": "#E9967A",
"salmon": "#FA8072",
"salmon_100": "#FF8C69",
"salmon_200": "#EE8262",
"salmon_300": "#CD7054",
"salmon_400": "#8B4C39",
"coral": "#FF7F50",
"coral_100": "#FF7256",
"coral_200": "#EE6A50",
"coral_300": "#CD5B45",
"coral_400": "#8B3E2F",
"burntumber": "#8A3324",
"tomato": "#FF6347",
"tomato_100": "#EE5C42",
"tomato_200": "#CD4F39",
"tomato_300": "#8B3626",
"mistyrose": "#FFE4E1",
"mistyrose_100": "#EED5D2",
"mistyrose_200": "#CDB7B5",
"mistyrose_300": "#8B7D7B",
"snow": "#FFFAFA",
"snow_100": "#EEE9E9",
"snow_200": "#CDC9C9",
"snow_300": "#8B8989",
"rosybrown": "#BC8F8F",
"rosybrown_100": "#FFC1C1",
"rosybrown_200": "#EEB4B4",
"rosybrown_300": "#CD9B9B",
"rosybrown_400": "#8B6969",
"lightcoral": "#F08080",
"indianred": "#CD5C5C",
"indianred_100": "#FF6A6A",
"indianred_200": "#EE6363",
"indianred_300": "#CD5555",
"indianred_500": "#B0171F",
"indianred_400": "#8B3A3A",
"brown": "#BE4625",
"brown_100": "#9C4736",
"brown_200": "#8A3324",
"brown_300": "#662222",
"brown_400": "#421C1C",
"firebrick": "#B22222",
"firebrick_100": "#FF3030",
"firebrick_200": "#EE2C2C",
"firebrick_300": "#CD2626",
"firebrick_400": "#8B1A1A",
"red": "#FF0000",
"red_100": "#EE0000",
"red_200": "#CD0000",
"red_300": "#AA0000",
"red_400": "#7C0000",
"darkred": "#8B0000",
"maroon": "#800000",
"maroon_100": "#FF34B3",
"maroon_200": "#EE30A7",
"maroon_300": "#CD2990",
"maroon_400": "#8B1C62",
"maroon_500": "#7A0B4B",
"maroon_600": "#610B38",
"maroon_700": "#4A1022",
"maroon_800": "#340D17",
"maroon_900": "#1E0B10",
"white": "#FFFFFF",
"ghostwhite": "#F8F8FF",
"lavender": "#E6E6FA",
"white_smoke": "#F5F5F5",
"whitesmoke": "#F5F5F5FF",
"gainsboro": "#DCDCDC",
"lightgrey": "#D3D3D3",
"silver": "#C0C0C0",
"darkgray": "#A9A9A9",
"gray": "#808080",
"dimgray": "#696969",
"black": "#000000",
"sgi_beet": "#8E388E",
"sgi_slateblue": "#7171C6",
"sgi_lightblue": "#7D9EC0",
"sgi_teal": "#388E8E",
"sgi_chartreuse": "#71C671",
"sgi_olivedrab": "#8E8E38",
"sgi_salmon": "#C67171",
"sgi_brightgray": "#C5C1AA",
"sgi_lightgray": "#AAAAAA",
"sgi_darkgray": "#555555",
"sgi_gray_900": "#1E1E1E",
"sgi_gray_800": "#282828",
"sgi_gray_700": "#515151",
"sgi_gray_600": "#5B5B5B",
"sgi_gray_500": "#848484",
"sgi_gray_400": "#8E8E8E",
"sgi_gray_300": "#B7B7B7",
"sgi_gray_200": "#C1C1C1",
"sgi_gray_100": "#EAEAEA",
"sgi_gray_50": "#F4F4F4",
"transparent": "#00000000",
} as const;
export type names2colors = typeof names2colors;
export const colors = Object.keys(names2colors).reduce(
(o, k) => ({ ...o, [names2colors[k as keyof names2colors]]: k }),
{} as { readonly [K in keyof names2colors as names2colors[K]]: K },
);
export type colors = typeof colors;
export const keywords = {
"aliceblue": "#f0f8ff",
"antiquewhite": "#faebd7",
"aqua": "#00ffff",
"aquamarine": "#7fffd4",
"azure": "#f0ffff",
"beige": "#f5f5dc",
"bisque": "#ffe4c4",
"black": "#000000",
"blanchedalmond": "#ffebcd",
"blue": "#0000ff",
"blueviolet": "#8a2be2",
"brown": "#a52a2a",
"burlywood": "#deb887",
"burntsienna": "#ea7e5d",
"cadetblue": "#5f9ea0",
"chartreuse": "#7fff00",
"chocolate": "#d2691e",
"coral": "#ff7f50",
"cornflowerblue": "#6495ed",
"cornsilk": "#fff8dc",
"crimson": "#dc143c",
"cyan": "#00ffff",
"darkblue": "#00008b",
"darkcyan": "#008b8b",
"darkgoldenrod": "#b8860b",
"darkgray": "#a9a9a9",
"darkgrey": "#a9a9a9",
"darkgreen": "#006400",
"darkkhaki": "#bdb76b",
"darkmagenta": "#8b008b",
"darkolivegreen": "#556b2f",
"darkorange": "#ff8c00",
"darkorchid": "#9932cc",
"darkred": "#8b0000",
"darksalmon": "#e9967a",
"darkseagreen": "#8fbc8f",
"darkslateblue": "#483d8b",
"darkslategray": "#2f4f4f",
"darkslategrey": "#2f4f4f",
"darkturquoise": "#00ced1",
"darkviolet": "#9400d3",
"deeppink": "#ff1493",
"deepskyblue": "#00bfff",
"dimgray": "#696969",
"dimgrey": "#696969",
"dodgerblue": "#1e90ff",
"firebrick": "#b22222",
"floralwhite": "#fffaf0",
"forestgreen": "#228b22",
"fuchsia": "#ff00ff",
"gainsboro": "#dcdcdc",
"ghostwhite": "#f8f8ff",
"gold": "#ffd700",
"goldenrod": "#daa520",
"gray": "#808080",
"grey": "#808080",
"green": "#008000",
"greenyellow": "#adff2f",
"honeydew": "#f0fff0",
"hotpink": "#ff69b4",
"indianred": "#cd5c5c",
"indigo": "#4b0082",
"ivory": "#fffff0",
"khaki": "#f0e68c",
"lavender": "#e6e6fa",
"lavenderblush": "#fff0f5",
"lawngreen": "#7cfc00",
"lemonchiffon": "#fffacd",
"lightblue": "#add8e6",
"lightcoral": "#f08080",
"lightcyan": "#e0ffff",
"lightgoldenrodyellow": "#fafad2",
"lightgray": "#d3d3d3",
"lightgrey": "#d3d3d3",
"lightgreen": "#90ee90",
"lightpink": "#ffb6c1",
"lightsalmon": "#ffa07a",
"lightseagreen": "#20b2aa",
"lightskyblue": "#87cefa",
"lightslategray": "#778899",
"lightslategrey": "#778899",
"lightsteelblue": "#b0c4de",
"lightyellow": "#ffffe0",
"lime": "#00ff00",
"limegreen": "#32cd32",
"linen": "#faf0e6",
"magenta": "#ff00ff",
"maroon": "#800000",
"mediumaquamarine": "#66cdaa",
"mediumblue": "#0000cd",
"mediumorchid": "#ba55d3",
"mediumpurple": "#9370db",
"mediumseagreen": "#3cb371",
"mediumslateblue": "#7b68ee",
"mediumspringgreen": "#00fa9a",
"mediumturquoise": "#48d1cc",
"mediumvioletred": "#c71585",
"midnightblue": "#191970",
"mintcream": "#f5fffa",
"mistyrose": "#ffe4e1",
"moccasin": "#ffe4b5",
"navajowhite": "#ffdead",
"navy": "#000080",
"oldlace": "#fdf5e6",
"olive": "#808000",
"olivedrab": "#6b8e23",
"orange": "#ffa500",
"orangered": "#ff4500",
"orchid": "#da70d6",
"palegoldenrod": "#eee8aa",
"palegreen": "#98fb98",
"paleturquoise": "#afeeee",
"palevioletred": "#d87093",
"papayawhip": "#ffefd5",
"peachpuff": "#ffdab9",
"peru": "#cd853f",
"pink": "#ffc0cb",
"plum": "#dda0dd",
"powderblue": "#b0e0e6",
"purple": "#800080",
"rebeccapurple": "#663399",
"red": "#ff0000",
"rosybrown": "#bc8f8f",
"royalblue": "#4169e1",
"saddlebrown": "#8b4513",
"salmon": "#fa8072",
"sandybrown": "#f4a460",
"seagreen": "#2e8b57",
"seashell": "#fff5ee",
"sienna": "#a0522d",
"silver": "#c0c0c0",
"skyblue": "#87ceeb",
"slateblue": "#6a5acd",
"slategray": "#708090",
"slategrey": "#708090",
"snow": "#fffafa",
"springgreen": "#00ff7f",
"steelblue": "#4682b4",
"tan": "#d2b48c",
"teal": "#008080",
"thistle": "#d8bfd8",
"tomato": "#ff6347",
"turquoise": "#40e0d0",
"violet": "#ee82ee",
"wheat": "#f5deb3",
"white": "#ffffff",
"whitesmoke": "#f5f5f5",
"yellow": "#ffff00",
"yellowgreen": "#9acd32",
"transparent": "#00000000",
} as const;
export type keywords = typeof keywords;
export type ColorNames = keyof names2colors;
export const colors2names = Object.entries(names2colors).reduce(
(acc, [key, val]) => ({ ...acc, [val]: key }),
Object.create(null) as {
[K in keyof names2colors as names2colors[K]]-?: K;
},
);
export type colors2names = typeof colors2names;
// deno-lint-ignore-file no-explicit-any ban-types
const options = {
/**
* Left hand / opening delimiter, indicating start of a placeholder.
* Whitespace is allowed **after** this delimiter (**before** the start
* of the placeholder key). It is auto-trimmed by the template engine.
*/
left: "{",
/**
* Right hand / closing delimiter, indicating end of a placeholder.
* Whitespace is allowed **before** this delimiter (**after** the placeholder
* key or its default value). It is auto-trimmed by the template engine.
*/
right: "}",
/**
* Middle / default delimiters, separating placeholder keys from their
* default values within the {@link left} and {@link right} delimiters.
* Whitespace may surround this delimiter in input placeholders.
*/
default: ["?", ":", "=", ":=", "?=", "??", "||", "--"],
/**
* Prefixes (optional) may immediately precede the {@link left} delimiter,
* without any whitespace separating the two. Defining a prefix here **does
* not** require it in your template strings -- it will always be optional.
*/
prefix: ["$", "%", "#"],
} as const;
type options = typeof options;
/**
* The default {@linkcode Options} for compiling a {@linkcode Template} string.
*
* This is the value-level counterpart of the type that shares the same name.
*
* Take note of the subtle differences between the two, however, as they are
* not 100% identical: options represented as a union on the type-level, such
* as {@link Options.default} and {@link Options.prefix}, are represented as
* an **array** of strings on the value-level.
*
* This is intentional, and they both represent the same thing: several values
* that are **all** valid, but only **one** may be used at a time. Compilation
* converts them into RegExp "unions" by escaping their values, concatenating
* them together with the `|` character, and wrapping them in a capture group.
*
* @example
* ```ts
* // custom delimiters (value-level)
* const options = {
* left: "<",
* right: ">",
* // notice the literal tuples used below
* default: ["??", "||", ":=", "##"],
* prefix: ["$", "%"],
* } as const;
*
* // custom delimiters (type-level)
* type options = {
* left: "<",
* right: ">",
* // notice the arrays above have become string unions:
* default: "??" | "||" | ":=" | "##";
* prefix: "$" | "%";
* };
*
* // compiled RegExp expression:
*
* ```
*
* > **Warning**: If you accidentally use a string in places where the compiler
* > expects an array, you will unleash unknown (and unwanted) behavior.
* > Please just stick to the script and avoid the headaches. Thank you!
*/
const Defaults = { ...options };
type Defaults = {
-readonly [K in keyof options]: options[K] extends readonly string[]
? options[K][number]
: options[K];
};
export interface Options {
left: string;
right: string;
default: string;
prefix: string;
}
export declare namespace Options {
export { Defaults as default };
}
// deno-lint-ignore no-namespace
export namespace Options {
Options.default = Defaults;
}
/**
* Formats a template string `T` with the given substitutions `S`.
*
* @template {string} T The template string to format.
* @template {TemplateDict} S The substitutions to use when formatting the template string.
* @param {T} template The template string to format.
* @param {S} values The substitutions to use when formatting the template.
* @returns {Template<T, S>}
* @example
* ```ts
* const result = tpl("Thanks to {name} for their work!", { name: "John Doe" });
* console.log(result); // "Thanks to John Doe for their work!"
*
* const withFallback = tpl("Thanks to {name:anonymous} for their work!", { namr: "Ty Poe" });
* console.log(withFallback); // "Thanks to anonymous for their work!"
* ```
*
* ---
*
* ### Template Placeholders
*
* Placeholders are simple keys wrapped in 1-3 curly brackets, with an optional
* prefix `$`. Note that any `$` before a `{` **must** be escaped if you are
* using it in the form of a tagged template function. Otherwise, it will be
* interpreted as a substitution value by the runtime.
*
* @example
* The supported placeholder syntaxes are:
* - `{key}`, `{{key}}`, or `{{{key}}}`sZx
* - `${key}`, `${{key}}`, or `${{{key}}}`
*
* ### Substitutions
*
* Subtitutionss are provided as eiher a dictionary object or an array.
*
* For the standard syntax, substitutions are provided as a literal dictionary
* object, in which the keys are the placeholder names and the values are either
* the literal replacements or functions that return the replacements.
*
* ```ts
* @example
* tpl("Name: {0}, Age: {age}", { 0: "Nick", age: 30 })
*
* // => "Name: Nick, Age: 30"
* ```
*
* For the alternative syntax, the array may either be a literal tuple or a
* rest-parameter, either of which should contain positional values at indices
* corresponding to their numeric placeholders. See the section on numeric
* placeholders below for more information on this alternative syntax.
*
* #### Tagged Templates and Dictionaries
*
* When using the tagged template syntax (`tpl\`Hello {who}\``), the question
* arises for how the substitutions should be provided. The dictionary object
* can be provided inline when using this style syntax, demonstrated below. It
* isn't the most elegant solution, but currently it's the only one that doesn't
* require transforming the tag into a separate function call, which would cause
* unnecessary extra complexity and would also require that substitutions be
* provided _prior_ to the template.
*
* ```ts
* tpl`tagged templates are {what}, I think.${{ what: "cool" }}`
*
* // => "tagged templates are cool, I think."
* ```
*
* ### Numeric Placeholders
*
* Substitutions can be provided in a variety of formats to suit your needs. If
* the template declares numeric tokens (`{0}`, `{1}`, etc.), they should each
* have a substitution at an index corresponding to the placeholder number.
*
* In this scenario, you may provide a literal array for the 2nd argument, or
* make use of a rest-parameter format like below. Note that the following 3
* syntaxes are all equivalent to one another:
*
* ```ts
* // rest-parameter
* tpl("Holy {0}! What is {1}?", "macaroni", "updog");
*
* // literal tuple
* tpl("Holy {0}! What is {1}?", ["macaroni", "updog"]);
*
* // literal object, with numeric keys
* tpl("Holy {0}! What is {1}?", { 0: "macaroni", 1: "updog" });
*
* // => "Holy macaroni! What is updog?"
* ```
*
* ### Mixed Placeholders
*
* Mixing numeric and non-numeric placeholders, despite being bad practice, is
* also supported. The non-numeric keys must be specified as partial objects,
* similar to the standard dictionary-style syntax. The order of numeric keys
* must reflect the order in the template; non-numerics can have any order.
*
* The following 3 examples of different syntaxes are all equivalent:
*
* ```ts
* // rest-parameter, mixed values
* tpl("Howdy! This {0} is a {what}. Cool?", "example", { what: "mixup" });
*
* // literal tuple, mixed values
* tpl("Howdy! This {0} is a {what}. Cool?", ["example", { what: "mixup" }]);
*
* // literal object, mixed keys
* tpl("Howdy! This {0} is a {what}. Cool?", { 0: "example", what: "mixup" });
*
* // => "Howdy! This example is a mixup. Cool?"
* ```
*
* #### Callable Substitutions
*
* If the value of a given key is a function, it will be called with the full
* placeholder string, the extracted placeholder name, and the default value
* (if any) as its arguments, in that order. The return value of the function
* must be a printable primitive value (string, number, bigint, boolean, null)
* as it will be converted to a string and used as the substitution value.
*
* ```ts
* tpl("Dynamic numbers: {num}", { num: () => Math.random() });
*
* // => "Dynamic numbers: 0.923525342"
* ```
*
* ### Default Values
*
* If a placeholder provide a default / fallback value for a placeholder using a number
* of different syntaxes. The formatter will try to be as forgiving as possible
* when it comes to whitespace and delimiter variations. All of the following
* are valid and functionally equivalent:
*
* - `{key||default}`, `{{key || default}}`, etc.
* - `{key??default}`, `{{key ?? default}}`, etc.
* - `{key ?= default}`, `{{key ?= default}}`, etc.
* - `{key:=default}`, `{{key := default}}`, etc.
* - `{key:default}`, `{{key : default}}`, etc.
* - `{key=default}`, `{{key = default}}`, etc.
* - `{key ? default}`, `{{key ? default}}`, etc.
*
* These all compile to the following JavaScript expression:
*
* ```ts
* const value = substitutions[key] || defaultValue;
* ```
*
* Providing an empty default value means that the placeholder will be deleted
* from the output string if the key is not found in the substitutions object.
*/
export function tpl<
const This extends object,
T extends string,
const S extends ContextualTuple<This>,
>(this: This, template: T, ...values: S): Template<T, Collect<T, S>>;
/**
* Formats a given template string `T` with the given substitutions `S`. This
* overload supports numeric placeholder keys, which accept substitutions as
* either a tuple or an object with numeric keys. If the template string has
* mixed numeric / alphanumeric keys, you may specify either an object with
* mixed keys, or a tuple with literal replacements / replacement-returning
* functions for numeric keys, and partial objects for the alphanumeric keys.
*
* @template {string} T The template string to format.
* @template {TemplateTuple} S The substitutions to use when formatting the template string.
* @param {T} template The template string to format.
* @param {S} values The substitutions to use when formatting the template.
* @returns {Template<T, Collect<T, S>>}
* @example
* ```ts
* tpl("Hello {0}, this is a {1} mixed {type}", ["Nick", "example", { type: "template" }]);
* ```
* @example
* ```ts
* tpl("Hello {0}, this is a {1} mixed {type}", [{ 0: "Nick", 1: "example", type: "template" }]);
* ```
*/
export function tpl<
const This extends object,
T extends string,
const S extends ContextualTuple<This>,
>(this: This, template: T, values: S): Template<T, Collect<T, S>>;
/**
* Formats a given template string `T` with the given substitutions `S`. This
* overload supports numeric placeholder keys, which accept substitutions as
* either a rest parameter or an object with numeric keys.
*
* @template {object} This Contextual `this` object. Substitution functions are bound to this object.
* @template {string} T The template string to format. This is where the placeholders are located.
* @template {ContextualTuple<This>} S The substitutions to use when formatting the template string.
* @param {T} template The template string to format. This is where the placeholders are located.
* @param {S} values The substitutions to use when formatting the template.
* @returns {Template<T, Collect<T, S>>}
* @example
* ```ts
* tpl`Hello {0}, this is a {1} mixed {type}${
* ["Nick", () => "example", { type: () => "template" }]
* }`
* ```
* @example
* ```ts
* tpl`Hello {0}, this is a {1} mixed {type}${{
* 0: "Nick",
* 1: "example",
* type: () => "template",
* }}`
* ```
*/
export function tpl<
const This extends object,
const Vals extends ContextualTuple<This>,
>(this: This, str: TemplateStringsArray, ...values: Vals): string;
/**
* Formats a given template string `T` with the given substitutions `S`. This
* overload supports numeric placeholder keys, which accept substitutions as
* either a rest parameter or an object with numeric keys.
*
* @this {any} Contextual `this` object. Substitution functions are bound to this object.
* @template {string} T The template string to format. This is where the placeholders are located.
* @template {ContextualTuple<any>} S The substitutions to use when formatting the template string.
* @param {T} template The template string to format. This is where the placeholders are located.
* @param {S} values The substitutions to use when formatting the template.
* @returns {Template<T, Collect<T, S>>}
* @example
* ```ts
* tpl("Hello {0}, this is a {1} mixed {type}", "Nick", "example", {
* type: () => "template",
* });
* ```
* @example
* ```ts
* tpl("Hello {0}, this is a {1} mixed {type}", {
* 0: "Nick",
* 1: "example",
* type: () => "template",
* });
* ```
*/
export function tpl<
T extends string,
const S extends ContextualTuple<any>,
>(template: T, ...values: S): Template<T, Collect<T, S>>;
/**
* Formats a given template string `T` with the given substitutions `S`. This
* overload supports numeric placeholder keys, which accept substitutions as
* either a tuple or an object with numeric keys. If the template string has
* mixed numeric / alphanumeric keys, you may specify either an object with
* mixed keys, or a tuple with literal replacements / replacement-returning
* functions for numeric keys, and partial objects for the alphanumeric keys.
*
* @template {string} T The template string to format.
* @template {TemplateTuple} S The substitutions to use when formatting the template string.
* @param {T} template The template string to format.
* @param {S} values The substitutions to use when formatting the template.
* @returns {Template<T, Collect<T, S>>}
* @example
* ```ts
* tpl("Hello {0}, this is a {1} mixed {type}", ["Nick", "example", { type: "template" }]);
* ```
* @example
* ```ts
* tpl("Hello {0}, this is a {1} mixed {type}", [{ 0: "Nick", 1: "example", type: "template" }]);
* ```
*/
export function tpl<
T extends string,
const S extends ContextualTuple<any>,
>(template: T, values: S): Template<T, Collect<T, S>>;
export function tpl(
str: TemplateStringsArray | string,
...values: Sub[]
): string;
export function tpl(
this: any,
input: TemplateStringsArray | string,
...values: readonly unknown[]
): string {
const dicts = values.filter(isPlainObject<TemplateRecord>);
// const tuples = values.filter(Array.isArray).filter(isSubstitutions);
const subs = values.filter(isSubstitution);
const dict = dicts.reduce((a, o) => ({ ...a, ...o }), {} as TemplateRecord);
let template = "";
if (typeof input === "string") {
let length: number | undefined;
// edge case, but it could happen...
if ("length" in dict) {
length = dict.length as number;
dict.length = Object.keys(dict).length;
}
template = input;
for (let i = 0; i < subs.length; i++) dict[i] = subs[i];
if (length != null) Object.assign(dict, { length });
} else {
template = input.raw[0] ?? input[0];
for (let i = 0; i < subs.length; i++) {
const val = subs[i];
template += typeof val === "function"
? Reflect.apply(val, this, [])
: isSubstitution(val)
? val
: String(val ?? "");
template += input.raw[i + 1] ?? input[i + 1] ?? "";
}
}
const regex = compileTemplateRegExp(options);
return template.replace(regex, ($0, ...args) => {
const [, , $key, $delim, $defaultValue] = args;
const groups = (args.at(-1) ?? {}) as Record<string, string>;
let { key = $key, delim = $delim, defaultValue = $defaultValue } = groups;
key = key.trim();
let value = defaultValue?.trim() ?? $0;
if (Object.hasOwn(dict, key)) {
value = dict[key];
if (typeof value === "function") {
value = value.call(this, key, defaultValue, dict);
} else if (typeof value === "object" && value !== null) {
value = JSON.stringify(value);
} else if (!value && defaultValue) {
value = defaultValue.replace(/\$0/g, $0);
value = value.replace(
/\$([1-9]+)/g,
(...a: string[]) => args.slice(2)[+a[1]] ?? $0,
);
} else {
value = String(value);
if (delim && options.default.includes(delim)) value ||= defaultValue;
}
return value;
}
return String((defaultValue ? defaultValue : $0) ?? "").trim();
});
}
tpl.ctx = tpl.with = templateWithContext;
tpl.tpl = tpl.default = tpl;
tpl.compileRegExp = compileTemplateRegExp;
tpl.options = Options;
function templateWithContext<const This extends object>(
thisArg: This,
): <T extends string, const S extends ContextualTuple<This>>(
template: T,
...substitutions: S
) => Template<T, Collect<T, S>> {
return (str, ...subs) => Reflect.apply(tpl, thisArg, [str, ...subs]);
}
function compileTemplateRegExp(opts = Options.default, flags = "dg"): RegExp {
// TODO: configurable delimiters and stuff, just like in the type utilities below
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const DL = esc(opts.left);
const DR = esc(opts.right);
const DD = opts.default.map(esc).join("|");
const PRE = opts.prefix.map(esc).join("|");
flags = [...new Set(flags + "dg")].join("");
const length = 3;
const closing = Array.from({ length }).fill(0).map((_, i, a) => {
const n = a.length - i;
return `(?:(?<=(?<!${DL})${DL}{${n}}\\2)${DR}{${n}}(?!${DR}))`;
}).join("|");
// this pattern ensures the left and right brackets are evenly balanced.
return new RegExp(
`(?:${PRE})?(${DL}{1,${length}})(\\s*(?<key>.+?)\\s*` +
`(?:(?<delim>${DD})\\s*(?<defaultValue>.+?)\\s*|))(?:${closing})`,
flags,
);
}
type OmitIndexSignature<T> = {
[K in keyof T as {} extends Record<K, unknown> ? never : K]: T[K];
};
function isPrintable(x: unknown): x is Printable {
return typeof x === "string" || typeof x === "number" ||
typeof x === "bigint" ||
typeof x === "boolean" || x == null;
}
function isSubstitution(x: unknown): x is Sub {
return isPrintable(x) || (typeof x === "function" && isPrintable(x()));
}
// function isSubstitutions(a: unknown): a is Sub[] {
// if (Array.isArray(a) && a.every(isSubstitution)) return true;
// return false;
// }
function isPlainObject<T extends Record<string | number, unknown>>(
value: unknown,
): value is T {
return typeof value === "object" && value !== null && !Array.isArray(value) &&
[null, Object.prototype].includes(Reflect.getPrototypeOf(value));
}
export default tpl;
/**
* Formats a given template string `T` with the given substitutions `S`. This
* is the type-level equivalent of {@linkcode tpl}, and is used to generate the
* return type of that function.
*
* For further documentation and examples, refer to {@linkcode tpl}.
*/
export type Template<
T extends string,
V extends TemplateDict = Subs<T>,
O extends Options = Defaults,
> = V extends TemplateTuple ? Template<T, Collect<T, V>, O>
: T extends
`${infer L}${O["prefix"]}${infer R extends `${O["left"]}${string}`}`
? Template<`${L}${R}`, V, O>
: TemplateBase<T, V, ResolveSubstitutions<V>, O> extends
infer F extends string ? F
: never;
/* @internal */
type Printable = string | number | bigint | boolean | null | undefined;
/* @internal */
interface PrintableFunction<
T = any,
A extends readonly any[] = any[],
R extends Printable = Printable,
> {
(this: T, ...args: A): R;
}
export type Sub<This extends object = any> =
| Printable
| PrintableFunction<This>;
export type ContextualTuple<This extends object = any> = readonly (
| Sub<This>
| Record<(string | number) & IsPlaceholder, Sub<This> | undefined>
)[];
/* @internal */
export type Collect<
T extends string,
S extends TemplateTuple,
O extends TemplateRecord = OmitIndexSignature<Subs<T>>,
R extends ConvertToRecord<S> = ConvertToRecord<S>,
> = Override<Override<O, R>, CollectPartials<S, never>>;
/* @internal */
interface TemplateRecord<This extends object = any> {
[key: (string | number) & IsPlaceholder]: Sub<This> | undefined;
} // | { [key: number]: Printable | undefined };
type TemplateTuple<This extends object = any> =
readonly (Sub<This> | TemplateRecord<This>)[];
type TemplateDict<This extends object = any> =
| TemplateRecord<This>
| TemplateTuple<This>;
type BuildTupleHelper<
N extends number,
V,
T extends readonly unknown[] = readonly [],
> = T["length"] extends N ? T
: BuildTupleHelper<
N,
V,
readonly [...T, [V] extends [never] ? T["length"] : V]
>;
type BuildTuple<N extends number, T = never, FALLBACK = never> =
BuildTupleHelper<N, T> extends infer R extends readonly any[] ? R : FALLBACK;
enum SubKind {
Record,
Tuple,
}
/* @internal */
type Subs<
T extends string,
O extends Options = Defaults,
K extends SubKind = SubKind.Record,
> = K extends SubKind.Tuple ? BuildTuple<
Union.ToTuple<NumericSubstitutionKeys<T, O>> extends
infer A extends readonly unknown[] ? A["length"] : 0,
Sub | Partial<Subs<T>>,
never
>
: { [P in ExtractKeys<T, O> | BrandedSubs]?: Sub };
/* @internal */
type TemplateBase<
T extends string | number,
V extends TemplateDict,
S extends TemplateDict = ResolveSubstitutions<V>,
O extends Options = Defaults,
> = O extends {
left: infer DL extends string;
right: infer DR extends string;
default: infer DD extends string;
prefix: infer PRE extends string;
}
? T extends `${infer A}${PRE}${infer R extends `${DL}${string}`}`
? TemplateBase<`${A}${R}`, V, S, O>
: T extends `${infer A}${DL}${DL}${DL}${infer I}${DR}${DR}${DR}${infer R}`
? TemplateBase<`${A}${DL}${I}${DR}${R}`, V, S, O>
: T extends `${infer A}${DL}${DL}${infer I}${DR}${DR}${infer R}`
? TemplateBase<`${A}${DL}${I}${DR}${R}`, V, S, O>
: T extends `${infer A}${DL}${infer I}${DR}${infer R}`
? TemplateInner<A, I, R, V, S, O>
: T extends `${infer L}${DD}${infer D}${infer R}`
? `${MaybeKey<S, Trim<L>, Trim<D>>}${TemplateBase<R, V, S, O>}`
: T extends `${infer L}${infer R}` ? `${L}${TemplateBase<R, V, S, O>}`
: T
: TemplateBase<T, V, S, Defaults & O>;
/* @internal */
type TemplateInner<
A extends string,
I extends string | number,
R extends string,
V extends TemplateDict,
S extends TemplateDict,
O extends Options = Defaults,
> = O extends {
left: infer DL extends string;
right: infer DR extends string;
default: infer DD extends string;
prefix: infer PRE extends string;
}
? Unwrap<`${I}`> extends `${infer L}${DD}${infer D}`
? `${Exclude<A, PRE>}${MaybeKey<S, Trim<L>, Trim<D>>}${TemplateBase<
R,
V,
S,
O
>}`
: Unwrap<`${I}`> extends infer K extends string | number
? `${Exclude<A, PRE>}${MaybeKey<
S,
K,
`${K}` extends `${infer K}${DD}${infer D}` ? MaybeKey<S, Trim<K>, Trim<D>>
: ""
>}${TemplateBase<R, V, S>}`
: `${Exclude<A, PRE>}${TemplateBase<R, V, S>}`
: TemplateInner<A, I, R, V, S, Defaults & O>;
/* @internal */
type MaybeKey<
S extends TemplateDict,
K extends string | number,
D extends Printable = "",
> = S extends { [P in K | `${K}`]: infer V extends Sub }
? V extends PrintableFunction<any, any[], infer R> ? R : V
: D;
/* @internal */
type Unwrap<
S extends string | number,
O extends Options = Defaults,
> = O extends {
left: infer DL extends string;
right: infer DR extends string;
prefix: infer PRE extends string;
} ? S extends `${PRE}${infer K extends `${DL}${string}`}` ? Unwrap<K, O>
: S extends `${DL}${DL}${DL}${infer K}${DR}${DR}${DR}` ? Unwrap<K, O>
: S extends `${DL}${DL}${infer K}${DR}${DR}` ? Unwrap<K, O>
: S extends `${DL}${infer K}${DR}` ? Unwrap<K, O>
: S extends string ? Trim<S>
: S extends number ? S
: never
: Unwrap<S, Defaults & O>;
/* @internal */
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T;
/* @internal */
type TrimRight<T extends string> = T extends `${infer L} ` ? TrimRight<L> : T;
/* @internal */
type Trim<T extends string> = TrimLeft<TrimRight<T>>;
/* @internal */
type ResolveSubstitutions<T> = T extends TemplateTuple
? T extends [infer A, ...infer B extends TemplateTuple]
? A extends Printable ? [A, ...ResolveSubstitutions<B>]
: A extends PrintableFunction<any, any[], infer R>
? [R, ...ResolveSubstitutions<B>]
: ResolveSubstitutions<B>
: []
: T extends TemplateRecord ? {
[K in keyof T as T[K] extends Sub ? K : never]: T[K] extends
PrintableFunction ? ReturnType<T[K]> : T[K];
}
: T extends Printable ? T
: T extends PrintableFunction ? ReturnType<T>
: T;
/* @internal */
type ExtractKeys<
T extends string,
O extends Options = Defaults,
WithValues extends boolean = false,
> = O extends {
left: infer DL extends string;
right: infer DR extends string;
default: infer DD extends string;
prefix: infer PRE extends string;
}
? T extends
`${infer L}${PRE | ""}${infer R extends `${DL}${string}${DR}${string}`}`
? ExtractKeys<L, O, WithValues> | ExtractKeys<`${R}`, O, WithValues>
: T extends `${string}${DL}${DL}${DL}${infer K}${DR}${DR}${DR}${infer Rest}`
? ExtractKeys<K, O, WithValues> | ExtractKeys<Rest, O, WithValues>
: T extends `${string}${DL}${DL}${infer K}${DR}${DR}${infer Rest}`
? ExtractKeys<K, O, WithValues> | ExtractKeys<Rest, O, WithValues>
: T extends `${string}${DL}${infer K}${DR}${infer Rest}` ?
| (
K extends `${infer K2}${DD}${infer V}`
? [WithValues] extends [true] ? readonly [
Unwrap<Trim<K2>, O>,
Unwrap<Trim<V>, O>,
]
: Unwrap<Trim<K2>, O>
: [WithValues] extends [true] ? readonly [Unwrap<Trim<K>, O>, Sub]
: Unwrap<Trim<K>, O>
)
| ExtractKeys<Rest, O, WithValues>
: never
: ExtractKeys<T, Defaults & O, WithValues>;
/* @internal */
interface IsPlaceholder {
__tpl?: never;
}
/* @internal */
type BrandedSubs = (string | number) & IsPlaceholder;
/* @internal */
type IsNumberLiteral<T, True = true, False = false> = [T] extends [number]
? [number] extends [T] ? False : True
: False;
/* @internal */
type ParseInt<T> = T extends `${infer N extends number}` ? N
: T extends number ? T
: T extends bigint ? `${T}` extends `${infer N extends number}` ? N
: never
: never;
/* @internal */
type NumericKeys<T> = keyof OmitIndexSignature<T> extends infer K
? K extends `${infer N extends number}` ? N
: never
: never;
/* @internal */
type NumericSubstitutionKeys<
T extends string,
O extends Options = Defaults,
> = NumericKeys<Required<Subs<T, O>>>;
type Override<A extends object, B extends object> = NoNever<
Omit<A, keyof B> & Pick<B, keyof B>
>;
/* @internal */
type Filter<T extends readonly unknown[], U> = T extends
readonly [infer H, ...infer R]
? H extends U ? readonly [H, ...Filter<R, U>] : Filter<R, U>
: [];
/* @internal */
type Reshape<T, Deep = false, A = T> = T extends object ? {
[K in keyof T]: [T[K]] extends [never] ? K extends keyof A ? A[K] : never
: Deep extends true ? Reshape<T[K], true, A>
: T[K];
}
: T;
/* @internal */
type IsNever<T, True = true, False = false> = [T] extends [never] ? True
: False;
/* @internal */
type NoNever<T> = { [K in keyof T as IsNever<T[K], never, K>]: T[K] };
/* @internal */
type CollectPartials<
S extends TemplateTuple,
Fallback = never,
R extends readonly TemplateRecord[] = Extract<
Filter<S, TemplateRecord>,
readonly TemplateRecord[]
>,
> = Union.ToIntersection<R[number]> extends infer O ? {
[K in keyof O]: IsNever<
O[K],
R[number] extends { [P in K]: infer V extends Sub } ? V : never,
O[K]
>;
}
: Fallback;
/* @internal */
type ConvertToRecord<
S extends TemplateTuple,
A extends Filter<S, Sub> = Filter<S, Sub>,
> = NoNever<
{
[K in keyof OmitIndexSignature<A>]: K extends number | `${number}` ? A[K]
: never;
}
>;
/* @internal */
declare namespace Union {
/** Union.ToIntersection<{ a: 1 } | { b: 2 }> = { a: 1 } & { b: 2 }. */
export type ToIntersection<U> = (
U extends any ? (arg: U) => 0 : never
) extends (arg: infer I) => 0 ? I : never;
/** Union.ToFlatIntersection<{ a: 1 } | { b: 2 }> = { a: 1; b: 2 }. */
export type ToFlatIntersection<U, Deep = false> =
Reshape<ToIntersection<U>, Deep, U> extends infer I ? I : never;
/** Union.Last<1 | 2> = 2. */
export type Last<U> = Union.ToIntersection<
U extends any ? (x: U) => 0 : never
> extends (x: infer L) => 0 ? L : never;
/** UnionToTuple<1 | 2> = [1, 2]. */
export type ToTuple<U, FALLBACK = never> = ToTupleWorker<U> extends
infer T extends readonly unknown[] ? T : FALLBACK;
type ToTupleWorker<U, Last = Union.Last<U>> = [U] extends [never] ? []
: [...ToTupleWorker<Exclude<U, Last>>, Last];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment