Skip to content

Instantly share code, notes, and snippets.

@arenagroove
Created April 20, 2025 04:28
Show Gist options
  • Select an option

  • Save arenagroove/ffec49d5322245c71d6fc7867066c5eb to your computer and use it in GitHub Desktop.

Select an option

Save arenagroove/ffec49d5322245c71d6fc7867066c5eb to your computer and use it in GitHub Desktop.
Optimized JavaScript color conversion — RGB, HSL, HSV, OKLCH, Lab, LCH, Display-P3, Hex, and Color Blindness Simulation

JavaScript Color Conversion

Optimized JavaScript color conversions across sRGB, HEX, HSL, HSV, Lab, LCH, OKLCH, Display-P3, and includes color blindness simulation.

Supported Color Spaces

  • RGB ↔️ HEX
  • RGB ↔️ HSL
  • RGB ↔️ HSV
  • RGB ↔️ Lab / LCH
  • RGB ↔️ OKLab / OKLCH
  • RGB ↔️ Display-P3 (wide gamut)
  • Color Blindness Simulation: Protanopia, Deuteranopia, Tritanopia

File

  • color-conversion.js

Usage Example

// Convert RGB to HEX
const hex = rgbToHex(255, 0, 127); // "#ff007f"

// Convert HEX to RGB
const [r, g, b] = hexToRgb("#ff007f"); // [255, 0, 127]

// Convert RGB to HSL
const hsl = rgbToHsl(255, 100, 50); // [value, value, value]

// Convert RGB to OKLCH
const oklch = rgbToOklch(200, 100, 50); // ["Lightness", "Chroma", "Hue"]

// Simulate Protanopia (color blindness)
const simulated = simulateColorBlindness(200, 100, 50, "protanopia"); // [r, g, b]

Performance Notes

  • Uses bitwise and precomputed matrix operations for speed
  • All color models work on standard 8-bit RGB input

CodePen Demo

CodePen Example

