mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
266 lines
6.3 KiB
JavaScript
266 lines
6.3 KiB
JavaScript
/**
|
|
* Convert hex color to HSL
|
|
*/
|
|
function hexToHSL(hex) {
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
if (!result) return null;
|
|
|
|
let r = parseInt(result[1], 16) / 255;
|
|
let g = parseInt(result[2], 16) / 255;
|
|
let b = parseInt(result[3], 16) / 255;
|
|
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
let h,
|
|
s,
|
|
l = (max + min) / 2;
|
|
|
|
if (max === min) {
|
|
h = s = 0;
|
|
} 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)) / 6;
|
|
break;
|
|
case g:
|
|
h = ((b - r) / d + 2) / 6;
|
|
break;
|
|
case b:
|
|
h = ((r - g) / d + 4) / 6;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return { h: h * 360, s: s * 100, l: l * 100 };
|
|
}
|
|
|
|
/**
|
|
* Convert HSL to hex color
|
|
*/
|
|
function hslToHex(h, s, l) {
|
|
s /= 100;
|
|
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 (0 <= h && h < 60) {
|
|
r = c;
|
|
g = x;
|
|
b = 0;
|
|
} else if (60 <= h && h < 120) {
|
|
r = x;
|
|
g = c;
|
|
b = 0;
|
|
} else if (120 <= h && h < 180) {
|
|
r = 0;
|
|
g = c;
|
|
b = x;
|
|
} else if (180 <= h && h < 240) {
|
|
r = 0;
|
|
g = x;
|
|
b = c;
|
|
} else if (240 <= h && h < 300) {
|
|
r = x;
|
|
g = 0;
|
|
b = c;
|
|
} else if (300 <= h && h < 360) {
|
|
r = c;
|
|
g = 0;
|
|
b = x;
|
|
}
|
|
|
|
r = Math.round((r + m) * 255);
|
|
g = Math.round((g + m) * 255);
|
|
b = Math.round((b + m) * 255);
|
|
|
|
return (
|
|
"#" +
|
|
[r, g, b]
|
|
.map((x) => {
|
|
const hex = x.toString(16);
|
|
return hex.length === 1 ? "0" + hex : hex;
|
|
})
|
|
.join("")
|
|
);
|
|
}
|
|
|
|
function hexToRgb(hex) {
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
return result ? {
|
|
r: parseInt(result[1], 16),
|
|
g: parseInt(result[2], 16),
|
|
b: parseInt(result[3], 16)
|
|
} : { r: 0, g: 0, b: 0 };
|
|
}
|
|
|
|
function rgbToHex(r, g, b) {
|
|
return "#" + [r, g, b].map(x => {
|
|
const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16);
|
|
return hex.length === 1 ? "0" + hex : hex;
|
|
}).join("");
|
|
}
|
|
|
|
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);
|
|
let h, s, l = (max + min) / 2;
|
|
|
|
if (max === min) {
|
|
h = s = 0;
|
|
} 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)) / 6; break;
|
|
case g: h = ((b - r) / d + 2) / 6; break;
|
|
case b: h = ((r - g) / d + 4) / 6; break;
|
|
}
|
|
}
|
|
|
|
return { h: h * 360, s: s * 100, l: l * 100 };
|
|
}
|
|
|
|
function hslToRgb(h, s, l) {
|
|
h /= 360;
|
|
s /= 100;
|
|
l /= 100;
|
|
|
|
let r, g, b;
|
|
|
|
if (s === 0) {
|
|
r = g = b = l;
|
|
} 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 = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
const p = 2 * l - q;
|
|
|
|
r = hue2rgb(p, q, h + 1/3);
|
|
g = hue2rgb(p, q, h);
|
|
b = hue2rgb(p, q, h - 1/3);
|
|
}
|
|
|
|
return { r: r * 255, g: g * 255, b: b * 255 };
|
|
}
|
|
|
|
// Calculate relative luminance (WCAG standard)
|
|
function getLuminance(hex) {
|
|
const rgb = hexToRgb(hex);
|
|
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(val => {
|
|
val /= 255;
|
|
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
|
|
});
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
}
|
|
|
|
// Calculate contrast ratio between two colors
|
|
function getContrastRatio(hex1, hex2) {
|
|
const lum1 = getLuminance(hex1);
|
|
const lum2 = getLuminance(hex2);
|
|
const brightest = Math.max(lum1, lum2);
|
|
const darkest = Math.min(lum1, lum2);
|
|
return (brightest + 0.05) / (darkest + 0.05);
|
|
}
|
|
|
|
// Check if a color is considered "light"
|
|
function isLightColor(hex) {
|
|
return getLuminance(hex) > 0.5;
|
|
}
|
|
|
|
// Adjust color lightness
|
|
function adjustLightness(hex, amount) {
|
|
const rgb = hexToRgb(hex);
|
|
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
hsl.l = Math.max(0, Math.min(100, hsl.l + amount));
|
|
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
}
|
|
|
|
// Adjust color saturation
|
|
function adjustSaturation(hex, amount) {
|
|
const rgb = hexToRgb(hex);
|
|
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
hsl.s = Math.max(0, Math.min(100, hsl.s + amount));
|
|
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
}
|
|
|
|
// Generate "on" color with proper contrast (for text/icons)
|
|
function generateOnColor(baseColor, isDarkMode) {
|
|
const isBaseLight = isLightColor(baseColor);
|
|
|
|
// If base is light, we need dark text; if base is dark, we need light text
|
|
if (isBaseLight) {
|
|
// Try darker variants
|
|
let testColor = "#000000";
|
|
if (getContrastRatio(baseColor, testColor) >= 4.5) {
|
|
return testColor;
|
|
}
|
|
// Fallback to dark gray
|
|
return "#1c1b1f";
|
|
} else {
|
|
// Try lighter variants
|
|
let testColor = "#ffffff";
|
|
if (getContrastRatio(baseColor, testColor) >= 4.5) {
|
|
return testColor;
|
|
}
|
|
// Fallback to light gray
|
|
return "#e6e1e5";
|
|
}
|
|
}
|
|
|
|
// Generate container color (lighter in light mode, darker in dark mode)
|
|
function generateContainerColor(baseColor, isDarkMode) {
|
|
const rgb = hexToRgb(baseColor);
|
|
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
|
|
if (isDarkMode) {
|
|
// In dark mode, containers are darker and more saturated
|
|
hsl.l = Math.max(10, Math.min(30, hsl.l - 20));
|
|
hsl.s = Math.min(100, hsl.s + 10);
|
|
} else {
|
|
// In light mode, containers are lighter and less saturated
|
|
hsl.l = Math.min(90, Math.max(75, hsl.l + 30));
|
|
hsl.s = Math.max(0, hsl.s - 10);
|
|
}
|
|
|
|
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
}
|
|
|
|
// Generate surface variant colors
|
|
function generateSurfaceVariant(backgroundColor, step, isDarkMode) {
|
|
const rgb = hexToRgb(backgroundColor);
|
|
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
|
|
if (isDarkMode) {
|
|
// In dark mode, variants get progressively lighter
|
|
hsl.l = Math.min(100, hsl.l + (step * 3));
|
|
} else {
|
|
// In light mode, variants get progressively darker
|
|
hsl.l = Math.max(0, hsl.l - (step * 2));
|
|
}
|
|
|
|
const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
return rgbToHex(newRgb.r, newRgb.g, newRgb.b);
|
|
}
|