136 lines
6.2 KiB
TypeScript
136 lines
6.2 KiB
TypeScript
import {watch} from "vue";
|
|
import {darkModeActive} from "@models/globals.ts";
|
|
|
|
function getLuminance(color: string): number {
|
|
let r = 0, g = 0, b = 0;
|
|
//parse hex color to RGB
|
|
if(/^#.*$/.test(color)) {
|
|
if (/^#(|[0-9A-Fa-f]{6})$/.test(color)) {
|
|
// Parse hex color
|
|
r = parseInt(color.substring(1, 3), 16);
|
|
g = parseInt(color.substring(3, 5), 16);
|
|
b = parseInt(color.substring(5, 7), 16);
|
|
} else if (/^#([0-9A-Fa-f]{3})$/.test(color)) {
|
|
// Parse short hex color
|
|
r = parseInt(color[1] + color[1], 16);
|
|
g = parseInt(color[2] + color[2], 16);
|
|
b = parseInt(color[3] + color[3], 16);
|
|
} else if (/^#([0-9A-Fa-f]{8})$/.test(color)) {
|
|
// Parse hex with alpha
|
|
r = parseInt(color.substring(1, 3), 16);
|
|
g = parseInt(color.substring(3, 5), 16);
|
|
b = parseInt(color.substring(5, 7), 16);
|
|
} else {
|
|
throw new Error('Invalid color format: cause: ' + color);
|
|
}
|
|
} else if (/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([\d.]+)\s*)?\)$/.test(color)) {
|
|
// Parse RGB or RGBA
|
|
const rgbaMatch = color.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([\d.]+)\s*)?\)/);
|
|
if (rgbaMatch) {
|
|
r = parseInt(rgbaMatch[1], 10);
|
|
g = parseInt(rgbaMatch[2], 10);
|
|
b = parseInt(rgbaMatch[3], 10);
|
|
}
|
|
} else {
|
|
throw new Error('Invalid color format: cause: ' + color);
|
|
}
|
|
// RGB to sRGB conversion
|
|
r = r / 255;
|
|
g = g / 255;
|
|
b = b / 255;
|
|
|
|
// sRBG to linear RGB conversion
|
|
r = r <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
|
|
g = g <= 0.04045 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
|
|
b = b <= 0.04045 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
|
|
|
|
// Calculate luminance
|
|
return (0.1726 * r + 0.7552 * g + 0.0722 * b);
|
|
}
|
|
|
|
const blackTextColor = '#181818';
|
|
const whiteTextColor = '#E7E7E7';
|
|
const blackLuminance = getLuminance(blackTextColor);
|
|
const whiteLuminance = getLuminance(whiteTextColor);
|
|
|
|
const blackTextColorDeep = '#0B0B0B';
|
|
const whiteTextColorDeep = '#F4F4F4';
|
|
const blackLuminanceDeep = getLuminance(blackTextColorDeep);
|
|
const whiteLuminanceDeep = getLuminance(whiteTextColorDeep);
|
|
|
|
const blackTextColorAbsolute = '#000000';
|
|
const whiteTextColorAbsolute = '#FFFFFF';
|
|
const blackLuminanceAbsolute = getLuminance(blackTextColorAbsolute);
|
|
const whiteLuminanceAbsolute = getLuminance(whiteTextColorAbsolute);
|
|
|
|
export function useReadableTextColor(bgColor: string): string {
|
|
const bgLuminance = getLuminance(bgColor);
|
|
|
|
let blackLighter = Math.max(bgLuminance, blackLuminance);
|
|
let blackDarker = Math.min(bgLuminance, blackLuminance);
|
|
let whiteLighter = Math.max(bgLuminance, whiteLuminance);
|
|
let whiteDarker = Math.min(bgLuminance, whiteLuminance);
|
|
|
|
// Calculate contrast ratio
|
|
let blackRatio = (blackLighter + 0.05) / (blackDarker + 0.05);
|
|
let whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05);
|
|
// Return black or white based on luminance
|
|
if (blackRatio >= 4.5 || whiteRatio >= 4.5) {
|
|
//console.debug("bgL:", bgLuminance, "bL:", blackLuminance, "wL:", whiteLuminance, "bR:", blackRatio, "wR:", whiteRatio);
|
|
return blackRatio > whiteRatio ? '#181818' : '#E7E7E7';
|
|
}
|
|
// If contrast is not enough, use deep colors
|
|
blackLighter = Math.max(bgLuminance, blackLuminanceDeep);
|
|
blackDarker = Math.min(bgLuminance, blackLuminanceDeep);
|
|
whiteLighter = Math.max(bgLuminance, whiteLuminanceDeep);
|
|
whiteDarker = Math.min(bgLuminance, whiteLuminanceDeep);
|
|
blackRatio = (blackLighter + 0.05) / (blackDarker + 0.05);
|
|
whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05);
|
|
if (blackRatio >= 4.5 || whiteRatio >= 4.5) {
|
|
//console.debug("bgL:", bgLuminance, "bL:", blackLuminanceDeep, "wL:", whiteLuminanceDeep, "bR:", blackRatio, "wR:", whiteRatio);
|
|
return blackRatio > whiteRatio ? '#0B0B0B' : '#F4F4F4';
|
|
}
|
|
// If still not enough, use absolute colors
|
|
blackLighter = Math.max(bgLuminance, blackLuminanceAbsolute);
|
|
blackDarker = Math.min(bgLuminance, blackLuminanceAbsolute);
|
|
whiteLighter = Math.max(bgLuminance, whiteLuminanceAbsolute);
|
|
whiteDarker = Math.min(bgLuminance, whiteLuminanceAbsolute);
|
|
blackRatio = (blackLighter + 0.05) / (blackDarker + 0.05);
|
|
whiteRatio = (whiteLighter + 0.05) / (whiteDarker + 0.05);
|
|
if (blackRatio >= 4.5 || whiteRatio >= 4.5) {
|
|
//console.debug("bgL:", bgLuminance, "bL:", blackLuminanceAbsolute, "wL:", whiteLuminanceAbsolute, "bR:", blackRatio, "wR:", whiteRatio);
|
|
return blackRatio > whiteRatio ? '#000000' : '#FFFFFF';
|
|
}
|
|
//console.warn(`Not enough contrast for background color: ${bgColor}. Using fallback colors.`);
|
|
return bgLuminance > 0.5 ? blackTextColor : whiteTextColor; // Fallback to default colors
|
|
}
|
|
|
|
function updateTextColor(el: HTMLElement) {
|
|
el.style.color = useReadableTextColor(window.getComputedStyle(el).backgroundColor);
|
|
}
|
|
|
|
export default {
|
|
mounted(el: HTMLElement) {
|
|
const updateColor = () => updateTextColor(el);
|
|
// Initial color update
|
|
updateColor();
|
|
// Listen for background color changes
|
|
el.addEventListener('click', updateColor);
|
|
el.addEventListener('mouseover', updateColor);
|
|
el.addEventListener('mouseout', updateColor);
|
|
el.addEventListener('transitionend', updateColor);
|
|
watch(() => darkModeActive.value, () => updateTextColor(el));
|
|
(el as any)._readableTextListeners = [updateColor];
|
|
},
|
|
unmounted(el: HTMLElement) {
|
|
// Remove event listeners
|
|
const listeners = (el as any)._readableTextListeners || [];
|
|
for (const listener of listeners) {
|
|
el.removeEventListener('click', listener);
|
|
el.removeEventListener('mouseover', listener);
|
|
el.removeEventListener('mouseout', listener);
|
|
el.removeEventListener('transitionend', listener);
|
|
}
|
|
delete (el as any)._readableTextListeners;
|
|
}
|
|
} |