CSS Paint API给CSS的扩展带来了曙光

发布于 大漠

2020年底@Una KravetsCDS(Chrome Developer Summit 2020)分享了使用Houdini扩展CSS的话题,同时推出了 Houdini.how。让我们可以使用CSS Houdini的Paint API进行创作,并通过 CSS Paint Polyfill 让不支持CSS Houdini的Paint API的现代浏览器也能运行Paint API扩展的CSS能力。你或许和我一样,对此感到好奇是吧?如果是的话,请继续往下阅读。

什么是Houdini?

在开始了解CSS Paint API和Houdini.how之前,让我们先了解一下CSS Houdini。

MDN是这样描述Houdini的

Houdini是一组底层API,它们公开了CSS引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。

在没有Houdini之前,我们仅限于通过JavaScript和CSSOM(CSS对象模型)的某些API进行交互,而有了Houdini之后,可以使用JavaScript方式来扩展CSS。

Houdini通过 Typed Object Model 实现了更多语义化的CSS,开发者可以通过 Properties and Value API 来定义高级CSS自定义属性,包括语法(syntax)、默认值和继承。

Houdini API在这里与CSS解析器、CSSOM、级联、布局、绘画和合层阶段一起工作:

主要有两组Houdini API:CSS Properties and Value APIWorklets。而Worklets涵盖了渲染状态的访问,比如 布局(Layout)绘制(Paint)合层(Composiite);而Properties and Value API侧重于解析器扩展(Parser)CSSOM级联(Cascade)

为了更好的理解上图,我们拆分成下面三个部分来理解,CSS Houdini出现前后CSS的工作。

CSS如何工作?

简单地说,CSS的工作如下:

  • 浏览器加载Web页面,并接收HTML
  • 浏览器将HTML转换为DOM,并存储在计算机的内存中
  • 浏览器获取CSS等资源
  • 浏览器对获取的CSS进行解析。根据它找到的选择器,将CSS规则应用于DOM中的对应节点上
  • 创建一个渲染树
  • 在屏幕上显示

渲染管道

渲染管道是一个承载Web基本结构的过程。其包括:

  • 将HTML解析为DOM
  • 创建渲染树(Render Tree)
  • 渲染树的显示(布局)
  • 绘制渲染树

在这个过程中有一个问题,即 如何在现有的渲染管道流程中应用一个钩子(Hooks)来修改常规流程?而这些Hooks就是CSS Houdini的API

简单地说,我们在 浏览器加载CSS到解析CSS过程中引入Hooks(即 CSS Houdini相关的API)

虽然这样做,在技术上有了很多进步,但用户代理(比如浏览器)还是面临具大的挑战,比如说不能直接对默认的选择框进行主题化。如果要对它进行主题化,就需要基本的CSS代码和相应的Polyfill(用JavaScript编写的Polyfill)。这又引出另一个问题?

JavaScript编写的Polyfill和CSS Houdini的区别?

如果要使用CSS属性来美化元素,就需要使用JavaScript来编写相对应的Polyfill。它的工作原理:

浏览器会通过这个解析器来设置和读取DOM和CSSOM的级联(Cascade)、布局(Layout)、绘制(Paint)和合成(Composite)过程,然后使用JavaScript编写的Polyfill重新绘制一遍,再重新将Polyfill绘制的样式运用到元素上:

而CSS Houdini则不是这样。通过CSS Houdini,我们可以钩住CSS解析过程,并使用我们定义属性(自定义属性)应用样式:

引入CSS Houdini前后,浏览器渲染处理的一个流程对比就像下图这样:

CSS Houdini自定义属性和Worlets

了解了CSS Houdini是什么?以及它的一个基本工作过程之后,我们还需要理解CSS Houdini自定义属性和Worlets两个部分。不过在这里不会详细介绍他们。

CSS Houdini自定义属性

CSS Houdini自定义属性指的是CSS Properties and Value API。它和CSS原生的自定义属性非常的相似,同时它也常被称为CSS Houdini变量。

