CSS Paint API

发布于 大漠

CSS Paint API是W3C规范中之一,目前的版本是Level1。它也被称为CSS Custom Paint或者Houdini's Paint Worklet。对于开发者而言,有一个值得高兴的是,Chrome65将会支持该API。也就是说,可以使用CSS Paint API提供的registerPaint(name, paintCtor)做一些事情。

那么CSS Paint API是什么?你能用它做什么?它又是如何工作的呢?带着一系列的为什么,我们开启对CSS Paint API的初探。

什么是CSS Paint

先来了解CSS Paint是什么?在理解这个概念之前,我们先来回忆一下,我们在平时写CSS时是如何给一个元素设置背景图片。了解CSS的同学,都应该知道,采用background-image属性,比如:

background-image: url(xxx.jpg)

或者:

background-image: linear-gradient(to bottom, red, green)

除了给background-image指定一个图片之外,还可以是渐变(CSS中的渐变相当于一张背景图片)。而我们要了解的CSS Paint则是通过JavaScript的方式,让你在CSS中能够引入用JavaScript编写的图形。感觉有点类似于HTML5中的canvas,对吗?如果你继续往后看,你会越加有这样的感觉。

写一个Paint Worklet

先定义一个叫作myPainter的Paint Worklet,接下来使用CSS.paintWorklet.addModule('my-paint-worklet.js')来加载已定义好的CSS Paint Worklet。在my-paint-worklet.js文件中,使用registerPaint函数来注册一个Paint Woklet的类:

class MyPainter {
    paint(ctx, geometry, properties) {
        // ...
    }
}

registerPaint('myPainter', MyPainter)

paint()回调中,我们可以使用<canvas>CanvasRenderingContext2Dctx方法。如果你熟悉<canvas>,那么你就可以知道怎么绘制Paint Worklet。geometry用来指定画布的widthheightproperties可以获取自定义元素属性,这么说有点抽象,后面会介绍到该属性。

**特别声明:**CSS Paint中的Paint Worklet的ctx<canvas>中的ctx并不是百分之百的相同。到目前为止,文本渲染的方法是无法使用的,这主要是出于安全原因,你无法从画布上读取像素。

来看一个简单的示例。先创建一个index.html文件:

<!doctype html>
<html>
    <head>
        <style>
            body {
                width: 100vw;
                height: 100vh;
                background-image: paint(checkerboard);
            }
        </style>
        <script>
            CSS.paintWorklet.addModule('checkerboard.js')
        </script>
    </head>
    <body>
    </body>
</html>

然后创建checkerboard.js,并在这个文件中添加下面的代码:

class CheckerboardPainter {
    paint(ctx, geom, properties) {
        const colors = ['red', 'green', 'blue'];
        const size = 32;
        for(let y = 0; y < geom.height/size; y++) {
            for(let x = 0; x < geom.width/size; x++) {
                const color = colors[(x + y) % colors.length];
                ctx.beginPath();
                ctx.fillStyle = color;
                ctx.rect(x * size, y * size, size, size);
                ctx.fill();
            }
        }
    }
}

registerPaint('checkerboard', CheckerboardPainter);

到时将会看到的效果如下所示:

上面的示例,先定义了一个叫CheckboardPainter的Paint Worklet,并且将之注册为checkboard,然后通过CSS.paintWorklet.addModule()的方法加载这个Paint Worklet。最后在CSS中,使用paint(checkboard)给指定的元素添加背景图。其最终效果正如上图所示。

可能你会纳闷,这样的效果使用background-image就能实现(不管是调用图片,还是使用linear-gradient都可以实现类似效果)。那他们两者之间有何区别吗?其实两者是有所区别的:

CSS Paint与background-image的差别就是background-image是根据代码计算出来的,不会随着元素的大小变化而伸缩。而CSS Paint绘制的图像总是会和元素容器所需保持一样的大。也就是说,让你修改元素大小可视区域时,CSS Paint绘制的图像会重新绘制。言外之意,背景图像总是和它所需要的一样大,包括对高密度(Hight-density)显示器的补偿。

是不是很酷。

个性化Paint Worklet

前面提到过,定义一个Paint Worklet时paint()方法接受三个参数,其中第三个参数properties可以让Paint Worklet可以访问其他CSS属性,这就是properties参数的强大之处。通过一个静态的类,比如inputProperties属性,你可以对任何CSS属性,包括自定义属性进行更改。这些值将通过properties参数提供给你。

比如下面这个例子。同样先创建一个index.html文件,在文件中加入下面的代码:

<!doctype html>
<html>
    <head>
        <style>
            body {
                width: 100vw;
                height: 100vh;
                --checkerboard-spacing: 10;
                --checkerboard-size: 32;
                background-image: paint(checkerboard);
            }
        </style>  
        <script>
            CSS.paintWorklet.addModule('checkerboard.js')
        </script>  
    </head>
    <body>
    </body>
</html>

然后在创建的checkerboard.js文件中加入下面的代码:

class CheckerboardPainter {
    static get inputProperties() {
        return [
            '--checkerboard-spacing',
            '--checkerboard-size'
        ]
    }

