The Konva group is a bit hard to pin-down, almost a normal shape but not quite. Its super-powers can be a bit challenging though once you understand a few fundamentals its got your back. Lets dive in...
A few immediate pointers about the superpowers of the Group which has:
- Position, but size (width or height) is irrelevant
- No visible attributes - no stroke or fill
- Can't be dragged except by dragging a child shape
A Group has position but width&height are irrelevant
When a group is added to a layer, its position is always {x: 0, y: 0} unless you changed it in the creation step. You can change its position by setting its x(), y() or position() attributes just like a normal shape.
But though it has them, a Group's width & height are largely irrelevant since the group has no visible attributes and the empty space between its child shapes is not listening for events. The group size will not change as shapes are added, so don't rely on these attributes for measuring the group size.
But since it inherits from the Container, it has the special getClientRect() method that returns the smallest box that entirely encloses the child shapes of the group. This is what you'll find useful in most practical cases. More about that later.
A group has no visible attributes
There is no direct way to add a border (stroke) or fill to a group. Which is kind-of the whole point of groups - they stay quietly invisible but enable you to manage their child shapes as a collection. If you 'do' want a border and fill then jump to the end of this article where I show a simple way to achieve this.
A group can't be dragged except by dragging a child shape
Nope - the empty space between the child shapes of a group does not receive events, so it cannot be used to click-and-drag on. Again jump to the end of this article to find the simple workaround.
Adding shapes to a group
The sequence is not important - whether you add shapes to the layer then to a group or add shapes direct into a group - the position of the shape is made relative to the position of the group. Much of the time you will be adding a group 'around' some shapes and since the group will be positioned at (0, 0) then you will not notice the group-position offset effect, but if you do reposition the group then you might get a brain jolt when the shapes appear to be translated to some random location.
Introducing the demo setup
Here's the setup for some experimenting. The blue rect and pink circle are added to a group with their draggable attribute set false . The group is configured with a starting position of { x: 150, y: 40}. To illustrate the bounding box of the Group I add a rect with a green stroke to the layer and move it under the Group sized to the same dimensions as the Group.getClientRect(). The red rect is contrived to show the Group.position() and extend to its Group.clientRect(). The code for this demo is in this CodePen.
Dragging the entire group
So we know the group is there, and we can visualise it as a rect region enclosing its child shapes (the green rect here but normally invisible) - but you can't click on it directly, and you can't drag it directly - you have to do these things on the groups child shapes.
Aside: Group.listening()
Having said that you can't click it or drag it, why then does the Group have a listening() attribute ? The Group.listening() setting controls listening on all the child shapes of the group. Set it to false and all the shape's listeners are disabled.
Lets get on with the dragging...
So, I mousedown on the Circle - the top line of the text gives some information about the Circle's location. Notice the circles position {x: 200, y: 100} is not changing even though the group is being dragged. This is because the circle's position is relative to the group. The absolute position of the Circle, meaning the position on the stage, has an x value of 350, which is the 150 from the position of the group plus the 200 from the position of the Circle itself. Same idea for the y dimension. [Note that the absolute position of the circle will change with the drag - I just wanted to convey the values at click-time here]
Another aspect illustrated by the video is on the second line of text where we can see information about the groups initial position and its current position during the drag operation. The current position changes as the group is dragged, so dragging a group changes its position - at least that makes some sense!
Now look at the third line of text which shows information about the group's clientRect. We can see that dragging the group as a whole changes only the x & y of the clientRect but not the dimensions. A mini conclusion is that we'll generally be best using the position and size components of the Group.getClientRect() instead of the Group.position() and Group.size() values for most of our work with groups.
Dragging a shape in the group
We already looked at dragging the whole group - now lets look at what is happening when we are dragging a shape that is a child in the group. Setting draggable=true on the rect and circle shapes allow them to be dragged without moving the group.
Watch the top line of text which shows how the rect's position changes and goes negative when the rect is dragged 'outside' the group. So the child shapes of a group 'can' be 'outside' of the group but their position stays connected to the position of the group.
Another thing to note is that the group's clientRect (green box) expands to enclose the dragged shape but its position stays unchanged (red box). Probably underlining that Group.getClientRect() is more practically useful than Group.position() and group.size().
Groups & Clipping
Just while we're looking at dragging a shape outside the group we should touch on the group.clip({x, y, width, height}) attribute. This allows the configuration of a rect, relative to the position of the group, which will act as a clipping region for the group. What that means is that a shape or part of a shape that is outside of the clipping region will be, well, clipped - or chopped out of view.
IMPORTANT - the Group.getClientRect() function does NOT respect the effect of clipping. What this means is that if you have any child shape that has any clipping then the size, and maybe the (x, y) of the group that you get via getClientRect() will be wrong.
Using a transparent events rect / catching group events on empty space
Quick answer - it's not possible to capture events on the empty space in a group. There is, however, a simple solution:
- create a new Rect which is positioned and sized the same as the group.getClientRect()
- make the rect opacity 0 to make it transparent
- set listening = true for this rect
- and after adding the rect to the group, use rect.moveToBottom to place the events rect underneath all other shapes in the group.
- Now set up your listeners for the new rect, et voila.
Here's an example where the I've filled the events rect with lime and made it almost transparent so that we can see it for the demo. And as a result I can now click and drag on the group's empty space.
If you actually need to show a background color or border on your group then leave the opacity of the events rect unchanged and add the fill and stroke settings that you need.
Group Caching
One useful feature of groups is caching. The docs say that "we cache nodes to improve drawing performance, apply filters, or create more accurate hit regions.". Besides performance you might be wondering if a cached group has event detection on the empty space in the group? Answer is no - you still need to use the shapes for dragging even a cached group, or throw a transparent rect into the group as the bottom element and detect events on that, as explained above.
Note that the Group.cache() method includes a bunch of config parameters. One on these is 'drawBorder' which will draw a red box around the cached group. You might find this useful to get a visual confirmation that a group is actually cached.
Summary
We've seen that it's most practical to think clientRect() when dealing with the location and size of a group. And that a rectangle can be added to a group to make it's empty space apparently clickable and draggable.
Thanks for reading.
VW July 2021
Photo by Martin Katler on Unsplash