You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为图片缩放平移脚本添加无跳变的移动端触摸捏合缩放支持

图片移动端触摸缩放平移实现(解决捏合位置跳变)

已有鼠标滚轮缩放、拖拽平移功能正常,需添加移动端触摸支持,核心解决捏合操作时基于当前缩放比例计算,避免图片位置跳变的问题。

修正后的完整代码

JavaScript

const img = document.getElementById('image');
const stage = document.querySelector('.stage');
const container = document.querySelector('.container');
let mouseX;
let mouseY;
let mouseTX;
let mouseTY;
let panning = false;

const ts = {
  scale: 1,
  translate: {
    x: 0,
    y: 0
  }
};

// 鼠标滚轮缩放
stage.onwheel = function(event) {
  event.preventDefault();

  var func = stage.onwheel;
  stage.onwheel = null;

  let rec = stage.getBoundingClientRect();
  let x = (event.clientX - rec.x) / ts.scale;
  let y = (event.clientY - rec.y) / ts.scale;

  let delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
  ts.scale = (delta > 0) ? (ts.scale + 0.2) : (ts.scale - 0.2);
  if (ts.scale < 1) ts.scale = 1

  let m = (delta > 0) ? 0.1 : -0.1;
  ts.translate.x += (-x * m * 2) + (stage.offsetWidth * m);
  ts.translate.y += (-y * m * 2) + (stage.offsetHeight * m);

  setTransform();
  stage.onwheel = func;
};

// 鼠标拖拽平移
stage.onmousedown = function(event) {
  event.preventDefault();
  panning = true;
  mouseX = event.clientX;
  mouseY = event.clientY;
  mouseTX = ts.translate.x;
  mouseTY = ts.translate.y;
};

stage.onmouseup = () => panning = false;
document.onmouseup = () => panning = false;

stage.onmousemove = function(event) {
  event.preventDefault();
  if (!panning) return;

  const x = event.clientX;
  const y = event.clientY;
  ts.translate.x = mouseTX + (x - mouseX);
  ts.translate.y = mouseTY + (y - mouseY);

  setTransform();
};

// 设置变换
function setTransform() {
  const steps = `translate(${ts.translate.x}px,${ts.translate.y}px) scale(${ts.scale}) translate3d(0,0,0)`;
  stage.style.transform = steps;
}

// 重置
function reset() {
  ts.scale = 1;
  ts.translate = { x: 0, y: 0 };
  stage.style.transform = 'none';
}

setTransform();

// 移动端触摸支持
if ("ontouchstart" in window) {
  const touchState = {
    isPinching: false,
    start: {
      scale: 1,
      translateX: 0,
      translateY: 0,
      midX: 0,
      midY: 0,
      distance: 0
    }
  };

  // 计算两指距离
  const getTouchDistance = (event) => {
    return Math.hypot(
      event.touches[0].pageX - event.touches[1].pageX,
      event.touches[0].pageY - event.touches[1].pageY
    );
  };

  // 获取触摸中点相对于容器的坐标
  const getTouchMidpoint = (event) => {
    const rect = container.getBoundingClientRect();
    return {
      x: (event.touches[0].pageX + event.touches[1].pageX) / 2 - rect.left,
      y: (event.touches[0].pageY + event.touches[1].pageY) / 2 - rect.top
    };
  };

  stage.addEventListener('touchstart', (event) => {
    event.preventDefault();

    if (event.touches.length === 2) {
      // 开始捏合
      touchState.isPinching = true;
      const midPoint = getTouchMidpoint(event);
      
      // 记录当前状态:缩放比例、平移量、触摸中点(转换为图片内部坐标)
      touchState.start.scale = ts.scale;
      touchState.start.translateX = ts.translate.x;
      touchState.start.translateY = ts.translate.y;
      touchState.start.midX = (midPoint.x - ts.translate.x) / ts.scale;
      touchState.start.midY = (midPoint.y - ts.translate.y) / ts.scale;
      touchState.start.distance = getTouchDistance(event);
    } else if (event.touches.length === 1) {
      // 开始单点平移
      panning = true;
      const rect = container.getBoundingClientRect();
      mouseX = event.touches[0].pageX;
      mouseY = event.touches[0].pageY;
      mouseTX = ts.translate.x;
      mouseTY = ts.translate.y;
    }
  });

  stage.addEventListener('touchmove', (event) => {
    event.preventDefault();

    if (event.touches.length === 2 && touchState.isPinching) {
      // 捏合缩放
      const currentDistance = getTouchDistance(event);
      const midPoint = getTouchMidpoint(event);
      // 计算相对缩放倍数
      const scaleFactor = currentDistance / touchState.start.distance;
      // 更新总缩放比例,限制范围1-4
      ts.scale = Math.min(Math.max(touchState.start.scale * scaleFactor, 1), 4);
      
      // 调整平移量,保证触摸中点位置不变
      ts.translate.x = midPoint.x - touchState.start.midX * ts.scale;
      ts.translate.y = midPoint.y - touchState.start.midY * ts.scale;

      setTransform();
    } else if (event.touches.length === 1 && panning) {
      // 单点平移
      const x = event.touches[0].pageX;
      const y = event.touches[0].pageY;
      ts.translate.x = mouseTX + (x - mouseX);
      ts.translate.y = mouseTY + (y - mouseY);

      setTransform();
    }
  });

  stage.addEventListener('touchend', (event) => {
    touchState.isPinching = false;
    panning = false;
  });
}

CSS

.container {
  position: relative;
  width: 600px;
  border: 1px solid;
  overflow: hidden;
}

.stage {
  transform-origin: 50% 50%;
  cursor: grab;
  position: relative;
  width: 100%;
}

#image {
  width: 100%;
  height: auto;
  display: block;
}

/* 触摸设备下隐藏鼠标样式 */
@media (hover: none) and (pointer: coarse) {
  .stage {
    cursor: default;
  }
}

HTML

<div class="container">
  <div class="stage">
    <img id="image" src="https://cdn.pixabay.com/photo/2018/01/14/23/12/nature-3082832__480.jpg" />
  </div>
</div>

关键修改说明

  • 捏合初始状态记录:在touchstart时,不仅记录两指距离,还保存当前的缩放比例、平移量,以及触摸中点转换为图片内部的坐标(去除平移和缩放影响后的坐标)
  • 相对缩放计算:捏合时用当前两指距离与初始距离的比值作为相对缩放倍数,乘以初始缩放比例得到新的缩放值,而非直接覆盖
  • 平移量修正:根据新的缩放比例重新计算平移量,确保触摸中点在缩放过程中始终对应图片上的同一位置,彻底避免跳变
  • 统一单点触摸逻辑:将单点触摸平移和鼠标平移逻辑对齐,保证跨设备体验一致

内容的提问来源于stack exchange,提问作者Toniq

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 18:17:53