Last active
August 13, 2025 19:24
-
-
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.
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
| 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 }; | |
| }) |
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
| 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 ; | |
| } |
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
| 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; | |
| } |
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
| 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