图片及其定位点一起放大缩小、拖拽功能

2,420 阅读2分钟

业务需求

用户上传一张户型图,并且在图片描很多测试点。

  • 鼠标放大缩小、拖拽
  • 触屏手势放大缩小、拖拽
  • 测试点需要跟随图片放大缩小、移动

先看最终完成的效果

录制_2023_06_07_15_11_59_81.gif

基本页面结构

  <div class="popup">
    <img src="https://picsum.photos/800/600" alt="户型图">
    <div class="site-numbers">
      <div class="site-number" style="left: 100px; top: 100px;">A</div>
      <div class="site-number" style="left: 200px; top: 200px;">B</div>
      <div class="site-number" style="left: 300px; top: 300px;">C</div>
      <div class="site-number" style="left: 400px; top: 400px;">D</div>
    </div>
  </div>

这里比较简单,一个div 跟一堆的站点编号定位点


.popup {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 800px;
    height: 600px;
    background-color: #fff;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
    overflow: hidden;
  }

  .popup img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    cursor: move;
  }

  .popup .site-numbers {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none; // 重点
    transform-origin: top left; // 重点
  }

  .popup .site-number {
    position: absolute;
    width: 20px;
    height: 20px;
    line-height: 20px;
    text-align: center;
    background-color: #f00;
    color: #fff;
    border-radius: 50%;
    pointer-events: auto;  // 重点
    cursor: pointer;
  }

开始js表演

鼠标滑轮放大缩小

  const popup = document.querySelector('.popup');
  const img = popup.querySelector('img');
  const siteNumbers = popup.querySelector('.site-numbers');

  let scale = 1;

  // 缩放
  const handlePinch = (event) => {
    event.preventDefault();
    // 根据deltaY正负值决定放大还是缩小
    scale *= event.deltaY > 0 ? 0.9 : 1.1;
    // 设置放大倍数最大值最小值
    scale = Math.min(Math.max(0.5, scale), 2);
    
    // 使用css对元素直接放大缩小
    img.style.transform = `scale(${scale})`;
    siteNumbers.style.transform = `scale(${scale}`;
  
  };
  
  // 注册事件
  img.addEventListener('wheel', handlePinch);

鼠标拖拽

  // 拖拽
  const popup = document.querySelector('.popup');
  const img = popup.querySelector('img');
  const siteNumbers = popup.querySelector('.site-numbers');
  const siteNumberList = siteNumbers.querySelectorAll('.site-number');
  
  let lastX = 0;
  let lastY = 0;
  let isDragging = false;
  let imgOffsetX = 0;
  let imgOffsetY = 0;
  const rectBefore = img.getBoundingClientRect();
    
  // 记录鼠标按下的位置并标记拖拽状态
  const handleDragStart = (event) => {
    event.preventDefault();
    lastX = event.clientX || event.touches[0].clientX;
    lastY = event.clientY || event.touches[0].clientY;
    isDragging = true;
  };

  const handleDragMove = (event) => {
    event.preventDefault();
    if (!isDragging) {
      return;
    }

    const deltaX = (event.clientX || event.touches[0].clientX) - lastX;
    const deltaY = (event.clientY || event.touches[0].clientY) - lastY;
    
    // 移动图片
    imgOffsetX += deltaX;
    imgOffsetY += deltaY;
    img.style.transform = `translate(${imgOffsetX}px, ${imgOffsetY}px) scale(${scale})`;
    
    // 这里很重要,因为img放大缩小后,其位置是会发生改变的
    const rectAfter = img.getBoundingClientRect();
    const offsetX = rectAfter.left - rectBefore.left;
    const offsetY = rectAfter.top - rectBefore.top;
    siteNumbers.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
    // 记录上次移动xy
    lastX = event.clientX || event.touches[0].clientX;
    lastY = event.clientY || event.touches[0].clientY;
  };

  const handleDragEnd = (event) => {
    event.preventDefault();
    isDragging = false;
  };
  
  // 注册事件
  img.addEventListener('mousedown', handleDragStart);
  // 鼠标移动和弹起绑定在document操作更流畅
  document.addEventListener('mousemove', handleDragMove);
  document.addEventListener('mouseup', handleDragEnd);

移动端

  // 移动端手势事件
  let lastDistance = 0;
  let isPinching = false;

  const handleTouchStart = (event) => {
    // 判断双指操作
    if (event.touches.length === 2) {
     // 勾股定理求斜边
      lastDistance = Math.sqrt(
        (event.touches[0].clientX - event.touches[1].clientX) ** 2 +
        (event.touches[0].clientY - event.touches[1].clientY) ** 2
      );
      isPinching = true;
    } else if (event.touches.length === 1) {
      handleDragStart(event);
    }
  };

  const handleTouchMove = (event) => {
    if (isPinching) {
      const distance = Math.sqrt(
        (event.touches[0].clientX - event.touches[1].clientX) ** 2 +
        (event.touches[0].clientY - event.touches[1].clientY) ** 2
      );
      // 计算放大比例
      scale *= distance / lastDistance;
      scale = Math.min(Math.max(0.5, scale), 2);

      img.style.transform = `translate(${imgOffsetX}px, ${imgOffsetY}px) scale(${scale})`;
      const rectAfter = img.getBoundingClientRect();
      const offsetX = rectAfter.left - rectBefore.left;
      const offsetY = rectAfter.top - rectBefore.top;
      siteNumbers.style.transform = `translate(${imgOffsetX}px, ${imgOffsetY}px) scale(${scale})`;
      lastDistance = distance;
    } else {
      handleDragMove(event);
    }
  };

  const handleTouchEnd = (event) => {
    if (isPinching) {
      isPinching = false;
    } else {
      handleDragEnd(event);
    }
  };


  img.addEventListener('touchstart', handleTouchStart);
  img.addEventListener('touchmove', handleTouchMove);
  img.addEventListener('touchend', handleTouchEnd);

整体代码

总结

  • 使用pointer-events来阻止元素成为鼠标事件
  • 使用transform 改变大小,还不使用改变长宽,优点是啥?使用了GPU加速、不影响其它元素布局,减少重绘提高性能