Konva - zooming the stage but keeping 1:1 scale on specific shapes
Photo by Iván Díaz / Unsplash

Konva - zooming the stage but keeping 1:1 scale on specific shapes

I was fiddling with a simple toolbar when I changed the scale to 2x. Suddenly my toolbar is huge - not what I needed. So how to keep one shape at 1:1 scale while all around it change their size? Read on...

TLDR: Here's the magic


// call this when scale changes.
refresh() {

    // reset to scale 1:1
    myToolbar.scale({
        x: 1,
        y: 1,
    })  

    // get the new scale
    const scale = myToolbar.getAbsoluteScale()

    // apply the opposite of the new scale
    myToolbar.scale({
        x: 1 / scale.x,
        y: 1 / scale.y,
    })    
}

The Mission

As per the intro really - how can we keep one shape, or group, at a 1:1 scale while zooming the stage or layer? As a requirement its more common that you might think - for example annotation details in document or image approval workflows, the key in an architectural diagram or map, and toolbars, of course.

Challenges

Not too many scary issues here. All we need to do is to set the scale of our 1:1 shape to the reciprocal of the scale it should be. The small challenge is to realise that scale can be changed on any container - so that's the Stage, layer, and any ancestor groups that lead to the shape.

Solution

Konva has a neat solution in the Node.getAbsoluteScale() method, which does what it says in the name. We don't need to do the math ourselves - Konva does it all for us.

In the code sample above, myToolbar is the node I want to keep at 1:1. The refresh() function should be called any time the stage or layer change scale. In the function the node's scale is first set to 1 so that the Node.getAbsoluteScale() reports the scaling effects from all the other scaling in its ancestor tree. Finally, having found that, we apply it back on to myToolbar. Job done, time for cake.

Bonus technique

In my toolbar case, I needed to pop out another shape in close proximity to a button on the toolbar - imagine a dropdown list of options, for example. Because I'm using a lot of groups to combine shapes, I was working out the location for the popup using group.getClientRect(). However, without additional parameters, this function returns the rect in relation to the canvas surface, while I actually needed the position with scaling applied.

The answer lies in the relativeTo parameter. Set this to a container ancestor of both shapes - the button and the popout - and you can immediately use the returned value without any further math as it is immune to scaling affects.

const group = new Konva.Group()
const button = new Konva.Rect({
  x: 21,
  y:42,
  width: 50,
  height: 25})
const popup = new Konva.Rect({
  width: 40,
  height: 250})

group.add(button, popup)

// get the popup located in line and below the button
button.on("click", function(e){

  // Beacuse both button and popup have group as a container, 
  // getClientRect returns a value we can use to set the others position.
  const buttonPos = button.getClientRect({relativeTo: group})

  popup.position({
    x: buttonPos.x,
    y: buttonPos.y + button.height() + 10
  })

})

Summary

Now we know how to keep a specific shape at 1:1 scale when the stage, layer of parent groups use different scaling.

Thanks for reading.

VW Mar 2025