多点触控画布缩放与捏合缩放
如何为画布舞台启用平移和捏合缩放?
在 touchmove
回调中,我们可以通过 e.evt.touches
访问所有触控事件的原生属性。所以当使用两个指点触控时,我们只需手动计算舞台的位置和缩放属性。
注意:该实验仅在支持多点触控手势的设备上工作,因为它使用了多个触控事件。
说明: 使用支持多点触控手势的移动设备,用两个手指放大或缩小舞台。
- Vanilla
- React
- Vue
import Konva from 'konva'; // 默认情况下,Konva在节点拖动时会阻止某些事件 // 这提高了性能并在95%的情况下工作良好 // 我们需要在Konva上启用所有事件,即使在拖动节点时 // 这样可以正确触发touchmove Konva.hitOnDragEnabled = true; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, draggable: true, }); const layer = new Konva.Layer(); const triangle = new Konva.RegularPolygon({ x: 190, y: stage.height() / 2, sides: 3, radius: 80, fill: 'green', stroke: 'black', strokeWidth: 4, }); const circle = new Konva.Circle({ x: 380, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4, }); function getDistance(p1, p2) { return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); } function getCenter(p1, p2) { return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, }; } let lastCenter = null; let lastDist = 0; let dragStopped = false; stage.on('touchmove', function (e) { e.evt.preventDefault(); const touch1 = e.evt.touches[0]; const touch2 = e.evt.touches[1]; // 如果多点触控取消了拖动,我们需要恢复拖动 if (touch1 && !touch2 && !stage.isDragging() && dragStopped) { stage.startDrag(); dragStopped = false; } if (touch1 && touch2) { // 如果舞台在Konva的拖放中 // 我们需要停止它,并用两个指点实现我们自己的平移逻辑 if (stage.isDragging()) { dragStopped = true; stage.stopDrag(); } const p1 = { x: touch1.clientX, y: touch1.clientY, }; const p2 = { x: touch2.clientX, y: touch2.clientY, }; if (!lastCenter) { lastCenter = getCenter(p1, p2); return; } const newCenter = getCenter(p1, p2); const dist = getDistance(p1, p2); if (!lastDist) { lastDist = dist; } // 中心点的局部坐标 const pointTo = { x: (newCenter.x - stage.x()) / stage.scaleX(), y: (newCenter.y - stage.y()) / stage.scaleX(), }; const scale = stage.scaleX() * (dist / lastDist); stage.scaleX(scale); stage.scaleY(scale); // 计算舞台的新位置 const dx = newCenter.x - lastCenter.x; const dy = newCenter.y - lastCenter.y; const newPos = { x: newCenter.x - pointTo.x * scale + dx, y: newCenter.y - pointTo.y * scale + dy, }; stage.position(newPos); lastDist = dist; lastCenter = newCenter; } }); stage.on('touchend', function () { lastDist = 0; lastCenter = null; }); layer.add(triangle); layer.add(circle); stage.add(layer);
import { Stage, Layer, RegularPolygon, Circle } from 'react-konva'; import { useState, useEffect, useCallback } from 'react'; // 默认情况下,Konva在节点拖动时会阻止某些事件 // 这提高了性能并在95%的情况下工作良好 // 我们需要在Konva上启用所有事件,即使在拖动节点时 // 这样可以正确触发touchmove window.Konva.hitOnDragEnabled = true; const App = () => { const [stagePos, setStagePos] = useState({ x: 0, y: 0 }); const [stageScale, setStageScale] = useState({ x: 1, y: 1 }); const [lastCenter, setLastCenter] = useState(null); const [lastDist, setLastDist] = useState(0); const [dragStopped, setDragStopped] = useState(false); const getDistance = (p1, p2) => { return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); }; const getCenter = (p1, p2) => { return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, }; }; const handleTouchMove = useCallback((e) => { e.evt.preventDefault(); const touch1 = e.evt.touches[0]; const touch2 = e.evt.touches[1]; const stage = e.target.getStage(); // 如果多点触控取消了拖动,我们需要恢复拖动 if (touch1 && !touch2 && !stage.isDragging() && dragStopped) { stage.startDrag(); setDragStopped(false); } if (touch1 && touch2) { // 如果舞台在Konva的拖放中 // 我们需要停止它,并用两个指点实现我们自己的平移逻辑 if (stage.isDragging()) { stage.stopDrag(); setDragStopped(true); } const p1 = { x: touch1.clientX, y: touch1.clientY, }; const p2 = { x: touch2.clientX, y: touch2.clientY, }; if (!lastCenter) { setLastCenter(getCenter(p1, p2)); return; } const newCenter = getCenter(p1, p2); const dist = getDistance(p1, p2); if (!lastDist) { setLastDist(dist); return; } // 中心点的局部坐标 const pointTo = { x: (newCenter.x - stagePos.x) / stageScale.x, y: (newCenter.y - stagePos.y) / stageScale.x, }; const scale = stageScale.x * (dist / lastDist); setStageScale({ x: scale, y: scale }); // 计算舞台的新位置 const dx = newCenter.x - lastCenter.x; const dy = newCenter.y - lastCenter.y; setStagePos({ x: newCenter.x - pointTo.x * scale + dx, y: newCenter.y - pointTo.y * scale + dy, }); setLastDist(dist); setLastCenter(newCenter); } }, [dragStopped, lastCenter, lastDist, stagePos, stageScale]); const handleTouchEnd = () => { setLastDist(0); setLastCenter(null); }; return ( <Stage width={window.innerWidth} height={window.innerHeight} draggable x={stagePos.x} y={stagePos.y} scaleX={stageScale.x} scaleY={stageScale.y} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd} > <Layer> <RegularPolygon x={190} y={window.innerHeight / 2} sides={3} radius={80} fill="green" stroke="black" strokeWidth={4} /> <Circle x={380} y={window.innerHeight / 2} radius={70} fill="red" stroke="black" strokeWidth={4} /> </Layer> </Stage> ); }; export default App;
<template> <v-stage :config="stageConfig" @touchmove="handleTouchMove" @touchend="handleTouchEnd" > <v-layer> <v-regular-polygon :config="{ x: 190, y: stageConfig.height / 2, sides: 3, radius: 80, fill: 'green', stroke: 'black', strokeWidth: 4 }" /> <v-circle :config="{ x: 380, y: stageConfig.height / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4 }" /> </v-layer> </v-stage> </template> <script setup> import { ref, computed } from 'vue'; // 默认情况下,Konva在节点拖动时会阻止某些事件 // 这提高了性能并在95%的情况下工作良好 // 我们需要在Konva上启用所有事件,即使在拖动节点时 // 这样可以正确触发touchmove window.Konva.hitOnDragEnabled = true; const stagePos = ref({ x: 0, y: 0 }); const stageScale = ref({ x: 1, y: 1 }); const lastCenter = ref(null); const lastDist = ref(0); const dragStopped = ref(false); const stageConfig = computed(() => ({ width: window.innerWidth, height: window.innerHeight, draggable: true, x: stagePos.value.x, y: stagePos.value.y, scaleX: stageScale.value.x, scaleY: stageScale.value.y })); const getDistance = (p1, p2) => { return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); }; const getCenter = (p1, p2) => { return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, }; }; const handleTouchMove = (e) => { e.evt.preventDefault(); const touch1 = e.evt.touches[0]; const touch2 = e.evt.touches[1]; const stage = e.target.getStage(); // 如果多点触控取消了拖动,我们需要恢复拖动 if (touch1 && !touch2 && !stage.isDragging() && dragStopped.value) { stage.startDrag(); dragStopped.value = false; } if (touch1 && touch2) { // 如果舞台在Konva的拖放中 // 我们需要停止它,并用两个指点实现我们自己的平移逻辑 if (stage.isDragging()) { stage.stopDrag(); dragStopped.value = true; } const p1 = { x: touch1.clientX, y: touch1.clientY, }; const p2 = { x: touch2.clientX, y: touch2.clientY, }; if (!lastCenter.value) { lastCenter.value = getCenter(p1, p2); return; } const newCenter = getCenter(p1, p2); const dist = getDistance(p1, p2); if (!lastDist.value) { lastDist.value = dist; return; } // 中心点的局部坐标 const pointTo = { x: (newCenter.x - stagePos.value.x) / stageScale.value.x, y: (newCenter.y - stagePos.value.y) / stageScale.value.x, }; const scale = stageScale.value.x * (dist / lastDist.value); stageScale.value = { x: scale, y: scale }; // 计算舞台的新位置 const dx = newCenter.x - lastCenter.value.x; const dy = newCenter.y - lastCenter.value.y; stagePos.value = { x: newCenter.x - pointTo.x * scale + dx, y: newCenter.y - pointTo.y * scale + dy, }; lastDist.value = dist; lastCenter.value = newCenter; } }; const handleTouchEnd = () => { lastDist.value = 0; lastCenter.value = null; }; </script>