Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active April 17, 2025 07:02
Show Gist options
  • Select an option

  • Save nberlette/33a1a21f66a0b9814d73006cd080ce5f to your computer and use it in GitHub Desktop.

Select an option

Save nberlette/33a1a21f66a0b9814d73006cd080ce5f to your computer and use it in GitHub Desktop.
colorhash
import * as ansi from "jsr:@std/fmt@1/colors";
const GLOBAL_CACHE = new Map<string, Rgb>();
export interface Rgb {
r: number;
g: number;
b: number;
}
export interface ColorHashOptions {
/** Lightness of the color (0-100) */
lightness?: number;
/** Saturation of the color (0-100) */
saturation?: number;
/** Generate a background color instead of a foreground color */
background?: boolean;
/** Force the color to be generated, even if it is already cached */
force?: boolean;
/**
* Controls the format of the generated color code.
*
* - `ansi` (default): ANSI color codes (e.g. `\x1b[38;2;255;255;255m`)
* - `hex`: Hex color codes (e.g. `#ffffff`)
* - `rgb`: RGB color codes (e.g. `rgb(255, 255, 255)`)
*/
format?: "ansi" | "hex" | "rgb";
/** Cache for storing generated colors. Must be a Map-like object. */
cache?: Map<string, Rgb>;
}
/**
* Generates a deterministic ANSI color code based on the given string `s` and
* an optional set of `options`. The color is generated with the hash of the
* string's characters, ensuring that the same string will always generate the
* same color. Colors are cached for improved performance on subsequent calls.
*
* @param string The string to generate a color for.
* @param [options] Optional color generation options.
* @returns the input string `s` wrapped in the ANSI color code.
*/
export function colorhash(string: string, options?: ColorHashOptions): string {
const { background = false, cache = GLOBAL_CACHE, ...opts } = options ??= {};
let { lightness = 60, saturation = 60, format = "ansi" } = opts;
let key = String(string ??= "").trim().replace(/\W+/g, "_");
key += `:${lightness}:${saturation}:${background ? "bg" : "fg"}`;
let color = cache.get(key);
if (!color) {
const hash = Array.from(key).reduce(
(acc, c) => c.charCodeAt(0) + ((acc << 5) - acc),
0,
);
const { abs, max, min, } = Math;
let hue = hash % 360;
if (hue < 0) hue += 360;
// ensure hue is not too close to white or black
if (hue > 60 && hue < 240) hue += hue > 180 ? -60 : 60;
// ensure lightness/saturation are not too close to the extremes
[lightness, saturation] = [lightness, saturation].map(
(n) => max(25, min(75, n >= 0 && n <= 1 ? n * 100 : n <= 100 ? n : 50)),
);
// convert the hsl into rgb for terminal color codes
lightness /= 100, saturation /= 100;
const c = (1 - abs(2 * lightness - 1)) * saturation;
const x = c * (1 - abs((hue / 60) % 2 - 1));
const m = lightness - c / 2;
let r = 0, g = 0, b = 0;
// deno-fmt-ignore
switch (true) {
case hue >= 0 && hue < 60: [r, g, b] = [c, x, 0]; break;
case hue >= 60 && hue < 120: [r, g, b] = [x, c, 0]; break;
case hue >= 120 && hue < 180: [r, g, b] = [0, c, x]; break;
case hue >= 180 && hue < 240: [r, g, b] = [0, x, c]; break;
case hue >= 240 && hue < 300: [r, g, b] = [x, 0, c]; break;
case hue >= 300 && hue < 360: [r, g, b] = [c, 0, x]; break;
}
cache.set(
key,
color = {
r: ((r + m) * 255) >>> 0,
g: ((g + m) * 255) >>> 0,
b: ((b + m) * 255) >>> 0,
},
);
}
const { r, g, b } = color;
switch (format) {
case "hex": {
let hex = ((r << 16) | (g << 8) | b).toString(16);
if (hex.length === 3) hex = hex.replace(/./g, "$&$&");
hex = hex.padStart(6, "0");
return `#${hex}`;
}
case "rgb": return `rgb(${r}, ${g}, ${b})`;
case "ansi": // fallthrough
default: {
return ansi[background ? "bgRgb24" : "rgb24"](string, { r, g, b });
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment