用CSS Houdini實現一個Material風格的按鈕

Allan91發表於2021-12-29
本文首發於掘金,未經許可嚴禁轉載

一、一個簡單案例瞭解CSS Paint

例如,我們希望模擬實現一個谷歌material風格的波紋按鈕。如下這樣:

1.gif

完整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 大概的使用方式,先總結下:

  1. CSS 中使用 paint 方法
  2. JS 新增繪製程式碼指令碼
  3. ripple.js 中像寫 Canvas 一樣繪製圖形

目前為止,得倒如下靜態按鈕:

image.png

二、如何實現動態效果

先來思考動畫如何實現,相信大家都看過動畫片,其實際就是多張靜態圖連貫在一起組成。
image.png
當靜態圖越多,其動畫效果越流暢。
image.png

那麼我們將水波紋動態效果可以拆解一下:

  1. 以某點為圓形畫圓(帶 background 的圓)
  2. 圓的半徑逐漸變大,直至消失出按鈕邊界
    簡單畫下,橫軸為時間線,隨著時間圓慢慢變大。
    image.png
    那麼如何連貫成動畫呢,上面說靜態圖片越多越好,那在計算機上這樣一個動畫要多少張靜態圖最為合適呢?有人會說了,那直接使用定時器 SetInterval,不斷的畫圓,同時直徑慢慢變大。當半徑大到一定程度的時候 return 執行不就行了嘛?

這是一種思路,但 SetInterval 是 macro-task(巨集任務),和 SetTimeout 一樣,cb 執行時間會受到執行緒其它任務的影響,動畫效果並不理想。

一說到定時器,可能有人想到 requestAnimationFrame 了,沒錯,比起 setTimeout、setInterval 它有兩點優勢:

  1. requestAnimationFrame 會把每一幀中的所有DOM操作集中起來,在一次重繪或迴流中就完成,並且重繪或迴流的時間間隔緊緊跟隨瀏覽器的重新整理頻率,一般來說,這個頻率為每秒60幀。
  2. 在隱藏或不可見的元素中,requestAnimationFrame將不會進行重繪或迴流,這當然就意味著更少的的cpu,gpu和記憶體使用量。

上程式碼:

document.querySelector('button').addEventListener('click', evt => {
    requestAnimationFrame(function raf(now) {
      // 1. 不斷重新整理
      // 2. 不斷畫圓
      // 3. 滿足某條件,return 出去!停止畫圓
      requestAnimationFrame(raf);
    })
  })

當我們點選按鈕的時候,requestAnimationFrame 會被執行 16.7ms/次,通過不斷改變圓形大小,來實現視覺上的漣漪動畫效果。requestAnimationFrame 中我們做三件事:

  1. 不斷重新整理
  2. 不斷畫圓
  3. 滿足某條件,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();
  }
});

總結:

  1. 【不斷重新整理】:requestAnimationFrame
  2. 【不斷畫圓】:requestAnimationFrame + CSS Paint API
  3. 【滿足某條件,return 出去!停止畫圓】:超出1s,畫圓動作停止

以上三點就是基於 CSS Houdini 實現一個 material 風格按鈕的主要思路!

歡迎下載原始碼進行體驗,點選跳轉

好了,本文內容就這樣,感謝閱讀,歡迎分享。

相關文章