Someone asked on the Konva discord channel how to have a shape – in this case a group – be positioned and zoomed so that the shape fills the canvas view and is centered vertically and horizontally. Here’s my answer…
TLDR: See the working demo code here.
The concept is simple – it’s the old center-one-rect-inside-another gag with the twist that we need some scaling. Whenever translation (that’s changing position) and scaling are applied there is always scope for confusion but it is all doable.
The gist is
- measure both the rectangles
- do the math to calculate position and scale changes
- apply the changes
The rectangles in question are the view port, aka the stage container – or the stage itself if smaller than the container – and the target shape. The type of shape does not matter – the following covers all the types of shape. In my demo code I have added an option for padding since it is likely to be wanted, so assume the target rect is slightly inflated for padding.
To get the target shape’s rect we need to know the unscaled, unrotated rectangle that the shape occupies on the stage. We have at our disposal the shape.getClientRect() method which does this job. However, getClientRect gives the size based on the current stage scaling – we want unscaled size so use the ‘relativeTo: stage’ param to ensure consistent measurements. Avoid using the ‘skipTransform’ param because we actually care about the transforms, meaning that if the target shape is rotated or has some other transform then we want to know the bounding box around the shape with its transforms applied since all we are doing here is moving the view position – not changing any of the drawing on the canvas.
So, getClientRect() gives us the raw rectangle for the shape. Next we add the padding.
Having determined a final target rect size we compute the ratio of width and heights between the view rect and the target shape rect. We use the lesser of these as our target scale.
In terms of position, we need to move the stage origin from its current position to a position that will place the target rect in the intended position. However, we need to take into account that we will be applying some scaling, and as a final challenge we need to consider a small adjustment to move the target rect into the center of the view – all rect positions are based on the top-left co-ordinate rather than the center.
After all that we have the stage position and scale, so we apply them. Here’s the important function that does all the work:
/* Function to scale & position the stage as needed to have
** the target shape fill the viewport.
** padding parameter is optional - if given this number
** of pixels is used as padding around the target shape.
*/
function centerAndFitShape(shape, padding) {
// set padding if not provided.
padding = padding ? padding : 0;
const
// raw rect around shape, no padding applied.
// Note: getClientRect gives size based on scaling, but
// we want the unscaled size so use 'relativeTo: stage' param
// to ensure consistent measurements.
shapeRectRaw = shape.getClientRect({relativeTo: stage}),
// Add padding to make a larger rect - this is what we want to fill the view
shapeRect = {
x: shapeRectRaw.x - padding,
y: shapeRectRaw.y - padding,
width: shapeRectRaw.width + (2 * padding),
height: shapeRectRaw.height + (2 * padding)
},
// Get the space we can see in the web page = size of div containing stage
// or stage size, whichever is the smaller
viewRect = {
width: stage.width() < stage.container().offsetWidth ? stage.width() : stage.container().offsetWidth,
height: stage.height() < stage.container().offsetHeight ? stage.height() : stage.container().offsetHeight},
// Get the ratios of target shape v's view space widths and heights
widthRatio = viewRect.width / shapeRect.width,
heightRatio = viewRect.height / shapeRect.height,
// decide on best scale to fit longest side of shape into view
scale = widthRatio > heightRatio ? heightRatio : widthRatio,
// calculate the final adjustments needed to make
// the shape centered in the view
centeringAjustment = {
x: (viewRect.width - shapeRect.width * scale)/2,
y: (viewRect.height - shapeRect.height * scale)/2
},
// and the final position is...
finalPosition = {
x: centeringAjustment.x + (-shapeRect.x * scale),
y: centeringAjustment.y + (-shapeRect.y * scale)
};
// Apply the final position and scale to the stage.
stage.position(finalPosition);
stage.scale({x: scale, y: scale});
}
Summary
We’ve seen the approach for getting the unscaled position and size of any target shape, and how to get it front-and-center of the view. The same technique, with a few tweaks, can be used for moving the shape front-and-center without scaling the stage, and with a few other tweaks it could be made to cover a list of shapes so that it centered part of the stage – this might be useful for rubber-band selection of part of a diagram, for example.
Thanks for reading.
VW July 22