侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

JS实现双指缩放

蜗牛
2022-09-09 / 0 评论 / 0 点赞 / 5 阅读 / 14591 字 / 正在检测是否收录...

摘要

随着移动端设备的普及,作为前端开发,难免会遇到图片双指放大的需求。触控设备可以直接使用手指进行交互,而且基本上都支持多点触控。所以,查阅了网上大佬的案例,编写一个vue指令,来完成双指放大图片的需求。

缩放原理

原理其实很简单,双指向外扩张表示放大,向内收缩表示缩小,缩放比例是通过计算双指当前的距离 / 双指上一次的距离获得的。详见下图:

6be502dca6429723b45a1.png

计算出缩放比例后再通过下面两种方式实现缩放。

  1. 通过transform进行缩放
  2. 通过修改宽高来实现缩放 主流的方法都是采用transform来实现,因为性能更好。本篇文章两种方式都会介绍,任你选择。不过在讲之前,还是要先搞懂两个数学公式以及PointerEvent指针事件。因为接下来会用到。如果对PointerEvent指针事件不太熟悉的小伙伴,也可以看看这篇文章js PointerEvent指针事件简单介绍。

两点间距离公式

设两个点A、B以及坐标分别为A(x1, y1)、B(x2, y2),则A和B两点之间的距离为:

\left | AB \right | =\sqrt{(x1-x2)^2+(y1-y2)^2}

/**
 * 获取两点间距离
 * @param {object} a 第一个点坐标
 * @param {object} b 第二个点坐标
 * @returns
 */
function getDistance(a, b) {
    const x = a.x - b.x;
    const y = a.y - b.y;
    return Math.hypot(x, y); // Math.sqrt(x * x + y * y);
	}

中点坐标公式

\left \{ x,y \right \} = \left \{ \frac{x1+x2}{2}, \frac{y1+y2}{2} \right \}

/**
 * 获取中点坐标
 * @param {object} a 第一个点坐标
 * @param {object} b 第二个点坐标
 * @returns
 */
function getCenter(a, b) {
    const x = (a.x + b.x) / 2;
    const y = (a.y + b.y) / 2;
    return { x: x, y: y };
}

获取图片缩放尺寸

/**
 * 获取图片缩放尺寸
 * @param {number} naturalWidth 
 * @param {number} naturalHeight 
 * @param {number} maxWidth 
 * @param {number} maxHeight 
 * @returns 
 */
function getImgSize (naturalWidth, naturalHeight, maxWidth, maxHeight) {
  const imgRatio = naturalWidth / naturalHeight;
  const maxRatio = maxWidth / maxHeight;
  let width, height;
  // 如果图片实际宽高比例 >= 显示宽高比例
  if (imgRatio >= maxRatio) {
    if (naturalWidth > maxWidth) {
      width = maxWidth;
      height = maxWidth / naturalWidth * naturalHeight;
    } else {
      width = naturalWidth;
      height = naturalHeight;
    }
  } else {
    if (naturalHeight > maxHeight) {
      width = maxHeight / naturalHeight * naturalWidth;
      height = maxHeight;
    } else {
      width = naturalWidth;
      height = naturalHeight;
    }
  }
  return { width: width, height: height }
}

双指缩放逻辑

Vue.directive("doubleswiper", {
  bind: (el, binding) => {
    // 全局变量
    let isPointerdown = false, // 按下标识
      pointers = [], // 触摸点数组
      point1 = { x: 0, y: 0 }, // 第一个点坐标
      point2 = { x: 0, y: 0 }, // 第二个点坐标
      diff = { x: 0, y: 0 }, // 相对于上一次pointermove移动差值
      lastPointermove = { x: 0, y: 0 }, // 用于计算diff
      lastPoint1 = { x: 0, y: 0 }, // 上一次第一个触摸点坐标
      lastPoint2 = { x: 0, y: 0 }, // 上一次第二个触摸点坐标
      lastCenter; // 上一次中心点坐标

    let result, // 图片缩放宽高
      x, // x轴偏移量
      y, // y轴偏移量
      scale = 1, // 缩放比例
      maxScale,
      minScale = 1; // 边界恢复偏移量
    let leftX = new Map(), rightX = new Map();// 左边的边界偏移,右边的边界偏移
    let lastX, lastY, lastScale;
    // 由于图片是异步加载,需要在load方法里获取naturalWidth,naturalHeight
    el.addEventListener('load', function () {
      result = getImgSize(el.naturalWidth, el.naturalHeight, window.innerWidth, window.innerHeight);
      maxScale = Math.max(Math.round(el.naturalWidth / result.width), 3);
      // 图片宽高
      el.style.width = result.width + 'px';
      el.style.height = result.height + 'px';
      // 垂直水平居中显示
      x = (window.innerWidth - result.width) * 0.5;
      y = (window.innerHeight - result.height) * 0.5;
      lastX = x;
      lastY = y;
      lastScale = scale
      el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
    });

    // 图片赋值需放在load回调之后,因为图片缓存后读取很快,有可能不执行load回调
    el.src = `${el.src}?time=${new Date().getTime()}`;
    // 绑定 pointerdown
    el.addEventListener('pointerdown', function (e) {
      pointers.push(e);
      point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
      if (pointers.length === 1) {
        isPointerdown = true;
        el.setPointerCapture(e.pointerId);
        lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY };
      } else if (pointers.length === 2) {
        point2 = { x: pointers[1].clientX, y: pointers[1].clientY };
        lastPoint2 = { x: pointers[1].clientX, y: pointers[1].clientY };
        lastCenter = getCenter(point1, point2);
      }
      lastPoint1 = { x: pointers[0].clientX, y: pointers[0].clientY };
    });

    // 绑定 pointermove
    el.addEventListener('pointermove', function (e) {
      if (isPointerdown) {
        handlePointers(e, 'update');
        const current1 = { x: pointers[0].clientX, y: pointers[0].clientY };
        if (pointers.length === 1) {
          lastX = x;
          lastY = y;
          // 单指拖动查看图片
          diff.x = current1.x - lastPointermove.x;
          diff.y = current1.y - lastPointermove.y;
          lastPointermove = { x: current1.x, y: current1.y };
          x += diff.x;
          y += diff.y;
          realDistance(el, e)
          // el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
        } else if (pointers.length === 2) {
          lastX = x;
          lastY = y;
          lastScale = scale;

          const current2 = { x: pointers[1].clientX, y: pointers[1].clientY };
          // 计算相对于上一次移动距离比例 ratio > 1放大,ratio < 1缩小
          let ratio = getDistance(current1, current2) / getDistance(lastPoint1, lastPoint2);
          // 缩放比例
          const _scale = scale * ratio;
          if (_scale > maxScale) {
            scale = maxScale;
            ratio = maxScale / scale;
          } else if (_scale < minScale) {
            scale = minScale;
            ratio = minScale / scale;
          } else {
            scale = _scale;
          }
          // 计算当前双指中心点坐标
          const center = getCenter(current1, current2);
          // 计算图片中心偏移量,默认transform-origin: 50% 50%
          // 如果transform-origin: 30% 40%,那origin.x = (ratio - 1) * result.width * 0.3
          // origin.y = (ratio - 1) * result.height * 0.4
          // 如果通过修改宽高或使用transform缩放,但将transform-origin设置为左上角时。
          // 可以不用计算origin,因为(ratio - 1) * result.width * 0 = 0
          const origin = {
            x: (ratio - 1) * result.width * 0.5,
            y: (ratio - 1) * result.height * 0.5
          };

          // 计算偏移量,认真思考一下为什么要这样计算(带入特定的值计算一下)
          x -= (ratio - 1) * (center.x - x) - origin.x - (center.x - lastCenter.x);
          y -= (ratio - 1) * (center.y - y) - origin.y - (center.y - lastCenter.y);
          realDistance(el, e)
          // el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
          lastCenter = { x: center.x, y: center.y };
          lastPoint1 = { x: current1.x, y: current1.y };
          lastPoint2 = { x: current2.x, y: current2.y };
        }
      }
      e.preventDefault();
    });

    function getBoundsByElement (el) {
      const thumbAreaRect = el.getBoundingClientRect();
      return {
        x: thumbAreaRect.left,
        y: thumbAreaRect.top,
        w: thumbAreaRect.width
      };
    }
    /**
     * @description: 判断手势是滑动
     * @param {*} type  1 左移、 2 右移、3 上移、4 下移、
     * @return {*}
     * @Date: 2022-11-03 11:11:31
     * @Author: David
     */
    function distinguishSide (type) {
      let moveDistanceX = Math.abs(pointers[0].clientX - point1.x);
      let moveDistanceY = Math.abs(pointers[0].clientY - point1.y);
      let distance = Math.max(moveDistanceX, moveDistanceY)
      if ((moveDistanceX > moveDistanceY) && type > 2) {
        return
      } else if ((moveDistanceX < moveDistanceY) && type <= 2) {
        return
      }
      if (distance >= 100) {
        binding.value(type)
      }

    }
    function realDistance (el, e) {
      let { transX, transY, multiple } = getTransform(el)
      let { x: actualX, y: actualY } = getBoundsByElement(el)
      let scaleSize = { width: result.width * multiple, height: result.height * multiple }
      let parentWidth = el.offsetParent.clientWidth;
      let parentHeight = el.offsetParent.clientHeight;


      if (scaleSize.width <= parentWidth) {
        if ((actualX < 0) && (diff.x < 0)) {
          // 左移动要超出左边框
          x = lastX
          distinguishSide(1)
        } else if ((actualX > (parentWidth - scaleSize.width)) && (diff.x > 0)) {
          // 右移且超出右边框
          x = lastX
          distinguishSide(2)
        }
      } else {
        if ((actualX > 0) && (diff.x > 0)) {
          // 放大之后向右边移动,左边的到达边界
          x = lastX
          distinguishSide(2)
        } else if (((scaleSize.width + actualX) < parentWidth) && (diff.x < 0)) {
          // 放大之后向左边移动,右边边的到达边界
          x = lastX
          distinguishSide(1)
        }
      }

      if (scaleSize.height <= parentHeight) {
        if ((actualY < 0) && (diff.y < 0)) {
          // 上移要超出上边框
          y = lastY
          distinguishSide(3)
        } else if ((actualY > (parentHeight - scaleSize.height)) && (diff.y > 0)) {
          // 下移且超出下边框
          y = lastY
          distinguishSide(4)
        }
      } else {
        if ((actualY > 0) && (diff.y > 0)) {
          // 放大之后向下边移动,上边的到达边界
          y = lastY
          distinguishSide(4)
        } else if (((scaleSize.height + actualY) < parentHeight) && (diff.y < 0)) {
          // 放大之后向上边移动,下边的到达边界
          y = lastY
          distinguishSide(3)
        }
      }

      el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
    }


    function getTransform (DOM) {
      let arr = getComputedStyle(DOM).transform.split(',')
      return {
        transX: isNaN(+arr[arr.length - 2]) ? 0 : +arr[arr.length - 2], // 获取translateX
        transY: isNaN(+arr[arr.length - 1].split(')')[0]) ? 0 : +arr[arr.length - 1].split(')')[0], // 获取translateX
        multiple: +arr[3] // 获取图片缩放比例
      }
    }

    // 绑定 pointerup
    el.addEventListener('pointerup', function (e) {
      if (isPointerdown) {
        handlePointers(e, 'delete');
        if (pointers.length === 0) {
          isPointerdown = false;
        } else if (pointers.length === 1) {
          point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
          lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY };
        }
      }
    });

    // 绑定 pointercancel
    el.addEventListener('pointercancel', function (e) {
      if (isPointerdown) {
        isPointerdown = false;
        pointers.length = 0;
      }
    });

    el.addEventListener("wheel", function (e) {
      lastX = x;
      lastY = y;
      lastScale = scale
      let ratio = 1.1;
      // 缩小
      if (e.deltaY > 0) {
        ratio = 1 / 1.1;
      }
      // 限制缩放倍数
      const _scale = scale * ratio;
      if (_scale > maxScale) {
        ratio = maxScale / scale;
        scale = maxScale;
      } else if (_scale < minScale) {
        ratio = minScale / scale;
        scale = minScale;
      } else {
        scale = _scale;
      }
      // 目标元素是img说明鼠标在img上,以鼠标位置为缩放中心,否则默认以图片中心点为缩放中心
      if (e.target.tagName === 'IMG') {
        const origin = {
          x: (ratio - 1) * result.width * 0.5,
          y: (ratio - 1) * result.height * 0.5
        };
        // 计算偏移量
        x -= (ratio - 1) * (e.clientX - x) - origin.x;
        y -= (ratio - 1) * (e.clientY - y) - origin.y;
        lastCenter = { x: e.clientX, y: e.clientY };
      }
      // el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
      realDistance(el, e)
      e.preventDefault();
    })

    /**
     * 更新或删除指针
     * @param {PointerEvent} e
     * @param {string} type
     */
    function handlePointers (e, type) {
      for (let i = 0; i < pointers.length; i++) {
        if (pointers[i].pointerId === e.pointerId) {
          if (type === 'update') {
            pointers[i] = e;
          } else if (type === 'delete') {
            pointers.splice(i, 1);
          }
        }
      }
    }
  }
}
)

注册使用

在需要缩放的图片上使用v-doubleswiper就能完成图片的缩放和移动了。

注意事项

由于transform书写顺序并不满足交换律,换句话说transform: translateX(300px) scale(2);和transform: scale(2) translateX(300px);是不相等的。开发时请根据相应的书写顺序做处理。详见下图:

a37c583fa5cb64390179d.png

参考文章

https://juejin.cn/post/7020243158529212423#heading-6

0

评论区