React实现画布——可绘制矩形和箭头
本文将使用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>
<input
type="radio" // 单选按钮
checked={elementType === type} // 选中状态
onChange={() => setElementType(type)} // 更改元素类型
/>
{children} // 显示选项文本
</label>
);
}
return (
<div>
<ElementOption type="rectangle">Rectangle</ElementOption> // 矩形选项
<ElementOption type="arrow">Arrow</ElementOption> // 箭头选项
<canvas
id="canvas" // 画布 ID
width={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(); // 初始绘制场景