Konva – focus border around a group

How can we make a focus border around the shapes in a group whilst the group and shape are draggable ?

Why not use a transformer?

The transformer gives a border around the group, and it has a padding option. However, the border is removed as soon as the transformer is switched off. The Konva.Group does not have a built-in border option, so if you need to have a focus rect around a group when not transforming, then there’s no easy option. I would always try a transformer first and only revert to the approach in this article as a last resort. Though this solution here is not overly complex, if you can get away with a transformer then life will be much more simple for you!

But, if you really need it, read on…

The requirement

  • We want a padded border around a group
  • The shapes in the group must be individually draggable
  • If a shape is dragged outside the border then the border should expand
  • The group must be draggable

Unpicking those a little:

We want a padded border around a group

The group should be surrounded by a border rectangle with a pleasing anount of padding. Thinking about a transformer, the border stroke should remain unscaled if the group is scaled, but the border rectangle must stretch and rotate with the group.

The shapes in the group must be individually draggable

As stated, the user must be able to reposition the shapes in the group by click-dragging them around the stage.

If a shape is dragged outside the border then the border should expand

The border rectangle should grow & shrink so that it always contains the shapes in the group. If the user drags a shape ‘outside’ the border rectangle, then the border rectangle should extend so that the shape is always contained within the border. In this use-case the dragging shape must not be contrained by the border, but if you needed to trap the shape inside a specified rectangle then you would use a dragBoundFunc or override the new position in the dragmove listener.

The group must be draggable

Normally the click-drag function can only be used on a group if the user places the pointer over a visible shape. The requirement is to make the group draggable even when the user places the pointer on empty space within the group. To achieve this we will place a transparent rect at the ‘bottom’ of the group. The trick is that as a member of the group, this shape will influence the group rectangle size around which we must draw the border – therefore we must ensure that we resize this rect to fit the bounding box of the other shapes in the group.

So what’s so hard?

Glad you asked me that! We could, of course, just stick an extra rect into the group. We could do some math to calculate where to place it taking into account the desired padding. However, the presence of that rect would influence the sizing of the group itself, meaning it would affect the group’s getClientRect() method. That interferes with some of the math we need to do to set the size and position of the border rect. Ideally we would want the border rect to be as unobtrusive as possible. Therefore, the border rect has to live outside of the group, but positioned underneath the group and sized to match its dimensions. The border rect has to be draggable, and has to respond to any transformer or dragging of group shapes.

What did you try?

I tried a solution with the border rect as a member of the group but that got too messy. My final solution has the border rect function wrapped up in a class and it meets the requirements without being over-complex or having any unintended consequences.

Any special points to note?

  • The code includes a neat way to have fill opacity without affecting the stroke. It’s applicable to any Konva.Rect and such a useful technique to know that I put it in its own blog post here.
  • Because the group might be rotated, we have to calculate the corner position of the border rect with padding and rotation. We could solve that with math, but I used the handy Konva.Shape.getAbsoluteTransform().point(pt); feature. With this you give it a point without rotation and it spits back the point that this maps to based on the rotation of the shape in question. It saves a ton of work – I wrote about the technique in detail with a demo here.
  • Getting the group’s un-rotated & un-scaled bounding box was interesting. The Group.getClientRect() method accepts a configuration parameter which can have {skipTransform: true}, which gives the bounding rectangle without any transformations. From that we can work out the size and position of the border rect.
  • Part of the solution involves manipulating the stacking order of the shapes on the stage – in other words the zIndex. Setting zIndex() works like this: if you set shape2’s zIndex to the value of shape1’s zIndex, then shape2 will be positioned immediately ‘below’ shape1, and shape1 and all other shapes hight than it in the z-plane will have their zIndex increased by +1. No two shapes can share the same zIndex. Them’s the rules. I mention this because I got over-complex trying to work out how to get the border rect at group.zIndex() – 1. The I realised the -1 waunnecessaryry and life got easy.

Here’s the code – also available here.

Summary

This was an interesting day out looking at how to position a rect underneath a group and react to the effects of dragging and transforms.

Thanks for reading.

VM April 2022.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: