matterbridge
Version:
Matterbridge plugin manager for Matter
280 lines • 8.82 kB
JavaScript
/**
* This file contains the color utilities.
*
* @file colorUtils.ts
* @author Luca Liguori
* @created 2023-10-05
* @version 1.3.0
* @license Apache-2.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';
/**
* Converts from HSL to RGB color space
*
* @param {number} hue - The hue value (0-360).
* @param {number} saturation - The saturation value (0-100).
* @param {number} luminance - The luminance value (0-100).
* @returns {RGB} An object containing the RGB values.
*/
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 RGB color space to CIE 1931 XY color space
*
* @param {RGB} rgb - The RGB color object.
* @returns {XY} An object containing the x and y values in CIE 1931 XY 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 };
}
/**
* Converts CIE 1931 XY color space to RGB color space
*
* @param {number} x - The x value in CIE 1931 XY color space.
* @param {number} y - The y value in CIE 1931 XY color space.
* @param {number} [brightness] - The brightness value (1-254). Defaults to 254.
* @returns {RGB} An object containing the RGB values.
*/
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 even if this code should never be reached...
if (isNaN(red) || red < 0) {
/* istanbul ignore next */
red = 0;
}
if (isNaN(green) || green < 0) {
/* istanbul ignore next */
green = 0;
}
if (isNaN(blue) || blue < 0) {
/* istanbul ignore next */
blue = 0;
}
// Fix negative zero issue by ensuring we return positive zero
return {
r: red === 0 ? 0 : red,
g: green === 0 ? 0 : green,
b: blue === 0 ? 0 : blue,
};
}
/**
* Converts RGB color space to HSL color space
*
* @param {RGB} rgb - The RGB color object.
* @returns {HSL} An object containing the HSL values.
*/
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),
};
}
/**
* Converts CIE 1931 XY color space to HSL color space
*
* @param {number} x - The x value in CIE 1931 XY color space.
* @param {number} y - The y value in CIE 1931 XY color space.
* @returns {HSL} An object containing the HSL values.
*/
export function xyToHsl(x, y) {
const rgb = xyColorToRgbColor(x, y);
return rgbColorToHslColor(rgb);
}
/**
* Converts mireds to kelvin.
*
* @param {number} mired - The mired value to convert.
* @returns {number} The converted kelvin value.
*/
export function miredToKelvin(mired) {
return Math.round(1000000 / mired);
}
/**
* Converts kelvin to mireds.
*
* @param {number} kelvin - The kelvin value to convert.
* @returns {number} The converted mired value.
*/
export function kelvinToMired(kelvin) {
return Math.round(1000000 / kelvin);
}
/**
* Converts kelvin to RGB color space.
*
* @param {number} kelvin - The kelvin value to convert (1000K to 40000K).
* @returns {RGB} An object containing the RGB values.
*/
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) };
}
//# sourceMappingURL=colorUtils.js.map