本文首發於掘金,未經許可嚴禁轉載
一、一個簡單案例瞭解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 程式碼如下:
<button class="ripple"> Click me! </button>
<script>
CSS.paintWorklet.addModule('ripple.js')
</script>
繪製圖形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 風格按鈕的主要思路!
歡迎下載原始碼進行體驗,點選跳轉。
好了,本文內容就這樣,感謝閱讀,歡迎分享。