一、效果
二、内容
- canvas绘制基本流程
- 生成指定范围随机数
- 在浏览器每一帧绘制的时机做一些自己的逻辑
- 处理canvas元素的拖拽
- 画布元素的简单碰撞检测
- 编写线性逻辑的代码
三、在线
四、代码
import { useEffect, useRef } from "react";
// 圆
class Circle {
constructor(x, y, r, randomColor) {
this.x = x;
this.y = y;
this.r = r;
this.color = randomColor;
this.rotate = 0;
}
draw(ctx) {
if (!ctx) {
return;
}
ctx.save();
ctx.beginPath();
// 动态颜色
ctx.fillStyle = this.color;
// 动态角度
ctx.arc(this.x, this.y, this.r, 0 + this.rotate, 1 * Math.PI + this.rotate);
ctx.fill();
ctx.restore();
}
}
const Panel = () => {
const refCanvas = useRef();
const store = useRef({
nodeList: [], // 存放画布添加的圆
preX: 0, // 拖拽前的圆X偏移
preY: 0, // 拖拽前的圆Y偏移
target: null, // 当前正在拖拽中的圆
});
const draw = () => {
const ctx = refCanvas.current.getContext("2d");
ctx.clearRect(0, 0, 500, 500); // 绘制前先清空
store.current?.nodeList?.forEach?.((item: any) => {
item.rotate += 0.02;
item.draw(ctx); // 调用每一个圆的绘制方法
});
// 启动绘制监测
requestAnimationFrame(draw);
};
useEffect(() => {
draw();
}, []);
// 获取鼠标偏移坐标
const getPos = (e: any) => {
const { offsetX, offsetY } = e.nativeEvent;
return {
x: offsetX,
y: offsetY,
};
};
// 生成随机坐标
const genPos = () => {
// [r, 500 - r]
const start = 50;
const end = 450;
return {
x: Math.round(Math.random() * (end - start) + start),
y: Math.round(Math.random() * (end - start) + start),
};
};
// 随机生成圆到花布
const genCircle = () => {
const randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16); // 随机颜色
const { x, y } = genPos(); // 随机坐标
const arc = new Circle(x, y, 50, randomColor);
store.current.nodeList.push(arc); // 放入追踪队列
};
// 清空圆
const clear = () => {
store.current.nodeList = [];
};
// 判断是否点击位于圆内
const isHitNode = (node: any, pos: { x: number; y: number }) => {
const distance = Math.hypot(node.x - pos.x, node.y - pos.y);
return distance < node.r;
};
// 处理拖拽中的碰撞检测
const compact = (x, y) => {
const curNode = store.current.target;
const hitNode = store.current.nodeList
.filter((i) => i !== curNode) // 排除自己和自己对比
.find((item) => {
const distance = Math.hypot(item.x - x, item.y - y); // 距离
return distance < curNode.r + item.r; // 距离之和小于半径之和
});
return hitNode;
};
const onMouseMove = (e) => {
// 取得鼠标坐标
const { offsetX, offsetY } = e.nativeEvent;
const newX = offsetX - store.current.preX;
const newY = offsetY - store.current.preY;
// 更新坐标前先检测这个坐标是否和别的圆有重叠
if (store.current.target && !compact(newX, newY)) {
store.current.target.x = newX; // 修正偏移并更新坐标
store.current.target.y = newY;
}
};
const onMouseDown = (e) => {
const { offsetX, offsetY } = e.nativeEvent;
// 从圆列表中找到当前点击位置命中的圆
const hitNode = store.current.nodeList.find((item) =>
isHitNode(item, getPos(e))
);
if (hitNode) {
store.current.target = hitNode; // 记住当前命中的圆
// 记住初始和目标点的中心偏移
store.current.preX = offsetX - hitNode.x;
store.current.preY = offsetY - hitNode.y;
}
};
const onMouseUp = (e) => {
store.current.target = null; // 鼠标抬起,忘掉这个圆
};
return (
<div>
<button onClick={genCircle}> add </button>
<button onClick={clear}> clear </button>
<div
onMouseMove={onMouseMove}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
>
<canvas
width={500}
height={500}
style={{ width: "500px", height: "500px", border: "1px solid red" }}
ref={refCanvas}
/>
</div>
</div>
);
};
export default Panel; // (ts类型暂时忽略)
@bysking