目录
- 思路
- 代码
- 效果
本文将使用React、JSX、Rough.js实现一个简单的画布,可以绘制矩形和箭头。
思路
- 每一个图形包括:绘制的类型、起点的x坐标、起点的y坐标、宽、高。调用rough的generator()函数传入图形信息进行绘制,其中对于箭头需要进一步处理:根据宽高确定终点,并且定义角度等生成箭头的另外两条短线。
- 使用 React 的状态管理来跟踪当前拖动的元素和选中的元素类型。创建一个组件提供单选按钮供用户选择绘制的元素类型。添加画布元素:鼠标按下:创建新元素并开始拖动。鼠标抬起:结束拖动并保存元素状态。鼠标移动:更新当前元素的宽度和高度,实时反映在画布上。
代码
import React from "react";
import ReactDOM from "react-dom";
import rough from "roughjs/dist/rough.umd.js"; // 导入 Rough.js 库用于绘制粗糙图形import "./styles.css";var elements = []; // 定义一个数组用于存储绘制的元素// 创建一个新元素的函数
function newElement(type, x, y) {const element = { // 定义元素对象type: type, // 元素类型x: x, // 元素的 x 坐标y: y, // 元素的 y 坐标width: 0, // 元素的宽度height: 0 // 元素的高度};generateShape(element); // 生成元素的形状return element; // 返回元素对象
}// 旋转函数,围绕指定点旋转线段
function rotate(x1, y1, x2, y2, angle) {// 旋转公式:// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.return [(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2, // 计算新的 x 坐标(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2 // 计算新的 y 坐标];
}var generator = rough.generator(); // 创建 Rough.js 生成器实例// 生成元素形状的函数
function generateShape(element) {if (element.type === "rectangle") { // 如果元素是矩形element.shapes = [ // 生成矩形形状并存储generator.rectangle(element.x, element.y, element.width, element.height)];}if (element.type === "arrow") { // 如果元素是箭头const x1 = element.x; // 起点 x 坐标const y1 = element.y; // 起点 y 坐标const x2 = element.x + element.width; // 终点 x 坐标const y2 = element.y + element.height; // 终点 y 坐标const size = 30; // 箭头大小const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); // 计算起点与终点的距离const minSize = Math.min(size, distance / 2); // 确保箭头不会太小const xs = x2 - ((x2 - x1) / distance) * minSize; // 箭头基础线的 x 坐标const ys = y2 - ((y2 - y1) / distance) * minSize; // 箭头基础线的 y 坐标const angle = 20; // 箭头的角度const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180); // 计算箭头左侧的点const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180); // 计算箭头右侧的点element.shapes = [ // 生成箭头的形状并存储generator.line(x1, y1, x2, y2), // 主线generator.line(x3, y3, x2, y2), // 左侧箭头线generator.line(x4, y4, x2, y2) // 右侧箭头线];}
}// 主应用组件
function App() {var [draggingElement, setDraggingElement] = React.useState(null); // 用于跟踪拖动的元素var [elementType, setElementType] = React.useState("arrow"); // 当前选择的元素类型// 元素选项组件function ElementOption({ type, children }) {return (<label><inputtype="radio" // 单选按钮checked={elementType === type} // 选中状态onChange={() => setElementType(type)} // 更改元素类型/>{children} // 显示选项文本</label>);}return (<div><ElementOption type="rectangle">Rectangle</ElementOption> // 矩形选项<ElementOption type="arrow">Arrow</ElementOption> // 箭头选项<canvasid="canvas" // 画布 IDwidth={window.innerWidth} // 画布宽度height={window.innerHeight} // 画布高度onMouseDown={e => { // 鼠标按下事件const element = newElement( // 创建新元素elementType, // 使用当前选择的类型e.clientX - e.target.offsetLeft, // 计算 x 坐标e.clientY - e.target.offsetTop // 计算 y 坐标);elements.push(element); // 将元素添加到数组setDraggingElement(element); // 设置当前拖动的元素drawScene(); // 绘制场景}}onMouseUp={e => { // 鼠标抬起事件setDraggingElement(null); // 清空拖动的元素drawScene(); // 绘制场景}}onMouseMove={e => { // 鼠标移动事件if (!draggingElement) return; // 如果没有拖动的元素,退出draggingElement.width = // 更新元素宽度e.clientX - e.target.offsetLeft - draggingElement.x;draggingElement.height = // 更新元素高度e.clientY - e.target.offsetTop - draggingElement.y;generateShape(draggingElement); // 生成更新后的形状drawScene(); // 绘制场景}}/></div>);
}const rootElement = document.getElementById("root"); // 获取根元素// 绘制场景的函数
function drawScene() {ReactDOM.render(<App />, rootElement); // 渲染应用到根元素const canvas = document.getElementById("canvas"); // 获取画布const rc = rough.canvas(canvas); // 创建 Rough.js 画布实例canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); // 清空画布elements.forEach(element => { // 遍历所有元素element.shapes.forEach(shape => rc.draw(shape)); // 绘制每个形状});
}drawScene(); // 初始绘制场景