    paint(ctx, geom, properties) {
        const size = parseInt(properties.get('--checkerboard-size').toString());
        const spacing = parseInt(properties.get('--checkerboard-spacing').toString());
        const colors = ['red', 'green', 'blue'];

        for(let y = 0; y < geom.height/size; y++) {
            for(let x = 0; x < geom.width/size; x++) {
                ctx.fillStyle = colors[(x + y) % colors.length];
                ctx.beginPath();
                ctx.rect(x*(size + spacing), y*(size + spacing), size, size);
                ctx.fill();
            }
        }
    }
}

registerPaint('checkerboard', CheckerboardPainter);

现在我们可以使用相同的代码来处理所有不同类型的格子效果。但更爽的是,现在可以通过开发者调试工具,在找到正确的外观之前,对这些值进行修改。

CheckerboardPainter 类中,给静态属性 inputProperties 定义了两个属性:--checkerboard-spacing--checkerboard-size,这两个属性可以像一般的 CSS 的属性一样使用于 HTML 元素,而这两个属性的值将被 paint() 的第三个参数 properties 获得,被用于生产图像。所以,如果修改 body--checkerboard-spacing 或者 --checkerboard-size 属性,背景图将会发生改变,正如上面的录屏展示的效果一样。

**注意:**如果把颜色也参数化是不是很有意?spec允许paint()函数获取参数列表。这个特性还没有在Chrome中实现,因为它目前还严重依赖于Houdini的属性和值的API,让其能正常工作,还需要一些时间。

不支持Paint Worklet的浏览器

到目前为止,只有Chrome浏览器支持Paint Worklet(Chrome 65可以看到效果)。尽管其他浏览器都发出响应将会支持CSS Paint API的特性,但到目前依旧没有啥进展。为了跟上时代的发展,Houdini支持吗?与此同时,就算浏览器还不支持CSS Paint的Paint Worklet,我们也要确保使用渐进增加来保证你的代码能正常运行。为了确保这一点,你必须在CSS和JavaScript中调整你的代码。

如果你从未接触过Houdini,甚至说都不知道他是什么?建议你点击这里对Houdini进行一些简单的了解。另外这里有一个CSS Houdini的示例仓库,这些示例将向你展示Houdini的神奇之处。

回到正题上来吧,对于不支持的浏览器,可以通过检查CSS对象,实现对JavaScript中Paint Worklet支持情况做一个检测:

if ('paintWorklet' in CSS) {
    CSS.paintWorklet.addModule('mystuff.js');
}

而在CSS通过@supports来做相应的检测:

@supports (background: paint(id)) {
    body {
        background-image: paint(checkerboard);
    }
}

另外,众所周知,如果浏览器遇到一个未知的属性,则会忽略此属性声明的规则。如果你对同一个属性进行两次声明,前者是CSS的属性,其后紧跟Paint Worklet,你就可以对不支持CSS Paint的浏览器做降级处理。比如下面这样:

body {
    background-image: linear-gradient(0, red, blue);
    background-image: paint(myGradient, red, blue);
}

这样一来,支持CSS Paint的浏览器,第二个background-image将会覆盖第一个。在不支持CSS Paint的浏览器,第二个属性规则将会示为无效,而第一个background-image将会起作用。

示例

在众多CSS Paint的示例中,其中一些比其他的示列更易理解。其中一个比较易于理解的示例就是使用CSS Paint来减少对DOM元素。通常,元素的添加纯粹是为了使用CSS创建修饰。例如,在Material Design Lite中的button的Ripple效果。为了实现这个效果,添加了两个额外的<span>元素。如果你的Web页面有很多这样的按钮效果,那么你就要增加很多个DOM元素,因此可能影响你的页面性能。如果使用CSS Paint来实现Ripple效果,你不需要添加任何额外的DOM元素。此外,你还拥有更易于自定义的属性。

使用CSS Paint的另一个好处是,在大多数的情况下,能解决很多CSS无法解决的事情,而且代码量也少。当然,其中也有一个取舍问题。当画布的大小或任何参数发生变化时,绘图的代码将会运行。因此,如果你的代码很复杂,那需要很长的时间,它可能会引入jank。Chrome正在处理主要线程上的Paint Worklet,因此即使运行时间长,Paint Worklet也不会影响主线程的响应能力。

对于我来说,最令人兴奋的前景是,CSS Paint可以很快的为CSS新特性创建Polyfill。比如conic-gradient的Polyfill。另外的一个例子,在CSS会议中,它决定你现在可以有多个边框颜色。在这个会议还在进行之时,@Kilpatrick就通过CSS Paint为其写了一个对应的Polyfill

不规则盒子的思考

大多数人在学习CSS Paint时都从background-imageborder-image着手。其实另一个值得我们思考的是,如果使用 CSS Paint的mask-image可以让DOM元素具有任意形状。比如钻石形状

写在最后

CSS Paint在Chrome Canary中已有一段时间。值得庆幸的是,在Chrome 65中将会默认启用CSS Paint。对于开发者而言,我们应该不断去尝试新的可能性,CSS Paint将会让我们打开更多的思路,为你的灵感提供更多于创作的空间。可以看看@Vincent De Oliveira收藏的一些案例。或许在这些案例中,你也能产生一些新的灵感。

**特别声明:**此篇文章内容来源于@Surma的《CSS Paint API》一文。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/css/css-paint-api.htmlNike Magista Obra