// -------------------------
// rgb
// -------------------------
function rgbToHex(r, g, b) {
return `#${[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
}
function hexToRgb(hex) {
// Remove the '#' if present
hex = hex.replace(/^#/, "");
// Parse short hex (e.g., #FFF → #FFFFFF)
if (hex.length === 3) {
hex = hex
.split("")
.map((char) => char + char)
.join("");
}
// Convert hex to RGB values
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return [r, g, b];
}
// -------------------------
// oklch
// -------------------------
// sRGB to Linear RGB
function rgbToLinear([r, g, b]) {
const gammaCorrection = (v) => {
v /= 255;
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
return [gammaCorrection(r), gammaCorrection(g), gammaCorrection(b)];
}
function linearRgbToLms([r, g, b]) {
return [
r * 0.412165612 + g * 0.536275208 + b * 0.0514575653,
r * 0.211859107 + g * 0.6807189584 + b * 0.107406579,
r * 0.0883097947 + g * 0.2818474174 + b * 0.6298384131
];
}
function lmsToOklab(lms) {
const lmsPrime = lms.map((v) => Math.cbrt(v));
return [
0.2104542553 * lmsPrime[0] +
0.793617785 * lmsPrime[1] -
0.0040720468 * lmsPrime[2],
1.9779984951 * lmsPrime[0] -
2.428592205 * lmsPrime[1] +
0.4505937099 * lmsPrime[2],
0.0259040371 * lmsPrime[0] +
0.7827717662 * lmsPrime[1] -
0.808675766 * lmsPrime[2]
];
}
function oklabToOklch([L, a, b]) {
const C = Math.sqrt(a * a + b * b);
const H = (Math.atan2(b, a) * (180 / Math.PI) + 360) % 360;
return [
(L * 100).toFixed(2), // Lightness
C.toFixed(4), // Chroma
H.toFixed(2) // Hue
];
}
function rgbToOklch(r, g, b) {
const linearRgb = rgbToLinear([r, g, b]);
const lms = linearRgbToLms(linearRgb);
const oklab = lmsToOklab(lms);
return oklabToOklch(oklab);
}
function oklchToOklab([L, C, H]) {
const hRad = (H * Math.PI) / 180;
return [L, C * Math.cos(hRad), C * Math.sin(hRad)];
}
function oklabToLms([L, a, b]) {
const lmsPrime = [
L + 0.3963377774 * a + 0.2158037573 * b,
L - 0.1055613458 * a - 0.0638541728 * b,
L - 0.0894841775 * a - 1.291485548 * b
];
// Convert back from cube root
return lmsPrime.map((v) => v ** 3);
}
function lmsToLinearRgb([l, m, s]) {
return [
l * 4.0767416621 - m * 3.3077115913 + s * 0.2309699292,
l * -1.2684380046 + m * 2.6097574011 - s * 0.3413193965,
l * -0.0041960863 - m * 0.7034186147 + s * 1.707614701
];
}
function linearRgbToRgb(rgb) {
return rgb.map((v) => {
v = v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
return Math.round(Math.max(0, Math.min(1, v)) * 255);
});
}
function oklchToRgb(L, C, H) {
const oklab = oklchToOklab([L, C, H]);
const lms = oklabToLms(oklab);
const linearRgb = lmsToLinearRgb(lms);
return linearRgbToRgb(linearRgb);
}
// -------------------------
// hsl
// -------------------------
function computeHue(r, g, b, max, delta) {
let h = 0;
if (delta !== 0) {
if (max === r) h = ((g - b) / delta) % 6;
else if (max === g) h = (b - r) / delta + 2;
else if (max === b) h = (r - g) / delta + 4;
h *= 60;
if (h < 0) h += 360;
}
return parseFloat(h.toFixed(2));
}
function rgbToHsl(r, g, b) {
(r /= 255), (g /= 255), (b /= 255);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = computeHue(r, g, b, max, delta);
let l = (max + min) / 2;
let s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
return [
h, // Hue in degrees
parseFloat((s * 100).toFixed(2)), // Saturation in %
parseFloat((l * 100).toFixed(2)) // Lightness in %
];
}
function hslToRgb(h, s, l) {
s = parseFloat(s) / 100;
l = parseFloat(l) / 100;
const C = (1 - Math.abs(2 * l - 1)) * s;
const X = C * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - C / 2;
let r = 0,
g = 0,
b = 0;
if (h >= 0 && h < 60)[r, g, b] = [C, X, 0];
else if (h >= 60 && h < 120)[r, g, b] = [X, C, 0];
else if (h >= 120 && h < 180)[r, g, b] = [0, C, X];
else if (h >= 180 && h < 240)[r, g, b] = [0, X, C];
else if (h >= 240 && h < 300)[r, g, b] = [X, 0, C];
else if (h >= 300 && h < 360)[r, g, b] = [C, 0, X];
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
// -------------------------
// lch
// -------------------------
function linearRgbToXyz([r, g, b]) {
// sRGB to XYZ (D50) matrix
return [
r * 0.4360747 + g * 0.3850649 + b * 0.1430804,
r * 0.2225045 + g * 0.7168786 + b * 0.0606169,
r * 0.0139322 + g * 0.0971045 + b * 0.7141733
];
}
function xyzToLab([x, y, z]) {
// D50 reference white point
const refX = 0.96422;
const refY = 1.0;
const refZ = 0.82521;
x /= refX;
y /= refY;
z /= refZ;
const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116);
const fx = f(x);
const fy = f(y);
const fz = f(z);
const L = 116 * fy - 16;
const a = 500 * (fx - fy);
const b = 200 * (fy - fz);
return [L, a, b];
}
function labToLch([L, a, b]) {
const C = Math.sqrt(a * a + b * b);
let H = Math.atan2(b, a) * (180 / Math.PI);
if (H < 0) H += 360;
return [L, C, H];
}
function rgbToLch(r, g, b) {
const linearRgb = rgbToLinear([r, g, b]);
const xyz = linearRgbToXyz(linearRgb);
const lab = xyzToLab(xyz);
return labToLch(lab).map((v) => parseFloat(v.toFixed(2)));
}
function lchToLab([L, C, H]) {
const hRad = (H * Math.PI) / 180;
return [L, C * Math.cos(hRad), C * Math.sin(hRad)];
}
function labToXyz([L, a, b]) {
const refX = 0.96422,
refY = 1.0,
refZ = 0.82521; // D50 white
const fy = (L + 16) / 116;
const fx = fy + a / 500;
const fz = fy - b / 200;
const fInv = (t) => (t ** 3 > 0.008856 ? t ** 3 : (t - 16 / 116) / 7.787);
const x = fInv(fx) * refX;
const y = fInv(fy) * refY;
const z = fInv(fz) * refZ;
return [x, y, z];
}
function xyzToLinearRgb([x, y, z]) {
return [
x * 3.1338561 + y * -1.6168667 + z * -0.4906146,
x * -0.9787684 + y * 1.9161415 + z * 0.033454,
x * 0.0719453 + y * -0.2289914 + z * 1.4052427
];
}
function lchToRgb(L, C, H) {
const lab = lchToLab([L, C, H]);
const xyz = labToXyz(lab);
const linearRgb = xyzToLinearRgb(xyz);
return linearRgbToRgb(linearRgb);
}
// -------------------------
// lab
// -------------------------
function rgbToLab(r, g, b, usePercentage = false) {
const linearRgb = rgbToLinear([r, g, b]);
const xyz = linearRgbToXyz(linearRgb);
let [L, a, b_] = xyzToLab(xyz);
if (usePercentage) {
L = `${parseFloat(L.toFixed(2))}%`;
} else {
L = parseFloat(L.toFixed(2));
}
return [L, parseFloat(a.toFixed(2)), parseFloat(b_.toFixed(2))];
}
function labToRgb(L, a, b) {
const xyz = labToXyz([L, a, b]);
const linearRgb = xyzToLinearRgb(xyz);
return linearRgbToRgb(linearRgb);
}
// -------------------------
// hsv
// -------------------------
function rgbToHsv(r, g, b) {
(r /= 255), (g /= 255), (b /= 255);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = computeHue(r, g, b, max, delta);
let s = max === 0 ? 0 : delta / max;
let v = max;
return [
h, // Hue in degrees
parseFloat((s * 100).toFixed(2)), // Saturation in %
parseFloat((v * 100).toFixed(2)) // Value in %
];
}
function hsvToRgb(h, s, v) {
s /= 100;
v /= 100;
const C = v * s;
const X = C * (1 - Math.abs(((h / 60) % 2) - 1));
const m = v - C;
let r = 0,
g = 0,
b = 0;
if (h >= 0 && h < 60)[r, g, b] = [C, X, 0];
else if (h >= 60 && h < 120)[r, g, b] = [X, C, 0];
else if (h >= 120 && h < 180)[r, g, b] = [0, C, X];
else if (h >= 180 && h < 240)[r, g, b] = [0, X, C];
else if (h >= 240 && h < 300)[r, g, b] = [X, 0, C];
else if (h >= 300 && h < 360)[r, g, b] = [C, 0, X];
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
// -------------------------
// display-p3
// -------------------------
// sRGB to Linear RGB
function toLinear(v) {
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}
// sRGB to XYZ (D65)
function linearSrgbToXyz([r, g, b]) {
return [
r * 0.4124564 + g * 0.3575761 + b * 0.1804375,
r * 0.2126729 + g * 0.7151522 + b * 0.072175,
r * 0.0193339 + g * 0.119192 + b * 0.9503041
];
}
// Convert XYZ to Linear Display-P3 (Corrected Matrix)
function xyzToLinearP3([x, y, z]) {
return [
x * 2.493496911941425 + y * -0.9313836179191239 + z * -0.4027107844507168, // R
x * -0.8294890169312207 + y * 1.7626640603183463 + z * 0.02362468584194357, // G
x * 0.03584583024378447 + y * -0.07617238926804182 + z * 0.9568845240076872 // B
];
}
// Display-P3 Gamma Encoding
function toGammaP3(v) {
return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1.0 / 2.4) - 0.055;
}
// Apply Gamma After Clamping
function applyDisplayP3Gamma([r, g, b]) {
return [
toGammaP3(Math.max(0, Math.min(1, r))),
toGammaP3(Math.max(0, Math.min(1, g))),
toGammaP3(Math.max(0, Math.min(1, b)))
];
}
// Convert RGB to Display-P3
function rgbToDisplayP3(r, g, b) {
(r /= 255), (g /= 255), (b /= 255);
// sRGB to Linear RGB
const linearR = toLinear(r);
const linearG = toLinear(g);
const linearB = toLinear(b);
// Linear RGB → XYZ (D65)
let [x, y, z] = linearSrgbToXyz([linearR, linearG, linearB]);
let [p3R, p3G, p3B] = xyzToLinearP3([x, y, z]);
let [finalR, finalG, finalB] = applyDisplayP3Gamma([p3R, p3G, p3B]);
return [
parseFloat(finalR.toFixed(4)),
parseFloat(finalG.toFixed(4)),
parseFloat(finalB.toFixed(4))
];
}
// Inverse Gamma Correction for Display-P3
function fromGammaP3(v) {
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}
// Convert Display-P3 to XYZ (D65) - Inverse Matrix
function linearP3ToXyz([r, g, b]) {
return [
r * 0.515102 + g * 0.2919656 + b * 0.1571934,
r * 0.2411823 + g * 0.6922363 + b * 0.0665814,
r * -0.001049 + g * 0.0418819 + b * 0.7843782
];
}
// Convert XYZ to Linear sRGB - Inverse Matrix
function xyzToLinearSrgb([x, y, z]) {
return [
x * 3.1338561 + y * -1.6168667 + z * -0.4906146,
x * -0.9787684 + y * 1.9161415 + z * 0.033454,
x * 0.0719453 + y * -0.2289914 + z * 1.4052427
];
}
// Standard sRGB Gamma Correction
function toGamma(v) {
return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
}
function displayP3ToRgb(r, g, b) {
// Inverse Gamma Correction for Display-P3
r = fromGammaP3(r);
g = fromGammaP3(g);
b = fromGammaP3(b);
// Convert Linear Display-P3 to XYZ (D65)
let [x, y, z] = linearP3ToXyz([r, g, b]);
// Convert XYZ (D65) to Linear sRGB
let [srgbR, srgbG, srgbB] = xyzToLinearSrgb([x, y, z]);
// Apply Gamma Correction for sRGB
srgbR = toGamma(srgbR);
srgbG = toGamma(srgbG);
srgbB = toGamma(srgbB);
// Scale to 0-255 range and round
return [
Math.round(srgbR * 255),
Math.round(srgbG * 255),
Math.round(srgbB * 255)
];
}
// Color Blindness Simulation Matrices
const colorBlindMatrix = {
protanopia: [
[0.567, 0.433, 0],
[0.558, 0.442, 0],
[0, 0.242, 0.758]
],
deuteranopia: [
[0.625, 0.375, 0],
[0.7, 0.3, 0],
[0, 0.3, 0.7]
],
tritanopia: [
[0.95, 0.05, 0],
[0, 0.433, 0.567],
[0, 0.475, 0.525]
]
};
function simulateColorBlindness(r, g, b, type = "protanopia") {
const matrix = colorBlindMatrix[type];
return [
r * matrix[0][0] + g * matrix[0][1] + b * matrix[0][2],
r * matrix[1][0] + g * matrix[1][1] + b * matrix[1][2],
r * matrix[2][0] + g * matrix[2][1] + b * matrix[2][2]
].map((v) => Math.round(v));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment