Skip to main content

如何限制形状在画布舞台上的拖动和调整大小

该演示展示了如何限制形状的拖动和调整大小,使其保持在画布舞台的边界内。通过实现自定义边界函数,我们可以防止形状移动或调整到可见区域之外。

该实现结合了拖动限制演示调整大小限制演示中的技术,以增加对用户交互的限制。

说明: 尝试旋转、拖动或调整形状的大小。注意它们如何被限制在画布边界内。

import Konva from 'konva';

// 计算边界框的辅助函数
function getCorner(pivotX, pivotY, diffX, diffY, angle) {
  const distance = Math.sqrt(diffX * diffX + diffY * diffY);

  // 从枢轴到拐角的角度
  angle += Math.atan2(diffY, diffX);

  // 获取新的 x 和 y 坐标
  const x = pivotX + distance * Math.cos(angle);
  const y = pivotY + distance * Math.sin(angle);

  return { x, y };
}

// 计算考虑旋转的客户端矩形
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,
  };
}

// 计算多个形状的总边界框
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,
  };
}

// 设置舞台
const stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight,
});

const layer = new Konva.Layer();
stage.add(layer);

// 创建第一个形状(红色矩形)
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);

// 创建第二个形状(绿色矩形)
const shape2 = shape1.clone({
  x: stage.width() / 2 + 10,
  y: stage.height() / 2 + 10,
  fill: 'green',
});
layer.add(shape2);

// 添加包括两个形状的变形器
const tr = new Konva.Transformer({
  nodes: [shape1, shape2],
  // 设置调整大小操作的边界函数
  boundBoxFunc: (oldBox, newBox) => {
    // 计算变换后形状的实际边界框
    const box = getClientRect(newBox);
    
    // 检查新框是否超出了舞台边界
    const isOut =
      box.x < 0 ||
      box.y < 0 ||
      box.x + box.width > stage.width() ||
      box.y + box.height > stage.height();

    // 如果超出边界,保持旧框
    if (isOut) {
      return oldBox;
    }
    
    // 如果在边界内,允许变换
    return newBox;
  },
});
layer.add(tr);

// 处理拖动事件以保持形状在舞台内
tr.on('dragmove', () => {
  // 获取所有选定节点的客户端矩形
  const boxes = tr.nodes().map((node) => node.getClientRect());
  
  // 获取所有形状的总边界框
  const box = getTotalBox(boxes);
  
  // 保持形状在舞台边界内
  tr.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 > 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);
  });
});