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