Olle
Olle

Reputation: 57

Scaling IMAGE around pointer in Konva

Can't wrap my head around this and get it to work. Trying to transpose this sample on Konva, but can not get it to work with an image inside a layer, inside the stage. The sample I am trying to reproduce is the "Zooming stage relative to pointer position" sample.

https://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.html

Any help would generate kudos.

Upvotes: 0

Views: 898

Answers (1)

Vanquished Wombat
Vanquished Wombat

Reputation: 9525

The trick with zooming from an arbitrary point such as the mouse position is to understand that you must relocate the stage to keep the point under the mouse consistent. I provide the working vanilla JS snippet below to illustrate the point. The guts are based on the original demo at the Konva docs site, just with more comments in the code. Translating into React should not be difficult once the basis of the technique is explained.

The gray area is the canvas, and the pink rect is the stage. Note that we start with the stage positioned into the canvas just to show that the stage can be moved like any other object.

The stage has a dot-pattern to help show the effects in the demo. The solution works for any layout of layers, shapes or images (as per OP question). Note that the shapes themselves do not need to be handled individually - the scaling is applied to the stage.

Move your mouse anywhere on the stage and scroll to see the zoom effect, noting how the top-left of the pink stage rectangle moves with the zoom. Now un-tick the checkbox to turn off the stage position adjustment - note now that the zoom is no longer respecting the mouse position because the stage position is not being moved.

Thus we have illustrated that it is necessary to move the stage sympathetically during the zoom but how do we know the amount to move by?

The tip to know is that you need to get the absolute distance of the mouse position over the stage before the new scale is applied, then apply the new scale to the stage, then use the computed distance * new scale to compute the new stage position. See the stage.on('wheel') event in the snippet for working code.

See also the codepen here.

// Making the stage
let stage = new Konva.Stage({
  container: "container1", 
  width: $('#container1').width(),  
  height: $('#container1').height(), 
  draggable: true,
  x: 40,
  y: 60
});


// Make the layer
let layer = new Konva.Layer();

// make a rect to fill stage to show where it is.
let rect = new Konva.Rect({
  x:0, 
  y: 0,
  width: $('#container1').width(),  
  height: $('#container1').height(), 
  fill: 'magenta',
  opacity: 0.3
})
layer.add(rect);

let grid = {w: $('#container1').width(), h: $('#container1').height()}, 
    gridStep = 40, 
    mouseCircleRadius = 80, 
    circles = [], 
    dotRadius = 10;

// Make the grid of circles
for (let i = gridStep; i < grid.w; i = i + gridStep ){
  for (let j = gridStep; j < grid.h; j = j + gridStep ){
    let c = new Konva.Circle({ x: i, y: j, radius: dotRadius, fill: 'cyan'})
    circles.push(c);
    layer.add(c)
  }
}

// Add layer to stage
stage.add(layer)

stage.on('mousemove', function (e) {
  var pointer = stage.getPointerPosition();
  
  // show the pointer and stage positions for illustration
  $('#trace').html('Pointer.x = ' + pointer.x + ' stage.x() = ' + stage.x())

});
// this is the scale factor to be applied at each step.
var scaleBy = 1.01; 

// this is the event that fires when the mouse wheel spins.
stage.on('wheel', (e) => {
    e.evt.preventDefault();
  
    // note the old scale to be used when deciding the current stage position
    var oldScale = stage.scaleX();

    // note the mouse pointer position relative to the stage at current scale 
    var pointer = stage.getPointerPosition();

    // calculate the mouse pointer position at scale = 1.
    // pointer.x is relative to the canvas - not affected by stage scale,
    // stage.x() is the poistion of the stage on the canvas. 
    //  This gives the distance from the pointer to the  
    var mousePointTo = {
        x: (pointer.x - stage.x()) / oldScale,
        y: (pointer.y - stage.y()) / oldScale,
    };

    // Calculate the new scale - slightly different calc for zoom in versus zoom out, as you would expect.
    var newScale = (e.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy);

    // Apply the new scale to the stage. Note that, assuming a zoom-in op, at this point in time the shapes on the stage would 
    // seem to have jumped down + right. We will shortly 'move' the stage left+up to counter this effect.  
    stage.scale({ x: newScale, y: newScale });

    // To do that we have to calculate what the position of the stage must be relative to the mouse pointer position at the new scale. 
    // Note - remove the 'if' for your live code, the checkbox is for illustration only.
  if ($('#fixstagepos').prop('checked')){
    var newPos = {
        x: pointer.x - mousePointTo.x * newScale,
        y: pointer.y - mousePointTo.y * newScale,
    };    
    // and apply the new position to the stage. Again in the case of a zoom-in op this has the effect of moving the stage left + up, countering the apparent movement 
    // caused by the change in scale. 
    stage.position(newPos);
  }

});
.container {
  width: 800px;
  height: 400px;
  background-color: silver;
  margin: 5px;
}
#trace {
  max-height: 200px;
  overflow-y: scroll;
  font-family: 'Courier'
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva@8/konva.min.js"></script>
<p id="info">Grey is the canvas and the pink rect shows the stage position. The stage is intitially positioned at {x: 40, y: 60} and can be dragged.</p>
<p>Move mouse over stage and scroll to zoom in & out. Use checkbox to see what happens without stage pos correction. </p>
<p>Apply correction to stage position <input id='fixstagepos' type='checkbox' checked='1' /></p> 
<div id='container1' class='container'>
</div>

  <p id='trace'></p>

Upvotes: 0

Related Questions