β

CSS Paint API

W3CPlus 58 阅读

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> CanvasRenderingContext2D ctx 方法。如果你熟悉 <canvas> ,那么你就可以知道怎么绘制Paint Worklet。 geometry 用来指定画布的 width height properties 可以获取自定义元素属性,这么说有点抽象,后面会介绍到该属性。

特别声明: 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-image border-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.w3cplus.com/css/css-paint-api.html

作者:W3CPlus
原文地址:CSS Paint API, 感谢原作者分享。

发表评论