Files
Website-template/client/src/directives/vRecolorText.ts

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;
}
}