CSS Houdini自定义属性注册主要有两种方式,一种是使用 @property ,另外一种是使用 CSS.registerProperty()

对于注册好的自定义使用,也是使用var()函数来调用:

@property --box-shadow-blur { 
    syntax: "<length>"; 
    inherits: false; 
    initial-value: 0; 
} 

.el { 
    --box-shadow-blur: 3px; 
    box-shadow: 0 3px var(--box-shadow-blur) #000; 
    transition: --box-shadow-blur 0.45s; 
} 

.el:hover { 
    --box-shadow-blur: 10px; 
}

有关于CSS Houdini的自定义属性更详细的介绍可以阅读:

理解CSS Houdini的Worklets

CSS Houdini的Worklets是其另一个API,是渲染引擎的一种扩展。从概念上讲,它与 Web Worker 类似,但有几个关键点例外:

  • 它可以并行,每个Worklet必须有两个或更多的实例,任何一个实例都可以在调用时运行
  • 仅限于不访问全局作用域的项目(除了Worklet的函数名)
  • 扩展的渲染引擎会在需要的时候调用

CSS Houdini的Worklet也是JavaScript模块,通过调用Worklet的addModule函数来添加,它也是一个Promise。

简单地说,CSS Houdini Worklets是脱离主线程运行的浏览器指令,可以在需要时调用。Worklets使你能够使用JavaScript编写一个模块化的CSS来完成特定的任务,并且骂人要一行JavaScript来导入和注册。就像是为CSS样式服务的Service Workers一样,CSS Houdini Worklets被注册到你的应用程序中,一旦注册,就可以在你的CSS中通过名字来使用。

CSS Houdini Worklets使用过程很简单:

  • 使用CSS.paintWorklet.addModule(workletURL)注册一个Worklet
  • 使用background: paint(confetti)调用已注册的Worklet

@snugug为CSS Houdini Worklets生命周期绘制了一个图

有关于CSS Houdini Worklets更多的介绍可以阅读:

CSS Paint Worklet

