Reputation: 125
I'm using Konva to pan and zoom a stage containing an image and other elements. I have set draggable to true and I am using touchmove and touchend to set the stage scale. However, the stage zooms from the upper left corner instead of the pinch point. If I try to adjust the stage position based on the pinch center, the stage position seems to jump around because draggable seems to also be setting the position. Is there a good way to set the stage position on pinch while zooming?
Upvotes: 1
Views: 2014
Reputation: 301
I ran into the same problem when working on react konva and what I noticed was when my stage was positioned fixed to the top-left
.stage-container{
position:fixed;
top:0;
left:0;
}
the pinch-zoom functionality works fine but when it is placed somewhere else in the window it jumps around on zooming or scaling if I set stage position, what I found out after some experimenting is that the further it is from the windows(0,0) the more stage position jumps around on pinch zoom. what I did to solve the issue was I passed a reference to the stage container and got its relative position from the window origin and excluded it from the touch positions. It works fine now.
const handleMultiTouch = (e) => {
e.evt.preventDefault();
var touch1 = e.evt.touches[0];
var touch2 = e.evt.touches[1];
const stage = e.target.getStage();
const containerPos = stageContainerRef?.current?.getBoundingClientRect();
if (touch1 && touch2) {
if (stageDraggable) {
setStageDraggable(false);
}
var p1 = {
x: touch1.clientX - containerPos.x,
y: touch1.clientY - containerPos.y,
};
var p2 = {
x: touch2.clientX - containerPos.x,
y: touch2.clientY - containerPos.y,
};
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;
setStageDraggable(true);
};
const getCenter = (p1, p2) => {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
};
as you can see I have added 2 more lines where I get the stage container div and its position from the window and subtract it from the touch positions.
Upvotes: 0
Reputation: 198
Here's what I did for my game. Essentially you have to setPosition of the stage on the touchMove event handler to a calculated offset to a focal point being set at the beginning of the zoom.
function getDistance(p1, p2) {
return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y),2));
}
function getMapPointFromPoint(windowX,windowY) {
if (windowX === undefined) {
//inverse the coordinates and account for stage position
var mapX = Math.floor((-(stage.getX()) + (window.innerWidth*mapOffset)) / zoomLevel);
var mapY = Math.floor((-(stage.getY()) + (window.innerHeight/2)) / zoomLevel);
return({x: mapX, y: mapY});
} else {
var mapX = Math.floor((-(stage.getX()) + windowX) / zoomLevel);
var mapY = Math.floor((-(stage.getY()) + windowY) / zoomLevel);
return({x: mapX, y: mapY});
}
}
function setFocalPoint(mapX,mapY) {
if (mapX === undefined) {
// reverse the zoom to figure out the map coordinates at center of screen
var focalPoint = getMapPointFromPoint();
focalMapX = focalPoint.x;
focalMapY = focalPoint.y;
} else {
focalMapX = mapX;
focalMapY = mapY;
}
}
function scrollToMapPoint(x,y) {
stage.setPosition({x: Math.floor((window.innerWidth*mapOffset) - (x * zoomLevel)) , y: Math.floor((window.innerHeight/2) - (y * zoomLevel)) });
cullView();
stage.draw();
}
stage.getContent().addEventListener('touchstart', function(evt) {
var touch1 = evt.touches[0];
var touch2 = evt.touches[1];
touchInitPoint.x = touch1.clientX;
touchInitPoint.y = touch1.clientY;
if (touch1 && touch2) {
if (!pinching) {
setFocalPoint();
pinching = true;
cacheON();
}
}
});
stage.getContent().addEventListener('touchmove', function(evt) {
var touch1 = evt.touches[0];
var touch2 = evt.touches[1];
if (touch1 && touch2) {
var dist = getDistance({
x: touch1.clientX,
y: touch1.clientY
}, {
x: touch2.clientX,
y: touch2.clientY
});
if (!lastDist) {
lastDist = dist;
}
var scale = stage.getScaleX() * dist / lastDist;
if (scale < 0.2) {
scale = 0.2;
} else if (scale > 2) {
scale = 2;
}
stage.scaleX(scale);
stage.scaleY(scale);
zoomLevel = scale;
scrollToMapPoint(focalMapX,focalMapY);
stage.draw();
lastDist = dist;
} else {
if (pinching) {
//if losing contact with one touch, assume we start over on distance
lastDist = 0;
}
}
}, false);
stage.getContent().addEventListener('touchend', function(evt) {
var touch1 = evt.touches[0];
var touch2 = evt.touches[1];
if (pinching && (touch1 || touch2)) {
//wait until both gone
return;
} else {
if (pinching) {
lastDist = 0;
scrollToMapPoint(focalMapX,focalMapY);
if (pinchTimer) {
clearTimeout(pinchTimer);
}
pinchTimer = setTimeout(function () {
pinching = false;
cacheOFF();
},1000);
}
}
}, false);
Upvotes: 2