konvajs / react-konva

React + Canvas = Love. JavaScript library for drawing complex canvas graphics using React.
https://konvajs.github.io/docs/react/
MIT License
5.8k stars 260 forks source link

How to achieve a layered separation between children of the same group? #782

Closed MarcusOy closed 1 year ago

MarcusOy commented 1 year ago

Hi there,

This is a bit of a design question that I'm running into regarding the z-index of my complex components (specific to react-konva).

Hypothetically, let's say I have my component, which consists of a Circle and a Wedge (it could be up to 10 types of layers): Screenshot 2023-11-28 at 10 58 43 AM

Let's represent this component as such:

<Group>
  <Circle />
  <Wedge opacity={0.75} />
  ...
</Group>

My goal would be to render all the Wedge children, then all the Circle children so that all Circle components are not covered by Wedge components to ensure visibility. Otherwise, this is what would happen: Screenshot 2023-11-28 at 11 03 31 AM

Because konva-react relies on the React node ordering for z-index, my original intuition was to return my complex components already split into pieces:

...
return {
  Circle: <Circle />,
  Wedge: <Wedge />
}

Then recombining the components later:

...
<Layer name="layer-circles">
  {components.map(c => c.Circle)}
</Layer>
<Layer name="layer-wedges" opacity={0.75}>
  {components.map(c => c.Wedge)}
</Layer>

A bonus would be to reduce the opacity of the layer-wedges layer rather than the Wedge components themselves, so that they would look like one contiguous shape while still allowing translucency

This does not work, as these individual components are no longer underneath a single Group Parent, which means I'll have to do a lot of manual work to get group movement + other features working again.

I also entertained the use of portals, but this also ignores the parent Group.

What would be the best way to achieve a layered separation between children of a group?

lavrton commented 1 year ago

I think the only solution is to use Portals. What did you try? What is the issue with "but this also ignores the parent Group"?

MarcusOy commented 1 year ago

I think the only solution is to use Portals. What did you try? What is the issue with "but this also ignores the parent Group"?

I tried the following:

<Group draggable x={props.x} y={props.y}>
  <Portal selector=".layer-circles">
    <Circle />
  </Portal>
  <Portal selector=".layer-wedges">
    <Wedge />
  </Portal>
  ...
</Group>

Then to put them back together at the stage level:

<Stage>
  <Layer name="layer-wedges"/>
  <Layer name="layer-circles"/>
</Stage>

The group did not respond to drag attempts. Also, I had a test with n complex components, but only the first (or last) component rendered.

lavrton commented 1 year ago

Please make a small demo. I think you need to put only circles into the portal.

MarcusOy commented 1 year ago

Please make a small demo. I think you need to put only circles into the portal.

I put together a small demo: https://codesandbox.io/p/sandbox/icy-shape-352chk Within this demo, I’ve created the hypothetical component (from above) in two ways: the standard way, then with portals. You can see the traditional konva-react components rendered on x=100 behaving as normal. You can see the “portalized” components (which are supposed to be on x=200) bunched up into the top left corner (hence ignoring parent group transformations)

Also, I want to be able to do this for any number of layers (which is why I'm doing both the Circle and Wedge).

Lastly, I appreciate your time on this. You've done an awesome job creating this library!

lavrton commented 1 year ago

Not very elegant. But this may work:

const PortaledComplexComponent = (props) => {
  const [pos, setPos] = React.useState({ x: props.x, y: props.y });

  // sync local state with props
  React.useEffect(() => {
    setPos({ x: props.x, y: props.y });
  }, [props.x, props.y]);

  const handleDragMove = (e) => {
    setPos(e.target.position());
  };
  return (
    <Group x={pos.x} y={pos.y} draggable onDragMove={handleDragMove}>
      <Wedge radius={80} angle={60} rotation={-30} fill="#90EE90" />
      <Portal selector=".layer-circles" enabled>
        <Circle
          radius={15}
          fill="green"
          x={pos.x}
          y={pos.y}
          draggable
          onDragMove={handleDragMove}
        />
      </Portal>
    </Group>
  );
};
MarcusOy commented 1 year ago

Not very elegant. But this may work...

Thank you for your solution! Seems like a combination of Portals and manual state management of transformations is the way to go here. I've updated the codesandbox above to include your solution (labeled as green ImprovedPortaledComplexComponents) for posterity.

Going to close this issue, but I was looking at the portal.tsx source code and it looks like the overhead for using Portal components includes:

Wanted to ask: any major KonvaJS-specific performance implications regarding any of this overhead?

lavrton commented 1 year ago

There should be no much overhead. As most of these actions will be called once on mount.

If you relatively small number of targets, it should work just fine.

MarcusOy commented 1 year ago

There should be no much overhead. As most of these actions will be called once on mount.

If you relatively small number of targets, it should work just fine.

Awesome, good to hear! My use case will have lots of targets, so I will let you know if I run into performance issues.

Thanks again for letting me pick your brain!

lavrton commented 1 year ago

You can try to use one layer. And use portal to put into different groups. (So just use groups for layering instead of layers). I am not sure it will help, but it may.