AI 生成界面的色彩无障碍检查:自动对比度计算与配色建议
一、当"好看的配色"让 8% 的男性用户看不清文字
设计师用 Figma 给我切了一张促销 Banner:浅粉背景 #FFE4E1 上叠加了金色文字 #FFD700。视觉上确实温暖而有节日感,但当我跑完对比度检测——1.47:1。WCAG 2.1 AA 标准要求普通文本至少 4.5:1。
这不是个例。全球约 8% 的男性、0.5% 的女性有不同程度的色觉障碍(最常见的是红绿色盲)。加上低视力、环境强光等因素,色彩无障碍不是"锦上添花"而是基本要求。
AI 在色彩无障碍检查中的角色是什么?不是告诉你"对比度不够"(这有成熟算法),而是自动生成满足对比度要求的替代配色方案,并且在设计系统 Token 层面做到"一改全改"。
二、WCAG 对比度算法与 AI 介入点
WCAG 对比度公式:
$$对比度 = \frac{L_1 + 0.05}{L_2 + 0.05}$$
其中 $L_1$ 是较亮颜色的相对亮度,$L_2$ 是较暗颜色的。相对亮度的计算基于 sRGB → 线性 RGB → 加权亮度的三步转换:
$$L = 0.2126 \times R_{linear} + 0.7152 \times G_{linear} + 0.0722 \times B_{linear}$$
AI 的介入不在这一步(这是纯数学),而是在配色推荐阶段。给定一个"对比度不合格"的颜色对,AI 在 HSL 空间中搜索满足以下条件的最优解:最小化色相差、保持饱和度不变或略微调整、明度调整幅度最小。
三、生产级实现
// color-accessibility/contrast-checker.ts // 色彩无障碍检查与 AI 配色推荐引擎 /** * 将 hex 颜色转换为 sRGB 通道值 [0, 1] */ function hexToSRGB(hex: string): [number, number, 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; return [r, g, b]; } /** * sRGB 通道 → 线性 RGB 通道 * sRGB 使用伽马压缩(gamma ≈ 2.2),需要先线性化才能计算亮度 */ function sRGBToLinear(channel: number): number { return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4); } /** * 计算相对亮度(WCAG 定义) * L = 0.2126*R + 0.7152*G + 0.0722*B * 系数来源于人眼对三原色的敏感度差异:绿色最敏感,蓝色最不敏感 */ function relativeLuminance(hex: string): number { const [r, g, b] = hexToSRGB(hex); return ( 0.2126 * sRGBToLinear(r) + 0.7152 * sRGBToLinear(g) + 0.0722 * sRGBToLinear(b) ); } /** * 计算两个颜色的 WCAG 对比度 * @returns 对比度值(1:1 到 21:1) */ function contrastRatio(foreground: string, background: string): number { const l1 = relativeLuminance(foreground); const l2 = relativeLuminance(background); const lighter = Math.max(l1, l2); const darker = Math.min(l1, l2); return (lighter + 0.05) / (darker + 0.05); } /** * WCAG 合规等级判定 */ type ComplianceLevel = 'AAA' | 'AA' | 'AA-Large' | 'FAIL'; function getComplianceLevel(ratio: number, isLargeText: boolean): ComplianceLevel { if (ratio >= 7) return 'AAA'; if (ratio >= 4.5) return 'AA'; if (isLargeText && ratio >= 3) return 'AA-Large'; return 'FAIL'; } /** * AI 配色推荐引擎的核心逻辑 * * 算法思路:在 HSL 颜色空间中对前景色做局部搜索 * — 优先调整明度(Lightness),因为这是影响对比度的主因 * — 其次微调饱和度(Saturation),保持色彩语义 * — 尽量不改变色相(Hue),否则颜色语义会变化(蓝色变绿色) * * @param fg - 前景色 hex * @param bg - 背景色 hex * @param targetRatio - 目标对比度(默认 4.5,WCAG AA 标准) * @returns 推荐的替代配色方案列表,按"与原色视觉距离"排序 */ function recommendColors( fg: string, bg: string, targetRatio: number = 4.5 ): Array<{ hex: string; hsl: [number, number, number]; ratio: number }> { const results: Array<{ hex: string; hsl: [number, number, number]; ratio: number }> = []; const [h, s, l] = hexToHSL(fg); // 策略 1:沿明度轴搜索(保持色相和饱和度不变) // L 轴步进 2%,最多调整到 0 或 100 for (let deltaL = 2; deltaL <= 100; deltaL += 2) { // 同时尝试变暗和变亮 for (const sign of [-1, 1]) { const newL = Math.max(0, Math.min(100, l + sign * deltaL)); const newHex = hslToHex(h, s, newL); const ratio = contrastRatio(newHex, bg); if (ratio >= targetRatio) { results.push({ hex: newHex, hsl: [h, s, newL], ratio }); // 找到第一个满足条件的就记录(这是最小 delta L 的方案) // 但继续搜索,因为用户可能有多个偏好 if (results.length >= 3) break; } } if (results.length >= 3) break; } // 策略 2:如果明度调整幅度过大(超过 30%),尝试调饱和度 if (results.length === 0 || Math.abs(l - results[0].hsl[2]) > 30) { for (let deltaS = 5; deltaS <= 100; deltaS += 5) { for (const sign of [-1, 1]) { const newS = Math.max(0, Math.min(100, s + sign * deltaS)); const newHex = hslToHex(h, newS, l); const ratio = contrastRatio(newHex, bg); if (ratio >= targetRatio) { results.push({ hex: newHex, hsl: [h, newS, l], ratio }); break; } } } } return results; } // ---- 辅助工具函数 ---- /** * Hex → HSL 转换 */ function hexToHSL(hex: string): [number, number, number] { let [r, g, b] = hexToSRGB(hex); const max = Math.max(r, g, b), min = Math.min(r, g, b); const l = (max + min) / 2; if (max === min) return [0, 0, Math.round(l * 100)]; const d = max - min; const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); let h = 0; 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 [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]; } function hslToHex(h: number, s: number, l: number): string { s /= 100; l /= 100; const a = s * Math.min(l, 1 - l); const f = (n: number) => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color).toString(16).padStart(2, '0'); }; return `#${f(0)}${f(8)}${f(4)}`; }// color-accessibility/design-token-fixer.ts // 设计 Token 自动修复模块 // 扫描 tokens.json 中的所有颜色 Token,检测对比度问题并生成修复方案 interface TokenDiff { tokenPath: string; // 如 "color.text.primary" currentValue: string; // 当前 hex 值 background: string; // 关联的背景色 currentRatio: number; // 当前对比度 recommendations: Array<{ value: string; ratio: number; deltaL: number; // 明度变化量 }>; } /** * 扫描设计 Token 文件,检测所有前景色-背景色配对 * @returns 需要修复的 Token 列表 */ function auditDesignTokens(tokens: Record<string, any>): TokenDiff[] { const issues: TokenDiff[] = []; // 遍历所有文本色 Token(前景色) const textTokens = extractTokensByCategory(tokens, 'color.text'); // 获取所有背景色 Token const bgTokens = extractTokensByCategory(tokens, 'color.background'); for (const [textPath, textColor] of Object.entries(textTokens)) { for (const [bgPath, bgColor] of Object.entries(bgTokens)) { const ratio = contrastRatio(textColor, bgColor); if (ratio < 4.5) { issues.push({ tokenPath: textPath, currentValue: textColor, background: bgColor, currentRatio: ratio, recommendations: recommendColors(textColor, bgColor) }); } } } return issues; }四、不能只看对比度——色彩无障碍的三个盲区
盲区一:色盲友好 ≠ 高对比度。红绿色盲用户区分红色和绿色的方式是靠明度差,不是色相差。即便对比度通过了 WCAG AA,如果信息传达仅依赖"红色=错误、绿色=成功"的色相差异,色盲用户依然无法区分。解法是加图标、加形状、加文字——永远不做"仅靠颜色传达信息"的设计。
盲区二:暗黑模式的对比度陷阱。暗黑模式下,白色文字 #FFFFFF 在深灰背景 #333333 上的对比度是 13.49:1(远超 AA),但大面积白色文字在深色背景上会产生光晕效应(halation),长时间阅读反而比适中的浅灰更疲劳。Google Material Design 推荐暗黑模式正文字色为 #E0E0E0。
盲区三:AI 推荐算法无法理解品牌语义。如果品牌主色是特定色值的橙色,AI 推荐"稍微降低饱和度提升对比度"可能导致颜色看起来"不是那个颜色了"。AI 应该提供多种策略(改前景/改背景/加描边/加阴影),由设计师选择。
五、总结
色彩无障碍检查的技术栈分为三层:
- 计算层(自动化)——WCAG 对比度公式是纯数学,不需要 AI,脚本就能做到
- 推荐层(AI 辅助)——在 HSL 空间搜索满足对比度要求的替代色方案,AI 的核心价值在这里
- 决策层(人工)——最终选择哪个方案需要设计师的品牌感知判断,AI 只负责提供数据和选项
落地路径:从设计 Token 文件入手,CI 中加一个脚本,每次 Token 变更自动跑对比度审计,不合格的直接打断构建。这不是锦上添花,而是工程基线。