本文首发于掘金,未经许可严禁转载
一、一个简单案例了解CSS Paint
例如,我们希望模拟实现一个谷歌material风格的波纹按钮。如下这样:
完整CSS代码及JS代码如下:
.ripple {
width: 100px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
border: none;
font-size: 16px;
border-radius: 6px;
background-color: rgb(64 179 255);
--ripple-x: 0;
--ripple-y: 0;
--animation-tick: 0;
}
/* 点击后增加的动画效果 */
.ripple.animating {
background-image: paint(ripple);
}
HTML 代码如下:
绘制图形JS需要以模块引入,CSS.paintWorklet.addModule
能让 web 引入 ripple.js 这个脚本,并另外开辟线程执行。它不会影响主线程,这是 worklet
的重要“卖点”!
ripple.js代码如下:
registerPaint('ripple', class {
paint(ctx, geom, properties) {
// 像写canvas一样绘制动画
}
});
以上就是 CSS Paint API 大概的使用方式,先总结下:
- CSS 中使用 paint 方法
- JS 添加绘制代码脚本
- ripple.js 中像写 Canvas 一样绘制图形
目前为止,得倒如下静态按钮:
二、如何实现动态效果
先来思考如何实现涟漪波纹状效果。
先来思考动画如何实现,相信大家都看过动画片,其实际就是多张静态图连贯在一起组成。
当静态图越多,其动画效果越流畅。
那么我们将水波纹动态效果可以拆解一下:
- 以某点为圆形画圆(带 background 的圆)
- 圆的半径逐渐变大,直至消失出按钮边界
简单画下,横轴为时间线,随着时间圆慢慢变大。
那么如何连贯成动画呢,上面说静态图片越多越好,那在计算机上这样一个动画要多少张静态图最为合适呢?有人会说了,那直接使用定时器SetInterval
,不断的画圆,同时直径慢慢变大。当半径大到一定程度的时候return
执行不就行了嘛?
这是一种思路,但 SetInterval
是 macro-task(宏任务),和 SetTimeout
一样,cb 执行时间会受到线程其它任务的影响,动画效果并不理想。
一说到定时器,可能有人想到 requestAnimationFrame
了,没错,比起 setTimeout、setInterval 它有两点优势:
- requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
- 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
上代码:
document.querySelector('button').addEventListener('click', evt => {
requestAnimationFrame(function raf(now) {
// 1. 不断刷新
// 2. 不断画圆
// 3. 满足某条件,return 出去!停止画圆
requestAnimationFrame(raf);
})
})
当我们点击按钮的时候,requestAnimationFrame
会被执行 16.7ms/次,通过不断改变圆形大小,来实现视觉上的涟漪动画效果。requestAnimationFrame
中我们做三件事:
- 不断刷新
- 不断画圆
- 满足某条件,return 出去!停止画圆
那么如何不断刷新我们解决了,再来思考如何不断画圆?
这里我们可以在点击的时候为按钮添加一个动画的 class
,为按钮添加一个 background-image
!如果你看过上一篇文章,那一看到 background-image
应该会立马想到 Houdini 中的 CSS Paint API
。它可以动态改变 CSS 变量,那么直接上代码:
registerPaint('ripple', class {
static get inputProperties() {
return ['--animation-tick', '--ripple-x', '--ripple-y'];
}
// Canvas 画圆
paint(ctx, geom, properties) {
// 点击事件的坐标,作为画圆的圆形
const x = parseFloat(properties.get('--ripple-x').toString());
const y = parseFloat(properties.get('--ripple-y').toString());
// 当前倒计时剩余时间
let tick = parseFloat(properties.get('--animation-tick').toString());
// 倒计时在1秒内,超出,Canvas 画圆动作结束
if(tick < 0) tick = 0;
if(tick > 1000) tick = 1000;
ctx.fillStyle = '#ddd'; // 圆形背景颜色
ctx.globalAlpha = 0.5; // 背景透明度
// 画圆
ctx.arc(
x, y, // 圆心坐标
geom.width * tick/1000, // 半径
0, // 起始角
2 * Math.PI, // 结束角
);
ctx.fill();
}
});
总结:
- 【不断刷新】:requestAnimationFrame
- 【不断画圆】:requestAnimationFrame + CSS Paint API
- 【满足某条件,return 出去!停止画圆】:超出1s,画圆动作停止
以上三点就是基于 CSS Houdini 实现一个 material 风格按钮的主要思路!
欢迎下载源码进行体验,点击跳转。
好了,本文内容就这样,感谢阅读,欢迎分享。