AI 生成界面的色彩无障碍检查:自动对比度计算与配色建议

发布时间:2026/7/16 18:15:03
AI 生成界面的色彩无障碍检查:自动对比度计算与配色建议 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.5WCAG 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: Recordstring, 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 变更自动跑对比度审计不合格的直接打断构建。这不是锦上添花而是工程基线。