基于背景生成符合WCAG/APCA标准的非黑白适配文本颜色(TypeScript)
实现适配背景的保留原色调文本颜色方案(符合WCAG 2 AA/APCA)
核心思路
要保留文本原有色彩同时满足对比度标准,核心是在不改变色相(H)和饱和度(S)的前提下,动态调整亮度(L),再通过对比度算法校验是否达标,避免直接切换黑白的生硬效果。
具体实现步骤
1. 颜色空间转换工具
先实现十六进制与HSL的互转,方便单独调整亮度:
// 十六进制转HSL function hexToHsl(hex: string): { h: number, s: number, l: number } { 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, 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 }; } // HSL转十六进制 function hslToHex(h: number, s: number, l: number): string { h /= 360; s /= 100; l /= 100; let r, g, b; if (s === 0) { r = g = b = l; } else { const hue2rgb = (p: number, q: number, t: number) => { 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); } const toHex = (x: number) => { const hex = Math.round(x * 255).toString(16); return hex.length === 1 ? '0' + hex : hex; }; return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }
2. 对比度计算(支持双标准)
WCAG 2 AA 对比度计算
基于相对亮度的标准算法:
// 计算颜色相对亮度 function getRelativeLuminance(hex: string): number { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); const sRGB = [r, g, b].map(val => { const normalized = val / 255; return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4); }); return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2]; } // WCAG对比度计算(返回比值,AA标准要求≥4.5) function calculateWCAGContrast(fgHex: string, bgHex: string): number { const l1 = getRelativeLuminance(fgHex); const l2 = getRelativeLuminance(bgHex); return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); }
APCA 对比度计算
更贴合人眼感知的现代算法,针对文本可读性优化:
// APCA对比度计算(返回感知值,正文要求≥45) function calculateAPCAContrast(fgHex: string, bgHex: string): number { const getPerceptualLuminance = (hex: string) => { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); const rLin = r <= 10.3 ? r / 3294.6 : Math.pow((r + 14.025) / 269.025, 2.4); const gLin = g <= 10.3 ? g / 3294.6 : Math.pow((g + 14.025) / 269.025, 2.4); const bLin = b <= 10.3 ? b / 3294.6 : Math.pow((b + 14.025) / 269.025, 2.4); return 0.2126729 * rLin + 0.7151522 * gLin + 0.0721750 * bLin; }; const bgY = getPerceptualLuminance(bgHex); const fgY = getPerceptualLuminance(fgHex); let contrast = 0; if (bgY > fgY) { const raw = (Math.pow(bgY, 0.55) - Math.pow(fgY, 0.55)) * 1.14; contrast = raw > 0.02 ? raw : 0; } else { const raw = (Math.pow(bgY, 0.55) - Math.pow(fgY, 0.55)) * 1.14; contrast = raw < -0.02 ? raw : 0; } return Math.abs(contrast) * 100; }
3. 动态调整文本亮度
根据背景亮度方向,逐步调整文本亮度直到满足对比度要求:
function adjustTextColor(originalTextHex: string, bgHex: string, standard: 'WCAG_AA' | 'APCA'): string { const target = standard === 'WCAG_AA' ? 4.5 : 45; const { h, s, l } = hexToHsl(originalTextHex); const bgLuminance = getRelativeLuminance(bgHex); let adjustedL = l; // 背景亮则降低文本亮度,背景暗则提高 const step = bgLuminance > 0.5 ? -2 : 2; // 循环调整,加次数限制避免死循环 for (let i = 0; i < 50; i++) { const currentHex = hslToHex(h, s, adjustedL); const currentContrast = standard === 'WCAG_AA' ? calculateWCAGContrast(currentHex, bgHex) : calculateAPCAContrast(currentHex, bgHex); if ((standard === 'WCAG_AA' && currentContrast >= target) || (standard === 'APCA' && currentContrast >= target)) { return currentHex; } // 亮度边界限制在0-100 adjustedL += step; if (adjustedL < 0) adjustedL = 0; if (adjustedL > 100) adjustedL = 100; } // 极端情况 fallback 到黑白 return bgLuminance > 0.5 ? '#000000' : '#ffffff'; }
针对动漫卡片的优化建议
- 取背景平均亮度:如果背景是渐变或复杂图案,计算背景区域的平均亮度再做调整,避免单一像素的偏差
- 饱和度微调:若调整亮度后色彩辨识度下降,可在满足对比度的前提下,小幅微调饱和度,保留原色彩特征
- 缓存计算结果:对重复的背景+文本色组合缓存结果,提升性能
内容的提问来源于stack exchange,提问作者Entropy
相关产品推荐
相关产品推荐

