Created
March 11, 2020 20:17
-
-
Save rafaelvieiras/e587cae53cc24605ece874ed8f2c744f to your computer and use it in GitHub Desktop.
Color Generator Typescript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // randomColor by David Merfield under the CC0 license | |
| // https://github.com/davidmerfield/randomColor/ | |
| export class RandomColor { | |
| constructor() { | |
| this.populateColorDictionary(); | |
| } | |
| // Seed to get repeatable colors | |
| private seed: number = null; | |
| // Shared color dictionary | |
| private colorDictionary: { | |
| [key: string]: { | |
| hueRange?: any[][]; | |
| lowerBounds?: any[][]; | |
| saturationRange?: any; | |
| brightnessRange?: any; | |
| } | |
| }; | |
| // check if a range is taken | |
| private colorRanges = []; | |
| private defineColor(options?: {name: string, hueRange: number[][], lowerBounds: number[][]}) { | |
| const sMin = options.lowerBounds[0][0]; | |
| const sMax = options.lowerBounds[options.lowerBounds.length - 1][0]; | |
| const bMin = options.lowerBounds[options.lowerBounds.length - 1][1]; | |
| const bMax = options.lowerBounds[0][1]; | |
| const tempObj = {}; | |
| tempObj[options.name] = { | |
| hueRange: options.hueRange, | |
| lowerBounds: options.lowerBounds, | |
| saturationRange: [sMin, sMax], | |
| brightnessRange: [bMin, bMax] | |
| }; | |
| this.colorDictionary = { | |
| ...this.colorDictionary, | |
| ...tempObj | |
| }; | |
| } | |
| private populateColorDictionary() { | |
| this.defineColor({ | |
| name: 'monochrome', | |
| hueRange: null, | |
| lowerBounds: [[0,0],[100,0]] | |
| }); | |
| this.defineColor({ | |
| name: 'red', | |
| hueRange: [-26,18], | |
| lowerBounds: [[20,100],[30,92],[40,89],[50,85],[60,78],[70,70],[80,60],[90,55],[100,50]] | |
| }); | |
| this.defineColor({ | |
| name: 'orange', | |
| hueRange: [19,46], | |
| lowerBounds: [[20,100],[30,93],[40,88],[50,86],[60,85],[70,70],[100,70]] | |
| }); | |
| this.defineColor({ | |
| name: 'yellow', | |
| hueRange: [47,62], | |
| lowerBounds: [[25,100],[40,94],[50,89],[60,86],[70,84],[80,82],[90,80],[100,75]] | |
| }); | |
| this.defineColor({ | |
| name: 'green', | |
| hueRange: [63,178], | |
| lowerBounds: [[30,100],[40,90],[50,85],[60,81],[70,74],[80,64],[90,50],[100,40]] | |
| }); | |
| this.defineColor({ | |
| name: 'blue', | |
| hueRange: [179, 257], | |
| lowerBounds: [[20,100],[30,86],[40,80],[50,74],[60,60],[70,52],[80,44],[90,39],[100,35]] | |
| }); | |
| this.defineColor({ | |
| name: 'purple', | |
| hueRange: [258, 282], | |
| lowerBounds: [[20,100],[30,87],[40,79],[50,70],[60,65],[70,59],[80,52],[90,45],[100,42]] | |
| }); | |
| this.defineColor({ | |
| name: 'pink', | |
| hueRange: [283, 334], | |
| lowerBounds: [[20,100],[30,90],[40,86],[60,84],[80,80],[90,75],[100,73]] | |
| }); | |
| } | |
| public randomColor(options?: { seed?: any, count?: any, hue?: any, luminosity?: any, format?: any, alpha?: any}) { | |
| // Check if there is a seed and ensure it's an | |
| // integer. Otherwise, reset the seed value. | |
| if (options.seed !== undefined && options.seed !== null && options.seed === parseInt(options.seed, 10)) { | |
| this.seed = options.seed; | |
| // A string was passed as a seed | |
| } else if (typeof options.seed === 'string') { | |
| this.seed = this.stringToInteger(options.seed); | |
| // Something was passed as a seed but it wasn't an integer or string | |
| } else if (options.seed !== undefined && options.seed !== null) { | |
| throw new TypeError('The seed value must be an integer or string'); | |
| // No seed, reset the value outside. | |
| } else { | |
| this.seed = null; | |
| } | |
| // Check if we need to generate multiple colors | |
| if (options.count !== null && options.count !== undefined) { | |
| const totalColors = options.count; | |
| const colors = []; | |
| // Value false at index i means the range i is not taken yet. | |
| for (let i = 0; i < options.count; i++) { | |
| this.colorRanges.push(false); | |
| } | |
| options.count = null; | |
| while (totalColors > colors.length) { | |
| // Since we're generating multiple colors, | |
| // increment the seed. Otherwise we'd just | |
| // generate the same color each time... | |
| if (this.seed && options.seed) { | |
| options.seed += 1; | |
| } | |
| colors.push(this.randomColor(options)); | |
| } | |
| options.count = totalColors; | |
| return colors; | |
| } | |
| // First we pick a hue (H) | |
| const hue = this.pickHue(options); | |
| // Then use H to determine saturation (S) | |
| const saturation = this.pickSaturation({ hue }); | |
| // Then use S and H to determine brightness (B). | |
| const brightness = this.pickBrightness({hue, saturation, extras: { luminosity: options.luminosity }}); | |
| // Then we return the HSB color in the desired format | |
| return this.setFormat({ | |
| hsv: [ | |
| hue, | |
| saturation, | |
| brightness | |
| ], | |
| extra: { | |
| format: options.format, | |
| alpha: options.alpha | |
| } | |
| }); | |
| } | |
| private stringToInteger(value: string): number { | |
| let total = 0; | |
| for (let i = 0; i !== value.length; i++) { | |
| if (total >= Number.MAX_SAFE_INTEGER) { | |
| break; | |
| } | |
| total += value.charCodeAt(i); | |
| } | |
| return total; | |
| } | |
| private pickHue(options?: { seed?: any, count?: any, hue?: any}) { | |
| if (this.colorRanges.length > 0) { | |
| let hueRange = this.getRealHueRange(options.hue); | |
| let hue = this.randomWithin(hueRange); | |
| // Each of colorRanges.length ranges has a length equal approximately one step | |
| const step = (hueRange[1] - hueRange[0]) / this.colorRanges.length; | |
| let j = (hue - hueRange[0]) / step; | |
| // Check if the range j is taken | |
| if (this.colorRanges[j] === true) { | |
| j = (j + 2) % this.colorRanges.length; | |
| } else { | |
| this.colorRanges[j] = true; | |
| } | |
| const min = (hueRange[0] + j * step) % 359; | |
| const max = (hueRange[0] + (j + 1) * step) % 359; | |
| hueRange = [min, max]; | |
| hue = this.randomWithin(hueRange); | |
| if (hue < 0) { | |
| hue = 360 + hue; | |
| } | |
| return hue; | |
| } else { | |
| const hueRange = this.getHueRange(options.hue); | |
| let hue = this.randomWithin(hueRange); | |
| // Instead of storing red as two separate ranges, | |
| // we group them, using negative numbers | |
| if (hue < 0) { | |
| hue = 360 + hue; | |
| } | |
| return hue; | |
| } | |
| } | |
| // get The range of given hue when options.count!=0 | |
| private getRealHueRange(colorHue) { | |
| if (!isNaN(colorHue)) { | |
| const colorNumber = parseInt(colorHue, 0); | |
| if (colorNumber < 360 && colorNumber > 0) { | |
| return this.getColorInfo(colorHue).hueRange; | |
| } | |
| } else if (typeof colorHue === 'string') { | |
| if (this.colorDictionary[colorHue]) { | |
| const color = this.colorDictionary[colorHue]; | |
| if (color.hueRange) { | |
| return color.hueRange; | |
| } | |
| } else if (colorHue.match(/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i)) { | |
| const hue = this.HexToHSB(colorHue)[0]; | |
| return this.getColorInfo(hue).hueRange; | |
| } | |
| } | |
| return [0, 360]; | |
| } | |
| private randomWithin(range) { | |
| if (this.seed === null) { | |
| // generate random evenly distinct number from : https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/ | |
| const goldenRatio = 0.618033988749895; | |
| let randomNumber = Math.random(); | |
| randomNumber += goldenRatio; | |
| randomNumber %= 1; | |
| return Math.floor(range[0] + randomNumber * (range[1] + 1 - range[0])); | |
| } else { | |
| // Seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ | |
| const max = range[1] || 1; | |
| const min = range[0] || 0; | |
| this.seed = (this.seed * 9301 + 49297) % 233280; | |
| const rnd = this.seed / 233280.0; | |
| return Math.floor(min + rnd * (max - min)); | |
| } | |
| } | |
| private getHueRange(colorInput) { | |
| if (typeof colorInput === 'number') { | |
| const colorNumber = colorInput; | |
| if (colorNumber < 360 && colorNumber > 0) { | |
| return [colorNumber, colorNumber]; | |
| } | |
| } | |
| if (typeof colorInput === 'string') { | |
| if (this.colorDictionary[colorInput]) { | |
| const color = this.colorDictionary[colorInput]; | |
| if (color.hueRange) { | |
| return color.hueRange; | |
| } | |
| } else if (colorInput.match(/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i)) { | |
| const hue = this.HexToHSB(colorInput)[0]; | |
| return [ hue, hue ]; | |
| } | |
| } | |
| return [0, 360]; | |
| } | |
| private HexToHSB(hex) { | |
| hex = hex.replace(/^#/, ''); | |
| hex = hex.length === 3 ? hex.replace(/(.)/g, '$1$1') : hex; | |
| const red = parseInt(hex.substr(0, 2), 16) / 255; | |
| const green = parseInt(hex.substr(2, 2), 16) / 255; | |
| const blue = parseInt(hex.substr(4, 2), 16) / 255; | |
| const cMax = Math.max(red, green, blue); | |
| const delta = cMax - Math.min(red, green, blue); | |
| const saturation = cMax ? (delta / cMax) : 0; | |
| switch (cMax) { | |
| case red: return [ 60 * (((green - blue) / delta) % 6) || 0, saturation, cMax ]; | |
| case green: return [ 60 * (((blue - red) / delta) + 2) || 0, saturation, cMax ]; | |
| case blue: return [ 60 * (((red - green) / delta) + 4) || 0, saturation, cMax ]; | |
| } | |
| } | |
| private pickSaturation(options?: {luminosity?: string, hue?: any}) { | |
| if (options.hue === 'monochrome') { | |
| return 0; | |
| } | |
| if (options.luminosity === 'random') { | |
| return this.randomWithin([0, 100]); | |
| } | |
| const saturationRange = this.getSaturationRange(options.hue); | |
| let sMin = saturationRange[0]; | |
| let sMax = saturationRange[1]; | |
| switch (options.luminosity) { | |
| case 'bright': | |
| sMin = 55; | |
| break; | |
| case 'dark': | |
| sMin = sMax - 10; | |
| break; | |
| case 'light': | |
| sMax = 55; | |
| break; | |
| } | |
| return this.randomWithin([sMin, sMax]); | |
| } | |
| private getSaturationRange(hue) { | |
| return this.getColorInfo(hue).saturationRange; | |
| } | |
| private getColorInfo(hue) { | |
| // Maps red colors to make picking hue easier | |
| if (hue >= 334 && hue <= 360) { | |
| hue -= 360; | |
| } | |
| for (const colorName of this.colorDictionary) { | |
| const color = this.colorDictionary[colorName]; | |
| if ( | |
| color.hueRange && | |
| hue >= color.hueRange[0] && | |
| hue <= color.hueRange[1] | |
| ) { | |
| return this.colorDictionary[colorName]; | |
| } | |
| } | |
| return { | |
| hueRange: '', | |
| lowerBounds: [], | |
| saturationRange: '', | |
| brightnessRange: '' | |
| }; | |
| } | |
| private pickBrightness(options?: {hue?: any, saturation?: number, extras: {luminosity: string}}) { | |
| let bMin = this.getMinimumBrightness({ hue: options.hue, saturation: options.saturation}); | |
| let bMax = 100; | |
| switch (options.extras.luminosity) { | |
| case 'dark': | |
| bMax = bMin + 20; | |
| break; | |
| case 'light': | |
| bMin = (bMax + bMin) / 2; | |
| break; | |
| case 'random': | |
| bMin = 0; | |
| bMax = 100; | |
| break; | |
| } | |
| return this.randomWithin([bMin, bMax]); | |
| } | |
| private getMinimumBrightness(options?: {hue?: string, saturation?: number}) { | |
| const lowerBounds = this.getColorInfo(options.hue).lowerBounds; | |
| for (let i = 0; i < lowerBounds.length - 1; i++) { | |
| const s1 = lowerBounds[i][0]; | |
| const v1 = lowerBounds[i][1]; | |
| const s2 = lowerBounds[i + 1][0]; | |
| const v2 = lowerBounds[i + 1][1]; | |
| if (options.saturation >= s1 && options.saturation <= s2) { | |
| const m = (v2 - v1) / (s2 - s1); | |
| const b = v1 - m * s1; | |
| return m * options.saturation + b; | |
| } | |
| } | |
| return 0; | |
| } | |
| private setFormat(options?: {hsv: number[], extra: { format: any, alpha: any}}) { | |
| let alphaColor; | |
| switch (options.extra.format) { | |
| case 'hsvArray': | |
| return options.hsv; | |
| case 'hslArray': | |
| return this.HSVtoHSL(options.hsv); | |
| case 'hsl': | |
| const hsl = this.HSVtoHSL(options.hsv); | |
| return 'hsl(' + hsl[0] + ', ' + hsl[1] + '%, ' + hsl[2] + '%)'; | |
| case 'hsla': | |
| const hslColor = this.HSVtoHSL(options.hsv); | |
| alphaColor = options.extra.alpha || Math.random(); | |
| return 'hsla(' + hslColor[0] + ', ' + hslColor[1] + '%, ' + hslColor[2] + '%, ' + alphaColor + ')'; | |
| case 'rgbArray': | |
| return this.HSVtoRGB(options.hsv); | |
| case 'rgb': | |
| const rgb = this.HSVtoRGB(options.hsv); | |
| return 'rgb(' + rgb.join(', ') + ')'; | |
| case 'rgba': | |
| const rgbColor = this.HSVtoRGB(options.hsv); | |
| alphaColor = options.extra.alpha || Math.random(); | |
| return 'rgba(' + rgbColor.join(', ') + ', ' + alphaColor + ')'; | |
| default: | |
| return this.HSVtoHex(options.hsv); | |
| } | |
| } | |
| private HSVtoRGB(hsv: any) { | |
| // this doesn't work for the values of 0 and 360 | |
| // here's the hacky fix | |
| let h = hsv[0]; | |
| if (h === 0) {h = 1;} | |
| if (h === 360) {h = 359;} | |
| // Rebase the h,s,v values | |
| h = h / 360; | |
| const s = hsv[1] / 100; | |
| const v = hsv[2] / 100; | |
| const hI = Math.floor(h * 6); | |
| const f = h * 6 - hI; | |
| const p = v * (1 - s); | |
| const q = v * (1 - f * s); | |
| const t = v * (1 - (1 - f) * s); | |
| let r = 256; | |
| let g = 256; | |
| let b = 256; | |
| switch (hI) { | |
| case 0: r = v; g = t; b = p; break; | |
| case 1: r = q; g = v; b = p; break; | |
| case 2: r = p; g = v; b = t; break; | |
| case 3: r = p; g = q; b = v; break; | |
| case 4: r = t; g = p; b = v; break; | |
| case 5: r = v; g = p; b = q; break; | |
| } | |
| const result = [Math.floor( r * 255), Math.floor(g * 255), Math.floor(b * 255)]; | |
| return result; | |
| } | |
| private HSVtoHex(hsv: any) { | |
| const rgb = this.HSVtoRGB(hsv); | |
| function componentToHex(component) { | |
| const hexValue = component.toString(16); | |
| return hexValue.length === 1 ? '0' + hexValue : hexValue; | |
| } | |
| const hex = '#' + componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]); | |
| return hex; | |
| } | |
| private HSVtoHSL(hsv) { | |
| const h = hsv[0]; | |
| const s = hsv[1] / 100; | |
| const v = hsv[2] / 100; | |
| const k = (2 - s) * v; | |
| return [ | |
| h, | |
| Math.round(s * v / (k < 1 ? k : 2 - k) * 10000) / 100, | |
| k / 2 * 100 | |
| ]; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment