1
|
|
|
import { cssNamedColors } from './cssNamedColors' |
2
|
|
|
import { CssColor } from './CssNamedColorsType' |
3
|
|
|
|
4
|
|
|
export enum NumberType { |
5
|
|
|
COLOR = 0xff, |
6
|
|
|
RGB = 0xffffff, |
7
|
|
|
THRESHOLD = 1, |
8
|
|
|
} |
9
|
|
|
|
10
|
|
|
export class FontColorContrast { |
11
|
|
|
red = 0 |
12
|
|
|
green = 0 |
13
|
|
|
blue = 0 |
14
|
|
|
|
15
|
|
|
#hexColorOrRedOrArray: string | number | number[] |
16
|
|
|
#greenOrThreshold?: number |
17
|
|
|
#blue?: number |
18
|
|
|
#threshold?: number |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Contrast threshold to control the resulting font color, float values from 0 to 1. Default is 0.5 |
22
|
|
|
*/ |
23
|
|
|
threshold = 0.5 |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Sets the #params in the instance |
27
|
|
|
* @param hexColorOrRedOrArray One of the options: hex color number, hex color string, named CSS color, array with red, green and blue or string or the red portion of the color |
28
|
|
|
* @param greenOrThreshold The green portion of the color or the contrast threshold to control the resulting font color |
29
|
|
|
* @param blue The blue portion of the color |
30
|
|
|
* @param threshold Contrast threshold to control the resulting font color |
31
|
|
|
*/ |
32
|
|
|
constructor (hexColorOrRedOrArray: string | number | number[] | CssColor, greenOrThreshold?: number, blue?: number, threshold?: number) { |
33
|
|
|
this.#hexColorOrRedOrArray = hexColorOrRedOrArray |
34
|
|
|
this.#greenOrThreshold = greenOrThreshold |
35
|
|
|
this.#blue = blue |
36
|
|
|
this.#threshold = threshold |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Analyses the color (normally used in the background) and retrieves what color (black or white) has a better contrast. |
41
|
|
|
* @returns The best contrast between black and white |
42
|
|
|
*/ |
43
|
|
|
getColor () { |
44
|
|
|
if (this.isRgb()) { |
45
|
|
|
this.setColorsFromRgbNumbers() |
46
|
|
|
} else if (this.isHexString()) { |
47
|
|
|
this.setColorsFromHexString() |
48
|
|
|
} else if (this.isNumber()) { |
49
|
|
|
this.setColorsFromNumber() |
50
|
|
|
} else if (this.isArray()) { |
51
|
|
|
this.setColorsFromArray() |
52
|
|
|
} else { |
53
|
|
|
return '#ffffff' |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
return this.contrastFromHSP() |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Checks if the color is set as RGB on each param |
61
|
|
|
* @returns True if color is set as RGB on each param |
62
|
|
|
*/ |
63
|
|
|
isRgb () { |
64
|
|
|
return ( |
65
|
|
|
FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray, NumberType.COLOR) && |
66
|
|
|
FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.COLOR) && |
67
|
|
|
FontColorContrast.isValidNumber(this.#blue, NumberType.COLOR) && |
68
|
|
|
FontColorContrast.isValidNumber(this.#threshold, NumberType.THRESHOLD) |
69
|
|
|
) |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Checks if color is set on the first param as a hex string and removes the hash of it |
74
|
|
|
* @returns True if color is a hex string |
75
|
|
|
*/ |
76
|
|
|
isHexString () { |
77
|
|
|
const [cleanString, hexNum] = this.getCleanStringAndHexNum() |
78
|
|
|
|
79
|
|
|
if (FontColorContrast.isValidNumber(hexNum, NumberType.RGB) && |
80
|
|
|
FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.THRESHOLD) && |
81
|
|
|
FontColorContrast.isNotSet(this.#blue) && |
82
|
|
|
FontColorContrast.isNotSet(this.#threshold) |
83
|
|
|
) { |
84
|
|
|
this.#hexColorOrRedOrArray = cleanString |
85
|
|
|
return true |
86
|
|
|
} |
87
|
|
|
return false |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Checks if color is set on the first param as a number |
92
|
|
|
* @returns True if color is a valid RGB nunbernumber |
93
|
|
|
*/ |
94
|
|
|
isNumber () { |
95
|
|
|
return ( |
96
|
|
|
FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray, NumberType.RGB) && |
97
|
|
|
FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.THRESHOLD) && |
98
|
|
|
FontColorContrast.isNotSet(this.#blue) && |
99
|
|
|
FontColorContrast.isNotSet(this.#threshold) |
100
|
|
|
) |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Checks if color is set as an RGB array |
105
|
|
|
* @returns True if color is set as an RGB array |
106
|
|
|
*/ |
107
|
|
|
isArray () { |
108
|
|
|
return ( |
109
|
|
|
Array.isArray(this.#hexColorOrRedOrArray) && |
110
|
|
|
this.#hexColorOrRedOrArray.length === 3 && |
111
|
|
|
FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray[0], NumberType.COLOR) && |
112
|
|
|
FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray[1], NumberType.COLOR) && |
113
|
|
|
FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray[2], NumberType.COLOR) && |
114
|
|
|
FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.THRESHOLD) && |
115
|
|
|
FontColorContrast.isNotSet(this.#blue) && |
116
|
|
|
FontColorContrast.isNotSet(this.#threshold) |
117
|
|
|
) |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Converts a color array or separated in RGB to the respective RGB values |
122
|
|
|
* @example All these examples produces the same value |
123
|
|
|
* arrayOrRgbToRGB(0, 0xcc, 153) |
124
|
|
|
* arrayOrRgbToRGB(0x0, 0xcc, 153) |
125
|
|
|
* arrayOrRgbToRGB(0, 204, 0x99) |
126
|
|
|
*/ |
127
|
|
|
setColorsFromRgbNumbers (): void { |
128
|
|
|
this.red = this.#hexColorOrRedOrArray as number |
129
|
|
|
this.green = this.#greenOrThreshold as number |
130
|
|
|
this.blue = this.#blue as number |
131
|
|
|
this.setThreshold(this.#threshold) |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Converts a color array or separated in RGB to the respective RGB values |
136
|
|
|
* @param this.#hexColorOrRedOrArray The RGB array |
137
|
|
|
* @param threshold The threshold |
138
|
|
|
* @example All these examples produces the same value |
139
|
|
|
* arrayOrRgbToRGB([0, 0xcc, 153]) |
140
|
|
|
* arrayOrRgbToRGB([0x0, 0xcc, 153]) |
141
|
|
|
* arrayOrRgbToRGB([0, 204, 0x99]) |
142
|
|
|
*/ |
143
|
|
|
setColorsFromArray (): void { |
144
|
|
|
this.red = (this.#hexColorOrRedOrArray as number[])[0] |
145
|
|
|
this.green = (this.#hexColorOrRedOrArray as number[])[1] |
146
|
|
|
this.blue = (this.#hexColorOrRedOrArray as number[])[2] |
147
|
|
|
this.setThreshold(this.#greenOrThreshold) |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Converts a ColorIntensity string or number, with all possibilities (e.g. '#009', '009', '#000099', '000099', 153, 0x00099) to the respective RGB values |
152
|
|
|
* @param hexColor The color string or number |
153
|
|
|
* @param threshold The threshold |
154
|
|
|
* @example All these examples produces the same value |
155
|
|
|
* hexColorToRGB('#0C9') |
156
|
|
|
* hexColorToRGB('0C9') |
157
|
|
|
* hexColorToRGB('#00CC99') |
158
|
|
|
* hexColorToRGB('00cc99') |
159
|
|
|
* hexColorToRGB(52377) |
160
|
|
|
* hexColorToRGB(0x00Cc99) |
161
|
|
|
*/ |
162
|
|
|
setColorsFromHexString (): void { |
163
|
|
|
switch ((this.#hexColorOrRedOrArray as string).length) { |
164
|
|
|
// Color has one char for each color, so they must be repeated |
165
|
|
|
case 3: |
166
|
|
|
this.red = parseInt((this.#hexColorOrRedOrArray as string)[0].repeat(2), 16) |
167
|
|
|
this.green = parseInt((this.#hexColorOrRedOrArray as string)[1].repeat(2), 16) |
168
|
|
|
this.blue = parseInt((this.#hexColorOrRedOrArray as string)[2].repeat(2), 16) |
169
|
|
|
break |
170
|
|
|
// All chars are filled, so no transformation is needed |
171
|
|
|
default: |
172
|
|
|
this.red = parseInt((this.#hexColorOrRedOrArray as string).substring(0, 2), 16) |
173
|
|
|
this.green = parseInt((this.#hexColorOrRedOrArray as string).substring(2, 4), 16) |
174
|
|
|
this.blue = parseInt((this.#hexColorOrRedOrArray as string).substring(4, 6), 16) |
175
|
|
|
break |
176
|
|
|
} |
177
|
|
|
this.setThreshold(this.#greenOrThreshold) |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Converts the RGB number and sets the respective RGB values. |
182
|
|
|
*/ |
183
|
|
|
setColorsFromNumber (): void { |
184
|
|
|
/* |
185
|
|
|
* The RGB color has 24 bits (8 bits per color). |
186
|
|
|
* This function uses binary operations for better performance, but can be tricky to understand. A 24 bits color could be represented as RRRRRRRR GGGGGGGG BBBBBBBB (the first 8 bits are red, the middle 8 bits are green and the last 8 bits are blue). |
187
|
|
|
* To get each color we perform some RIGHT SHIFT and AND operations. |
188
|
|
|
* Gets the first 8 bits of the color by shifting it 16 bits |
189
|
|
|
* RIGHT SHIFT operation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Right_shift) |
190
|
|
|
* AND operation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_AND) |
191
|
|
|
*/ |
192
|
|
|
|
193
|
|
|
// To get red, we shift the 24 bits number 16 bits to the right, leaving the number only with the leftmost 8 bits (RRRRRRRR) |
194
|
|
|
this.red = (this.#hexColorOrRedOrArray as number) >> 16 |
195
|
|
|
// To get green, the middle 8 bits, we shift it by 8 bits (removing all blue bits - RRRRRRRR GGGGGGGG) and use an AND operation with "0b0000000011111111 = 0xff" to get only the rightmost bits (GGGGGGGG) |
196
|
|
|
this.green = ((this.#hexColorOrRedOrArray as number) >> 8) & 0xff |
197
|
|
|
// To get blue we use an AND operation with "0b000000000000000011111111 = 0xff" to get only the rightmost bits (BBBBBBBB) |
198
|
|
|
this.blue = (this.#hexColorOrRedOrArray as number) & 0xff |
199
|
|
|
this.setThreshold(this.#greenOrThreshold) |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Sets the threshold to the passed value (if valid - less than or equal 1) or the dafault (0.5) |
204
|
|
|
* @param threshold The passed threshold or undefined if not passed |
205
|
|
|
*/ |
206
|
|
|
setThreshold (threshold: any) { |
207
|
|
|
this.threshold = threshold || this.threshold |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Verifies if a number is a valid color number (numberType = NumberType.COLOR = 0xff) or a valid RGB (numberType = NumberType.RGB = 0xffffff) or a valid threshold (numberType = NumberType.THRESHOLD = 1) |
212
|
|
|
* @param num The number to be checked |
213
|
|
|
* @param numberType The type of number to be chacked that defines maximum value of the number (default = NumberType.COLOR = 0xff) |
214
|
|
|
* @returns True if the number is valid |
215
|
|
|
*/ |
216
|
|
|
static isValidNumber (num: any, numberType: NumberType): boolean { |
217
|
|
|
if (numberType === NumberType.THRESHOLD && (num === undefined || num === null)) return true |
218
|
|
|
return ( |
219
|
|
|
typeof num === 'number' && |
220
|
|
|
((numberType !== NumberType.THRESHOLD && Number.isInteger(num)) || numberType === NumberType.THRESHOLD) && |
221
|
|
|
num !== undefined && |
222
|
|
|
num !== null && |
223
|
|
|
num >= 0 && |
224
|
|
|
num <= numberType |
225
|
|
|
) |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Verifies if a string is a valig string to be used as a color and if true, returns the correspondent hex number |
230
|
|
|
* @returns Array with an empty string and false if the string is invalid or an array with the clean string and the converted string number] |
231
|
|
|
*/ |
232
|
|
|
getCleanStringAndHexNum (): ['', false]|[string, number] { |
233
|
|
|
if (typeof this.#hexColorOrRedOrArray !== 'string') return ['', false] |
234
|
|
|
|
235
|
|
|
const cleanRegEx = /(#|\s)/ig |
236
|
|
|
|
237
|
|
|
const namedColor = cssNamedColors.find(color => color.name === this.#hexColorOrRedOrArray) |
238
|
|
|
|
239
|
|
|
if (namedColor) { |
240
|
|
|
this.#hexColorOrRedOrArray = namedColor.hex.replace(cleanRegEx, '') |
241
|
|
|
} |
242
|
|
|
const cleanString = (this.#hexColorOrRedOrArray).replace(cleanRegEx, '') |
243
|
|
|
if (cleanString.length !== 3 && cleanString.length !== 6) return ['', false] |
244
|
|
|
|
245
|
|
|
const hexNum = Number('0x' + cleanString) |
246
|
|
|
|
247
|
|
|
return [cleanString, hexNum] |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Verifies if a value is not set |
252
|
|
|
* @param value The value that should be undefined or null |
253
|
|
|
* @returns True if the value is not set |
254
|
|
|
*/ |
255
|
|
|
static isNotSet (value: any): boolean { |
256
|
|
|
return (value === undefined || value === null) |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Calculates the best color (black or white) to contrast with the passed RGB color using the algorithm from https://alienryderflex.com/hsp.html |
261
|
|
|
* @returns Black or White depending on the best possible contrast |
262
|
|
|
*/ |
263
|
|
|
contrastFromHSP (): '#000000'|'#ffffff' { |
264
|
|
|
const pRed = 0.299 |
265
|
|
|
const pGreen = 0.587 |
266
|
|
|
const pBlue = 0.114 |
267
|
|
|
|
268
|
|
|
const contrast = Math.sqrt( |
269
|
|
|
pRed * (this.red / 255) ** 2 + |
270
|
|
|
pGreen * (this.green / 255) ** 2 + |
271
|
|
|
pBlue * (this.blue / 255) ** 2 |
272
|
|
|
) |
273
|
|
|
|
274
|
|
|
return contrast > this.threshold |
275
|
|
|
? '#000000' |
276
|
|
|
: '#ffffff' |
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
|