Skip to main content

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.

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);
  });
});