需求

在div或者button中,触发点击时,出现水波纹效果

思路

当鼠标点击时,触发从当前点向外层最远距离发散一个圆,圆的半径达到最远距离后消失。最远距离是从点击处到达矩形角的距离。
在这里插入图片描述

代码实现1

解题

当鼠标点击时,新建一个当前元素的内部span元素,此元素是绝对定位,初始宽高为0,整体是一个圆形。从点击的那一刻开始,宽高不断增加,而透明度不断减小,当此元素的半径大于最远距离时,此元素消失。

代码

<template>
  <div class="content">
    <div class="ripple-content" v-ripple="{color: color, duration: duration}"></div>
  </div>
</template>

<script>
export default {
  name: "ripple-page",
  data() {
    return {
      color: '#00ff00',
      duration: 700
    }
  }
}
</script>
export default {
  inserted: (el, binding) => {
    el.addEventListener('pointerdown', event => {
      // 设置外层元素相对定位且隐藏多余部分
      el.style.position = 'relative';
      el.style.overflow = 'hidden';

      const rect = el.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const y = event.clientY - rect.top;
      const rectHeight = rect.height;
      const rectWidth = rect.width;

      //在鼠标位置增加一个span标签,将此标签插入当前元素内部
      let span = document.createElement("span")
      span.style.position = "absolute"
      span.style.background = binding.value.color || '#5e7ce0'
      span.style.borderRadius = '50%'
      el.append(span)

      // 初始化元素的宽、高、透明度
      let width = 0;
      let height = 0;
      let opacity = 1;
      let diameter = getMaxRadius(x, y, rectWidth, rectHeight) * 2;

      // 通过定时器不断增大宽高,减小透明度
      let time = setInterval(() => {
        width += 5;
        height += 5;
        opacity -= 0.01;
        //判断超出最大值时,清除定时,并且删除span
        if (width < diameter) {
          span.style.width = width + 'px'
          span.style.height = height + 'px'
          span.style.opacity = opacity;
          span.style.left = x - span.offsetWidth / 2 + 'px'
          span.style.top = y - span.offsetHeight / 2 + 'px'
        } else {
          clearInterval(time)
          time = null;
          span.remove()
        }
      }, binding.value.duration / 100 || 5)
    })
  }
}

/**
 * 计算当前点到达角的距离
 * @param {Number} x1
 * @param {Number} y1
 * @param {Number} x2
 * @param {Number} y2
 * */
function getDistance(x1, y1, x2, y2) {
  const deltaX = x1 - x2;
  const deltaY = y1 - y2;
  return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}

/**
 * 计算当前最大的半径
 * @param {Number} x 点击处到外层元素左上角的横向距离
 * @param {Number} y 点击处到外层元素左上角的纵向距离
 * @param {Number} width 外层元素的宽度
 * @param {Number} height 外层元素的高度
 * */
function getMaxRadius(x, y, width, height) {
  let topLeft = getDistance(x, y, 0, 0);
  let topRight = getDistance(x, y, width, 0);
  let bottomLeft = getDistance(x, y, 0, height);
  let bottomRight = getDistance(x, y, width, height);
  let radius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
  return radius;
}

效果

在这里插入图片描述

问题

  1. 此种方法生成出来的水波纹显得比较生硬。
  2. 连续点击时,中心区域是一个实质性的点,显得不美观。
  3. 在最外层控制了用户自己的元素定位方式,可能出现其他问题。
  4. 通过定时器调整存在一定程度的性能浪费。

代码实现2

解题

此方法源自于 devui 框架方案,写法参考于此篇文章 《Ripple:这个广受好评的水波纹组件,你不打算了解下怎么实现的吗?》

ps: 原版使用的 ts ,虽然兼容 vue2 ,但是我没详细用,这里是大致模仿的效果。

在当前元素的内部复制一个样式相同的元素,同时创建一个水波纹元素插入此元素中,形成三级嵌套的DOM结构。设置水波纹元素的 translate 为 -50% -50% scale(0),让此元素存在但无法显示出来。点击外层元素时,将水波纹元素设置为translate:scale(1),通过 transform 设置缓慢显示出来,达到水波效果。

