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

KonvaJS(vue-konva)在Mac上实现双指平移与缩放的问题求助

解决方案:Vue-Konva 在 Mac Chrome 上实现双指平移与缩放

核心问题分析

Mac Chrome 会优先拦截触摸手势(如双指滚动、缩放)作为浏览器默认行为,导致 Konva 的 gesture* 事件无法正常触发;同时双指操作的逻辑需要区分平移/缩放的触发条件(搭配按键),需通过原生触摸事件+自定义逻辑实现。

实现代码(Vue 3 + vue-konva)

<template>
  <v-stage
    ref="stageRef"
    :config="stageConfig"
    @mousedown="onMouseDown"
    @mousemove="onMouseMove"
    @mouseup="onDragEnd"
    @mouseleave="onDragEnd"
    @wheel="onWheel"
    @touchstart="onTouchStart"
    @touchmove="onTouchMove"
    @touchend="onDragEnd"
  >
    <v-layer ref="layerRef">
      <!-- 你的图形元素 -->
    </v-layer>
  </v-stage>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const stageRef = ref(null);
const layerRef = ref(null);
const stageConfig = ref({
  width: window.innerWidth,
  height: window.innerHeight
});

const isDragging = ref(false);
const lastPointerPos = ref({ x: 0, y: 0 });
const lastGestureScale = ref(1);
let ctrlPressed = false;

// 监听Ctrl按键状态
const handleKeyDown = (e) => {
  if (e.key === 'Control') ctrlPressed = true;
};
const handleKeyUp = (e) => {
  if (e.key === 'Control') ctrlPressed = false;
};

onMounted(() => {
  window.addEventListener('keydown', handleKeyDown);
  window.addEventListener('keyup', handleKeyUp);
});

onUnmounted(() => {
  window.removeEventListener('keydown', handleKeyDown);
  window.removeEventListener('keyup', handleKeyUp);
});

// 单指鼠标平移逻辑
const onMouseDown = (e) => {
  isDragging.value = true;
  const stage = stageRef.value.getStage();
  lastPointerPos.value = stage.getPointerPosition();
};

const onMouseMove = (e) => {
  if (!isDragging.value) return;
  const stage = stageRef.value.getStage();
  const currentPos = stage.getPointerPosition();
  const dx = currentPos.x - lastPointerPos.value.x;
  const dy = currentPos.y - lastPointerPos.value.y;

  stage.position({
    x: stage.x() + dx,
    y: stage.y() + dy
  });
  lastPointerPos.value = currentPos;
  stage.batchDraw();
};

// 双指触摸处理
const onTouchStart = (e) => {
  // 阻止浏览器默认手势(关键)
  e.evt.preventDefault();
  const stage = stageRef.value.getStage();
  const touches = e.evt.touches;

  if (touches.length === 2) {
    // 记录双指初始状态
    const dx = touches[0].clientX - touches[1].clientX;
    const dy = touches[0].clientY - touches[1].clientY;
    lastGestureScale.value = stage.scaleX();
    // 计算双指中心位置
    lastPointerPos.value = {
      x: (touches[0].clientX + touches[1].clientX) / 2,
      y: (touches[0].clientY + touches[1].clientY) / 2
    };
  } else if (touches.length === 1) {
    // 单指触摸平移
    isDragging.value = true;
    lastPointerPos.value = stage.getPointerPosition();
  }
};

const onTouchMove = (e) => {
  e.evt.preventDefault();
  const stage = stageRef.value.getStage();
  const touches = e.evt.touches;

  if (touches.length === 2) {
    if (ctrlPressed) {
      // 双指+Ctrl 触发缩放
      const dx = touches[0].clientX - touches[1].clientX;
      const dy = touches[0].clientY - touches[1].clientY;
      const currentDistance = Math.sqrt(dx * dx + dy * dy);
      const initialDx = touches[0].clientX - touches[1].clientX;
      const initialDy = touches[0].clientY - touches[1].clientY;
      const initialDistance = Math.sqrt(initialDx * initialDx + initialDy * initialDy);
      const scaleFactor = currentDistance / initialDistance;
      const newScale = lastGestureScale.value * scaleFactor;

      // 以双指中心为缩放原点,避免偏移
      const currentCenter = {
        x: (touches[0].clientX + touches[1].clientX) / 2,
        y: (touches[0].clientY + touches[1].clientY) / 2
      };
      const mousePointTo = {
        x: (currentCenter.x - stage.x()) / stage.scaleX(),
        y: (currentCenter.y - stage.y()) / stage.scaleY()
      };

      stage.scale({ x: newScale, y: newScale });
      stage.position({
        x: currentCenter.x - mousePointTo.x * newScale,
        y: currentCenter.y - mousePointTo.y * newScale
      });
    } else {
      // 双指平移
      const currentCenter = {
        x: (touches[0].clientX + touches[1].clientX) / 2,
        y: (touches[0].clientY + touches[1].clientY) / 2
      };
      const dx = currentCenter.x - lastPointerPos.value.x;
      const dy = currentCenter.y - lastPointerPos.value.y;

      stage.position({
        x: stage.x() + dx,
        y: stage.y() + dy
      });
      lastPointerPos.value = currentCenter;
    }
    stage.batchDraw();
  } else if (touches.length === 1 && isDragging.value) {
    // 单指触摸平移
    const currentPos = stage.getPointerPosition();
    const dx = currentPos.x - lastPointerPos.value.x;
    const dy = currentPos.y - lastPointerPos.value.y;

    stage.position({
      x: stage.x() + dx,
      y: stage.y() + dy
    });
    lastPointerPos.value = currentPos;
    stage.batchDraw();
  }
};

const onDragEnd = () => {
  isDragging.value = false;
};

// 原有鼠标滚轮缩放逻辑(兼容Mac双指滚动)
const onWheel = (e) => {
  e.evt.preventDefault();
  const stage = stageRef.value.getStage();
  const scaleBy = 1.05;
  const pointer = stage.getPointerPosition();
  const mousePointTo = {
    x: (pointer.x - stage.x()) / stage.scaleX(),
    y: (pointer.y - stage.y()) / stage.scaleY()
  };
  const newScale = e.evt.deltaY > 0 ? stage.scaleX() / scaleBy : stage.scaleX() * scaleBy;

  stage.scale({ x: newScale, y: newScale });
  stage.position({
    x: pointer.x - mousePointTo.x * newScale,
    y: pointer.y - mousePointTo.y * newScale
  });
  stage.batchDraw();
};
</script>

关键注意事项

  1. 阻止默认行为:在 touchstart 和 touchmove 中必须调用 e.evt.preventDefault(),否则 Chrome 会优先触发页面滚动/缩放,导致自定义逻辑失效。
  2. 双指逻辑区分:通过监听 Ctrl 按键状态,实现“双指默认平移,按住 Ctrl 双指缩放”的交互。
  3. 缩放原点校准:缩放时以双指中心为原点计算位置偏移,避免画布缩放后偏离触摸点。
  4. 版本兼容:确保 vue-konva 和 Konva 版本为最新稳定版,旧版本可能存在触摸事件兼容性问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 19:07:42