Skip to content

Instantly share code, notes, and snippets.

@coxato
Created March 5, 2025 12:50
Show Gist options
  • Save coxato/910502752d903652dc49d7d86053cc69 to your computer and use it in GitHub Desktop.
Save coxato/910502752d903652dc49d7d86053cc69 to your computer and use it in GitHub Desktop.
Useful class for getting random colors in a color wheel, you can pass the number of steps, also returning as hex or rgba, and more params.
// Types for configuration
interface ColorGeneratorConfig {
initialColor?: string;
numberOfSteps?: number;
currentIndex?: number;
}
// State that we can use later
export interface ColorGeneratorState {
startColor: string;
numberOfSteps: number;
currentIndex: number;
}
// Interface for color options
interface ColorOptions {
alpha?: number; // Value from 0-100
format?: 'hex' | 'rgba'; // Output format
}
// Convert hex to HSL for easier color manipulation
const hexToHSL = (hex: string): [number, number, number] => {
// Remove the hash if present
hex = hex.replace(/^#/, "");
// Parse the hex values
const r = parseInt(hex.slice(0, 2), 16) / 255;
const g = parseInt(hex.slice(2, 4), 16) / 255;
const b = parseInt(hex.slice(4, 6), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h * 360, s * 100, l * 100];
};
// Convert HSL to hex or rgba
const hslToColor = (h: number, s: number, l: number, options?: { alpha?: number, format?: 'hex' | 'rgba' }): string => {
h /= 360;
s /= 100;
l /= 100;
const hue2rgb = (p: number, q: number, t: number): number => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = (x: number): string => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
// Default values
const alpha = options?.alpha;
const format = options?.format || 'hex';
// If we have an alpha value
if (alpha !== undefined) {
const normalizedAlpha = alpha / 100; // Convert from 0-100 to 0-1
// Return rgba format if requested
if (format === 'rgba') {
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${normalizedAlpha.toFixed(2)})`;
}
// Otherwise return hex with alpha
const alphaHex = Math.round(normalizedAlpha * 255).toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}${alphaHex}`;
}
// No alpha value, return standard hex
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
export class ColorGenerator {
private currentIndex: number = 0;
private readonly numberOfSteps: number;
private readonly hueStep: number;
private readonly startHSL: [number, number, number];
public readonly defaultColor: string = "#da4e22";
public readonly startColor: string;
constructor(config: ColorGeneratorConfig = {}) {
this.startColor = config.initialColor || this.defaultColor;
this.numberOfSteps = Math.min(config.numberOfSteps || 10, 360);
this.hueStep = 360 / this.numberOfSteps;
this.startHSL = hexToHSL(this.startColor);
this.currentIndex = config.currentIndex || 0;
}
getNextColor(options?: ColorOptions): string {
// Calculate the new hue by stepping around the color wheel
const newHue = (this.startHSL[0] + this.hueStep * this.currentIndex) % 360;
// Keep saturation and lightness relatively constant for visibility
const saturation = 70; // Fixed saturation for vibrant colors
const lightness = 55; // Fixed lightness for good visibility
// Increment the index for next time
this.currentIndex = (this.currentIndex + 1) % this.numberOfSteps;
// Pass options to get color in the requested format
return hslToColor(newHue, saturation, lightness, options);
}
// Get current state
getState(): ColorGeneratorState {
return {
startColor: this.startColor,
numberOfSteps: this.numberOfSteps,
currentIndex: this.currentIndex,
};
}
// Get state as JSON string
serialize(): string {
return JSON.stringify(this.getState());
}
// Create a new generator from a serialized state
static fromSerialized(serializedJSONString: string): ColorGenerator {
// If the serialized string is empty, return a new generator with default values
if (serializedJSONString === "") {
return new ColorGenerator();
}
try {
const { startColor, numberOfSteps, currentIndex } =
JSON.parse(serializedJSONString);
return new ColorGenerator({
initialColor: startColor,
numberOfSteps,
currentIndex,
});
} catch (err) {
console.error("Error parsing serialized state", err);
console.log("serializedJSONString", serializedJSONString);
console.log("Returning default ColorGenerator");
return new ColorGenerator();
}
}
}
// Example usage:
// Create initial generator
// const generator = new ColorGenerator({ numberOfSteps: 5 });
// // Generate some colors
// console.log(generator.getNextColor()); // First color with full opacity (hex)
// console.log(generator.getNextColor({ alpha: 50 })); // Second color with 50% opacity (hex with alpha)
// console.log(generator.getNextColor({ alpha: 75, format: 'rgba' })); // Color with 75% opacity in rgba format
// // Save the state
// const serializedState = generator.serialize();
// // This string can be sent to backend/stored
// // Later, restore the generator from the saved state
// const restoredGenerator = ColorGenerator.fromSerialized(serializedState);
// // Will continue from where the previous generator left off
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment