使用 Konva 连接 HTML5 画布中的对象
如何用线或箭头连接两个对象?
Konva
不能自动连接两个对象并更新其位置。您必须手动更新线的位置。通常,当用户拖动连接的对象之一时,我们需要更新线的位置。在简单的情况下,可以像这样完成:
const obj1 = new Konva.Circle({ ...obj1Props })
const obj2= new Konva.Circle({ ...obj2Props });
const line = new Konva.Line({ ...lineProps });
obj1.on('dragmove', updateLine);
obj2.on('dragmove', updateLine);
function updateLine() {
line.points([obj1.x(), obj1.y(), obj2.x(), obj2.y]);
}
但是在这个演示中,我们将创建一个更复杂的案例,涉及应用程序的状态和多个连接的对象。
说明:尝试拖动圆圈,查看连接如何更新。
- Vanilla
- React
- Vue
import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); // 生成“目标”(圆圈)列表的函数 function generateTargets() { const number = 10; const result = []; while (result.length < number) { result.push({ id: 'target-' + result.length, x: stage.width() * Math.random(), y: stage.height() * Math.random(), }); } return result; } const targets = generateTargets(); // 生成目标之间的箭头的函数 function generateConnectors() { const number = 10; const result = []; while (result.length < number) { const from = 'target-' + Math.floor(Math.random() * targets.length); const to = 'target-' + Math.floor(Math.random() * targets.length); if (from === to) { continue; } result.push({ id: 'connector-' + result.length, from: from, to: to, }); } return result; } function getConnectorPoints(from, to) { const dx = to.x - from.x; const dy = to.y - from.y; let angle = Math.atan2(-dy, dx); const radius = 50; return [ from.x + -radius * Math.cos(angle + Math.PI), from.y + radius * Math.sin(angle + Math.PI), to.x + -radius * Math.cos(angle), to.y + radius * Math.sin(angle), ]; } const connectors = generateConnectors(); // 从应用程序的状态更新画布上的所有对象 function updateObjects() { targets.forEach((target) => { const node = layer.findOne('#' + target.id); node.x(target.x); node.y(target.y); }); connectors.forEach((connect) => { const line = layer.findOne('#' + connect.id); const fromNode = layer.findOne('#' + connect.from); const toNode = layer.findOne('#' + connect.to); const points = getConnectorPoints( fromNode.position(), toNode.position() ); line.points(points); }); } // 为应用程序生成节点 connectors.forEach((connect) => { const line = new Konva.Arrow({ stroke: 'black', id: connect.id, fill: 'black', }); layer.add(line); }); targets.forEach((target) => { const node = new Konva.Circle({ id: target.id, fill: Konva.Util.getRandomColor(), radius: 20 + Math.random() * 20, shadowBlur: 10, draggable: true, }); layer.add(node); node.on('dragmove', () => { // 修改状态 target.x = node.x(); target.y = node.y(); // 根据新状态更新节点 updateObjects(); }); }); updateObjects();
import { useState, useEffect } from 'react'; import { Stage, Layer, Circle, Arrow } from 'react-konva'; const App = () => { // 生成初始目标 const generateTargets = () => { const number = 10; const result = []; while (result.length < number) { result.push({ id: 'target-' + result.length, x: window.innerWidth * Math.random(), y: window.innerHeight * Math.random(), radius: 20 + Math.random() * 20, fill: '#' + Math.floor(Math.random()*16777215).toString(16), }); } return result; }; // 生成目标之间的连接器 const generateConnectors = (targets) => { const number = 10; const result = []; while (result.length < number) { const from = 'target-' + Math.floor(Math.random() * targets.length); const to = 'target-' + Math.floor(Math.random() * targets.length); if (from === to) { continue; } result.push({ id: 'connector-' + result.length, from, to, }); } return result; }; const [targets, setTargets] = useState([]); const [connectors, setConnectors] = useState([]); useEffect(() => { const initialTargets = generateTargets(); setTargets(initialTargets); setConnectors(generateConnectors(initialTargets)); }, []); const getConnectorPoints = (from, to) => { const dx = to.x - from.x; const dy = to.y - from.y; let angle = Math.atan2(-dy, dx); const radius = 50; return [ from.x + -radius * Math.cos(angle + Math.PI), from.y + radius * Math.sin(angle + Math.PI), to.x + -radius * Math.cos(angle), to.y + radius * Math.sin(angle), ]; }; const handleDragMove = (e) => { const id = e.target.id(); setTargets( targets.map((target) => target.id === id ? { ...target, x: e.target.x(), y: e.target.y() } : target ) ); }; return ( <Stage width={window.innerWidth} height={window.innerHeight}> <Layer> {connectors.map((connector) => { const fromNode = targets.find((t) => t.id === connector.from); const toNode = targets.find((t) => t.id === connector.to); if (!fromNode || !toNode) return null; const points = getConnectorPoints(fromNode, toNode); return ( <Arrow key={connector.id} id={connector.id} points={points} fill="black" stroke="black" /> ); })} {targets.map((target) => ( <Circle key={target.id} id={target.id} x={target.x} y={target.y} radius={target.radius} fill={target.fill} shadowBlur={10} draggable onDragMove={handleDragMove} /> ))} </Layer> </Stage> ); }; export default App;
<template> <v-stage :config="stageConfig"> <v-layer> <v-arrow v-for="connector in connectors" :key="connector.id" :config="getArrowConfig(connector)" /> <v-circle v-for="target in targets" :key="target.id" :config="getCircleConfig(target)" @dragmove="handleDragMove" /> </v-layer> </v-stage> </template> <script setup> import { ref, onMounted } from 'vue'; const stageConfig = { width: window.innerWidth, height: window.innerHeight }; // 生成初始目标 const generateTargets = () => { const number = 10; const result = []; while (result.length < number) { result.push({ id: 'target-' + result.length, x: window.innerWidth * Math.random(), y: window.innerHeight * Math.random(), radius: 20 + Math.random() * 20, fill: '#' + Math.floor(Math.random()*16777215).toString(16), }); } return result; }; // 生成目标之 间的连接器 const generateConnectors = (targets) => { const number = 10; const result = []; while (result.length < number) { const from = 'target-' + Math.floor(Math.random() * targets.length); const to = 'target-' + Math.floor(Math.random() * targets.length); if (from === to) { continue; } result.push({ id: 'connector-' + result.length, from, to, }); } return result; }; const targets = ref([]); const connectors = ref([]); onMounted(() => { const initialTargets = generateTargets(); targets.value = initialTargets; connectors.value = generateConnectors(initialTargets); }); const getConnectorPoints = (from, to) => { const dx = to.x - from.x; const dy = to.y - from.y; let angle = Math.atan2(-dy, dx); const radius = 50; return [ from.x + -radius * Math.cos(angle + Math.PI), from.y + radius * Math.sin(angle + Math.PI), to.x + -radius * Math.cos(angle), to.y + radius * Math.sin(angle), ]; }; const getArrowConfig = (connector) => { const fromNode = targets.value.find((t) => t.id === connector.from); const toNode = targets.value.find((t) => t.id === connector.to); if (!fromNode || !toNode) return { points: [0, 0, 0, 0] }; return { id: connector.id, points: getConnectorPoints(fromNode, toNode), fill: 'black', stroke: 'black', }; }; const getCircleConfig = (target) => ({ id: target.id, x: target.x, y: target.y, radius: target.radius, fill: target.fill, shadowBlur: 10, draggable: true, }); const handleDragMove = (e) => { const id = e.target.id(); targets.value = targets.value.map((target) => target.id === id ? { ...target, x: e.target.x(), y: e.target.y() } : target ); }; </script>