CSS Paint Worklet(PaintWorklet就是CSS Houdini Worklet中一种。它允许你创建自定义的CSS函数,用JavaScript绘制图像作为背景。然后将这个函数用于任何CSS图像的属性中,比如background-imageborder-imagemask-imagelist-style-image等。但更让人兴奋的是,开发者(CSSer)还可以使用PaintWorklet中(CSS Painting API)注册好的CSS自定义属性来控制JavaScript绘制好的图像。

对于JavaScript绘制图像,就像是使用部分Canvas的API绘制一样。

Houdini.how

Houdini.how是一个Web网站,它由@Una Kravets创建,也可以称作为CSS Houdini的库(Library)。它提供了CSS Houdini的Worklets、资源和相关参考资料。换句话说,它提供了解CSS Houdini的一切信息,比如浏览器兼容性、各处API的概术、使用信息、资源以及Paint API构建的Demo演示。具体的使用可以点击这里查阅

CSS Paint API

有了上面的基础之后,我们开始开启CSS Paint API之旅。通过下面的学习,你可以掌握如何使用CSS Houdini的Paint API来构建一些特殊的CSS功能。在开始之前,我们先简单地了解一下为什么要使用CSS Paint API?

为什么要使用CSS Paint API

通过前面的介绍,我们或多或少知道通过CSS Houdini,可以编写浏览器能够理解的代码,并能给CSSer提供最简单的使用方式实现更为复杂,甚至是一些特殊的效果。而且使用CSS Houdini有其自身的优势:

  • 实现复杂样式,并且解析时间更快
  • 开发人员不再需要等待浏览器支持就可以使用一些CSS特性
  • 比Polyfill的性能更强,页面渲染速度会更快
  • 更好的分离逻辑和样式(样式依旧在CSS中,逻辑在JavaScript中)
  • 更多定制化的样式和设计系统

使用CSS Houdini,我们可以使用JavaScript编写CSS(比如使用CSS Paint API实现的一些现有CSS无法实现的效果),然后在CSS中通过paint()调用已注册的效果(JavaScript编写好的效果)。其过程大致如下:

如何创建一个自定义CSS Paint

创建一个Paint,会有下面三个基本过程:

  • 使用class声明一个Paint
  • 使用registerPaint()注册一个Paint
  • 使用CSS.paintWorklet.addModule()加载Paint Worklet

先写一个最简单的效果。实现这个效果先要创建两个文件,比如paint.htmlpaint-worklet.js。先来看paint.html:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>CSS Houdini || CSS Paint API</title>
        <script>
            // 加载Paint Worklet,即 paint-worklet.js
            if('paintWorklet' in CSS) {
                CSS.paintWorklet.addModule('./paint-worklet.js')
            } else {
                document.querySelector('html').innerText = '你的浏览器暂时不支持CSS Houdini的Paint API'
            }
        </script>
        <style>
            .circles {
                width: 300px;
                height: 300px;
                border: 1px solid #000;
                /* 使用panit()将注册的Paint引入 */
                background: paint(paint-name);
            }
        </style>
    </head>
    <body>
        <div class="circles"></div>
    </body>
</html>

在HTML文件中做了两个事情:

  • 使用CSS.paintWorklet.addModule('./paint-worklet.js')来加载Paint Worlet
  • 在CSS使用paint()使用已注册好的Paint

接着在已创建的paint-worklet.js添加下面的代码:

class PaintName {
    static get inputProperties() {
        return [
            // 在这里设置控制图形的CSS自定义属性
        ]
    }

    static get inputArguments() {
        return [
            // 设置的一些参数
        ]
    }

    paint(ctx, geom, props, args) {
        // ctx => Canvas 2D API的一个子集
        // geom => 要绘制的区域的宽度和高度
        // props => 输入属性的计算值
        // args => 输入参数
        console.log('paint render')
        console.log('geom===>',geom)
        console.log('props===>',props)
        console.log('args===>',args)
        
        ctx.fillStyle = 'green';
        ctx.fillRect(10, 10, 150, 100);
    }
}

registerPaint('paint-name', PaintName)

这个JavaScript中的代码很简单:

  • 使用class声明了一个名叫PaintName的Paint Worklet
  • 使用registerPaint()注册了一个名叫paint-name的Paint,将会运用到CSS中的paint()函数中
  • paint(){}使用Canvas 2D API的一个子集绘制所需要的东东,比如上面的代码绘制了一个“绿色的矩形”

你将看到的效果如下:

简单地解释一下。

首先我们使用class声明了一个CSS Paint。它具有一个paint方法的JavaScript类。在这个paint中可以设置一些参数,但这方面我们放到后面来探讨。这里只看它的基本实现:

class PaintName {

    paint(ctx, geom, props, args) {
        // paint的事情都在这个方法中完成
    }
}

之后,使用registerPaint()注册一个Paint,该方法接受两个参数registerPaint(name, paintCtro):

  • name是Paint Worlket的名称,这个参数将被CSS的paint()函数中引用,而且该参数是必填项,且全局是唯一的
  • paintCtor是一个class,比如上面示例中的paninName。这里使用class主要是因为类之间可以相互组合,比如继承;类可以执行一些预初始化的工作

paintCtro的类里有个paint()函数,它是渲染引擎在浏览器绘制阶段的回调。paint()函数接受ctxgeompropsargs四个参数:

  • ctx绘制的渲染上下文,即 PaintRenderingContext2D提供的API
  • geom绘制的图像大小,即 pageSize,它只有widthheight两个属性
  • props当前绘制元素的计算样式,它只包含 inputProperties里列出来的属性
  • args 它只包含inputArguments里列出来CSS类型

以下几种情况都会触发回调的调用:

  • 视口要显示绘制的元素,比如初始创建Paint类的实例对象时
  • 绘制区域的大小变了,比如视窗窗口大小改变
  • inputProperties列出的属性值变了,比如图像可根据参数的改变而改变
  • inputArguments列出的CSS类型,它是CSS Houdini的另一个API(CSS Typed OM),允许你使用内置的CSS引擎类型(比如,颜色、图像、长度等)。

接下来,就是使用CSS.paintWorklet.addModule()加载Paint Worklet。

CSS 的 paintWorklet 属性提供了与绘制相关的 Worklet,它的全局执行上下文是 PaintWorkletGlobalScopePaintWorkletGlobalScope 里存了 devicePixelRatio 属性,它和 Window.devicePixelRatio 一样。

CSS.paintWorklet.addModule('filename.js')负责加载定义了 Paint Worklet 的脚本文件。 Paint相关的事情都是在filename.js处理。

最后在CSS中通过paint()函数来引用已注册的Paint Worklet:

.circles {
    background: paint(paint-name);
}

paint()是 CSS 的 <image> 类型支持的一种写法。我们平时用url()加载图片或者用渐变函数linear-gradient()的地方都可以使用paint()。它可以用在 background-imageborder-imagelist-style-imagemask-imagecursor 等属性上。

paint()可以接 Paint Worklet 的名字,即在registerPaint(name, paintCtor)里提供的 name,比如示例中的paint-name;也可以接受paint(){}函数中inputArguments列出的值,比如paint(circles, 2, 10, 100%)

有一点需要注意,并不是在 CSS 里每调用一次piant()就执行一次 Paint Worklet 类的 paint 方法,而是当元素的大小改变时,或者 inputProperties(或inputArguments) 里声明的属性值改变时才会触发。

写一个真正的Paint

有了前面的基础,我们来实战一下Paint。接下来这个示例并不复杂,就是绘制几个圆作为背景。

开始吧:

class CirclePainter {
    paint(ctx, geom) {
        // 设置相邻两个圆之间的间距
        const offset = 10;

        // 设置画布大小
        const size = Math.min(geom.width, geom.height);

        // 设置圆半径
        const radius = size / 4 - offset;

        const point = radius + offset;

        // 绘制圆
        for (let i = 0; i < 2; i++) { 
            for (let j = 0; j < 2; j++) {
                // 设置圆的填充色
                ctx.fillStyle = `rgb(0, ${Math.floor(255 - 42.5 * i)}, ${Math.floor(255 - 42.5 * j)})`

                ctx.beginPath();

                // ctx.arc(圆心x坐标, 圆心y坐标, 圆半径,圆弧起点, 圆弧结束点)
                ctx.arc(point + (i * (point * 2)), point + (j * (point * 2)), radius, 0, 2 * Math.PI);

                ctx.fill()
            }
        }
    }
}

registerPaint('circle', CirclePainter);

上面示例使用class创建了一个CirclePainter的类,该类有一个paint(){}方法,给这个方法传了ctxgeom两个参数(其中ctx是指画布Canvas的上下文,gemo包含了画布的widthheight)。然后在画布中绘绘制了四个圆(水平方向i<2,垂直方向j<2),并给圆填充了相应的颜色(近似蓝色)。最后使用registerPaint()注册了一个名叫circle的Paint。

如果没有任何Canvas相关的基础,可以先花点时间阅读MDN上有关于Canvas相关的教程,如果仅仅是了解怎么使用Cavans中相关API绘制圆,可以阅读《Canvas学习:绘制圆和圆弧》一文,另外站上还有关于Canvas相关的教程,感兴趣的话可以点击这里阅读

接下来,需要加载这个Paint Worklet:

<script>
    if('paintWorklet' in CSS) {
        CSS.paintWorklet.addModule('./paint-worklet.js')
    } else {
        document.querySelector('html').innerText = '你的浏览器暂时不支持CSS Houdini的Paint API'
    }
</script>

为了使用这个Paint,需要创建一个DOM元素,比如我们创建一个带有类名为circlediv,同时给这个.circle添加一点基本样式,但其中paint(circle)是必不可少的:

<!-- HTML -->
<div class="circle"></div>

/* CSS */
.circle {
    width: 300px;
    height: 300px;
    border: 1px solid #000;
    background: paint(circle);
}

效果如下:

前面提到过了,Paint和别的元素渲染不一样,只要视窗大小改变,就会触发Paint,也就是说,即使我们没有在代码中添加任何有关于resize方面的监听事件,但浏览器在调整大小的时候会自动调用paint方法:

从上图的效果中我们还可以发现,虽然显式在.circle上显式设置了width的值为300px,但视窗足够小的时候,.circle的宽度也会随着变化,而且paint中绘制的圆在不需要任何resize的监听事件也会跟着变小。

在接着上面的示例继续往下走,添加几个CSS自定义属性来控制Paint绘制出来的效果:

  • --circle-offset:控制圆之间间距
  • --circle-count:控制圆的个数
  • --circle-opacity:控制圆的透明度

通过这些CSS自定义属性与paint()中的inputProperties结合,在CSS中可以使用这些自定义属性来控制Paint中绘制的图形。添加CSS自定义属性的CirclesPainter代码像下面这样:

class CirclePainter {
    // 设置CSS自定义属性
    static get inputProperties() {
        return [
            '--circle-offset',
            '--circle-count',
            '--circle-opacity'
        ]
    }

    paint(ctx, geom, props) {
        console.log('geom===>', geom);
        // 设置相邻两个圆之间的间距, 如果没有设置--circle-offset,其默认值为10
        const offset = parseInt(props.get('--circle-offset').toString(), 10) || 10;

        // 设置圆的数量,如果没有设置--circle-count,其默认值为 2
        const count = parseInt(props.get('--circle-count').toString(), 10) || 2;

        // 设置圆的透明度,如果没有设置--circle-opacity,其默认值为1
        const opacity = parseFloat(props.get('--circle-opacity').toString()) || 1;

        // 设置画布大小
        const size = Math.min(geom.width, geom.height);

        // 设置圆半径
        const radius = Math.max(Math.round(((size / count) - offset * 2) / 2), 10);

        const point = radius + offset;

        // 绘制圆
        for (let i = 0; i < count; i++) { 
            for (let j = 0; j < count; j++) {
                // 设置圆的填充色
                ctx.fillStyle = `rgba(0, ${Math.floor(255 - 42.5 * i)}, ${Math.floor(255 - 42.5 * j)}, ${opacity})`

                ctx.beginPath();

                ctx.arc(point + (i * (point * 2)), point + (j * (point * 2)), radius, 0, 2 * Math.PI);

                ctx.fill()
            }
        }
    }
}

registerPaint('circle', CirclePainter);

CirclePainter类中添加了inputPropertiesgetter,它返回我们想要的使用的CSS自定义属性数组;然后在paint()函数中通过propsget获取到设置的CSS自定义属性的值。另外在示例中,还给他们定义了相应的默认值,如果在CSS中未使用这些定义的CSS自定义属性就会使用这些默认值。因此,我们不修改任何CSS的情况之下,同样有效果:

我们可以在CSS中显式设置定义的CSS自定义属性:

.circle {
    --circle-offset: 15;
    --circle-count: 4;
    --circle-opacity:.8;
    width: 300px;
    height: 300px;
    border: 1px solid #000;
    background: paint(circle);
}

这个时候效果如下:

我们可以在这个示例基础上,添加CSS自定义属性的控制手柄:

<!-- HTML -->
<form>
    <div class="controle">
        <label for="offset">--circle-offset:</label>
        <input type="range" name="offset" id="offset" min="0" max="30" step="2" value="10" />
        <output for="offset" id="output-offset">10</output>
    </div>
    <div class="controle">
        <label for="count">--circle-count:</label>
        <input type="range" name="count" id="count" min="2" max="10" step="1" value="2" />
        <output for="count" id="output-count">2</output>
    </div>
    <div class="controle">
        <label for="opacity">--circle-opacity:</label>
        <input type="range" name="opacity" id="opacity" min="0.1" max="1" step="0.1" value="0.8" />
        <output for="opacity" id="output-opacity">0.8</output>
    </div>
</form>

input添加一些基本操作,通过JavaScript来修改CSS自定义属性的值:

<script>
    const circleElement = document.querySelector('.circle');
    const rangeHandler = document.querySelectorAll('input[type="range"]')
    const outputEles = document.querySelectorAll("output");

    rangeHandler.forEach((element, index) => {
        element.addEventListener('input', (evt) => {
            circleElement.style.setProperty(`--circle-${evt.target.id}`, evt.target.value);
            outputEles[index].innerText = evt.target.value;
        })
    })
</script>

拖动示例中的滑块,可以看到下图这样的效果:

这里我们使用的是CSS自定义属性,它对于值的设置是没有明确的类型确认,换句话说,它就是一个字符串。不过CSS Houdini中同样为我们提供了自定义属性的设置,它有@property(在CSS中定义CSS Houdini自定义属性)和JavaScript中使用CSS.registerProperty()来注册CSS Houdini自定义属性。为了省事,使用@property注册CSS Houdini自定义属性来替代CSS原生的CSS自定义属性:

@property --circle-offset {
    syntax: '<length>';
    initial-value: 15;
    inherits: false;
}

@property --circle-count {
    syntax: '<length>';
    initial-value: 4;
    inherits: false;
}

@property --circle-opacity {
    syntax: '<length-percentage>';
    initial-value: 0.8;
    inherits: false;
}

.circle {
    --circle-offset: 15;
    --circle-count: 4;
    --circle-opacity:.8;
    width: 300px;
    height: 300px;
    border: 1px solid #000;
    background: paint(circle);
}

在来看另一种方式,给paint()中传参数,即使用inputArgumentsinputArguments类似于inputProperties,可以订阅inputArguments列出的参数(它是一个CSS类型的数组),也就是说,可以用它来替代CSS变量(inputProperties)。

前面提到过,inputArgumentsCSS Typed OM的API(CSS Houdini中的另一个API)。 改造后的代码如下:

class CirclePainter {

    static get inputArguments() {
        return [
            '<number>',
            '<number>',
            '<percentage>'
        ]
    }

    paint(ctx, geom, props, args) {
        console.log('geom===>', geom);
        console.log('props===>', props);
        console.log('args====>', args);

        // 设置相邻两个圆之间的间距
        const offset = parseInt(args[0].toString(), 10);

        // 设置圆的数量
        const count = parseInt(args[1].toString(), 10);

        // 设置圆的透明度
        const opacity = parseInt(args[2].toString(), 10) / 100;

        // 设置画布大小
        const size = Math.min(geom.width, geom.height);

        // 设置圆半径
        const radius = Math.max(Math.round(((size / count) - offset * 2) / 2), 10);

        const point = radius + offset;

        // 绘制圆
        for (let i = 0; i < count; i++) { 
            for (let j = 0; j < count; j++) {
                // 设置圆的填充色
                ctx.fillStyle = `rgba(0, ${Math.floor(255 - 42.5 * i)}, ${Math.floor(255 - 42.5 * j)}, ${opacity})`

                ctx.beginPath();

                ctx.arc(point + (i * (point * 2)), point + (j * (point * 2)), radius, 0, 2 * Math.PI);

                ctx.fill()
            }
        }
    }
}

registerPaint('circle', CirclePainter);

上面的代码在CirclePainterinputArguments注入了三个参数,<number><number><percentage>。它们都是CSS Typed OM中的CSS类型,即<type name>。然后在paint方法中,我们得到了类似于JavaScript函数参数对象的args参数,但它只是一个包含CSSUnitValue对象的Array。每个单元对象都是由valueunit两个属性组成。

在CSS使用的时候,可以在paint()中传入定义的参数:

.circle {
    width: 300px;
    height: 300px;
    border: 1px solid #000;
    background: paint(circle, 2, 10, 100%);
}

效果如下:

同样可以调整paint()是的参数值,得到不同的效果:

使用CSS Paint API构建一个带有波浪的动效按钮

当你把鼠标移动到按钮上时,它会有一个水波的动效:

代码就不详细介绍了,按上面的过程,创建一个Paint Worklet:

registerPaint(
    "wave",
    class {
        static get inputProperties() {
            return ["--animation-tick", "--button-color"];
        }

        paint(ctx, geom, props) {
            let tick = Number(props.get("--animation-tick"));
            let bgcolor = String(props.get("--button-color"));

            const { width, height } = geom;

            const initY = height * 0.4;
            tick = tick * 2;

            ctx.beginPath();
            ctx.moveTo(0, initY + Math.sin(tick / 20) * 10);

            for (let i = 1; i <= width; i++) {
                ctx.lineTo(i, initY + Math.sin((i + tick) / 20) * 10);
            }

            ctx.lineTo(width, height);
            ctx.lineTo(0, height);
            ctx.lineTo(0, initY + Math.sin(tick / 20) * 10);
            ctx.closePath();

            ctx.fillStyle = `${bgcolor}`;
            ctx.fill();
        }
    }
);

.button上添加相应的样式:

.button {
    --button-color: rgba(29, 39, 129, 0.3);
    min-width: 200px;
    min-height: 40px;
    margin: auto;
    font-size: 2rem;
    border-radius: 10rem;
    color: #121212;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    transition: all 0.5s;
    box-shadow: 0 0 6px #eee;
    background-color: #008effc9;
    text-shadow: -1px -1px 1px rgba(255, 255, 255, 0.8);
    padding: 10px 30px;
    background-image: linear-gradient(
        to right,
        #fbc2eb 0%,
        #a6c1ee 51%,
        #fbc2eb 100%
    );
}
.button:hover {
    background-image: paint(wave);
    background-color: #008effc9;
    background-blend-mode: multiply;
    box-shadow: inset 0 0 6px #eee;
    color: #fff;
    text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
}

注意,这里是在.button:hover下使用的paint(wave)。而且其动效还需要借助JS来完成:

<div class="button">Hove Me!</div>

<script>
    if ("paintWorklet" in CSS) {
        CSS.paintWorklet.addModule("https://codepen.io/airen/pen/ZEpjebX.js");

        const wave = document.querySelector(".button");
        let tick = 0;
        let isRun = false;

        requestAnimationFrame(function raf(now) {
            if (isRun) {
                tick += 1;
                wave.style.setProperty('--animation-tick', tick);
            }
            requestAnimationFrame(raf);
        });

        const play = () => {
            isRun = true
        }

        const stop = () => {
            isRun = false
        }

        wave.addEventListener('mouseover', () => {
            play()
        })

        wave.addEventListener('mouseleave', () => {
            stop()
        })
    }
</script>

基于这个示例,我想你也能想到,有了CSS Paint API之后,可以把一些有意思,有创意的效果使用到一些交互上。比如这个按钮,甚至你也可以使用类似的方案,实现像下面这个效果:

感兴趣的同学,可以尝试一下!

小结

这篇文章介绍了如何使用CSS Houdini的Paint API来绘制一些特殊的图形以及一些有意的动效和创意,并且运用到一些实际场景中。如果浏览器对CSS Houdini支持度更好的话,对于CSSer来说有了一个新的挑战空间。当然,实现这些效果,还是需要对Canvas有一定的了解。

Houdini.how上面还有很多CSS Paint API的效果,我们也可以根据网站上提供的方法将自己的创意发到该网站上。下一次可以和大家一起探讨一下,如何将自己的创意或者Demo放到网站上。另外我们还可以根据网站上提供的案例,学习更多这方面的知识。如果你有好的创意,欢迎在下面的评论中与我们一起分享。

扩展阅读

如果你对这方面感兴趣的话,还可以阅读下面这几篇文章: