用 CSS Houdini 打造像素风组件的边框魔法 —— 解构 pixel-ui 的 PixelBox

1,135 阅读5分钟

用 CSS Houdini 打造像素风组件的边框魔法 —— 解构 pixel-uiPixelBox

本文是继上一篇介绍像素风组件库 pixel-ui 的延续,这次我们深入聊聊其中的魔法工具 —— PixelBox 背后的 CSS Houdini 技术,看看是如何利用 paintWorklet 实现灵活可配置的像素风边框、圆角与阴影效果的。欢迎点赞、收藏、评论支持,给组件库来个 Star!⭐️


🔙 回顾一下:什么是 pixel-ui

QQ_1747449535102.png

@mmt817/pixel-ui 是一个采用像素风格美学打造的 Vue 3 组件库,核心理念是“复古 ✖️ 现代”,结合 NES 时代的像素图形与现代前端技术,包括:

  • 使用 Vue 3 + TypeScript 构建
  • 采用 UnoCSS 作为原子化 CSS 引擎
  • 利用 CSS Houdini 构建自定义图形渲染
  • 集成 Playground / Storybook / VitePress 演示和文档
  • 支持像素风 按钮 / 卡片 / 折叠面板 / 动画精灵 等组件

其中 PxCardPxButtonGroupPxCollapse 等组件都依赖一个核心能力:像素风的边框绘制 —— 这就是我们今天的主角 PixelBox 背后的技术实现。


🎨 什么是 CSS Houdini?

CSS Houdini 是浏览器暴露的一套底层 API,允许开发者 扩展 CSS 渲染机制。它的核心模块之一是:

paintWorklet:通过自定义 JavaScript 绘图函数,实现 CSS 背景绘制,支持响应 CSS 属性变化。

简单理解就是 JS in CSS, 在 CSS 代码中执行类似 canvas 绘制逻辑的 js 脚本

相比传统的 CSS 背景图片或伪元素绘制,Houdini 提供了:

  • 动态可控:可以通过 CSS 变量实时传值
  • 高性能:在浏览器的渲染管线中执行,无需 DOM 操作
  • 可组合性强:可以组合多个 Paint Worklet、继承边框等机制

📦 PixelBox 背后的 pixelbox.worklet.ts 做了什么?

这段 Worklet 脚本是整个 PxCard / PxButton 等组件视觉表现的核心,它负责绘制出那种 NES 风格的边框与阴影。

👇 下面我们分块分析这段核心逻辑:

1️⃣ 注册 Paint Worklet 和输入属性

registerPaint('pixel-box', class {
  static get inputProperties(): string[] {
    return [
      '--px-border',
      '--px-border-radius',
      '--px-bg-color',
      '--px-bg-shadow-border',
      // 更多……
    ];
  }
})

这部分定义了这个 Worklet 会监听哪些 CSS 属性。当这些属性发生变化时,paint(ctx, geom, props) 会被调用重新绘制。

属性设计非常灵活,比如:

  • --px-border: 控制边框像素宽度
  • --px-border-radius: 圆角半径
  • --px-bg-shadow-border: 像素阴影宽度
  • --px-bg-shadow-position: 像素阴影显示位置

2️⃣ 核心绘制逻辑 paint(ctx, size, props)

  paint(
    ctx: PaintRenderingContext2D,
    size: { width: number; height: number },
    props: StylePropertyMap
  ): void {
      const { width, height } = size
      const pbBorder = getInt(props, '--px-border') * 2
      let pbBorderRadius = getInt(props, '--px-border-radius')
      // ...其他属性的取值/预处理

      ctx.fillStyle = pbBackgroundColor
      const startY = pbBorder / 2
      const contentHeight = size.height - pbBorder

      // button 整体背景区域
      let startX
      let contentWidth
      if (buttonGroupFlag || buttonGroupLast) {
        startX = 0
        contentWidth = size.width - pbBorder / 2
      } else {
        startX = pbBorder / 2
        contentWidth = size.width - pbBorder
      }
      ctx.fillRect(startX, startY, contentWidth, contentHeight)


      // TODO: 侧边阴影/圆角侧边阴影/圆角边框/多余色块清理/边框绘制
}

在这个函数中,我们可以通过 JavaScript 绘图 API(Canvas)对每一个像素层进行精细控制,从而实现像素感极强的边框结构,还可以根据传入的属性决定阴影位置、按钮组合是否共享边框等。


🧪 实际应用效果

如下图中所示,我们的 PxCardPxButtonGroup 等组件都调用了:

background: paint(pixel-box);

通过设定对应 CSS 变量即可完全定制组件风格:

  --px-border: 3px;
  --px-border-t: 3px;
  --px-border-r: 3px;
  --px-border-b: 3px;
  --px-border-l: 3px;
  --px-border-radius: 0px;
  --px-border-color: var(--px-color-base);
  --px-bg-color: transparent;
  --px-bg-shadow-border: 3px;
  --px-bg-shadow-color: var(--px-button-bg-shadow-color);
  --px-bg-shadow-position: bottom-right;

QQ_1747448856024.png

这样每个组件的样式变成了动态渲染的画布,而不是死板的类名或背景图!


✨ 为什么选用 Paint Worklet 而不是别的方法?

方法灵活性动态能力性能适合像素风?
CSS 背景图❌ 固定图一般
Canvas 元素❌(脱离 DOM)
Paint Worklet✅✅✅✅(响应 CSS 变量)✅✅✅

CSS Houdini 的 paintWorklet 是实现像素边框最优雅 + 高效的方式之一!


📦 如何接入 pixelbox.worklet.ts

首先, 出于测试需要, 我在 xxx.worklet.ts文件中统一暴露注册函数并立即执行

export function registerPixelBox() {
  if (typeof registerPaint !== 'undefined') {
    registerPaint('pixelbox', PixelBox)
  }
}

registerPixelBox()

然后在对应 vue 组件处将 Worklet 注册逻辑封装为:

// CSS Houdini Paint Worklet
const paint = () => {
  try {
    if ('paintWorklet' in CSS) {
      ;(CSS as any).paintWorklet.addModule(workletURL)
    } else {
      debugWarn(
        COMP_NAME,
        'CSS Houdini Paint Worklet API is not supported in this browser.'
      )
    }
    // (CSS as any).paintWorklet.addModule(workletURL)
  } catch (error) {
    console.error('Error loading Paint Worklet:', error)
  }
}

onMounted(async () => {
  paint()
})

并在组件库打包时,将这段 Worklet 单独构建成浏览器能识别的 JS 文件。

这里注意, pixel-ui使用的是 *.worklet.ts 但是CSS.paintWorklet.addModule('pixelbox.worklet.js') 会在 独立作用域 中执行 worklet 文件,相当于 Web Worker 环境, 需要预先编译 ts 文件才能正常使用

开发者只需引入组件,即可自动加载绘制逻辑,无需额外配置。


在这期间, pixel-ui 调研过程中发现常规像素圆角的处理方式大致分为三种

770f967d3740384f0e227ef2c9c2f59e.png

最初仅支持圆角 radius 以斜向像素点形式呈现, 结果在 --px-border-radius 值偏大时会出现表现力较差的问题, 于是限定圆角形式如图三种, 并更新绘制逻辑, 例如 PxButton 由此引入新属性 chubby, 一个更圆的圆角??

QQ_1747449313375.png

不过当前绘制逻辑仍有缺陷, chubby 对各项属性要求, 适配性不高, 请合理使用

🔚 小结:未来计划 & 欢迎支持!

通过本文我们深入了解了 pixel-uiPixelBox 背后的原理 —— CSS Houdini 的 paintWorklet,它赋予了组件边框渲染的无限可能。未来我们还会支持:

  • ✅ 像素边框动画
  • ✅ 多层投影
  • ✅ 像素风边角图标装饰
  • ✅ 更多定制化像素风格背景(科幻/自然...)

🧱 项目地址: github.com/maomentai81…
🙌 欢迎大家来个 Star ⭐️,提提 Issue 🐛,一起来打造复古又现代的 UI 库!


📢 关注我,持续分享 Vue 3 + CSS Houdini 等前端高级玩法!

如果你对:

  • 像素风组件库
  • CSS Houdini / Canvas / Web Animation API
  • 自定义渲染系统 / 动画组件开发

感兴趣,欢迎关注我,评论交流,让我们把前端玩出花!🌸