Image placeholder

使用 SVG 创建 Material Design 涟漪纹波效果

Image placeholder
F2EX 2017-07-31

Google Material Design 是一种视觉设计语言,旨在为跨平台和设备创建统一的体验。在本教程中,我们将向您展示如何构建 Material Design 规范的“径向动作”下具体描述的涟漪效果,并将其与 SVG 和 GreenSock 的功能相结合。

创建 SVG

您不需要使用像 Adobe Illustrator 或 Sketch 一样花哨的应用程序来创作这种效果。SVG 的标记可以使用几个 XML 标签来编写。

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewBox="0 0 100 100"></symbol>
</svg>

symbol 元素接受诸如 viewBoxpreserveAspectRatio 之类的属性,它们在使用元素定义的矩形视图中提供缩放比例。简单地说,我们定义了初始的 x 和 y 轴坐标值(0,0),最终定义了 SVG 画布的宽度和高度(100,100)。

接下来我们需要添加一个形状,为动画添加涟漪效果。插入一个圆形元素。

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewBox="0 0 100 100">
    <circle />
  </symbol>
</svg>

circle 需要更多的属性才能在 SVG 的 viewBox 中正确显示。

<circle cx="1" cy="1" r="1"/>

cxcy 属性是相对于 SVG 的 viewBox 的坐标位置; 在我们的例子中,为了使点击显得更自然,我们需要确保在接收到输入时,直接在用户手指下方触发点击。

该图的中间示例的属性是创建一个半径为 1px 的 2px x 2px 的圆。这将确保我们的圆不会像上图中底部示例看到的那样裁剪。

<div style="height: 0; width: 0; position: absolute; visibility: hidden;" aria-hidden="true">
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false">
    <symbol id="ripply-scott" viewBox="0 0 100 100">
      <circle id="ripple-shape" cx="1" cy="1" r="1"/>
    </symbol>
  </svg>
</div>

最后,我们将包含一个 div ,以简洁地隐藏 sprite 。这样可以防止在渲染时占用页面中的空间。

创建标记

让我们创建一个用来实现涟漪效果的标记。为了利用先前创建的 symbol 元素,我们需要一种方法来引用它,方法是使用按钮的 SVG 中的 use 元素来引用 symbol 的 ID 属性值。

<button>
  Click for Ripple
  <svg>
    <use xlink:href="#ripply-scott"></use>
  </svg>
</button>

最终的标记具有 CSS 和 JavaScript 的附加属性。以 “js-” 开头的属性值表示仅存在于 JavaScript 中的值,如果删除它们将阻碍交互,但不会影响样式。这有助于区分 CSS 选择器与 JavaScript 钩子,以避免在将来需要删除或更新时混淆彼此。

<button id="js-ripple-btn" class="button styl-material">
  Click for Ripple
  <svg class="ripple-obj" id="js-ripple">
    <use width="100" height="100" xlink:href="#ripply-scott" class="js-ripple"></use>
  </svg>
</button>

use 元素必须有一个宽度和高度定义,否则它将不可见。你也可以在 CSS 中定义它,如果你决定将样式直接写在元素本身上(行内样式)。

样式

.ripple-obj {
  height: 100%;
  pointer-events: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 0;
  fill: #0c7cd5;
}

.ripple-obj use {
  opacity: 0;
}

使用 JavaScript 添加效果

我们将使用 GreenSock 的 TweenMax 库,因为它是使用 JavaScript 对对象进行动画处理的最佳库之一; 特别是当涉及 SVG 动画跨浏览器的兼容性问题时。

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script>
<script src="js/ripple.js"></script>

我们将使用所谓的模块模式,因为它有助于隐藏和保护全局命名空间。

var ripplyScott = (function() {}
  return {
    init: function() {}
  };
})();

我们将抓取一些元素并将它们存储在变量中; 特别是 use 元素,它在 button 中包含 svg 。整个动画逻辑将驻留在 rippleAnimation 函数中。此函数将接受动画时间和事件信息的参数。

var ripplyScott = (function() {}
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {…}
})();

我们需要定义一些变量:

var ripplyScott = (function() {}
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();  //创建动画序列的时间轴实例以及所有时间轴在TweenMax中实例化的方式。
        x            = event.offsetX,  //事件的偏移量是一个只读属性,它将鼠标指针的偏移量报告给目标节点的填充边。事件偏移量从左到右计算
        y            = event.offsetY,  //事件偏移量从上到下计算。(上面的 x 和当前的 y 都是从 0 开始计算)
        w            = event.target.offsetWidth,  //返回按钮的宽度。最终计算将包括元素边框和填充的大小。我们需要这个值来知道我们的元素有多大,因此我们可以将涟漪传播到最远的边缘。
        h            = event.target.offsetHeight,  //返回按钮的高度。
        offsetX      = Math.abs( (w / 2) - x ),  //通过将宽度除以一半找到中心,然后减去由我们的x坐标检测到的报告值。(见图2)
        offsetY      = Math.abs( (h / 2) - y ),  //通过将高度除以一半找到中心,然后减去由我们的y坐标检测到的报告值。
        deltaX       = (w / 2) + offsetX,  //Delta计算我们点击的整个距离,而不是距离中心的距离。从左到右检测点击。(见图3)
        deltaY       = (h / 2) + offsetY,  //从右到左
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));  //Math.pow()返回第一个参数的平方;将这两个值想加结果的平方根这是我们需要的涟漪比例。 
  }
})();

图 2

图 3

在 TweenMax 中使用 fromTo 方法,传递目标涟漪形状,并设置包含整个运动序列方向的对象文字。假如我们想要在中心形成动画,SVG 需要将变形原点设置为中间位置。由于我们希望在此之后进行动画处理,因此,缩放也需要调整到最小的位置,设置不透明度为1。因为我们在 CSS 中设置了不透明度为0的 use 元素,所以我们要从 1 返回到 0。 最后一部分是返回我们的时间轴实例。

var ripplyScott = (function() {
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) - x ),
        offsetY      = Math.abs( (h / 2) - y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

    tl.fromTo(ripple, timing, {
      x: x,
      y: y,
      transformOrigin: '50% 50%',
      scale: 0,
      opacity: 1,
      ease: Linear.easeIn
    },{
      scale: scale_ratio,
      opacity: 0
    });

    return tl;
  }
})();

返回的这个对象将通过将事件侦听器附加到所需的目标来控制涟漪,并调用 rippleAnimation

var ripplyScott = (function() {
  var circle = document.getElementById('js-ripple'),
      ripple = document.querySelectorAll('.js-ripple');

  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) - x ),
        offsetY      = Math.abs( (h / 2) - y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

    tl.fromTo(ripple, timing, {
      x: x,
      y: y,
      transformOrigin: '50% 50%',
      scale: 0,
      opacity: 1,
      ease: Linear.easeIn
    },{
      scale: scale_ratio,
      opacity: 0
    });

    return tl;
  }

  return {
    init: function(target, timing) {
      var button = document.getElementById(target);

      button.addEventListener('click', function(event) {
        rippleAnimation.call(this, event, timing);
      });
    }
  };
})();

最后,调用按钮!

ripplyScott.init('js-ripple-btn', 0.75);

2017-08-07