src/FontColorContrast.ts   A
last analyzed

Complexity

Total Complexity 19
Complexity/F 2.38

Size

Lines of Code 279
Function Count 8

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 19
eloc 151
mnd 11
bc 11
fnc 8
dl 0
loc 279
rs 10
bpm 1.375
cpm 2.375
noi 0
c 0
b 0
f 0

8 Functions

Rating   Name   Duplication   Size   Complexity  
A FontColorContrast.isArray 0 15 1
A FontColorContrast.setColorsFromArray 0 15 1
A FontColorContrast.setColorsFromHexString 0 21 2
A FontColorContrast.getColor 0 19 5
A FontColorContrast.isNumber 0 11 1
A FontColorContrast.setColorsFromRgbNumbers 0 13 1
A FontColorContrast.isRgb 0 11 1
A FontColorContrast.isHexString 0 17 2
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