• vue水波纹指令


    需求

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

    思路

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

    代码实现1

    解题

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

    代码

    
    
    
    
    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代码我现在看的还是有点儿云里雾里,回头再看吧。

  • 相关阅读:
    Camtasia Studio2024最新版本正式更新上线!
    multipart
    构造函数和原型
    UE Lyda项目学习 一、基础移动
    JavaWeb之Servlet、拦截器、监听器及编程思想
    【网络奇遇记】那年我与计算机网络的浅相知
    webpack——模块化技术、常见的打包工具、面试题
    JSD-2204-异常处理-SpringJDBC事务管理-Day14
    什么是跨域?怎么解决跨域问题
    Nacos-Go-Sdk代码逻辑解析
  • 原文地址:https://blog.csdn.net/Feng_ye__/article/details/127111001