@electrovir/color 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The longest CSS color name. Used for color name padding calculations.
3
+ *
4
+ * @category Internal
5
+ */
6
+ export declare const longestColorName: string;
7
+ /**
8
+ * All CSS color names with identical value matches to other CSS color names. Used for color name
9
+ * padding calculations.
10
+ *
11
+ * @category Internal
12
+ */
13
+ export declare const identicalColorNames: Partial<{
14
+ aliceblue: string[];
15
+ antiquewhite: string[];
16
+ aqua: string[];
17
+ aquamarine: string[];
18
+ azure: string[];
19
+ beige: string[];
20
+ bisque: string[];
21
+ black: string[];
22
+ blanchedalmond: string[];
23
+ blue: string[];
24
+ blueviolet: string[];
25
+ brown: string[];
26
+ burlywood: string[];
27
+ cadetblue: string[];
28
+ chartreuse: string[];
29
+ chocolate: string[];
30
+ coral: string[];
31
+ cornflowerblue: string[];
32
+ cornsilk: string[];
33
+ crimson: string[];
34
+ cyan: string[];
35
+ darkblue: string[];
36
+ darkcyan: string[];
37
+ darkgoldenrod: string[];
38
+ darkgray: string[];
39
+ darkgreen: string[];
40
+ darkgrey: string[];
41
+ darkkhaki: string[];
42
+ darkmagenta: string[];
43
+ darkolivegreen: string[];
44
+ darkorange: string[];
45
+ darkorchid: string[];
46
+ darkred: string[];
47
+ darksalmon: string[];
48
+ darkseagreen: string[];
49
+ darkslateblue: string[];
50
+ darkslategray: string[];
51
+ darkslategrey: string[];
52
+ darkturquoise: string[];
53
+ darkviolet: string[];
54
+ deeppink: string[];
55
+ deepskyblue: string[];
56
+ dimgray: string[];
57
+ dimgrey: string[];
58
+ dodgerblue: string[];
59
+ firebrick: string[];
60
+ floralwhite: string[];
61
+ forestgreen: string[];
62
+ fuchsia: string[];
63
+ gainsboro: string[];
64
+ ghostwhite: string[];
65
+ gold: string[];
66
+ goldenrod: string[];
67
+ gray: string[];
68
+ green: string[];
69
+ greenyellow: string[];
70
+ grey: string[];
71
+ honeydew: string[];
72
+ hotpink: string[];
73
+ indianred: string[];
74
+ indigo: string[];
75
+ ivory: string[];
76
+ khaki: string[];
77
+ lavender: string[];
78
+ lavenderblush: string[];
79
+ lawngreen: string[];
80
+ lemonchiffon: string[];
81
+ lightblue: string[];
82
+ lightcoral: string[];
83
+ lightcyan: string[];
84
+ lightgoldenrodyellow: string[];
85
+ lightgray: string[];
86
+ lightgreen: string[];
87
+ lightgrey: string[];
88
+ lightpink: string[];
89
+ lightsalmon: string[];
90
+ lightseagreen: string[];
91
+ lightskyblue: string[];
92
+ lightslategray: string[];
93
+ lightslategrey: string[];
94
+ lightsteelblue: string[];
95
+ lightyellow: string[];
96
+ lime: string[];
97
+ limegreen: string[];
98
+ linen: string[];
99
+ magenta: string[];
100
+ maroon: string[];
101
+ mediumaquamarine: string[];
102
+ mediumblue: string[];
103
+ mediumorchid: string[];
104
+ mediumpurple: string[];
105
+ mediumseagreen: string[];
106
+ mediumslateblue: string[];
107
+ mediumspringgreen: string[];
108
+ mediumturquoise: string[];
109
+ mediumvioletred: string[];
110
+ midnightblue: string[];
111
+ mintcream: string[];
112
+ mistyrose: string[];
113
+ moccasin: string[];
114
+ navajowhite: string[];
115
+ navy: string[];
116
+ oldlace: string[];
117
+ olive: string[];
118
+ olivedrab: string[];
119
+ orange: string[];
120
+ orangered: string[];
121
+ orchid: string[];
122
+ palegoldenrod: string[];
123
+ palegreen: string[];
124
+ paleturquoise: string[];
125
+ palevioletred: string[];
126
+ papayawhip: string[];
127
+ peachpuff: string[];
128
+ peru: string[];
129
+ pink: string[];
130
+ plum: string[];
131
+ powderblue: string[];
132
+ purple: string[];
133
+ rebeccapurple: string[];
134
+ red: string[];
135
+ rosybrown: string[];
136
+ royalblue: string[];
137
+ saddlebrown: string[];
138
+ salmon: string[];
139
+ sandybrown: string[];
140
+ seagreen: string[];
141
+ seashell: string[];
142
+ sienna: string[];
143
+ silver: string[];
144
+ skyblue: string[];
145
+ slateblue: string[];
146
+ slategray: string[];
147
+ slategrey: string[];
148
+ snow: string[];
149
+ springgreen: string[];
150
+ steelblue: string[];
151
+ tan: string[];
152
+ teal: string[];
153
+ thistle: string[];
154
+ tomato: string[];
155
+ turquoise: string[];
156
+ violet: string[];
157
+ wheat: string[];
158
+ white: string[];
159
+ whitesmoke: string[];
160
+ yellow: string[];
161
+ yellowgreen: string[];
162
+ }>;
163
+ /**
164
+ * The longest collection of CSS color names with identical values. Used for color name padding
165
+ * calculations.
166
+ *
167
+ * @category Internal
168
+ */
169
+ export declare const longestIdenticalColorNames: string[];
170
+ /**
171
+ * The maximum length that a single-value collection of matching CSS color names can be (when joined
172
+ * with `', '`). Used for color name padding calculations.
173
+ *
174
+ * @category Internal
175
+ */
176
+ export declare const maxColorNameLength: number;
@@ -0,0 +1,75 @@
1
+ import { check } from '@augment-vir/assert';
2
+ import { filterMap, filterObject, mapObjectValues } from '@augment-vir/common';
3
+ import colorNames from 'color-name';
4
+ /**
5
+ * The longest CSS color name. Used for color name padding calculations.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export const longestColorName = Object.keys(colorNames).reduce((longest, current) => {
10
+ if (current.length > longest.length) {
11
+ return current;
12
+ }
13
+ else {
14
+ return longest;
15
+ }
16
+ });
17
+ /**
18
+ * All CSS color names with identical value matches to other CSS color names. Used for color name
19
+ * padding calculations.
20
+ *
21
+ * @category Internal
22
+ */
23
+ export const identicalColorNames = filterObject(mapObjectValues(colorNames, (colorName, rgbValues) => {
24
+ const matches = filterMap(Object.entries(colorNames), ([colorName]) => colorName, (innerColorName, [, innerRgbValues,]) => {
25
+ if (innerColorName === colorName) {
26
+ return false;
27
+ }
28
+ return check.deepEquals(innerRgbValues, rgbValues);
29
+ });
30
+ return matches;
31
+ }), (colorName, matches) => !!matches.length);
32
+ /**
33
+ * The longest collection of CSS color names with identical values. Used for color name padding
34
+ * calculations.
35
+ *
36
+ * @category Internal
37
+ */
38
+ export const longestIdenticalColorNames = Object.entries(identicalColorNames)
39
+ .reduce((longest, current) => {
40
+ const longestString = [
41
+ longest[0],
42
+ ...longest[1],
43
+ ].join(', ');
44
+ const currentString = [
45
+ current[0],
46
+ ...current[1],
47
+ ].join(', ');
48
+ if (currentString.length > longestString.length) {
49
+ return current;
50
+ }
51
+ else {
52
+ return longest;
53
+ }
54
+ })
55
+ .reduce((combined, current) => {
56
+ if (check.isArray(current)) {
57
+ return [
58
+ ...combined,
59
+ ...current,
60
+ ];
61
+ }
62
+ else {
63
+ return [
64
+ ...combined,
65
+ current,
66
+ ];
67
+ }
68
+ }, []);
69
+ /**
70
+ * The maximum length that a single-value collection of matching CSS color names can be (when joined
71
+ * with `', '`). Used for color name padding calculations.
72
+ *
73
+ * @category Internal
74
+ */
75
+ export const maxColorNameLength = Math.max(longestColorName.length, longestIdenticalColorNames.length + (longestIdenticalColorNames.length - 1) * ', '.length);
@@ -0,0 +1,171 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type RequireExactlyOne } from 'type-fest';
3
+ import { type ColorCoordsByFormat, type ColorFormatName, type ColorValues, type HexColor } from './color-formats.js';
4
+ /**
5
+ * An update to an existing {@link Color} instance. Used in {@link Color.set}.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export type ColorUpdate = RequireExactlyOne<{
10
+ [FormatName in ColorFormatName]: PartialWithUndefined<Record<ColorCoordsByFormat[FormatName], number>>;
11
+ } & {
12
+ hex: HexColor;
13
+ name: string;
14
+ }>;
15
+ /**
16
+ * The values of all supported color formats for a given {@link Color} instance. Accessed via
17
+ * {@link Color.allColors}.
18
+ *
19
+ * @category Internal
20
+ */
21
+ export type AllColorsValues = ColorValues & {
22
+ hex: HexColor;
23
+ names: string[];
24
+ };
25
+ /**
26
+ * A `Color` class with state and the following features:
27
+ *
28
+ * - Color coordinates do not change when they become `'none'`, they stay at their previous value.
29
+ * This makes for a much smoother UI experience.
30
+ * - All relevant color spaces and formats are always set (no extra conversions necessary).
31
+ * - Matching CSS color names are tracked.
32
+ * - This class is exported correctly and the types aren't a mess.
33
+ *
34
+ * @category Color
35
+ */
36
+ export declare class Color {
37
+ #private;
38
+ /**
39
+ * Create a new {@link Color} instance by parsing the output of another instance's
40
+ * {@link Color.serialize} method.
41
+ */
42
+ static deserialize(input: string): Color;
43
+ /** All current color values. These are updated whenever {@link Color.set} is called. */
44
+ protected readonly _allColors: {
45
+ names: string[];
46
+ hex: HexColor;
47
+ rgb: {
48
+ r: number;
49
+ g: number;
50
+ b: number;
51
+ };
52
+ hsl: {
53
+ h: number;
54
+ s: number;
55
+ l: number;
56
+ };
57
+ hwb: {
58
+ h: number;
59
+ w: number;
60
+ b: number;
61
+ };
62
+ lab: {
63
+ l: number;
64
+ a: number;
65
+ b: number;
66
+ };
67
+ lch: {
68
+ l: number;
69
+ c: number;
70
+ h: number;
71
+ };
72
+ oklab: {
73
+ l: number;
74
+ a: number;
75
+ b: number;
76
+ };
77
+ oklch: {
78
+ l: number;
79
+ c: number;
80
+ h: number;
81
+ };
82
+ };
83
+ constructor(
84
+ /** Any valid CSS color string or an object of color coordinate values. */
85
+ initValue: string | Readonly<ColorUpdate>);
86
+ /** Create a new {@link Color} instance that matches this one exactly. */
87
+ clone(): Color;
88
+ /**
89
+ * Update the color to match the given string.
90
+ *
91
+ * @throws Error if `cssColorString` is not able to be parsed.
92
+ */
93
+ protected setByString(cssColorString: string): void;
94
+ /**
95
+ * Update the current color by setting a whole new color, a single coordinate in a single color
96
+ * format, or multiple coordinates in a single color format. This mutates the current
97
+ * {@link Color} instance.
98
+ */
99
+ set(newValue: Readonly<ColorUpdate> | string): void;
100
+ /**
101
+ * Update all internally stored color format values ({@link Color.allColors}) from the updated
102
+ * internal color object.
103
+ */
104
+ protected pullFromInternalColor(): void;
105
+ /**
106
+ * Create a string that can be serialized into a new {@link Color} instance which will exactly
107
+ * match the current {@link Color} instance.
108
+ */
109
+ serialize(): string;
110
+ /** This individual color expressed in all the supported color formats. */
111
+ get allColors(): AllColorsValues;
112
+ /**
113
+ * Converts the values for each supported color format into a padded string for easy display
114
+ * purposes.
115
+ */
116
+ toFormattedStrings(): Record<ColorFormatName | 'hex' | 'names', string>;
117
+ /**
118
+ * Converts the values for each supported color format in a CSS string that can be directly used
119
+ * in any modern CSS code.
120
+ */
121
+ toCss(): Record<ColorFormatName | 'hex' | 'name', string>;
122
+ /**
123
+ * The current color expressed as hardcoded CSS color keywords. If no CSS color keywords match
124
+ * the current color, this array will be empty.
125
+ */
126
+ get names(): string[];
127
+ /** The current color expressed as an RGB hex string. */
128
+ get hex(): HexColor;
129
+ /** The current color expressed as its RGB coordinate values. */
130
+ get rgb(): {
131
+ r: number;
132
+ g: number;
133
+ b: number;
134
+ };
135
+ /** The current color expressed as its HSL coordinate values. */
136
+ get hsl(): {
137
+ h: number;
138
+ s: number;
139
+ l: number;
140
+ };
141
+ /** The current color expressed as its HWB coordinate values. */
142
+ get hwb(): {
143
+ b: number;
144
+ h: number;
145
+ w: number;
146
+ };
147
+ /** The current color expressed as its LAB coordinate values. */
148
+ get lab(): {
149
+ b: number;
150
+ l: number;
151
+ a: number;
152
+ };
153
+ /** The current color expressed as its LCH coordinate values. */
154
+ get lch(): {
155
+ h: number;
156
+ l: number;
157
+ c: number;
158
+ };
159
+ /** The current color expressed as its Oklab coordinate values. */
160
+ get oklab(): {
161
+ b: number;
162
+ l: number;
163
+ a: number;
164
+ };
165
+ /** The current color expressed as its Oklch coordinate values. */
166
+ get oklch(): {
167
+ h: number;
168
+ l: number;
169
+ c: number;
170
+ };
171
+ }
package/dist/color.js ADDED
@@ -0,0 +1,243 @@
1
+ import { assert, assertWrap, check } from '@augment-vir/assert';
2
+ import { copyThroughJson, filterMap, getObjectTypedEntries, joinWithFinalConjunction, mapObjectValues, round, } from '@augment-vir/common';
3
+ import colorNames from 'color-name';
4
+ import { clampGamut, converter, formatHex, parse } from 'culori';
5
+ import { colorFormatNames, colorFormats, } from './color-formats.js';
6
+ import { maxColorNameLength } from './color-name-length.js';
7
+ /**
8
+ * A `Color` class with state and the following features:
9
+ *
10
+ * - Color coordinates do not change when they become `'none'`, they stay at their previous value.
11
+ * This makes for a much smoother UI experience.
12
+ * - All relevant color spaces and formats are always set (no extra conversions necessary).
13
+ * - Matching CSS color names are tracked.
14
+ * - This class is exported correctly and the types aren't a mess.
15
+ *
16
+ * @category Color
17
+ */
18
+ export class Color {
19
+ /**
20
+ * Create a new {@link Color} instance by parsing the output of another instance's
21
+ * {@link Color.serialize} method.
22
+ */
23
+ static deserialize(input) {
24
+ const parsed = JSON.parse(input);
25
+ const newColor = new Color('black');
26
+ getObjectTypedEntries(parsed).forEach(([key, value,]) => {
27
+ newColor._allColors[key] = value;
28
+ });
29
+ return newColor;
30
+ }
31
+ #internalColor = assertWrap.isDefined(parse('black'));
32
+ /** All current color values. These are updated whenever {@link Color.set} is called. */
33
+ _allColors = {
34
+ names: ['black'],
35
+ hex: '#000000',
36
+ rgb: {
37
+ r: 0,
38
+ g: 0,
39
+ b: 0,
40
+ },
41
+ hsl: {
42
+ h: 0,
43
+ s: 0,
44
+ l: 0,
45
+ },
46
+ hwb: {
47
+ h: 0,
48
+ w: 0,
49
+ b: 0,
50
+ },
51
+ lab: {
52
+ l: 0,
53
+ a: 0,
54
+ b: 0,
55
+ },
56
+ lch: {
57
+ l: 0,
58
+ c: 0,
59
+ h: 0,
60
+ },
61
+ oklab: {
62
+ l: 0,
63
+ a: 0,
64
+ b: 0,
65
+ },
66
+ oklch: {
67
+ l: 0,
68
+ c: 0,
69
+ h: 0,
70
+ },
71
+ };
72
+ constructor(
73
+ /** Any valid CSS color string or an object of color coordinate values. */
74
+ initValue) {
75
+ this.set(initValue);
76
+ }
77
+ /** Create a new {@link Color} instance that matches this one exactly. */
78
+ clone() {
79
+ return Color.deserialize(this.serialize());
80
+ }
81
+ /**
82
+ * Update the color to match the given string.
83
+ *
84
+ * @throws Error if `cssColorString` is not able to be parsed.
85
+ */
86
+ setByString(cssColorString) {
87
+ const newColor = parse(cssColorString);
88
+ if (!newColor) {
89
+ throw new Error(`Unable to parse invalid color string: '${cssColorString}'`);
90
+ }
91
+ this.#internalColor = newColor;
92
+ this.pullFromInternalColor();
93
+ }
94
+ /**
95
+ * Update the current color by setting a whole new color, a single coordinate in a single color
96
+ * format, or multiple coordinates in a single color format. This mutates the current
97
+ * {@link Color} instance.
98
+ */
99
+ set(newValue) {
100
+ if (check.isString(newValue)) {
101
+ return this.setByString(newValue);
102
+ }
103
+ assert.isLengthExactly(Object.keys(newValue), 1, `Cannot set multiple color formats at once: got '${joinWithFinalConjunction(Object.keys(newValue))}'`);
104
+ if (newValue.hex || newValue.name) {
105
+ this.setByString(newValue.hex || newValue.name);
106
+ }
107
+ else {
108
+ const [colorFormatName, colorValues,] = assertWrap.isDefined(getObjectTypedEntries(newValue)[0]);
109
+ const colorFormatDefinition = colorFormats[colorFormatName];
110
+ const orderedColorCoords = Object.values(mapObjectValues(colorFormatDefinition.coords, (coordName) => {
111
+ const coordValue = colorValues[coordName];
112
+ const coordDefinition = assertWrap.isDefined(colorFormatDefinition.coords[coordName]);
113
+ const rawCoordValue = coordValue != undefined &&
114
+ coordValue >= coordDefinition.min &&
115
+ coordValue <= coordDefinition.max
116
+ ? colorValues[coordName]
117
+ : this[colorFormatName][coordName];
118
+ return assertWrap.isDefined(rawCoordValue);
119
+ }));
120
+ this.setByString(`${colorFormatName}(${orderedColorCoords.join(' ')})`);
121
+ }
122
+ }
123
+ /**
124
+ * Update all internally stored color format values ({@link Color.allColors}) from the updated
125
+ * internal color object.
126
+ */
127
+ pullFromInternalColor() {
128
+ colorFormatNames.forEach((colorFormatName) => {
129
+ const colorFormatDefinition = colorFormats[colorFormatName];
130
+ const originalColorDefinition = check.isKeyOf(this.#internalColor.mode, colorFormats)
131
+ ? colorFormats[this.#internalColor.mode]
132
+ : undefined;
133
+ const converted = clampGamut(colorFormatDefinition.colorSpace === originalColorDefinition?.colorSpace
134
+ ? colorFormatName
135
+ : 'rgb')(converter(colorFormatName)(this.#internalColor));
136
+ /* node:coverage ignore next 5: technically this shouldn't happen, idk how to manually trigger it. */
137
+ if (!converted) {
138
+ assert.never(`Failed to convert color '${JSON.stringify(this.#internalColor)}' to '${colorFormatName}'.`);
139
+ }
140
+ Object.keys(this[colorFormatName]).forEach((coordName) => {
141
+ const coordValue = converted[coordName];
142
+ if (coordValue != undefined) {
143
+ this._allColors[colorFormatName][coordName] = round((coordValue || 0) * (colorFormatDefinition.coords[coordName]?.factor || 1), {
144
+ digits: colorFormatDefinition.coords[coordName]?.digits || 0,
145
+ });
146
+ }
147
+ });
148
+ });
149
+ this._allColors.hex = formatHex(this.#internalColor);
150
+ this._allColors.names = findMatchingColorNames(this.rgb);
151
+ }
152
+ /**
153
+ * Create a string that can be serialized into a new {@link Color} instance which will exactly
154
+ * match the current {@link Color} instance.
155
+ */
156
+ serialize() {
157
+ return JSON.stringify(this.allColors);
158
+ }
159
+ /** This individual color expressed in all the supported color formats. */
160
+ get allColors() {
161
+ return copyThroughJson(this._allColors);
162
+ }
163
+ /**
164
+ * Converts the values for each supported color format into a padded string for easy display
165
+ * purposes.
166
+ */
167
+ toFormattedStrings() {
168
+ const colorFormatStrings = mapObjectValues(colorFormats, (colorFormatName) => {
169
+ const coordValues = Object.values(this[colorFormatName]);
170
+ return coordValues.map((coordValue) => String(coordValue).padStart(6, ' ')).join(' ');
171
+ });
172
+ return {
173
+ hex: this.hex,
174
+ ...colorFormatStrings,
175
+ names: this.names.join(', ').padEnd(maxColorNameLength, ' '),
176
+ };
177
+ }
178
+ /**
179
+ * Converts the values for each supported color format in a CSS string that can be directly used
180
+ * in any modern CSS code.
181
+ */
182
+ toCss() {
183
+ const colorFormatStrings = mapObjectValues(colorFormats, (colorFormatName) => {
184
+ const coordValues = Object.values(this[colorFormatName]);
185
+ return `${colorFormatName}(${coordValues.join(' ')})`;
186
+ });
187
+ return {
188
+ hex: this.hex,
189
+ ...colorFormatStrings,
190
+ name: this.names[0] || '',
191
+ };
192
+ }
193
+ /**
194
+ * The current color expressed as hardcoded CSS color keywords. If no CSS color keywords match
195
+ * the current color, this array will be empty.
196
+ */
197
+ get names() {
198
+ return copyThroughJson(this._allColors.names);
199
+ }
200
+ /** The current color expressed as an RGB hex string. */
201
+ get hex() {
202
+ return copyThroughJson(this._allColors.hex);
203
+ }
204
+ /** The current color expressed as its RGB coordinate values. */
205
+ get rgb() {
206
+ return copyThroughJson(this._allColors.rgb);
207
+ }
208
+ /** The current color expressed as its HSL coordinate values. */
209
+ get hsl() {
210
+ return copyThroughJson(this._allColors.hsl);
211
+ }
212
+ /** The current color expressed as its HWB coordinate values. */
213
+ get hwb() {
214
+ return copyThroughJson(this._allColors.hwb);
215
+ }
216
+ /** The current color expressed as its LAB coordinate values. */
217
+ get lab() {
218
+ return copyThroughJson(this._allColors.lab);
219
+ }
220
+ /** The current color expressed as its LCH coordinate values. */
221
+ get lch() {
222
+ return copyThroughJson(this._allColors.lch);
223
+ }
224
+ /** The current color expressed as its Oklab coordinate values. */
225
+ get oklab() {
226
+ return copyThroughJson(this._allColors.oklab);
227
+ }
228
+ /** The current color expressed as its Oklch coordinate values. */
229
+ get oklch() {
230
+ return copyThroughJson(this._allColors.oklch);
231
+ }
232
+ }
233
+ function findMatchingColorNames(rgb) {
234
+ return filterMap(getObjectTypedEntries(colorNames), ([colorName,]) => {
235
+ return colorName;
236
+ }, (colorName, [, colorValues,]) => {
237
+ return check.deepEquals(colorValues, [
238
+ rgb.r,
239
+ rgb.g,
240
+ rgb.b,
241
+ ]);
242
+ });
243
+ }
@@ -0,0 +1,3 @@
1
+ export * from './color-formats.js';
2
+ export * from './color-name-length.js';
3
+ export * from './color.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './color-formats.js';
2
+ export * from './color-name-length.js';
3
+ export * from './color.js';