代码

export default {
  inserted: (el, binding) => {
    el.addEventListener('pointerdown', event => {
      let rect = el.getBoundingClientRect();
      let rectWidth = rect.width,
        rectHeight = rect.height,
        x = event.clientX - rect.left,
        y = event.clientY - rect.top;
      let radius = getMaxRadius(x, y, rectWidth, rectHeight);

      // 复制一个外层元素
      const computedStyles = window.getComputedStyle(el);
      const {
        borderTopLeftRadius,
        borderTopRightRadius,
        borderBottomLeftRadius,
        borderBottomRightRadius
      } = computedStyles;
      const rippleContainer = document.createElement('div');
      rippleContainer.style.top = '0';
      rippleContainer.style.left = '0';
      rippleContainer.style.width = '100%';
      rippleContainer.style.height = '100%';
      rippleContainer.style.position = 'absolute';
      rippleContainer.style.borderRadius =
        `${borderTopLeftRadius} ${borderTopRightRadius} ${borderBottomRightRadius} ${borderBottomLeftRadius}`;
      rippleContainer.style.overflow = 'hidden';
      rippleContainer.style.pointerEvents = 'none';

      // 创建一个内部水波纹元素
      const rippleElement = document.createElement('div');
      rippleElement.style.position = 'absolute';
      rippleElement.style.width = `${radius * 2}px`;
      rippleElement.style.height = `${radius * 2}px`;
      rippleElement.style.top = `${y}px`;
      rippleElement.style.left =`${x}px`;
      rippleElement.style.background = binding.value.color || '#5e7ce0';
      rippleElement.style.borderRadius = '50%';
      rippleElement.style.opacity = 0.1;
      rippleElement.style.transform = `translate(-50%,-50%) scale(0)`;
      rippleElement.style.transition = `transform ${binding.value.duration / 1000}s cubic-bezier(0, 0.5, 0.25, 1), opacity ${binding.value.duration / 1000}s cubic-bezier(0.0, 0, 0.2, 1)`;

      // 将元素组合插入最外层元素内
      rippleContainer.append(rippleElement);
      el.append(rippleContainer);

      setTimeout(()=>{
        rippleElement.style.transform = 'translate(-50%,-50%) scale(1)';
        rippleElement.style.opacity = 0.2;
        setTimeout(()=>{
          rippleElement.style.transition = 'opacity 120ms ease in out';
          rippleElement.style.opacity = '0';
          setTimeout(()=>{
            rippleContainer.remove();
          }, 120)
        }, 700)
      }, 0)

    })
  }
}

/**
 * 计算当前点到达角的距离
 * @param {Number} x1
 * @param {Number} y1
 * @param {Number} x2
 * @param {Number} y2
 * */
function getDistance(x1, y1, x2, y2) {
  const deltaX = x1 - x2;
  const deltaY = y1 - y2;
  return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}

/**
 * 计算当前最大的半径
 * @param {Number} x 点击处到外层元素左上角的横向距离
 * @param {Number} y 点击处到外层元素左上角的纵向距离
 * @param {Number} width 外层元素的宽度
 * @param {Number} height 外层元素的高度
 * */
function getMaxRadius(x, y, width, height) {
  let topLeft = getDistance(x, y, 0, 0);
  let topRight = getDistance(x, y, width, 0);
  let bottomLeft = getDistance(x, y, 0, height);
  let bottomRight = getDistance(x, y, width, height);
  let radius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
  return radius;
}

效果

在这里插入图片描述

问题

  1. 此方法需要创建多重DOM,效率不高
  2. 感觉改了贝塞尔曲线以后效果也并没有太好
  3. 使用了三重定时器,效率不高

总结

在绑定点击事件时,可以看到并未绑定 click 事件,而是绑定了 pointerdown 方法,原因是因为这个方法对于各种硬件设备的适配更好,可以有效响应鼠标点击,手指点击,触控笔等各类效果。

研究了大半天这个效果, devui 这个框架里面的 v-ripple 这个效果其实写的很好,但是ts代码我现在看的还是有点儿云里雾里,回头再看吧。

Logo

前往低代码交流专区

更多推荐