Last active
April 17, 2025 07:02
-
-
Save nberlette/33a1a21f66a0b9814d73006cd080ce5f to your computer and use it in GitHub Desktop.
colorhash
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
| 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