| 1 | const SATURATION_BOUND = [0, 100]; | | |
| 2 | const LIGHTNESS_BOUND = [0, 100]; | | |
| 3 | | | |
| 4 | const pad2 = str => `${str.length === 1 ? '0' : ''}${str}`; | | |
| 5 | | | |
| 6 | const clamp = (num, min, max) => Math.max(Math.min(num, max), min); | | |
| 7 | | | |
| 8 | const random = (min, max) => Math.floor(Math.random() * ((max - min) + 1)) + min; | | |
| 9 | | | |
| 10 | const randomExclude = (min, max, exclude) => { | | |
| 11 | const r = random(min, max); | | |
| 12 | | | |
| 13 | for (let i = 0; i < exclude?.length; i++) { | | |
| 14 | const value = exclude[i]; | | |
| 15 | | | |
| 16 | if (value?.length === 2 && r >= value[0] && r <= value[1]) { | | |
| 17 | return randomExclude(min, max, exclude); | | |
| 18 | } | | |
| 19 | } | | |
| 20 | | | |
| 21 | return r; | | |
| 22 | }; | | |
| 23 | | | |
| 24 | /** | | |
| 25 | * Generate hashCode | | |
| 26 | * @param {string} str | | |
| 27 | * @return {number} | | |
| 28 | */ | | |
| 29 | const hashCode = str => { | | |
| 30 | const len = str.length; | | |
| 31 | let hash = 0; | | |
| 32 | | | |
| 33 | for (let i = 0; i < len; i++) { | | |
| 34 | hash = ((hash << 5) - hash) + str.charCodeAt(i); | | |
| 35 | hash &= hash; // Convert to 32bit integer | | |
| 36 | } | | |
| 37 | | | |
| 38 | return hash; | | |
| 39 | }; | | |
| 40 | | | |
| 41 | /** | | |
| 42 | * Clamps `num` within the inclusive `range` bounds | | |
| 43 | * @param {number} num | | |
| 44 | * @param {number|Array} range | | |
| 45 | * @return {number} | | |
| 46 | */ | | |
| 47 | const boundHashCode = (num, range) => { | | |
| 48 | if (typeof range === 'number') { | | |
| 49 | return range; | | |
| 50 | } | | |
| 51 | | | |
| 52 | return (num % Math.abs(range[1] - range[0])) + range[0]; | | |
| 53 | }; | | |
| 54 | | | |
| 55 | /** | | |
| 56 | * Sanitizing the `range` | | |
| 57 | * @param {number|Array} range | | |
| 58 | * @param {Array} bound | | |
| 59 | * @return {number|Array} | | |
| 60 | */ | | |
| 61 | const sanitizeRange = (range, bound) => { | | |
| 62 | if (typeof range === 'number') { | | |
| 63 | return clamp(Math.abs(range), ...bound); | | |
| 64 | } | | |
| 65 | | | |
| 66 | if (range.length === 1 || range[0] === range[1]) { | | |
| 67 | return clamp(Math.abs(range[0]), ...bound); | | |
| 68 | } | | |
| 69 | | | |
| 70 | return [ | | |
| 71 | Math.abs(clamp(range[0], ...bound)), | | |
| 72 | clamp(Math.abs(range[1]), ...bound), | | |
| 73 | ]; | | |
| 74 | }; | | |
| 75 | | | |
| 76 | /** | | |
| 77 | * @param {number} p | | |
| 78 | * @param {number} q | | |
| 79 | * @param {number} t | | |
| 80 | * @return {number} | | |
| 81 | */ | | |
| 82 | const hueToRgb = (p, q, t) => { | | |
| 83 | if (t < 0) { | | |
| 84 | t += 1; | | |
| 85 | } else if (t > 1) { | | |
| 86 | t -= 1; | | |
| 87 | } | | |
| 88 | | | |
| 89 | if (t < 1 / 6) { | | |
| 90 | return p + ((q - p) * 6 * t); | | |
| 91 | } | | |
| 92 | | | |
| 93 | if (t < 1 / 2) { | | |
| 94 | return q; | | |
| 95 | } | | |
| 96 | | | |
| 97 | if (t < 2 / 3) { | | |
| 98 | return p + ((q - p) * ((2 / 3) - t) * 6); | | |
| 99 | } | | |
| 100 | | | |
| 101 | return p; | | |
| 102 | }; | | |
| 103 | | | |
| 104 | /** | | |
| 105 | * Converts an HSL color to RGB | | |
| 106 | * @param {number} h Hue | | |
| 107 | * @param {number} s Saturation | | |
| 108 | * @param {number} l Lightness | | |
| 109 | * @return {Array} | | |
| 110 | */ | | |
| 111 | const hslToRgb = (h, s, l) => { | | |
| 112 | let r; | | |
| 113 | let g; | | |
| 114 | let b; | | |
| 115 | | | |
| 116 | h /= 360; | | |
| 117 | s /= 100; | | |
| 118 | l /= 100; | | |
| 119 | | | |
| 120 | if (s === 0) { | | |
| 121 | // achromatic | | |
| 122 | r = g = b = l; | | |
| 123 | } else { | | |
| 124 | const q = l < 0.5 | | |
| 125 | ? l * (1 + s) | | |
| 126 | : (l + s) - (l * s); | | |
| 127 | const p = (2 * l) - q; | | |
| 128 | | | |
| 129 | r = hueToRgb(p, q, h + (1 / 3)); | | |
| 130 | g = hueToRgb(p, q, h); | | |
| 131 | b = hueToRgb(p, q, h - (1 / 3)); | | |
| 132 | } | | |
| 133 | | | |
| 134 | return [ | | |
| 135 | Math.round(r * 255), | | |
| 136 | Math.round(g * 255), | | |
| 137 | Math.round(b * 255), | | |
| 138 | ]; | | |
| 139 | }; | | |
| 140 | | | |
| 141 | /** | | |
| 142 | * Determines whether the RGB color is light or not | | |
| 143 | * http://www.w3.org/TR/AERT#color-contrast | | |
| 144 | * @param {number} r Red | | |
| 145 | * @param {number} g Green | | |
| 146 | * @param {number} b Blue | | |
| 147 | * @param {number} differencePoint | | |
| 148 | * @return {boolean} | | |
| 149 | */ | | |
| 150 | const rgbIsLight = (r, g, b, differencePoint) => ((r * 299) + (g * 587) + (b * 114)) / 1000 >= differencePoint; // eslint-disable-line max-len | | |
| 151 | | | |
| 152 | /** | | |
| 153 | * Converts an HSL color to string format | | |
| 154 | * @param {number} h Hue | | |
| 155 | * @param {number} s Saturation | | |
| 156 | * @param {number} l Lightness | | |
| 157 | * @return {string} | | |
| 158 | */ | | |
| 159 | const hslToString = (h, s, l) => `hsl(${h}, ${s}%, ${l}%)`; | | |
| 160 | | | |
| 161 | /** | | |
| 162 | * Converts RGB color to string format | | |
| 163 | * @param {number} r Red | | |
| 164 | * @param {number} g Green | | |
| 165 | * @param {number} b Blue | | |
| 166 | * @param {string} format Color format | | |
| 167 | * @return {string} | | |
| 168 | */ | | |
| 169 | const rgbFormat = (r, g, b, format) => { | | |
| 170 | switch (format) { | | |
| 171 | case 'rgb': | | |
| 172 | return `rgb(${r}, ${g}, ${b})`; | | |
| 173 | case 'hex': | | |
| 174 | default: | | |
| 175 | return `#${pad2(r.toString(16))}${pad2(g.toString(16))}${pad2(b.toString(16))}`; | | |
| 176 | } | | |
| 177 | }; | | |
| 178 | | | |
| 179 | /** | | |
| 180 | * Generate unique color from `value` | | |
| 181 | * @param {string|number} value | | |
| 182 | * @param {Object} [options={}] | | |
| 183 | * @param {string} [options.format='hex'] | | |
| 184 | * The color format, it can be one of `hex`, `rgb` or `hsl` | | |
| 185 | * @param {number|Array} [options.saturation=[50, 55]] | | |
| 186 | * Determines the color saturation, it can be a number or a range between 0 and 100 | | |
| 187 | * @param {number|Array} [options.lightness=[50, 60]] | | |
| 188 | * Determines the color lightness, it can be a number or a range between 0 and 100 | | |
| 189 | * @param {number} [options.differencePoint=130] | | |
| 190 | * Determines the color brightness difference point. We use it to obtain the `isLight` value | | |
| 191 | * in the output, it can be a number between 0 and 255 | | |
| 192 | * @return {Object} | | |
| 193 | * @example | | |
| 194 | * | | |
| 195 | * ```js | | |
| 196 | * uniqolor('Hello world!') | | |
| 197 | * // { color: "#5cc653", isLight: true } | | |
| 198 | * | | |
| 199 | * uniqolor('Hello world!', { format: 'rgb' }) | | |
| 200 | * // { color: "rgb(92, 198, 83)", isLight: true } | | |
| 201 | * | | |
| 202 | * uniqolor('Hello world!', { | | |
| 203 | * saturation: 30, | | |
| 204 | * lightness: [70, 80], | | |
| 205 | * }) | | |
| 206 | * // { color: "#afd2ac", isLight: true } | | |
| 207 | * | | |
| 208 | * uniqolor('Hello world!', { | | |
| 209 | * saturation: 30, | | |
| 210 | * lightness: [70, 80], | | |
| 211 | * differencePoint: 200, | | |
| 212 | * }) | | |
| 213 | * // { color: "#afd2ac", isLight: false } | | |
| 214 | * ``` | | |
| 215 | */ | | |
| 216 | const uniqolor = (value, { | | |
| 217 | format = 'hex', | | |
| 218 | saturation = [50, 55], | | |
| 219 | lightness = [50, 60], | | |
| 220 | differencePoint = 130, | | |
| 221 | } = {}) => { | | |
| 222 | const hash = Math.abs(hashCode(String(value))); | | |
| 223 | const h = boundHashCode(hash, [0, 360]); | | |
| 224 | const s = boundHashCode(hash, sanitizeRange(saturation, SATURATION_BOUND)); | | |
| 225 | const l = boundHashCode(hash, sanitizeRange(lightness, LIGHTNESS_BOUND)); | | |
| 226 | const [r, g, b] = hslToRgb(h, s, l); | | |
| 227 | | | |
| 228 | return { | | |
| 229 | color: format === 'hsl' | | |
| 230 | ? hslToString(h, s, l) | | |
| 231 | : rgbFormat(r, g, b, format), | | |
| 232 | isLight: rgbIsLight(r, g, b, differencePoint), | | |
| 233 | }; | | |
| 234 | }; | | |
| 235 | | | |
| 236 | /** | | |
| 237 | * Generate random color | | |
| 238 | * @param {Object} [options={}] | | |
| 239 | * @param {string} [options.format='hex'] | | |
| 240 | * The color format, it can be one of `hex`, `rgb` or `hsl` | | |
| 241 | * @param {number|Array} [options.saturation=[50, 55]] | | |
| 242 | * Determines the color saturation, it can be a number or a range between 0 and 100 | | |
| 243 | * @param {number|Array} [options.lightness=[50, 60]] | | |
| 244 | * Determines the color lightness, it can be a number or a range between 0 and 100 | | |
| 245 | * @param {number} [options.differencePoint=130] | | |
| 246 | * Determines the color brightness difference point. We use it to obtain the `isLight` value | | |
| 247 | * in the output, it can be a number between 0 and 255 | | |
| 248 | * @param {Array} [options.excludeHue] | | |
| 249 | * Exclude certain hue ranges. For example to exclude red color range: `[[0, 20], [325, 359]]` | | |
| 250 | * @return {Object} | | |
| 251 | * @example | | |
| 252 | * | | |
| 253 | * ```js | | |
| 254 | * // Generate random color | | |
| 255 | * uniqolor.random() | | |
| 256 | * // { color: "#644cc8", isLight: false } | | |
| 257 | * | | |
| 258 | * // Generate a random color with HSL format | | |
| 259 | * uniqolor.random({ format: 'hsl' }) | | |
| 260 | * // { color: "hsl(89, 55%, 60%)", isLight: true } | | |
| 261 | * | | |
| 262 | * // Generate a random color in specific saturation and lightness | | |
| 263 | * uniqolor.random({ | | |
| 264 | * saturation: 80, | | |
| 265 | * lightness: [70, 80], | | |
| 266 | * }) | | |
| 267 | * // { color: "#c7b9da", isLight: true } | | |
| 268 | * | | |
| 269 | * // Generate a random color but exclude red color range | | |
| 270 | * uniqolor.random({ | | |
| 271 | * excludeHue: [[0, 20], [325, 359]], | | |
| 272 | * }) | | |
| 273 | * // {color: '#53caab', isLight: true} | | |
| 274 | * ``` | | |
| 275 | */ | | |
| 276 | uniqolor.random = ({ | | |
| 277 | format = 'hex', | | |
| 278 | saturation = [50, 55], | | |
| 279 | lightness = [50, 60], | | |
| 280 | differencePoint = 130, | | |
| 281 | excludeHue, | | |
| 282 | } = {}) => { | | |
| 283 | saturation = sanitizeRange(saturation, SATURATION_BOUND); | | |
| 284 | lightness = sanitizeRange(lightness, LIGHTNESS_BOUND); | | |
| 285 | | | |
| 286 | const h = excludeHue ? randomExclude(0, 359, excludeHue) : random(0, 359); | | |
| 287 | const s = typeof saturation === 'number' | | |
| 288 | ? saturation | | |
| 289 | : random(...saturation); | | |
| 290 | const l = typeof lightness === 'number' | | |
| 291 | ? lightness | | |
| 292 | : random(...lightness); | | |
| 293 | const [r, g, b] = hslToRgb(h, s, l); | | |
| 294 | | | |
| 295 | return { | | |
| 296 | color: format === 'hsl' | | |
| 297 | ? hslToString(h, s, l) | | |
| 298 | : rgbFormat(r, g, b, format), | | |
| 299 | isLight: rgbIsLight(r, g, b, differencePoint), | | |
| 300 | }; | | |
| 301 | }; | | |
| 302 | | | |
| 303 | export default uniqolor; | | |