返回博客

Canvas 组件读 CSS 变量颜色,默认色全部不可见——正则解析 oklch 失效

给组件库接了一个粒子动画组件,传入显式 color="#6366f1" 能跑,但不传 color(走主题 token 默认值)时,粒子在浅色卡片上完全不可见。切到深色模式,又突然显示了。问题必然和颜色解析有关,但明明读到了 CSS 变量,为什么白屏?

现象

  • color="#6366f1"color="rgb(99,102,241)":粒子正常渲染
  • 不传 color,组件读取 --color-foreground token:粒子在浅色卡片上隐身
  • 在深色模式下"隐身"的粒子又出现了(因为深色背景白色粒子刚好可见)
  • 控制台无报错,getComputedStyle 拿到的值看起来也不是空字符串

根因

getComputedStyle(el).getPropertyValue("--color-foreground") 读到的值是:

oklch(0.2 0.01 270)

这是 Tailwind v4 / 现代设计系统默认的颜色格式。组件内部的 parseColorToRgb 是手写正则,只处理两种格式:

// 原来的实现
function parseColorToRgb(raw) {
  const str = raw.trim();
  if (str.startsWith('#')) {
    // hex 解析 ...
  }
  const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (m) return [+m[1], +m[2], +m[3]];
  return [255, 255, 255]; // 白色兜底 ← 问题在这
}

oklch(...) 走到末尾,命中 return [255, 255, 255]——白色粒子在浅色卡片上当然不可见。更诡异的是,某些版本里 token 读取函数在正则失败后还会二次 fallback 到 getComputedStyle(el).color,而 card 本身的 color 继承自父级,往往也是接近白色的值,等于错上加错。

坑:手写正则永远追不上 CSS 颜色格式的演进

#hex + rgb() 覆盖了过去十年 90% 的场景,但 Tailwind v4、shadcn/ui 新版 preset、以及各类现代设计系统已普遍切换到 oklch()color(display-p3 ...)hsl() 等格式。手写正则一旦匹配失败,静默走白色兜底不报错,问题排查极难——明明 getPropertyValue 拿到了值,canvas 却像没收到一样。

正解

不要手动解析 CSS 颜色字符串,让浏览器来做。把颜色字符串赋给一个 1×1 的离屏 canvas 的 fillStyle,再读回 sRGB 字节,浏览器负责处理所有颜色空间换算:

function parseColorToRgb(raw) {
  const str = raw.trim();
  if (!str) return [255, 255, 255];
 
  // 浏览器环境:交给浏览器解析任意 CSS 颜色格式
  if (typeof document !== "undefined") {
    try {
      const off = document.createElement("canvas");
      off.width = off.height = 1;
      const ctx = off.getContext("2d");
      if (ctx) {
        ctx.fillStyle = str;          // oklch / color() / hsl / hex / rgb 全支持
        ctx.fillRect(0, 0, 1, 1);
        const d = ctx.getImageData(0, 0, 1, 1).data;
        return [d[0], d[1], d[2]];
      }
    } catch {
      // SSR 或无 canvas 能力时 fall through
    }
  }
 
  // SSR 兜底:只保留原有 hex / rgb 正则
  // ...
}

同时,token 读取函数要直接返回原始 token 字符串(oklch(...)),不要提前用 startsWith('#') 过滤,也不要在正则失败时二次 fallback 到 computed color

// 错:预过滤导致 oklch 被丢弃
const raw = getComputedStyle(el).getPropertyValue("--color-foreground");
if (!raw.startsWith('#') && !raw.startsWith('rgb')) {
  return getComputedStyle(el).color; // ← 丢掉了有效的 oklch 值
}
 
// 对:把原始字符串直接交给 parseColorToRgb 处理
const raw = getComputedStyle(el).getPropertyValue("--color-foreground").trim();
return parseColorToRgb(raw);

颜色解析结果建议缓存在 ref 里,在 mount 和主题切换时重算一次,不要每帧都走离屏 canvas 路径。

正解:1×1 离屏 canvas 让浏览器解析任意颜色

把待解析的颜色字符串赋给 ctx.fillStyle,再 getImageData 读回 [r, g, b]。浏览器原生支持所有 CSS 颜色格式,oklch / display-p3 / hsl 全不在话下。SSR 时用 typeof document !== "undefined" 守卫,降级到旧正则即可。

一句话外卖

手写正则解析 CSS 颜色是在和规范赛跑,迟早输——把字符串塞进离屏 canvas 的 fillStyle,浏览器会替你做所有颜色空间换算。