| 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; |