导读:本期聚焦于小伙伴创作的《CSS Paint API是什么?如何用代码在CSS里动态绘制图像》,敬请观看详情。浏览器原生能力里,有一项常被忽略的技术叫CSS Paint API,它属于Houdini体系,允许开发者用JavaScript定义绘制逻辑,再像普通背景图一样在CSS中调用。传统CSS渐变和图片难以实现随参数变化的复杂纹理,而Paint API通过注册paint worklet,能把函数式绘图直接接入background-image等属性。使用时需在JS里调用registerPaint编写paint方法,CSS中通过paint(worklet名称)引用。它运行在独立线程,不阻塞主线程,适合做占位图、动态边框和噪声背景。要注意该特性目前主要在Chromium内核浏览器稳定支持,编写时需处理高清屏devicePixelRatio,并考虑不支持时的降级背景。

CSS Paint API是Houdini开放给开发者的底层接口之一,它让JavaScript能够参与CSS属性的图像生成过程。过去我们想要在元素的背景上画点阵、波浪或者随机纹理,要么切图,要么用复杂的CSS渐变堆叠,维护成本很高。Paint API的思路是直接提供一个paint worklet,你在里面用Canvas2D的绘制指令生成图像,然后CSS就能像使用url()图片一样使用它。

CSS Paint API是什么?如何用代码在CSS里动态绘制图像

一、CSS Paint API的基础概念

CSS Paint API的核心是两个端:一端是JavaScript里的worklet文件,用来定义绘制逻辑;另一端是CSS属性,通过paint()函数消费这个逻辑。worklet和普通的JS脚本不同,它运行在渲染线程的孤立环境中,不能直接操作DOM,也不能用window上的很多API,但可以用传入的CanvasRenderingContext2D来画图。

这种架构带来的好处是绘制过程和主线程解耦。比如列表里有上百个项都用同一个动态噪声背景,主线程的滚动和交互不会被绘图卡住。同时因为绘制参数可以从CSS自定义属性传进去,所以同一套worklet能根据--color、--size等变量画出不同效果,比写死图片灵活得多。

1.1 registerPaint的结构

在worklet脚本里,我们通过registerPaint注册一个绘制类。这个类必须实现paint方法,接收ctx、size和props三个参数。ctx就是画布上下文,size给出当前绘制区域的宽高,props是CSS自定义属性的集合。下面是一个最简单的例子,画一个实心方块背景:

// paint-solid.js
registerPaint('solid-box', class {
  static get inputProperties() {
    return ['--box-color'];
  }
  paint(ctx, size, props) {
    const color = props.get('--box-color') || 'red';
    ctx.fillStyle = color;
    ctx.fillRect(0, 0, size.width, size.height);
  }
});

上面代码里inputProperties声明了依赖的CSS变量,只有这些变量变化时才重新绘制。如果不声明,则绘制一次后缓存。这一点在写复杂动画背景时要特别注意,声明少了不会更新,声明多了又可能频繁重绘。

1.2 在页面中加载worklet

worklet文件不能通过普通script标签引入,必须用CSS.paintWorklet.addModule异步加载。由于网络或浏览器不支持可能失败,需要包裹在特性判断里。下面是在主页面注册并使用的典型写法:

if ('paintWorklet' in CSS) {
  CSS.paintWorklet.addModule('paint-solid.js').catch(function(e) {
    console.log('加载worklet失败', e);
  });
}

加载成功后,CSS里就可以用paint(solid-box)作为图像值。如果浏览器不支持Paint API,这行声明会被忽略,此时需要提前写好降级背景,比如纯色或渐变,避免元素透明。

二、在CSS中调用绘制结果

调用方式非常直观,把paint()放在background-image或border-image等接受图像的属性里。配合自定义属性,还能在运行时改变图像。以下样式让元素背景使用上面注册的solid-box,并通过变量控制颜色:

.my-element {
  --box-color: #3366ff;
  background-image: paint(solid-box);
  /* 不支持时的降级 */
  background-color: #3366ff;
}

这里background-color作为降级,当paint()无效时用户依然能看到蓝色块。如果去掉降级,旧版Safari上元素背景就会消失。实际项目中建议所有paint背景都带一层传统背景兜底。

2.1 传递动态参数

除了颜色,也可以传尺寸、间距等数值。worklet里通过props.get拿到的是CSSStyleValue,通常要用toString或parseFloat处理。比如画棋盘格,可以传--grid-size控制格子宽高:

registerPaint('checker', class {
  static get inputProperties() {
    return ['--grid-size', '--grid-color'];
  }
  paint(ctx, size, props) {
    const grid = parseFloat(props.get('--grid-size')) || 20;
    const color = props.get('--grid-color').toString() || '#000';
    ctx.fillStyle = color;
    for (let y = 0; y < size.height; y += grid * 2) {
      for (let x = 0; x < size.width; x += grid * 2) {
        ctx.fillRect(x, y, grid, grid);
        ctx.fillRect(x + grid, y + grid, grid, grid);
      }
    }
  }
});

上面代码用两层循环交错填充,形成国际象棋盘效果。注意循环里比较都用size.width而不是写死,这样元素尺寸变化时会自动铺满。如果在CSS里改--grid-size,浏览器会触发重新绘制,不需要手动调JS。

2.2 高清屏适配

默认情况下worklet的size是以CSS像素给出的,但在devicePixelRatio大于1的屏幕上直接按这个尺寸画会发虚。Paint API没有提供自动缩放,需要自己在paint里用ctx.scale或者按ratio乘宽高。简单做法是读取window.devicePixelRatio,不过worklet里不能直接访问window,可以通过CSS变量把ratio传进去:

:root {
  --dpr: 1;
}
@media (min-resolution: 2dppx) {
  :root { --dpr: 2; }
}

然后在worklet里取出--dpr并放大绘制坐标。虽然多一步,但能保证视网膜屏下边框和纹理清晰。这也是很多教程容易漏掉的点,导致开发者在手机上看到模糊背景。

三、适用场景与限制

CSS Paint API适合做那些用纯CSS表达太啰嗦、用图片又不灵活的场景。例如 loading占位骨架屏的脉冲波纹、随内容长度变化的虚线边框、以及根据主题变量生成的噪点遮罩。它把绘制逻辑收敛到一个文件,比内联SVG好维护。

但必须清醒看到兼容性现实:Chrome、Edge等Chromium系支持良好,Firefox需要手动开启实验特性,Safari长期未实装。所以如果面向公开Web,只能把它当作渐进增强。内部后台系统或Electron客户端则可以放心主打。

3.1 与SVG和Canvas的区别

有人会问,既然都能画图,为什么不用SVG或离屏Canvas。区别在于SVG是声明式且DOM化,不适合按CSS变量实时重绘大量实例;离屏Canvas要自己写JS把图贴到每个元素,绕开了CSS层叠。Paint API恰好填补中间空白:用JS画,但由CSS引擎调度和缓存。

方案是否随CSS变量更新是否主线程绘图兼容性
CSS Paint API否(worklet线程)Chromium为主
内联SVG部分广泛
JS离屏Canvas手动广泛

从表里能看出,Paint API在自动响应样式变化和线程隔离上有独特优势,但代价是兼容面窄。选型时如果目标用户清一色Chrome,那它几乎是最优解。

3.2 调试技巧

调试worklet不能像普通JS那样随便console,因为运行环境隔离。建议在paint里把关键参数通过props回写到另一个自定义属性,或者在支持的工作里用console.log,Chromium会在控制台单独显示worklet上下文日志。另外可以给元素加outline观察绘制区域是否和size吻合,避免图像被拉伸。

当绘制没出现时,先确认addModule路径正确且返回resolved,再检查CSS里paint名称是否和registerPaint字符串一致。大小写不符是常见低级错误。确认无误后,用DevTools的Rendering面板勾选Paint flashing,能看到背景重绘时机是否符合预期。

四、一个完整可运行示例

下面把前面知识点拼成一个小例子:页面加载worklet,画随变量变色的圆点阵列,并带降级背景。你可以把这当作模板改。

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<style>
  .dots {
    --dot-color: #ff6600;
    --dot-gap: 24;
    width: 300px;
    height: 200px;
    background-color: #eee;
    background-image: paint(dot-grid);
  }
</style>
</head>
<body>
<div class="dots"></div>
<script>
if ('paintWorklet' in CSS) {
  CSS.paintWorklet.addModule('dot-grid.js');
}
</script>
</body>
</html>

对应的dot-grid.js内容如下,它读取间隙和颜色画一排排圆点:

registerPaint('dot-grid', class {
  static get inputProperties() {
    return ['--dot-color', '--dot-gap'];
  }
  paint(ctx, size, props) {
    const color = props.get('--dot-color').toString() || '#000';
    const gap = parseFloat(props.get('--dot-gap')) || 20;
    ctx.fillStyle = color;
    for (let y = gap / 2; y < size.height; y += gap) {
      for (let x = gap / 2; x < size.width; x += gap) {
        ctx.beginPath();
        ctx.arc(x, y, gap / 4, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }
});

把两个文件放同目录用本地服务器打开,就能看到橙色圆点背景。改CSS里的--dot-gap,圆点疏密会立刻变化。若用不支持的浏览器打开,元素是浅灰底,也不至于空白。

总体来看,CSS Paint API把绘图权交回开发者手里,又保持和CSS变量体系的亲缘关系。理解worklet隔离性、inputProperties声明和降级策略,就能在合适项目中用它替代笨重的图片资源,让界面更轻和可定制。

CSS_Paint_APIworkletHoudini修改时间:2026-08-08 09:27:44

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。