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>
关键注意事项
- 阻止默认行为:在
touchstart和touchmove中必须调用e.evt.preventDefault(),否则 Chrome 会优先触发页面滚动/缩放,导致自定义逻辑失效。 - 双指逻辑区分:通过监听 Ctrl 按键状态,实现“双指默认平移,按住 Ctrl 双指缩放”的交互。
- 缩放原点校准:缩放时以双指中心为原点计算位置偏移,避免画布缩放后偏离触摸点。
- 版本兼容:确保 vue-konva 和 Konva 版本为最新稳定版,旧版本可能存在触摸事件兼容性问题。
内容的提问来源于stack exchange,提问作者Adam Silva
相关产品推荐
相关产品推荐

