Skip to content

Instantly share code, notes, and snippets.

@annibal
Last active August 13, 2025 19:24
Show Gist options
  • Select an option

  • Save annibal/b75241f069c72116d1f03d6009174881 to your computer and use it in GitHub Desktop.

Select an option

Save annibal/b75241f069c72116d1f03d6009174881 to your computer and use it in GitHub Desktop.
Converts many of the most common color formats, from and to. Also generates a monochromatic palette.
const makeColorFormatConverterHelpers = (() => {
function rgbToHsl([r, g, b]) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2, d = max - min;
if (!d) h = s = 0;
else {
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;
default: h = (r - g) / d + 4;
}
h /= 6;
}
return [h, s, l];
}
function hslToRgb([h, s, l]) {
const hue2rgb = (p, q, t) => {
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) 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);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
const FORMATS = {
ARRAY: "ARRAY",
RGB_ARRAY: "RGBARRAY",
HSL_ARRAY: "HSLARRAY",
OBJECT_FULL: "OBJECTFULL",
RGB_OBJECT_FULL: "RGBOBJECTFULL",
OBJECT: "OBJECT",
RGB_OBJECT: "RGBOBJECT",
HSL_OBJECT_RAW: "HSLOBJECTRAW",
HSL_OBJECT: "HSLOBJECT",
HSL_OBJECT_FULL: "HSLOBJECTFULL",
HEX: "HEX",
HEX_UNPREFIXED: "HEXUNPREFIXED",
HEX_ABBR: "HEXABBR",
HEX_SIMPLE: "HEXSIMPLE",
RGB: "RGB",
HSL: "HSL",
}
const gen777id = () => (new Date().valueOf() % 7377373).toString(36).toUpperCase().padStart(5, "0");
return { gen777id, FORMATS, hslToRgb, rgbToHsl };
})
function getShade(color, brightness, fadeSaturation = false, format = "RGB") {
let [h, s, l] = convertColorFormat(color, "HSL_ARRAY");
if (fadeSaturation) s *= Math.sqrt(brightness);
l = Math.max(0, Math.min(1, l * brightness));
return convertColorFormat([h, s, l], format);
}
function getMonochromaticPalette(color, format = "HEX", shadesArr=[.05, .1, .2, .3, .4, .5, .6, .7, .8, .9, .95], debug = false) {
return shadesArr
.map((shd) => getShade(color, shd, false, format, debug))
}
function getColorBrightness(color, debug=false) {
const [r, g, b] = convertColorFormat(color, "RGB_ARRAY");
return ((r*299)+(g*587)+(b*114))/255000 ;
}
function convertColorFormat(color, format = "HEX", debug=false) {
const CFCHelper = makeColorFormatConverterHelpers();
const execId = CFCHelper.gen777id();
function log(logstr) {
if (debug) {
console.log(
`%c ConvertColorFormat%c[%c${execId}%c]%c:%c ${logstr}`,
"background-color: #0C080044; color: #8a6624ff; font-weight: bold",
"background-color: #0C080044; color: #FDFFB2; font-weight: bold",
"background-color: #080c0144; color: #556b2f; font-weight: bold",
"background-color: #0C080044; color: #FDFFB2; font-weight: bold",
"background-color: #0C080044; color: #8a6624ff; font-weight: bold",
"all:unset"
);
}
}
log(`will try to convert "${typeof color === "object" ? JSON.stringify(color) : String(color)}" to "${format}"`);
let r, g, b, a = 1,
h, s, l;
let type;
let foundType = false;
log(`is it an Array with 3 numbers?`);
if (Array.isArray(color) && color.length >= 3 && color.slice(0,3).every(num => !isNaN(num))) {
const [v1, v2, v3] = color.map(num => Number(num));
log(`is it [H, S, L]?`);
if (
(v1 >= 0 && v1 <= 360) &&
(v2 >= 0 && v2 <= 1) &&
(v3 >= 0 && v3 <= 1)
) {
[h, s, l] = [v1 / (v1 < 1 ? 1 : 360), v2, v3];
[r, g, b] = CFCHelper.hslToRgb([h, s, l]);
type = "HSL_ARRAY";
foundType = true;
}
if (!foundType) {
[r, g, b] = color;
type = "ARRAY";
foundType = true;
}
}
if (!foundType) {
log(`is it {r:255,g:255,b:255} ?`);
if (typeof color === "object" && color) {
if ("red" in color && "green" in color && "blue" in color) {
({ red: r, green: g, blue: b, alpha: a = 1 } = color);
type = "OBJECT FULL";
foundType = true;
} else if ("r" in color && "g" in color && "b" in color) {
({ r, g, b, a = 1 } = color);
type = "OBJECT";
foundType = true;
}
}
}
if (!foundType && typeof color === "string") {
log(`is it "#RRGGBB" ?`);
let c = color.trim();
// Hex formats
if (/^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(c)) {
if (c.startsWith("#")) c = c.slice(1);
r = parseInt(c.slice(0, 2), 16);
g = parseInt(c.slice(2, 4), 16);
b = parseInt(c.slice(4, 6), 16);
if (c.length === 8) a = parseInt(c.slice(6, 8), 16) / 255;
type = c.length === 6 ? (color.startsWith("#") ? "HEX" : "HEX-UNPREFIXED") : "HEX";
foundType = true;
}
// Abbreviated hex #RGB or RGB
if (!foundType) {
log(`is it "#RGB" ?`);
if (/^#?[0-9A-Fa-f]{3}$/.test(c)) {
if (c.startsWith("#")) c = c.slice(1);
r = parseInt(c[0] + c[0], 16);
g = parseInt(c[1] + c[1], 16);
b = parseInt(c[2] + c[2], 16);
type = color.startsWith("#") ? "HEX-ABBR" : "HEX-SIMPLE";
foundType = true;
}
}
// rgb()/rgba() functions
if (!foundType) {
log(`is it "rgb(r, g, b)" ?`);
if (/^rgba?\(/i.test(c)) {
const parts = c.match(/[\d.]+%?/g);
if (!parts || parts.length < 3) throw new Error("Invalid rgb/rgba format");
r = parseFloat(parts[0]);
g = parseFloat(parts[1]);
b = parseFloat(parts[2]);
if (parts.length > 3) a = parseFloat(parts[3]) > 1 ? parseFloat(parts[3]) / 100 : parseFloat(parts[3]);
type = "RGB";
foundType = true;
}
}
// space-separated rgb with optional alpha: rgb(r g b / a)
if (!foundType) {
log(`is it "rgb(r g b)" ?`);
if (/^rgb/i.test(c)) {
const parts = c.match(/[\d.]+%?/g);
r = parseFloat(parts[0]);
g = parseFloat(parts[1]);
b = parseFloat(parts[2]);
if (parts.length > 3) a = parseFloat(parts[3]) > 1 ? parseFloat(parts[3]) / 100 : parseFloat(parts[3]);
type = "RGB";
foundType = true;
}
}
// hsl()/hsla()
if (!foundType) {
log(`is it "hsl(h s l)" ?`);
if (/^hsla?\(/i.test(c)) {
const parts = c.match(/[\d.]+%?/g);
if (!parts || parts.length < 3) throw new Error("Invalid hsl/hsla format");
h = parseFloat(parts[0]);
s = parseFloat(parts[1]) / 100;
l = parseFloat(parts[2]) / 100;
if (parts.length > 3) a = parseFloat(parts[3]) > 1
? parseFloat(parts[3]) / 100
: parseFloat(parts[3]);
[r, g, b] = hslToRgb([h / 360, s, l]);
type = "HSL";
foundType = true;
}
}
// nope
if (!foundType) {
throw new Error("Unsupported color format: " + color);
}
}
log(`identified '${typeof color === "object" ? JSON.stringify(color) : String(color)}' format to be "${type}".`);
log(`RED='${r}', GREEN='${g}', BLUE='${b}', ALPHA='${a}'`);
r = isNaN(r) ? null : r;
g = isNaN(g) ? null : g;
b = isNaN(b) ? null : b;
a = isNaN(a) ? null : a;
if (r == null || g == null || b == null) throw new Error("Could not parse color");
const fmts = CFCHelper.FORMATS;
// --- Output ---
switch (format.toUpperCase().replace(/[^A-Z]/gi, "")) {
case fmts.ARRAY:
case fmts.RGB_ARRAY:
retVal = [r, g, b, a];
break;
case fmts.HSL_ARRAY:
retVal = CFCHelper.rgbToHsl([r, g, b]);
break;
case fmts.OBJECT_FULL:
case fmts.RGB_OBJECT_FULL:
retVal = { red: r, green: g, blue: b, alpha: a };
break;
case fmts.OBJECT:
case fmts.RGB_OBJECT:
retVal = { r, g, b, a };
break;
case fmts.HSL_OBJECT_RAW:
[h, s, l] = CFCHelper.rgbToHsl([r, g, b]);
retVal = { h, s, l }
break;
case fmts.HSL_OBJECT:
[h, s, l] = CFCHelper.rgbToHsl([r, g, b]);
retVal = { h:+(h * 360), s:+(s * 100), l:+(l * 100) }
break;
case fmts.HSL_OBJECT_FULL:
[h, s, l] = CFCHelper.rgbToHsl([r, g, b]);
retVal = { hue: +(h * 360), saturation: +(s * 100), lightning: +(l * 100) }
break;
case fmts.HEX:
retVal =
"#" +
[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("") +
(a < 1
? Math.round(a * 255)
.toString(16)
.padStart(2, "0")
: "");
break;
case fmts.HEX_UNPREFIXED:
retVal = [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
break;
case fmts.HEX_ABBR:
retVal = "#" + [r, g, b].map((v) => (v >> 4).toString(16)).join("");
break;
case fmts.HEX_SIMPLE:
retVal = "" + [r, g, b].map((v) => (v >> 4).toString(16)).join("");
break;
case fmts.RGB:
retVal = a < 1
? `rgba(${r}, ${g}, ${b}, ${+a.toFixed(3)})`
: `rgb(${r}, ${g}, ${b})`;
break;
case fmts.HSL:
[h, s, l] = CFCHelper.rgbToHsl([r, g, b]);
const hStr = +(h * 360).toFixed(4);
const sStr = +(s * 100).toFixed(4);
const lStr = +(l * 100).toFixed(4);
retVal = a < 1
? `hsla(${hStr}, ${sStr}%, ${lStr}%, ${+a.toFixed(4)})`
: `hsl(${hStr}, ${sStr}%, ${lStr}%)`;
break;
default:
throw new Error("Unsupported target format: " + format);
}
log(`returning "${typeof retVal === "object" ? JSON.stringify(retVal) : String(retVal)}"`);
return retVal;
}
setTimeout(() => _viewMonochromaticPalette("#00FF88"), 500);
function _viewMonochromaticPalette(color, parentElm=document.body) {
const paletteColors = getMonochromaticPalette(color, "rgb")
paletteColors.forEach((col, i) => {
const height = 5;
const width = 18;
const clsName = "monoc-palette-single-item";
const strId = `col_test_${String(i).padStart(2, "0")}`
const elm = document.createElement("div");
const colBright = getColorBrightness(col);
elm.setAttribute("style", [
`position:fixed; left: 1rem; width: ${width}rem; height: ${height}rem; box-sizing: border-box;`,
`background: ${col}; top: ${1 + i * height}rem;`,
`display: flex; align-items: center; justify-content: space-between; gap: 1rem;`,
`padding: 1rem 2rem; font-size: 0.8rem; font-family: monospace; cursor: pointer;`,
colBright < 0.5 ? `color: white` : `color: black`
].join(" "));
elm.setAttribute("id", strId);
elm.setAttribute("class", clsName);
elm.innerHTML = [
`<div style="display: flex; flex-direction: column; gap: 0.5rem;">`,
`<b>#${i+1}</b>`,
`<span>[${colBright.toFixed(2)}]</span>`,
`</div>`,
`<div style="display: flex; flex-direction: column; gap: 0.5rem;">`,
`<span>${convertColorFormat(col, "hex").toUpperCase()}</span>`,
`<span>${col}</span>`,
`</div>`
].join(" ");
oldElm = document.getElementById(strId);
if (oldElm) { oldElm.remove() }
parentElm.appendChild(elm);
elm.addEventListener("click", () => {
const allPaletteColors = Array.from( document.querySelectorAll(`.${clsName}`) );
(allPaletteColors || []).reverse().forEach((celm, i) => {
celm.style.transition = "opacity 100ms linear";
celm.style.opacity = 1;
setTimeout(() => celm.style.opacity = 0, i * 100 + 100)
setTimeout(() => celm.remove(), i * 100 + 200)
});
});
})
;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment