// see: https://en.wikipedia.org/wiki/RGB_color_model // see: https://en.wikipedia.org/wiki/HSL_and_HSV // expects R, G, B, Cmax and chroma to be in number interval [0, 1] // returns undefined if chroma is 0, or a number interval [0, 360] degrees function hue(R, G, B, Cmax, chroma) { let H; if (chroma === 0) { return H; } if (Cmax === R) { H = ((G - B) / chroma) % 6; } else if (Cmax === G) { H = (B - R) / chroma + 2; } else if (Cmax === B) { H = (R - G) / chroma + 4; } H *= 60; return H < 0 ? H + 360 : H; } // returns the average of the supplied number arguments function average(...theArgs) { return theArgs.length ? theArgs.reduce((p, c) => p + c, 0) / theArgs.length : 0; } // expects R, G, B, Cmin, Cmax and chroma to be in number interval [0, 1] // type is by default 'bi-hexcone' equation // set 'luma601' or 'luma709' for alternatives // see: https://en.wikipedia.org/wiki/Luma_(video) // returns a number interval [0, 1] function lightness(R, G, B, Cmin, Cmax, type = "bi-hexcone") { if (type === "luma601") { return 0.299 * R + 0.587 * G + 0.114 * B; } if (type === "luma709") { return 0.2126 * R + 0.7152 * G + 0.0772 * B; } return average(Cmin, Cmax); } // expects L and chroma to be in number interval [0, 1] // returns a number interval [0, 1] function saturation(L, chroma) { return chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * L - 1)); } // returns the value to a fixed number of digits function toFixed(value, digits) { return Number.isFinite(value) && Number.isFinite(digits) ? value.toFixed(digits) : value; } // expects R, G, and B to be in number interval [0, 1] // returns a Map of H, S and L in the appropriate interval and digits function RGB2HSL(R, G, B, fixed = true) { const Cmin = Math.min(R, G, B); const Cmax = Math.max(R, G, B); const chroma = Cmax - Cmin; // default 'bi-hexcone' equation const L = lightness(R, G, B, Cmin, Cmax); // H in degrees interval [0, 360] // L and S in interval [0, 1] return new Map([ ["H", toFixed(hue(R, G, B, Cmax, chroma), fixed && 1)], ["S", toFixed(saturation(L, chroma), fixed && 3)], ["L", toFixed(L, fixed && 3)], ]); } // expects value to be number in interval [0, 255] // returns normalised value as a number interval [0, 1] function colourRange(value) { return value / 255; } // expects R, G, and B to be in number interval [0, 255] function RGBdec2HSL(R, G, B) { return RGB2HSL(colourRange(R), colourRange(G), colourRange(B)); } // converts a hexidecimal string into a decimal number function hex2dec(value) { return parseInt(value, 16); } // slices a string into an array of paired characters function pairSlicer(value) { return value.match(/../g); } // prepend '0's to the start of a string and make specific length function prePad(value, count) { return ("0".repeat(count) + value).slice(-count); } // format hex pair string from value function hexPair(value) { return hex2dec(prePad(value, 2)); } // expects R, G, and B to be hex string in interval ['00', 'FF'] // without a leading '#' character function RGBhex2HSL(R, G, B) { return RGBdec2HSL(hexPair(R), hexPair(G), hexPair(B)); } // expects RGB to be a hex string in interval ['000000', 'FFFFFF'] // with or without a leading '#' character function RGBstr2HSL(RGB) { const hex = prePad(RGB.charAt(0) === "#" ? RGB.slice(1) : RGB, 6); return RGBhex2HSL(...pairSlicer(hex).slice(0, 3)); } // expects value to be a Map object function logIt(value) { console.log(value); document.getElementById("out").textContent += JSON.stringify([...value]) + "\n"; }