UNPKG

matterbridge

Version:
384 lines (366 loc) • 15.9 kB
/** * This file contains the color utilities. * * @file colorUtils.ts * @author Luca Liguori * @date 2023-10-05 * @version 1.3.0 * * Copyright 2023, 2024, 2025 Luca Liguori. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import { assert } from 'node:console'; export function hslColorToRgbColor(hue, saturation, luminance) { if (hue === 360) { hue = 0; } assert(hue >= 0 && hue <= 359, 'hslColorToRgbColor Hue error'); assert(saturation >= 0 && saturation <= 100, 'hslColorToRgbColor Saturation error'); assert(luminance === 50, 'hslColorToRgbColor Luminance error'); saturation /= 100; luminance /= 100; let r, g, b; if (saturation === 0) { r = g = b = luminance; // achromatic } else { 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; }; const q = luminance < 0.5 ? luminance * (1 + saturation) : luminance + saturation - luminance * saturation; const p = 2 * luminance - q; r = hue2rgb(p, q, hue / 360 + 1 / 3); g = hue2rgb(p, q, hue / 360); b = hue2rgb(p, q, hue / 360 - 1 / 3); } return { r: Math.ceil(r * 255), g: Math.ceil(g * 255), b: Math.ceil(b * 255), }; } // Converts from RGB to the XY color (CIE 1931 color space) export function rgbColorToXYColor(rgb) { let r = rgb.r / 255; let g = rgb.g / 255; let b = rgb.b / 255; // Apply gamma correction r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; // Scale the values to the D65 illuminant r = r * 100; g = g * 100; b = b * 100; // Convert RGB to XYZ const X = r * 0.664511 + g * 0.154324 + b * 0.162028; const Y = r * 0.283881 + g * 0.668433 + b * 0.047685; const Z = r * 0.000088 + g * 0.07231 + b * 0.986039; // Normalization let x = X / (X + Y + Z); let y = Y / (X + Y + Z); // Round to 4 digits x = Math.round(x * 10000) / 10000; y = Math.round(y * 10000) / 10000; return { x, y }; } export function xyColorToRgbColor(x, y, brightness = 254) { const z = 1.0 - x - y; const Y = (brightness / 254).toFixed(2); const X = (Number(Y) / y) * x; const Z = (Number(Y) / y) * z; // Convert to RGB using Wide RGB D65 conversion let red = X * 1.656492 - Number(Y) * 0.354851 - Z * 0.255038; let green = -X * 0.707196 + Number(Y) * 1.655397 + Z * 0.036152; let blue = X * 0.051713 - Number(Y) * 0.121364 + Z * 1.01153; // If red, green or blue is larger than 1.0 set it back to the maximum of 1.0 if (red > blue && red > green && red > 1.0) { green = green / red; blue = blue / red; red = 1.0; } else if (green > blue && green > red && green > 1.0) { red = red / green; blue = blue / green; green = 1.0; } else if (blue > red && blue > green && blue > 1.0) { red = red / blue; green = green / blue; blue = 1.0; } // Reverse gamma correction red = red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * Math.pow(red, 1.0 / 2.4) - 0.055; green = green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * Math.pow(green, 1.0 / 2.4) - 0.055; blue = blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * Math.pow(blue, 1.0 / 2.4) - 0.055; // Convert normalized decimal to decimal red = Math.round(red * 255); green = Math.round(green * 255); blue = Math.round(blue * 255); // Normalize if (isNaN(red) || red < 0) { red = 0; } if (isNaN(green) || green < 0) { green = 0; } if (isNaN(blue) || blue < 0) { blue = 0; } return { r: Math.round(red), g: Math.round(green), b: Math.round(blue) }; } export function rgbColorToHslColor(rgb) { const r = rgb.r / 255; const g = rgb.g / 255; const b = rgb.b / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0, s = 0; const l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { const d = max - min; 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; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100), }; } export function xyToHsl(x, y) { const rgb = xyColorToRgbColor(x, y); return rgbColorToHslColor(rgb); } export function miredToKelvin(mired) { return Math.round(1000000 / mired); } export function kelvinToMired(kelvin) { return Math.round(1000000 / kelvin); } export function kelvinToRGB(kelvin) { // Clamp the temperature to the range 1000K to 40000K kelvin = Math.max(1000, Math.min(40000, kelvin)) / 100; let r, g, b; // Calculate red if (kelvin <= 66) { r = 255; } else { r = kelvin - 60; r = 329.698727446 * Math.pow(r, -0.1332047592); r = Math.max(0, Math.min(255, r)); } // Calculate green if (kelvin <= 66) { g = kelvin; g = 99.4708025861 * Math.log(g) - 161.1195681661; g = Math.max(0, Math.min(255, g)); } else { g = kelvin - 60; g = 288.1221695283 * Math.pow(g, -0.0755148492); g = Math.max(0, Math.min(255, g)); } // Calculate blue if (kelvin >= 66) { b = 255; } else if (kelvin <= 19) { b = 0; } else { b = kelvin - 10; b = 138.5177312231 * Math.log(b) - 305.0447927307; b = Math.max(0, Math.min(255, b)); } return { r: Math.round(r), g: Math.round(g), b: Math.round(b) }; } /* export function testColors(): void { // this table has been checked with different apps and sites and is correct 100% const colors = [ { name: 'Pure Red 0', hsl: { h: 0, s: 100, l: 50 }, rgb: { r: 255, g: 0, b: 0 }, xy: { x: 0.7006, y: 0.2993 } }, { name: 'Bright Orange 30', hsl: { h: 30, s: 100, l: 50 }, rgb: { r: 255, g: 128, b: 0 }, xy: { x: 0.6112, y: 0.375 } }, { name: 'Pure Yellow 60', hsl: { h: 60, s: 100, l: 50 }, rgb: { r: 255, g: 255, b: 0 }, xy: { x: 0.4442, y: 0.5166 } }, { name: 'Lime Green 90', hsl: { h: 90, s: 100, l: 50 }, rgb: { r: 128, g: 255, b: 0 }, xy: { x: 0.2707, y: 0.6635 } }, { name: 'Pure Green 120', hsl: { h: 120, s: 100, l: 50 }, rgb: { r: 0, g: 255, b: 0 }, xy: { x: 0.1724, y: 0.7468 } }, { name: 'Light Sea Green 150', hsl: { h: 150, s: 100, l: 50 }, rgb: { r: 0, g: 255, b: 128 }, xy: { x: 0.1642, y: 0.5886 } }, { name: 'Pure Cyan 180', hsl: { h: 180, s: 100, l: 50 }, rgb: { r: 0, g: 255, b: 255 }, xy: { x: 0.1513, y: 0.3425 } }, { name: 'Deep Sky Blue 210', hsl: { h: 210, s: 100, l: 50 }, rgb: { r: 0, g: 128, b: 255 }, xy: { x: 0.1406, y: 0.1382 } }, { name: 'Pure Blue 240', hsl: { h: 240, s: 100, l: 50 }, rgb: { r: 0, g: 0, b: 255 }, xy: { x: 0.1355, y: 0.0399 } }, { name: 'Blue Violet 270', hsl: { h: 270, s: 100, l: 50 }, rgb: { r: 128, g: 0, b: 255 }, xy: { x: 0.2181, y: 0.0778 } }, { name: 'Pure Magenta 300', hsl: { h: 300, s: 100, l: 50 }, rgb: { r: 255, g: 0, b: 255 }, xy: { x: 0.3855, y: 0.1546 } }, { name: 'Deep Pink 330', hsl: { h: 330, s: 100, l: 50 }, rgb: { r: 255, g: 0, b: 128 }, xy: { x: 0.5797, y: 0.2438 } }, { name: 'Pure Red 50% 0', hsl: { h: 0, s: 50, l: 50 }, rgb: { r: 192, g: 64, b: 64 }, xy: { x: 0.6036, y: 0.3069 } }, { name: 'Bright Orange 50% 30', hsl: { h: 30, s: 50, l: 50 }, rgb: { r: 192, g: 128, b: 64 }, xy: { x: 0.5194, y: 0.3928 } }, { name: 'Pure Yellow 50% 60', hsl: { h: 60, s: 50, l: 50 }, rgb: { r: 192, g: 192, b: 64 }, xy: { x: 0.4258, y: 0.4883 } }, { name: 'Lime Green 50% 90', hsl: { h: 90, s: 50, l: 50 }, rgb: { r: 128, g: 192, b: 64 }, xy: { x: 0.3159, y: 0.5639 } }, { name: 'Pure Green 50% 120', hsl: { h: 120, s: 50, l: 50 }, rgb: { r: 64, g: 192, b: 64 }, xy: { x: 0.2127, y: 0.6349 } }, { name: 'Light Sea Green 50% 150', hsl: { h: 150, s: 50, l: 50 }, rgb: { r: 64, g: 192, b: 128 }, xy: { x: 0.1932, y: 0.4845 } }, { name: 'Pure Cyan 50% 180', hsl: { h: 180, s: 50, l: 50 }, rgb: { r: 64, g: 192, b: 192 }, xy: { x: 0.1745, y: 0.3407 } }, { name: 'Deep Sky Blue 50% 210', hsl: { h: 210, s: 50, l: 50 }, rgb: { r: 64, g: 128, b: 192 }, xy: { x: 0.1752, y: 0.211 } }, { name: 'Pure Blue 50% 240', hsl: { h: 240, s: 50, l: 50 }, rgb: { r: 64, g: 64, b: 192 }, xy: { x: 0.1758, y: 0.102 } }, { name: 'Blue Violet 50% 270', hsl: { h: 270, s: 50, l: 50 }, rgb: { r: 128, g: 64, b: 192 }, xy: { x: 0.2688, y: 0.137 } }, { name: 'Pure Magenta 50% 300', hsl: { h: 300, s: 50, l: 50 }, rgb: { r: 192, g: 64, b: 192 }, xy: { x: 0.3772, y: 0.1777 } }, { name: 'Deep Pink 50% 330', hsl: { h: 330, s: 50, l: 50 }, rgb: { r: 192, g: 64, b: 128 }, xy: { x: 0.489, y: 0.2416 } }, ]; colors.forEach((color) => { // eslint-disable-next-line no-console console.log( `\x1b[48;2;${color.rgb.r};${color.rgb.g};${color.rgb.b}mColor: ${color.name}\x1b[0m, hsl: { h: ${color.hsl.h}, s: ${color.hsl.s}, l: ${color.hsl.l} }, rgb: { r: ${color.rgb.r}, g: ${color.rgb.g}, b: ${color.rgb.b} }, xy: { x: ${color.xy.x}, y: ${color.xy.y} }`, ); const rgb = hslColorToRgbColor(color.hsl.h, color.hsl.s, color.hsl.l); assert(rgb.r === color.rgb.r && rgb.g === color.rgb.g && rgb.b === color.rgb.b, `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}mColor: ${color.name}\x1b[0m hslColorToRgbColor { r: ${rgb.r}, g: ${rgb.g}, b: ${rgb.b} } conversion error`); const hsl = rgbColorToHslColor({ r: color.rgb.r, g: color.rgb.g, b: color.rgb.b }); assert(hsl.h === color.hsl.h && hsl.s === color.hsl.s && hsl.l === color.hsl.l, `Color: ${color.name} rgbColorToHslColor conversion error`); const xy = rgbColorToXYColor({ r: color.rgb.r, g: color.rgb.g, b: color.rgb.b }); assert(xy.x === color.xy.x && xy.y === color.xy.y, `Color: ${color.name} rgbColorToXYColor conversion error got x ${xy.x} y ${xy.y}`); const rgb2 = xyColorToRgbColor(color.xy.x, color.xy.y); assert( rgb2.r === color.rgb.r && rgb2.g === color.rgb.g && rgb2.b === color.rgb.b, `\x1b[48;2;${rgb2.r};${rgb2.g};${rgb2.b}mColor: ${color.name}\x1b[0m xyColorToRgbColor(${color.xy.x}, ${color.xy.y}) conversion error -> r: ${rgb2.r} g: ${rgb2.g} b: ${rgb2.g}`, ); }); } */ /** * Converts CIE color space to RGB color space * @param {Number} x * @param {Number} y * @param {Number} brightness - Ranges from 1 to 254 * @return {Array} Array that contains the color values for red, green and blue * From: https://github.com/usolved/cie-rgb-converter/blob/master/cie_rgb_converter.js */ /* export function cie_to_rgb(x: number, y: number, brightness = 254): RGB { // Set to maximum brightness if no custom value was given (Not the slick ECMAScript 6 way for compatibility reasons) const z = 1.0 - x - y; const Y = (brightness / 254).toFixed(2); const X = (Number(Y) / y) * x; const Z = (Number(Y) / y) * z; // Convert to RGB using Wide RGB D65 conversion let red = X * 1.656492 - Number(Y) * 0.354851 - Z * 0.255038; let green = -X * 0.707196 + Number(Y) * 1.655397 + Z * 0.036152; let blue = X * 0.051713 - Number(Y) * 0.121364 + Z * 1.01153; // If red, green or blue is larger than 1.0 set it back to the maximum of 1.0 if (red > blue && red > green && red > 1.0) { green = green / red; blue = blue / red; red = 1.0; } else if (green > blue && green > red && green > 1.0) { red = red / green; blue = blue / green; green = 1.0; } else if (blue > red && blue > green && blue > 1.0) { red = red / blue; green = green / blue; blue = 1.0; } // Reverse gamma correction red = red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * Math.pow(red, 1.0 / 2.4) - 0.055; green = green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * Math.pow(green, 1.0 / 2.4) - 0.055; blue = blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * Math.pow(blue, 1.0 / 2.4) - 0.055; // Convert normalized decimal to decimal red = Math.round(red * 255); green = Math.round(green * 255); blue = Math.round(blue * 255); // Normalize if (isNaN(red) || red < 0) { red = 0; } if (isNaN(green) || green < 0) { green = 0; } if (isNaN(blue) || blue < 0) { blue = 0; } return { r: red, g: green, b: blue }; } */ /** * Converts RGB color space to CIE color space * @param {Number} red * @param {Number} green * @param {Number} blue * @return {Array} Array that contains the CIE color values for x and y * From: https://github.com/usolved/cie-rgb-converter/blob/master/cie_rgb_converter.js */ /* export function rgb_to_cie(red: number, green: number, blue: number): XY { // Apply a gamma correction to the RGB values, which makes the color more vivid and more the like the color displayed on the screen of your device red = red > 0.04045 ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : red / 12.92; green = green > 0.04045 ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : green / 12.92; blue = blue > 0.04045 ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : blue / 12.92; // RGB values to XYZ using the Wide RGB D65 conversion formula const X = red * 0.664511 + green * 0.154324 + blue * 0.162028; const Y = red * 0.283881 + green * 0.668433 + blue * 0.047685; const Z = red * 0.000088 + green * 0.07231 + blue * 0.986039; // Calculate the xy values from the XYZ values let x = (X / (X + Y + Z)).toFixed(4); let y = (Y / (X + Y + Z)).toFixed(4); if (isNaN(Number(x))) { x = '0'; } if (isNaN(Number(y))) { y = '0'; } return { x: Number(x), y: Number(y) }; } */ /* testColors(); console.log('rgb_to_cie(0, 128, 255)', rgb_to_cie(0, 128, 255)); console.log('cie_to_rgb(0.1401, 0.1284, 254)', cie_to_rgb(0.1401, 0.1284, 254)); console.log('cie_to_rgb(0.1406, 0.1382, 254)', cie_to_rgb(0.1406, 0.1382, 254)); for (let h = 0; h < 360; h++) { const rgb = hslColorToRgbColor(h, 100, 50); const xy = rgbColorToXYColor({ r: rgb.r, g: rgb.g, b: rgb.b }); const rgb2 = cie_to_rgb(xy.x, xy.y, 254); const hsl = rgbColorToHslColor({ r: rgb2.r, g: rgb2.g, b: rgb2.b }); assert(rgb.r === rgb2.r && rgb.g === rgb2.g && rgb.b === rgb2.b, 'Color rgb conversion error'); assert(h === hsl.h, 'Color hsl conversion error'); console.log(`\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}mColor: r:${rgb.r} g:${rgb.g} b:${rgb.b}\x1b[0m => x:${xy.x} y:${xy.y} => \x1b[48;2;${rgb2.r};${rgb2.g};${rgb2.b}mColor: r:${rgb2.r} g:${rgb2.g} b:${rgb2.b} h:${hsl.h} s:${hsl.s}\x1b[0m\x1b[K`); } */ //# sourceMappingURL=colorUtils.js.map