如何限制形状在画布舞台上的拖动和调整大小
该演示展示了如何限制形状的拖动和调整大小,使其保持在画布舞台的边界内。通过实现自定义边界函数,我们可以防止形状移动或调整到可见区域之外。
该实现结合了 拖动限制演示 和 调整大小限制演示 中的技术,以增加对用户交互的限制。
说明: 尝试旋转、拖动或调整形状的大小。注意它们如何被限制在画布边界内。
- Vanilla
- React
- Vue
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);
});
});
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Rect, Transformer } from 'react-konva';
// 计算边界框的辅助函数
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);
// 在层装载后设置变形器
useEffect(() => {
if (trRef.current) {
const nodes = shapes.map(shape => shapeRefs.current.get(shape.id));
trRef.current.nodes(nodes);
}
}, [shapes]);
// 处理窗口大小调整
useEffect(() => {
const handleResize = () => {
setStageSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 变形器的边界函数
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(),
};
}));
};
// 处理变形器组的拖动
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() {
// 在组件挂载后设置变形器节点
this.$nextTick(() => {
const transformer = this.$refs.transformerRef.getNode();
const rects = transformer.getStage().find('.my-rect');
// 设置变形器以处理两个矩形
transformer.nodes(rects);
// 为变形器添加边界函数
transformer.boundBoxFunc(this.boundBoxFunc);
});
// 处理窗口大小调整
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) {
// 单个矩形的拖动处理由变形器处理
},
handleResize() {
this.stageConfig.width = window.innerWidth;
this.stageConfig.height = window.innerHeight;
}
}
}
</script>