Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
299
packages/utils/src/color.ts
Normal file
299
packages/utils/src/color.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Represents an RGB color with numeric values for red, green, and blue components
|
||||
* @typedef {Object} TRgb
|
||||
* @property {number} r - Red component (0-255)
|
||||
* @property {number} g - Green component (0-255)
|
||||
* @property {number} b - Blue component (0-255)
|
||||
*/
|
||||
export type TRgb = { r: number; g: number; b: number };
|
||||
|
||||
export type THsl = { h: number; s: number; l: number };
|
||||
|
||||
/**
|
||||
* @description Validates and clamps color values to RGB range (0-255)
|
||||
* @param {number} value - The color value to validate
|
||||
* @returns {number} Clamped and floored value between 0-255
|
||||
* @example
|
||||
* validateColor(-10) // returns 0
|
||||
* validateColor(300) // returns 255
|
||||
* validateColor(128) // returns 128
|
||||
*/
|
||||
export const validateColor = (value: number) => {
|
||||
if (value < 0) return 0;
|
||||
if (value > 255) return 255;
|
||||
return Math.floor(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a decimal color value to two-character hex
|
||||
* @param {number} value - Decimal color value (0-255)
|
||||
* @returns {string} Two-character hex value with leading zero if needed
|
||||
*/
|
||||
export const toHex = (value: number) => validateColor(value).toString(16).padStart(2, "0");
|
||||
|
||||
/**
|
||||
* Converts a hexadecimal color code to RGB values
|
||||
* @param {string} hex - The hexadecimal color code (e.g., "#ff0000" for red)
|
||||
* @returns {RGB} An object containing the RGB values
|
||||
* @example
|
||||
* hexToRgb("#ff0000") // returns { r: 255, g: 0, b: 0 }
|
||||
* hexToRgb("#00ff00") // returns { r: 0, g: 255, b: 0 }
|
||||
* hexToRgb("#0000ff") // returns { r: 0, g: 0, b: 255 }
|
||||
*/
|
||||
export const hexToRgb = (hex: string): TRgb => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: { r: 0, g: 0, b: 0 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts RGB values to a hexadecimal color code
|
||||
* @param {RGB} rgb - An object containing RGB values
|
||||
* @param {number} rgb.r - Red component (0-255)
|
||||
* @param {number} rgb.g - Green component (0-255)
|
||||
* @param {number} rgb.b - Blue component (0-255)
|
||||
* @returns {string} The hexadecimal color code (e.g., "#ff0000" for red)
|
||||
* @example
|
||||
* rgbToHex({ r: 255, g: 0, b: 0 }) // returns "#ff0000"
|
||||
* rgbToHex({ r: 0, g: 255, b: 0 }) // returns "#00ff00"
|
||||
* rgbToHex({ r: 0, g: 0, b: 255 }) // returns "#0000ff"
|
||||
*/
|
||||
export const rgbToHex = ({ r, g, b }: TRgb): string => `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
|
||||
/**
|
||||
* Converts Hex values to HSL values
|
||||
* @param {string} hex - The hexadecimal color code (e.g., "#ff0000" for red)
|
||||
* @returns {HSL} An object containing the HSL values
|
||||
* @example
|
||||
* hexToHsl("#ff0000") // returns { h: 0, s: 100, l: 50 }
|
||||
* hexToHsl("#00ff00") // returns { h: 120, s: 100, l: 50 }
|
||||
* hexToHsl("#0000ff") // returns { h: 240, s: 100, l: 50 }
|
||||
*/
|
||||
export const hexToHsl = (hex: string): THsl => {
|
||||
// return default value for invalid hex
|
||||
if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) return { h: 0, s: 0, l: 0 };
|
||||
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
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: h * 360,
|
||||
s: s * 100,
|
||||
l: l * 100,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts HSL values to a hexadecimal color code
|
||||
* @param {HSL} hsl - An object containing HSL values
|
||||
* @param {number} hsl.h - Hue component (0-360)
|
||||
* @param {number} hsl.s - Saturation component (0-100)
|
||||
* @param {number} hsl.l - Lightness component (0-100)
|
||||
* @returns {string} The hexadecimal color code (e.g., "#ff0000" for red)
|
||||
* @example
|
||||
* hslToHex({ h: 0, s: 100, l: 50 }) // returns "#ff0000"
|
||||
* hslToHex({ h: 120, s: 100, l: 50 }) // returns "#00ff00"
|
||||
* hslToHex({ h: 240, s: 100, l: 50 }) // returns "#0000ff"
|
||||
*/
|
||||
export const hslToHex = ({ h, s, l }: THsl): string => {
|
||||
if (h < 0 || h > 360) return "#000000";
|
||||
if (s < 0 || s > 100) return "#000000";
|
||||
if (l < 0 || l > 100) return "#000000";
|
||||
|
||||
l /= 100;
|
||||
const a = (s * Math.min(l, 1 - l)) / 100;
|
||||
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12;
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
};
|
||||
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate relative luminance of a color according to WCAG
|
||||
* @param {Object} rgb - RGB color object with r, g, b properties
|
||||
* @returns {number} Relative luminance value
|
||||
*/
|
||||
export const getLuminance = ({ r, g, b }: TRgb) => {
|
||||
// Convert RGB to sRGB
|
||||
const sR = r / 255;
|
||||
const sG = g / 255;
|
||||
const sB = b / 255;
|
||||
|
||||
// Convert sRGB to linear RGB with gamma correction
|
||||
const R = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4);
|
||||
const G = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4);
|
||||
const B = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4);
|
||||
|
||||
// Calculate luminance
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate contrast ratio between two colors
|
||||
* @param {Object} rgb1 - First RGB color object
|
||||
* @param {Object} rgb2 - Second RGB color object
|
||||
* @returns {number} Contrast ratio between the colors
|
||||
*/
|
||||
export function getContrastRatio(rgb1: { r: number; g: number; b: number }, rgb2: { r: number; g: number; b: number }) {
|
||||
const luminance1 = getLuminance(rgb1);
|
||||
const luminance2 = getLuminance(rgb2);
|
||||
|
||||
const lighter = Math.max(luminance1, luminance2);
|
||||
const darker = Math.min(luminance1, luminance2);
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lighten a color by a specified amount
|
||||
* @param {Object} rgb - RGB color object
|
||||
* @param {number} amount - Amount to lighten (0-1)
|
||||
* @returns {Object} Lightened RGB color
|
||||
*/
|
||||
export function lightenColor(rgb: { r: number; g: number; b: number }, amount: number) {
|
||||
return {
|
||||
r: rgb.r + (255 - rgb.r) * amount,
|
||||
g: rgb.g + (255 - rgb.g) * amount,
|
||||
b: rgb.b + (255 - rgb.b) * amount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Darken a color by a specified amount
|
||||
* @param {Object} rgb - RGB color object
|
||||
* @param {number} amount - Amount to darken (0-1)
|
||||
* @returns {Object} Darkened RGB color
|
||||
*/
|
||||
export function darkenColor(rgb: { r: number; g: number; b: number }, amount: number) {
|
||||
return {
|
||||
r: rgb.r * (1 - amount),
|
||||
g: rgb.g * (1 - amount),
|
||||
b: rgb.b * (1 - amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate appropriate foreground and background colors based on input color
|
||||
* @param {string} color - Input color in hex format
|
||||
* @returns {Object} Object containing foreground and background colors in hex format
|
||||
*/
|
||||
export function generateIconColors(color: string) {
|
||||
// Parse input color
|
||||
const rgbColor = hexToRgb(color);
|
||||
const luminance = getLuminance(rgbColor);
|
||||
|
||||
// Initialize output colors
|
||||
let foregroundColor = rgbColor;
|
||||
|
||||
// Constants for color adjustment
|
||||
const MIN_CONTRAST_RATIO = 3.0; // Minimum acceptable contrast ratio
|
||||
|
||||
// For light colors, use as foreground and darken for background
|
||||
if (luminance > 0.5) {
|
||||
// Make sure the foreground color is dark enough for visibility
|
||||
let adjustedForeground = foregroundColor;
|
||||
const whiteContrast = getContrastRatio(foregroundColor, { r: 255, g: 255, b: 255 });
|
||||
|
||||
if (whiteContrast < MIN_CONTRAST_RATIO) {
|
||||
// Darken the foreground color until it has enough contrast
|
||||
let darkenAmount = 0.1;
|
||||
while (darkenAmount <= 0.9) {
|
||||
adjustedForeground = darkenColor(foregroundColor, darkenAmount);
|
||||
if (getContrastRatio(adjustedForeground, { r: 255, g: 255, b: 255 }) >= MIN_CONTRAST_RATIO) {
|
||||
break;
|
||||
}
|
||||
darkenAmount += 0.1;
|
||||
}
|
||||
foregroundColor = adjustedForeground;
|
||||
}
|
||||
}
|
||||
// For dark colors, use as foreground and lighten for background
|
||||
else {
|
||||
// Make sure the foreground color is light enough for visibility
|
||||
let adjustedForeground = foregroundColor;
|
||||
const blackContrast = getContrastRatio(foregroundColor, { r: 0, g: 0, b: 0 });
|
||||
|
||||
if (blackContrast < MIN_CONTRAST_RATIO) {
|
||||
// Lighten the foreground color until it has enough contrast
|
||||
let lightenAmount = 0.1;
|
||||
while (lightenAmount <= 0.9) {
|
||||
adjustedForeground = lightenColor(foregroundColor, lightenAmount);
|
||||
if (getContrastRatio(adjustedForeground, { r: 0, g: 0, b: 0 }) >= MIN_CONTRAST_RATIO) {
|
||||
break;
|
||||
}
|
||||
lightenAmount += 0.1;
|
||||
}
|
||||
foregroundColor = adjustedForeground;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
foreground: rgbToHex({ r: foregroundColor.r, g: foregroundColor.g, b: foregroundColor.b }),
|
||||
background: `rgba(${foregroundColor.r}, ${foregroundColor.g}, ${foregroundColor.b}, 0.25)`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Generates a deterministic HSL color based on input string
|
||||
* @param {string} input - Input string to generate color from
|
||||
* @returns {THsl} An object containing the HSL values
|
||||
* @example
|
||||
* generateRandomColor("hello") // returns consistent HSL color for "hello"
|
||||
* generateRandomColor("") // returns { h: 0, s: 0, l: 0 }
|
||||
*/
|
||||
export const generateRandomColor = (input: string): THsl => {
|
||||
// If input is falsy, generate a random seed string.
|
||||
// The random seed is created by converting a random number to base-36 and taking a substring.
|
||||
const seed = input || Math.random().toString(36).substring(2, 8);
|
||||
|
||||
const uniqueId = seed.length.toString() + seed; // Unique identifier based on string length
|
||||
const combinedString = uniqueId + seed;
|
||||
|
||||
// Create a hash value from the combined string.
|
||||
const hash = Array.from(combinedString).reduce((acc, char) => {
|
||||
const charCode = char.charCodeAt(0);
|
||||
return (acc << 5) - acc + charCode;
|
||||
}, 0);
|
||||
|
||||
// Derive the HSL values from the hash.
|
||||
const hue = Math.abs(hash % 360);
|
||||
const saturation = 70; // Maintains a good amount of color
|
||||
const lightness = 70; // Increased lightness for a pastel look
|
||||
|
||||
return { h: hue, s: saturation, l: lightness };
|
||||
};
|
||||
Reference in New Issue
Block a user