How to Limit Dragging and Resizing of Shapes by Canvas Stage
This demo demonstrates how to restrict dragging and resizing of shapes to stay within the boundaries of the canvas stage. By implementing custom boundary functions, we can prevent shapes from being moved or resized outside the visible area.
The implementation combines techniques from the Drag Limit Demo and Resize Limit Demo to add restrictions to user interactions.
Instructions: Try to rotate, drag, or resize the shapes. Notice how they are constrained to stay within the canvas boundaries.
- Vanilla
- React
- Vue
import Konva from 'konva';
// Helper functions for calculating bounding boxes
function getCorner(pivotX, pivotY, diffX, diffY, angle) {
const distance = Math.sqrt(diffX * diffX + diffY * diffY);
// Find angle from pivot to corner
angle += Math.atan2(diffY, diffX);
// Get new x and y coordinates
const x = pivotX + distance * Math.cos(angle);
const y = pivotY + distance * Math.sin(angle);
return { x, y };
}
// Calculate client rect accounting for rotation
function getClientRect(rotatedBox) {
const { x, y, width, height } = rotatedBox;
const rad = rotatedBox.rotation;
const p1 = getCorner(x, y, 0, 0, rad);
const p2 = getCorner(x, y, width, 0, rad);
const p3 = getCorner(x, y, width, height, rad);
const p4 = getCorner(x, y, 0, height, rad);
const minX = Math.min(p1.x, p2.x, p3.x, p4.x);
const minY = Math.min(p1.y, p2.y, p3.y, p4.y);
const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);
const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
// Calculate total bounding box of multiple shapes
function getTotalBox(boxes) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
boxes.forEach((box) => {
minX = Math.min(minX, box.x);
minY = Math.min(minY, box.y);
maxX = Math.max(maxX, box.x + box.width);
maxY = Math.max(maxY, box.y + box.height);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
// Set up the stage
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create first shape (red rectangle)
const shape1 = new Konva.Rect({
x: stage.width() / 2 - 60,
y: stage.height() / 2 - 60,
width: 50,
height: 50,
fill: 'red',
draggable: true,
});
layer.add(shape1);
// Create second shape (green rectangle)
const shape2 = shape1.clone({
x: stage.width() / 2 + 10,
y: stage.height() / 2 + 10,
fill: 'green',
});
layer.add(shape2);
// Add transformer that includes both shapes
const tr = new Konva.Transformer({
nodes: [shape1, shape2],
// Set boundary function for resize operations
boundBoxFunc: (oldBox, newBox) => {
// Calculate the actual bounding box of the transformed shape
const box = getClientRect(newBox);
// Check if the new box is outside the stage boundaries
const isOut =
box.x < 0 ||
box.y < 0 ||
box.x + box.width > stage.width() ||
box.y + box.height > stage.height();
// If outside boundaries, keep the old box
if (isOut) {
return oldBox;
}
// If within boundaries, allow the transformation
return newBox;
},
});
layer.add(tr);
// Handle drag events to keep shapes within the stage
tr.on('dragmove', () => {
// Get client rects for all selected nodes
const boxes = tr.nodes().map((node) => node.getClientRect());
// Get the total bounding box of all shapes
const box = getTotalBox(boxes);
// Keep shapes within stage boundaries
tr.nodes().forEach((shape) => {
const absPos = shape.getAbsolutePosition();
// Calculate shape position relative to group bounding box
const offsetX = box.x - absPos.x;
const offsetY = box.y - absPos.y;
// Adjust position if outside boundaries
const newAbsPos = { ...absPos };
if (box.x < 0) {
newAbsPos.x = -offsetX;
}
if (box.y < 0) {
newAbsPos.y = -offsetY;
}
if (box.x + box.width > stage.width()) {
newAbsPos.x = stage.width() - box.width - offsetX;
}
if (box.y + box.height > stage.height()) {
newAbsPos.y = stage.height() - box.height - offsetY;
}
shape.setAbsolutePosition(newAbsPos);
});
});
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Rect, Transformer } from 'react-konva';
// Helper functions for calculating bounding boxes
const getCorner = (pivotX, pivotY, diffX, diffY, angle) => {
const distance = Math.sqrt(diffX * diffX + diffY * diffY);
angle += Math.atan2(diffY, diffX);
const x = pivotX + distance * Math.cos(angle);
const y = pivotY + distance * Math.sin(angle);
return { x, y };
};
const getClientRect = (rotatedBox) => {
const { x, y, width, height } = rotatedBox;
const rad = rotatedBox.rotation;
const p1 = getCorner(x, y, 0, 0, rad);
const p2 = getCorner(x, y, width, 0, rad);
const p3 = getCorner(x, y, width, height, rad);
const p4 = getCorner(x, y, 0, height, rad);
const minX = Math.min(p1.x, p2.x, p3.x, p4.x);
const minY = Math.min(p1.y, p2.y, p3.y, p4.y);
const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);
const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
};
const getTotalBox = (boxes) => {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
boxes.forEach((box) => {
minX = Math.min(minX, box.x);
minY = Math.min(minY, box.y);
maxX = Math.max(maxX, box.x + box.width);
maxY = Math.max(maxY, box.y + box.height);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
};
const LimitedDragAndResize = () => {
const [stageSize, setStageSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const [shapes, setShapes] = useState([
{
id: 'rect1',
x: window.innerWidth / 2 - 60,
y: window.innerHeight / 2 - 60,
width: 50,
height: 50,
fill: 'red',
rotation: 0,
scaleX: 1,
scaleY: 1,
},
{
id: 'rect2',
x: window.innerWidth / 2 + 10,
y: window.innerHeight / 2 + 10,
width: 50,
height: 50,
fill: 'green',
rotation: 0,
scaleX: 1,
scaleY: 1,
}
]);
const shapeRefs = useRef(new Map());
const trRef = useRef(null);
// Set up Transformer after the layer mounts
useEffect(() => {
if (trRef.current) {
const nodes = shapes.map(shape => shapeRefs.current.get(shape.id));
trRef.current.nodes(nodes);
}
}, [shapes]);
// Handle window resize
useEffect(() => {
const handleResize = () => {
setStageSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Boundary function for Transformer
const boundBoxFunc = (oldBox, newBox) => {
const box = getClientRect(newBox);
const isOut =
box.x < 0 ||
box.y < 0 ||
box.x + box.width > stageSize.width ||
box.y + box.height > stageSize.height;
if (isOut) {
return oldBox;
}
return newBox;
};
const syncShapeState = () => {
setShapes(currentShapes => currentShapes.map(shape => {
const node = shapeRefs.current.get(shape.id);
if (!node) return shape;
return {
...shape,
x: node.x(),
y: node.y(),
rotation: node.rotation(),
scaleX: node.scaleX(),
scaleY: node.scaleY(),
};
}));
};
// Handle drag for transformer group
const handleTransformerDrag = (e) => {
if (!trRef.current) return;
const nodes = trRef.current.nodes();
if (nodes.length === 0) return;
const boxes = nodes.map(node => node.getClientRect());
const box = getTotalBox(boxes);
nodes.forEach(shape => {
const absPos = shape.getAbsolutePosition();
const offsetX = box.x - absPos.x;
const offsetY = box.y - absPos.y;
const newAbsPos = { ...absPos };
if (box.x < 0) {
newAbsPos.x = -offsetX;
}
if (box.y < 0) {
newAbsPos.y = -offsetY;
}
if (box.x + box.width > stageSize.width) {
newAbsPos.x = stageSize.width - box.width - offsetX;
}
if (box.y + box.height > stageSize.height) {
newAbsPos.y = stageSize.height - box.height - offsetY;
}
shape.setAbsolutePosition(newAbsPos);
});
syncShapeState();
};
return (
<Stage width={stageSize.width} height={stageSize.height}>
<Layer>
{shapes.map(shape => (
<Rect
key={shape.id}
ref={(node) => {
if (node) shapeRefs.current.set(shape.id, node);
}}
x={shape.x}
y={shape.y}
width={shape.width}
height={shape.height}
fill={shape.fill}
rotation={shape.rotation}
scaleX={shape.scaleX}
scaleY={shape.scaleY}
draggable
onDragMove={handleTransformerDrag}
onDragEnd={syncShapeState}
/>
))}
<Transformer
ref={trRef}
boundBoxFunc={boundBoxFunc}
onDragMove={handleTransformerDrag}
onDragEnd={syncShapeState}
onTransformEnd={syncShapeState}
/>
</Layer>
</Stage>
);
};
export default LimitedDragAndResize;
<template>
<v-stage :config="stageConfig">
<v-layer>
<v-rect
v-for="(rect, i) in rectangles"
:key="i"
:config="rect"
@dragmove="handleRectDragMove"
/>
<v-transformer
ref="transformerRef"
:config="transformerConfig"
@dragmove="handleTransformerDragMove"
/>
</v-layer>
</v-stage>
</template>
<script>
export default {
data() {
return {
stageConfig: {
width: window.innerWidth,
height: window.innerHeight,
},
rectangles: [
{
x: window.innerWidth / 2 - 60,
y: window.innerHeight / 2 - 60,
width: 50,
height: 50,
fill: 'red',
draggable: true,
id: 'rect1',
name: 'my-rect'
},
{
x: window.innerWidth / 2 + 10,
y: window.innerHeight / 2 + 10,
width: 50,
height: 50,
fill: 'green',
draggable: true,
id: 'rect2',
name: 'my-rect'
}
],
transformerConfig: {
nodes: [],
}
};
},
mounted() {
// Setup transformer nodes after component is mounted
this.$nextTick(() => {
const transformer = this.$refs.transformerRef.getNode();
const rects = transformer.getStage().find('.my-rect');
// Set transformer to work with both rectangles
transformer.nodes(rects);
// Add boundary function for transformer
transformer.boundBoxFunc(this.boundBoxFunc);
});
// Handle window resize
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
getCorner(pivotX, pivotY, diffX, diffY, angle) {
const distance = Math.sqrt(diffX * diffX + diffY * diffY);
angle += Math.atan2(diffY, diffX);
const x = pivotX + distance * Math.cos(angle);
const y = pivotY + distance * Math.sin(angle);
return { x, y };
},
getClientRect(rotatedBox) {
const { x, y, width, height } = rotatedBox;
const rad = rotatedBox.rotation || 0;
const p1 = this.getCorner(x, y, 0, 0, rad);
const p2 = this.getCorner(x, y, width, 0, rad);
const p3 = this.getCorner(x, y, width, height, rad);
const p4 = this.getCorner(x, y, 0, height, rad);
const minX = Math.min(p1.x, p2.x, p3.x, p4.x);
const minY = Math.min(p1.y, p2.y, p3.y, p4.y);
const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);
const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
},
getTotalBox(boxes) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
boxes.forEach((box) => {
minX = Math.min(minX, box.x);
minY = Math.min(minY, box.y);
maxX = Math.max(maxX, box.x + box.width);
maxY = Math.max(maxY, box.y + box.height);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
},
boundBoxFunc(oldBox, newBox) {
const box = this.getClientRect(newBox);
const isOut =
box.x < 0 ||
box.y < 0 ||
box.x + box.width > this.stageConfig.width ||
box.y + box.height > this.stageConfig.height;
if (isOut) {
return oldBox;
}
return newBox;
},
handleTransformerDragMove(e) {
const transformer = this.$refs.transformerRef.getNode();
const nodes = transformer.nodes();
if (!nodes.length) return;
const boxes = nodes.map(node => node.getClientRect());
const box = this.getTotalBox(boxes);
nodes.forEach(shape => {
const absPos = shape.getAbsolutePosition();
const offsetX = box.x - absPos.x;
const offsetY = box.y - absPos.y;
const newAbsPos = { x: absPos.x, y: absPos.y };
if (box.x < 0) {
newAbsPos.x = -offsetX;
}
if (box.y < 0) {
newAbsPos.y = -offsetY;
}
if (box.x + box.width > this.stageConfig.width) {
newAbsPos.x = this.stageConfig.width - box.width - offsetX;
}
if (box.y + box.height > this.stageConfig.height) {
newAbsPos.y = this.stageConfig.height - box.height - offsetY;
}
shape.setAbsolutePosition(newAbsPos);
});
},
handleRectDragMove(e) {
// Individual rect drag handling is handled by transformer
},
handleResize() {
this.stageConfig.width = window.innerWidth;
this.stageConfig.height = window.innerHeight;
}
}
}
</script>