CSS Houdini:实现一个Material风格的按钮

一、一个简单案例了解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 风格按钮的主要思路!

欢迎下载源码进行体验,点击跳转

好了,本文内容就这样,感谢阅读,欢迎分享。

Guess you like

Origin juejin.im/post/7047029996845531149