Anthony
Anthony

Reputation: 125

Konva - stage centered on pinch zoom

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

Answers (2)

momin naveed
momin naveed

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

Timothy Ryan
Timothy Ryan

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

Related Questions