Skip to content

Instantly share code, notes, and snippets.

@rafaelvieiras
Created March 11, 2020 20:17
Show Gist options
  • Select an option

  • Save rafaelvieiras/e587cae53cc24605ece874ed8f2c744f to your computer and use it in GitHub Desktop.

Select an option

Save rafaelvieiras/e587cae53cc24605ece874ed8f2c744f to your computer and use it in GitHub Desktop.
Color Generator Typescript
// 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