Description:
When attempting to create a workspace inside a clipped layer, we are encountering an issue where the transformer aligns itself relative to the clipped layer’s origin rather than the canvas (stage) coordinates. This causes transformed shapes to be offset within the clipped layer, making positioning inaccurate relative to the full canvas. We suspect the issue lies in how we’re applying coordinate transformations within our code, especially around the clipped layer boundaries.
Steps to Reproduce: Set up a React component with Konva, using the provided code.
Add a clipped layer to the canvas (Konva stage) and populate it with a canvas rectangle, shapes, and a transformer.
Attempt to move or resize shapes using the transformer.
Observed Behavior:
The transformer tool calculates its position relative to the origin of the clipped layer, rather than the canvas (stage), causing alignment issues for shapes within the clipped area.
const Workspace: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: containerRef.current!,
width: width,
id: 'stage',
height: height,
});
const clipX = (width - 500) / 2;
const clipY = (height - 500) / 2;
const canvasHeight = 500;
const canvasWidth = 500;
stage.container().style.backgroundColor = '#ededed';
const layer = new Konva.Layer({
x: clipX,
y: clipY,
clipX: 0,
clipY: 0,
clipWidth: canvasWidth,
clipHeight: canvasHeight,
});
stage.add(layer);
const canvas = new Konva.Rect({
id: 'canvas',
fill: 'white',
height: canvasHeight,
width: canvasWidth,
listening: false,
});
layer.add(canvas);
const transformer = new Konva.Transformer({
id: 'transformer',
});
layer.add(transformer);
const rect1 = new Konva.Rect({
x: 60,
y: 60,
width: 50,
height: 50,
fill: 'royalblue',
name: 'rect',
id: 'rect1',
draggable: true,
});
layer.add(rect1);
const text = new Konva.Text({
id: 'text',
y: 300,
x: 300,
fontSize: 20,
text: 'Test',
fill: '#a43',
});
layer.add(text);
// Selection logic
const selectionRectangle = new Konva.Rect({
fill: 'rgba(41, 171, 241, 0.5)',
visible: false,
listening: false,
});
layer.add(selectionRectangle);
let x1: number, y1: number, x2: number, y2: number, selecting = false;
stage.on('mousedown touchstart', (e) => {
if (!['canvas', 'stage'].includes(e.target.attrs.id)) return;
e.evt.preventDefault();
x1 = stage.getPointerPosition()!.x;
y1 = stage.getPointerPosition()!.y;
x2 = stage.getPointerPosition()!.x;
y2 = stage.getPointerPosition()!.y;
selectionRectangle.width(0);
selectionRectangle.height(0);
selecting = true;
});
stage.on('mousemove touchmove', (e) => {
if (!selecting) return;
e.evt.preventDefault();
x2 = stage.getPointerPosition()!.x;
y2 = stage.getPointerPosition()!.y;
selectionRectangle.setAttrs({
visible: true,
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});
});
stage.on('mouseup touchend', (e) => {
selecting = false;
if (!selectionRectangle.visible()) return;
e.evt.preventDefault();
selectionRectangle.visible(false);
const shapes = stage.find('Text');
const box = selectionRectangle.getClientRect();
const selected = shapes.filter((shape) =>
Konva.Util.haveIntersection(box, shape.getClientRect()),
);
transformer.nodes(selected);
});
stage.on('click tap', (e) => {
if (selectionRectangle.visible()) return;
if (e.target === stage) {
transformer.nodes([]);
return;
}
if (!e.target.hasName('rect')) return;
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
const isSelected = transformer.nodes().indexOf(e.target) >= 0;
if (!metaPressed && !isSelected) {
transformer.nodes([e.target]);
} else if (metaPressed && isSelected) {
const nodes = transformer.nodes().slice();
nodes.splice(nodes.indexOf(e.target), 1);
transformer.nodes(nodes);
} else if (metaPressed && !isSelected) {
const nodes = transformer.nodes().concat([e.target]);
transformer.nodes(nodes);
}
});
return () => {
stage.destroy();
};
}, []);
return <div ref={containerRef} />;
};
Expected Behavior: The transformer should respect the canvas (stage) coordinates and accurately display transformations within the clipped area.
2
Answers
Edit: Can we clarify what the issue is please. Is it that the selection rectangle is offset, as in the video below?
If so then this is because the selection rectangle is a child of the layer. The hierarchy is stage > layer > shape. Because your code sets the (x,y) of the layer to be (x: clipX, y: clipY) but then in the selection logic you get the mouse position relative to the stage in
You could switch that to
To solve the issue.
Hi and welcome to SO and the Konva community.
The transformer is a child of Layer, not Stage.
In your creation of the Layer, try
and now do all the related work from that perspective, which will provide co-ordinates based on the Layer origin which itself is aligned with the stage origin, as it seems you require.
I switched your code into plain JS and made a working snippet.
This is because your selection rectangle is a child of the clipped layer.
Create a new unclipped layer that fits the entire stage dimension and move the selection rectangle to it.