如何限制形状在画布舞台上的拖动和调整大小
该演示展示了如何限制形状的拖动和调整大小,使其保持在画布舞台的边界内。通过实现自定义边界函数,我们可以防止形状移动或调整到可见区域之外。
该实现结合了拖动限制演示和调整大小限制演示中的技术,以增加对用户交互的限制。
说明: 尝试旋转、拖动或调整形状的大小。注意它们如何被限制在画布边界内。
- 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', }, { id: 'rect2', x: window.innerWidth / 2 + 10, y: window.innerHeight / 2 + 10, width: 50, height: 50, fill: 'green', } ]); 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 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); }); }; 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} draggable /> ))} <Transformer ref={trRef} boundBoxFunc={boundBoxFunc} onDragMove={handleTransformerDrag} /> </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>