I am using Konva.js to build a canvas app. This app allows user to drag an image and zoom at any point. This works perfectly on Desktop. However, I like this app to be responsive for mobile devices. The problem here is canvas zoom and image drag cannot work synchronously. Since 'onTouchMove' event works when image dragged, this app does not work as intended.
import React, { useEffect, useState, useRef } from 'react';
import {
Stage, Layer, Image,
} from 'react-konva';
import Rectangle from './rectangle.component';
const GenericCanvas = ({
canvasWidth,
canvasHeight,
imageUrl,
imgWidth,
imgHeight,
rects,
zoom,
imageMove,
alpha,
borderDash,
rectDraggable,
fillStatus,
onClickRect,
reset,
setReset,
imageType,
displayedFigure,
displayedTable,
clickedRect
}) => {
const [image, setImage] = useState(null);
const [stageScale, setStageScale] = useState(1);
const [stageX, setStageX] = useState(0);
const [stageY, setStageY] = useState(0);
const [lastX, setLastX] = useState(0);
const [lastY, setLastY] = useState(0);
const [hasImageLoaded, setHasImageLoaded] = useState(false);
const handleImageLastPosition = (e) => {
setLastX(e.target.attrs.x);
setLastY(e.target.attrs.y);
};
const handleWheel = (e) => {
e.evt.preventDefault();
const scaleBy = 1.2;
const stage = e.target.getStage();
const oldScale = stage.scaleX();
const mousePointTo = {
x: stage.getPointerPosition().x / oldScale - stage.x() / oldScale,
y: stage.getPointerPosition().y / oldScale - stage.y() / oldScale,
};
const newScale = e.evt.deltaY > 0
? (oldScale > 3 ? oldScale : (oldScale * scaleBy)) : oldScale < 1
? oldScale : (oldScale / scaleBy);
setStageScale(newScale);
setStageX(-(mousePointTo.x - stage.getPointerPosition().x / newScale) * newScale);
setStageY(-(mousePointTo.y - stage.getPointerPosition().y / newScale) * newScale);
};
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,
};
}
var lastCenter = null;
var lastDist = 0;
const handleMultiTouch = (e) => {
e.evt.preventDefault();
var touch1 = e.evt.touches[0];
var touch2 = e.evt.touches[1];
const stage = e.target.getStage();
if (touch1 && touch2) {
if (stage.isDragging()) {
stage.stopDrag();
}
imageMove= false;
var p1 = {
x: touch1.clientX,
y: touch1.clientY,
};
var p2 = {
x: touch2.clientX,
y: touch2.clientY,
};
if (!lastCenter) {
lastCenter = getCenter(p1, p2);
return;
}
var newCenter = getCenter(p1, p2);
var dist = getDistance(p1, p2);
if (!lastDist) {
lastDist = dist;
}
// local coordinates of center point
var pointTo = {
x: (newCenter.x - stage.x()) / stage.scaleX(),
y: (newCenter.y - stage.y()) / stage.scaleX(),
};
var scale = stage.scaleX() * (dist / lastDist);
stage.scaleX(scale);
stage.scaleY(scale);
// calculate new position of the stage
var dx = newCenter.x - lastCenter.x;
var dy = newCenter.y - lastCenter.y;
var newPos = {
x: newCenter.x - pointTo.x * scale + dx,
y: newCenter.y - pointTo.y * scale + dy,
};
stage.position(newPos);
stage.batchDraw();
lastDist = dist;
lastCenter = newCenter;
}
};
const multiTouchEnd = () => {
lastCenter = null;
lastDist = 0;
}
useEffect(() => {
setHasImageLoaded(false);
if (imageUrl) {
const matchedImg = new window.Image();
matchedImg.src = `${API_URL + imageUrl}`;
matchedImg.onload = () => {
setHasImageLoaded(true);
setImage(matchedImg);
};
}
}, [size, imageUrl]);
let imgDraggable = true
return (
<Stage
width={canvasWidth}
height={canvasHeight}
onWheel={zoom ? handleWheel : null}
scaleX={stageScale}
onTouchMove={handleMultiTouch}
onTouchEnd={() => {
multiTouchEnd()
}}
scaleY={stageScale}
x={stageX}
y={stageY}
>
<Layer>
<Image
x={lastX}
y={lastY}
ref={stageRef}
image={image}
width={imgWidth * alpha || 0}
height={imgHeight * alpha || 0}
onDragMove={(e) => {
if (e.evt.touches.length === 2) {
imgDraggable = false
}
handleImageLastPosition(e)
}}
draggable={imgDraggable}
/>
{rects?.length && hasImageLoaded ?
rects.map((rect) =>
(
<Rectangle
key={uuid()}
x={imageType === 'figure' ? rect.bbox.x0 * alpha + lastX : rect.PN_bbox.x0 * alpha + lastX}
y={imageType === 'figure' ? rect.bbox.y0 * alpha + lastY : rect.PN_bbox.y0 * alpha + lastY}
width={imageType === 'figure' ? rect.bbox.x1 * alpha - rect.bbox.x0 * alpha : rect.PN_bbox.x1 * alpha - rect.PN_bbox.x0 * alpha}
height={imageType === 'figure' ? rect.bbox.y1 * alpha - rect.bbox.y0 * alpha : rect.PN_bbox.y1 * alpha - rect.PN_bbox.y0 * alpha}
rectDraggable={rectDraggable}
borderDash={borderDash}
lastX={lastX}
lastY={lastY}
alpha={alpha}
rect={rect}
fillStatus={clickedRect?.item_no === rect?.item_no}
imageId={imageType === 'table' ? displayedTable?.img_id : displayedFigure?.img_id}
imageType={imageType}
onClickRect={onClickRect}
/>
)
) : null
}
</Layer>
</Stage>
);
};
I tried to control touch event length because if it is 2 this means it comes from multitouch so zoom can be active vice versa. However, it also did not work. Thanks for any help in advance. The demo can be found on https://codesandbox.io/s/react-konva-zoom-webmobile-demo-em5o6 .