AI 生成色彩系统:从品牌主色到完整语义色板的自动化
AI 生成色彩系统从品牌主色到完整语义色板的自动化一、一万种 CTA 蓝和一套活不过迭代的色彩规范产品给了你一个主色#3B82F6Tailwind 的 blue-500然后说后面你看着办。接下来你要回答 15 个问题hover 态是深 2% 还是 5%disabled 态是降 opacity 还是混灰色danger 色是纯红还是偏品红warning 色和主色的对比度够不够 3:1这些问题的答案散落在设计师的 Figma 堆里、Material Design 的文档里、还有你上次 Code Review 时被人指出的这个灰色层级没用对。手工推导一个设计系统级别的色彩体系不需要多高的技术门槛但它需要可怕的一致性。40 个 Color Token、15 种语义场景、亮暗双主题——任何一个 Token 的色相偏移 1 度都会在暗色模式下产生连锁反应因为暗色模式的感知亮度映射是非线性的。AI 在色彩生成上的优势恰恰在于保持全局一致性。色彩空间是一个三维连续域人类每次只能盯着一种颜色手动调而 AI 可以同时将所有 Token 约束在同一个色彩空间中确保每个 Token 的色相、饱和度、明度变化遵循统一的数学模型。二、色彩系统的 Token 层级与 AI 生成链路设计系统的色彩 Token 可以按抽象层级分为四层原始色值Primitive TokensHEX 或 HSL 值如#3B82F6是最基础的原子。色阶Palette从原始色值推导出的 10-13 级明度阶梯如 blue-50 到 blue-950。语义 TokenSemantic Tokens将色阶映射到语义角色如color-bg-primary、color-text-danger。组件 TokenComponent Tokens将语义 Token 绑定到具体组件如btn-primary-bg。AI 介入的关键节点是第 2 层色阶生成和第 3 层语义映射。这两个步骤需要处理可感知均匀的明度阶梯——人眼对暗色的明度变化比对亮色敏感得多所以 dark mode 下的色阶不能是线性插值。flowchart TD A[输入品牌主色 HEX] -- B[色彩空间转换br/HEX → OKLCH] B -- C[色阶生成br/L 通道等距采样] C -- D[亮色模式色阶br/L: 0.98 → 0.30] C -- E[暗色模式色阶br/L: 0.08 → 0.70] D -- F[语义角色分配] E -- F F -- G[WCAG 对比度校验] G -- H{对比度 ≥ AA?} H --|✅| I[输出完整 Token JSON] H --|❌| J[微调 L 值 重试] J -- G这里选择 OKLCH 色彩空间而非 HSL 或 RGB因为它有均匀感知亮度特性。在 HSL 空间中hsl(240, 100%, 50%)和hsl(240, 100%, 30%)的暗了 20%感受并不等于hsl(60, 100%, 50%)到hsl(60, 100%, 30%)的暗了 20%。而在 OKLCH 中L 通道的一致性可以保证每降低 0.05 的 L 值人眼感受到的亮度变化是均匀的——这让 AI 的等距采样变得有意义。三、色彩 Token 自动生成引擎/** * 色彩系统自动生成引擎 * * 输入品牌主色 HEX 配置参数 * 输出完整的 Semantic Token JSON亮色 暗色两套主题 * * 核心算法 * 1. HEX → OKLCH 转换使用 culori 库 * 2. 基于 OKLCH 的 L 通道做感知均匀的色阶插值 * 3. 自动分配语义角色并校验 WCAG 对比度 */ import { toOklch, formatHex } from culori; import { wcagContrast } from culori; // 类型定义 // 语义角色枚举描述 Token 的使用场景背景/文字/边框/强调 type SemanticRole | bg-primary | bg-secondary | bg-tertiary | text-primary | text-secondary | text-tertiary | border-default | border-strong | accent-primary | accent-hover | accent-active | danger | warning | success | info; // 色彩 Token 结构 interface ColorToken { name: string; value: string; // HEX 格式输出 role: SemanticRole; contrastRatio?: number; } // 第一步HEX → OKLCH 转换 function hexToOklch(hex: string): { l: number; c: number; h: number } { const oklchObj toOklch(hex); return { l: oklchObj.l, // 亮度 0-10黑1白 c: oklchObj.c, // 色度饱和度0灰0.4高饱和 h: oklchObj.h ?? 0, // 色相角度 0-360 }; } // 第二步基于 L 通道的均匀色阶生成 // 亮色模式L 从 0.98接近白递减到 0.30深色但仍可见色相 // 暗色模式L 从 0.08接近黑递增到 0.70亮色 function generatePalette( baseOklch: { l: number; c: number; h: number }, mode: light | dark ): string[] { const steps 13; // 生成 50 → 950 共 13 级色阶Tailwind 标准 const palette: string[] []; // 亮色模式L 从高到低白→深色 // 暗色模式L 从低到高黑→浅色 const lRange mode light ? [0.98, 0.30] // 亮色模式范围 : [0.08, 0.70]; // 暗色模式范围 for (let i 0; i steps; i) { const t i / (steps - 1); // 线性插值 L 值OKLCH 下这就是感知均匀的 const l lRange[0] (lRange[1] - lRange[0]) * t; // 饱和度衰减曲线浅色阶高 L调低饱和度避免荧光感 // 用 ease-out 曲线让饱和度平滑过渡 const chromaCurve mode light ? Math.pow(t, 0.7) // light mode深色步调饱和度更快下降 : Math.pow(1 - t, 0.5); // dark mode浅色步调降低饱和度 const c baseOklch.c * chromaCurve; const h baseOklch.h; // OKLCH → HEX 输出 const hex formatHex({ mode: oklch, l, c, h }); palette.push(hex); } return palette; } // 第三步语义角色分配 // 自动将色阶映射到语义 Token并校验 WCAG AA 对比度 function assignSemanticTokens( lightPalette: string[], darkPalette: string[], neutralLight: string[], // 中性灰阶亮色模式 neutralDark: string[] // 中性灰阶暗色模式 ): { light: ColorToken[]; dark: ColorToken[] } { const lightTokens: ColorToken[] []; const darkTokens: ColorToken[] []; // 亮色模式语义映射表 // 索引 0 最浅50索引 12 最深950 const lightMapping: [string, SemanticRole, number][] [ [color-bg-primary, bg-primary, 0], // 极浅色背景 ≈ white [color-bg-secondary, bg-secondary, 9], // 浅灰背景用中性灰阶 [color-bg-tertiary, bg-tertiary, 8], [color-text-primary, text-primary, 11], // 最深色文字保证 ≥ 7:1 对比度 [color-text-secondary, text-secondary, 9], [color-text-tertiary, text-tertiary, 7], [color-border-default, border-default, 3], [color-border-strong, border-strong, 5], [color-accent-primary, accent-primary, 5], // 主色中等深度 [color-accent-hover, accent-hover, 6], // hover 稍深 [color-accent-active, accent-active, 7], // active 更深 ]; for (const [name, role, idx] of lightMapping) { const isNeutral role.startsWith(bg) idx 8; const palette isNeutral ? neutralLight : lightPalette; const value palette[Math.min(idx, palette.length - 1)]; // 背景和文字 Token 需验证对比度 if (role.startsWith(text)) { const bgValue lightPalette[0]; // 假设文字在白背景上 const ratio wcagContrast(value, bgValue); lightTokens.push({ name, value, role, contrastRatio: ratio }); } else { lightTokens.push({ name, value, role }); } } // 暗色模式语义映射表对称逻辑 const darkMapping: [string, SemanticRole, number][] [ [color-bg-primary, bg-primary, 1], // 极暗背景 [color-bg-secondary, bg-secondary, 8], // 稍浅背景用中性暗灰阶 [color-bg-tertiary, bg-tertiary, 7], [color-text-primary, text-primary, 11], // 亮色调文字 [color-text-secondary, text-secondary, 9], [color-border-default, border-default, 4], [color-accent-primary, accent-primary, 7], // 暗色下主色需提亮 [color-accent-hover, accent-hover, 8], // hover 更亮 ]; for (const [name, role, idx] of darkMapping) { const isNeutral role.startsWith(bg) idx 1; const palette isNeutral ? neutralDark : darkPalette; const value palette[Math.min(idx, palette.length - 1)]; darkTokens.push({ name, value, role }); } return { light: lightTokens, dark: darkTokens }; } // 第四步输出 Design Token JSONCSS 变量格式 function tokensToCSSVariables( tokens: ColorToken[], selector: string :root ): string { const lines: string[] [${selector} {]; for (const token of tokens) { lines.push( --${token.name}: ${token.value};); } lines.push(}); return lines.join(\n); } // 使用示例 // const baseOklch hexToOklch(#7c3aed); // const lightPalette generatePalette(baseOklch, light); // const darkPalette generatePalette(baseOklch, dark); // const tokens assignSemanticTokens(lightPalette, darkPalette, neutralLight, neutralDark); // console.log(tokensToCSSVariables(tokens.light)); // console.log(tokensToCSSVariables(tokens.dark, [data-themedark])); export { hexToOklch, generatePalette, assignSemanticTokens, tokensToCSSVariables };框架集成搭配 Tailwind 的预设配置/** * Tailwind 预设将生成 Token 注入 Tailwind 主题配置 * 这样开发者既可以使用工具生成的 Token * 也可以继续用 Tailwind 的原生类名 */ import type { Config } from tailwindcss; function createColorPreset(lightTokens: ColorToken[], darkTokens: ColorToken[]): PartialConfig { const colors: Recordstring, string {}; for (const t of lightTokens) { // 将 --color-bg-primary 映射为 tailwind 的 bg-primary 类名 colors[t.name] var(--${t.name}); } return { theme: { extend: { colors, }, }, // dark mode 通过 CSS 变量层级覆盖不需要 Tailwind 的 class 策略 darkMode: [class, [data-themedark]], }; } export { createColorPreset };四、AI 生成色彩系统的三大局限性品牌色的feel无法量化。你给设计师#E53935Material Red 600设计师可能会说这个红的基调偏橙我希望它偏紫调用。AI 算法只能在给定主色的 LCH 空间中做插值它不理解偏紫意味着色相旋转 -15 度。这类主观调整仍需要人工介入——色彩系统的生成是 AI 人工协作不是 AI 替代。语义冲突的自动化处理能力为零。当一个色阶的color-text-primary在亮色模式下 WCAG AAA 达标7:1但在组件内部叠加在bg-secondary上时对比度骤降到 3.2:1——AI 不会主动发现这个问题。组件级 Token 的对比度往往是背景 Token — 文字 Token的配对约束问题目前需要手动编写校验规则。暗色模式的感知亮度翻转不是单纯的反转。在暗色模式下人眼对高饱和度亮色的疲劳度远高于低饱和度暗色。Material Design 3 的暗色模式策略是表面色的 L 值越低、饱和度也相应降低tonal 调色板方案。纯粹靠 L 值线性翻转生成的暗色 Token会让人感觉对比度够了但颜色很刺眼。这是 AI 生成色彩系统中最大的体验陷阱——数学指标达标不代表视觉舒适度达标。五、总结色彩 Token 体系的四层抽象原始色值 → 色阶 → 语义 Token → 组件 Token。OKLCH 的 L 通道保证了感知均匀的亮度变化是色阶生成的理想色彩空间。亮色模式的色阶 L 值范围 0.98→0.30暗色模式 0.08→0.70通过线性插值 饱和度衰减曲线生成。语义角色分配的本质是当前 Token 的对比度需求与色阶明度位置的映射。WCAG 对比度校验属于配对约束问题同一文字 Token 在不同背景 Token 上的对比度可能不达标。暗色模式下高饱和度亮色会加速视觉疲劳纯等距插值生成的暗色 Token 在体验上不够舒适。culori 库是目前 OKLCH 色彩空间运算的推荐选择TypeScript 原生tree-shakable。AI 的色彩生成能力瓶颈在主观审美——品牌色微调仍需要设计师的色感决策。输出 Token 时优先采用 CSS 变量var(--color-xxx)保证运行时主题切换能力。色彩系统生成的自动化程度可以到 80%剩余的 20% 是语义映射微调和暗色模式手感调校。