本文会带大家使用 TypeScript 继续封装构造函数 CanvasContainer。以实现 同时
选中
和 移动
多个
直线、矩形、多边形的功能。
二、Canvas 画布:绘制直线、矩形、多边形 👉👉👉 代码演示
三、Canvas 画布:图形的选中和移动 (上) 👉👉👉 代码演示
四、Canvas 画布:图形的选中和移动 (下) 👉👉👉 代码演示
一、优化图形的数据结构
第一步:首先我们需要对图形的数据结构进行扩展,并定义几个用到的变量。
ts
class CanvasContainer {
/** 是否移动多个图形 */
public isMoveGraphs = false;
/** 选中多个图形的框 */
public boxGraph?: GraphInter = undefined;
/** 当前已经被框选中的图形 */
public selectGraphs?: GraphInter[] = undefined;
/** 记录鼠标按下时所有已经被框选中的图形的位置信息 */
private mouseDownSelectGraphsInfo?: GraphInter[] = undefined;
/** 记录鼠标按下时选中框的位置信息 */
private mouseDownBoxGraphInfo?: GraphInter = undefined;
}
二、多个图形的选中
第二步:我们要实现以下效果:当鼠标按下后,鼠标移动会绘制一个蓝色的 矩形 多选框,如果图形有一个端点在蓝色的多选框内,则表示该图形被选中。
1. 绘制蓝色的矩形
改造 renderActive
函数,新增绘制蓝色多选框的功能。
ts
const boxGraph = this.boxGraph?.points || undefined;
if (boxGraph && boxGraph.length) {
this.ctx.save();
this.ctx.translate(this.offsetX, this.offsetY);
this.ctx.scale(this.scale, this.scale);
// 一个边框是 `rgb(5, 199, 243)`,背景色是 `rgba(5, 199, 243, 0.1)` 的矩形
this.ctx.strokeStyle = 'rgb(5, 199, 243)';
this.ctx.fillStyle = 'rgba(5, 199, 243, 0.1)';
this.ctx.beginPath();
this.ctx.rect(boxGraph[0].x, boxGraph[0].y, boxGraph[1].x - boxGraph[0].x, boxGraph[1].y - boxGraph[0].y);
this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
this.ctx.restore();
}
2. 计算多选框的位置
ts
// 矩形开始的坐标:( 鼠标按下的位置 - 画布的偏移量 ) / 画布的缩放比
const x = (this.mouseDownOffsetX - this.offsetX) / this.scale;
const y = (this.mouseDownOffsetY - this.offsetY) / this.scale;
// 矩形的宽高:( 鼠标当前的位置 - 画布的偏移量 ) / 画布的缩放比
const width = (event.x - this.offsetX) / this.scale - x;
const height = (event.y - this.offsetY) / this.scale - y;
this.boxGraph = {
id: -1, // 多选框的 id 恒定为 -1,且唯一
type: 'rect',
points: [
{ x: x, y: y },
{ x: x + width, y: y + height },
],
select: true,
area: width * height,
};
3. 判断图形是否被选中
ts
// 将上次选中列表的图形置空
this.selectGraphs = [];
const point = this.normalizationPoint('rect', this.boxGraph?.points as PointsType);
this.graphs.forEach((graph) => {
// 取消选中状态
graph.select = false;
const points = this.normalizationPoint(graph.type, graph.points);
for(let i = 0; i < points.length; i++) {
// 判断图形是否有端点在多选框内
if (this.isPointInGraph(points[i], point)) {
// 如果有在将其设置为选中状态
graph.select = true;
// 将其添加到选中列表
this.selectGraphs?.push(graph);
// 如果有一个端点被选中,则结束此次循环,节省性能
break;
}
}
});
// 更新画布
this.renderActive(this.selectGraphs);
4. 优化 renderActive 函数
在上个版本中,我们只能选中一个图形,现在我们可以选中多个图形了,所以 renderActive 函数需要有渲染多个图形的能力,我们进行一下优化。
ts
// 判断 selectGraph 是不是数组,如果是数组,遍历里面的选中图形,逐个渲染
if (Array.isArray(selectGraph)) {
selectGraph.forEach(graph => {
this.drawGraph(this.ctx, graph);
})
} else {
this.drawGraph(this.ctx, selectGraph);
}
三、多个图形的移动
第三步:上面我们已经实现了选中图形的功能,现在我们来实现:移动蓝色的多选框,同时移动蓝色多选框已经选中的所有图形。
1. 移动前的准备
在移动图形前,鼠标按下的时候,我们要判断按下的位置上是什么:是蓝色的多选框,还是图形,或者什么也没有。在不同的情况下,我们要做不同的事情。
-
优化
pointInGraph
函数,这个函数用来判断当前鼠标按下的位置是在那个图形内。此时它还没有判断是不是在蓝色的矩形框内,我们要新增这个功能。tsif ( this.boxGraph?.points && this.isPointInGraph( point, this.normalizationPoint('rect', this.boxGraph?.points as PointsType) ) ) { return this.boxGraph as GraphInter; }
-
当前位置是蓝色多选框时,我们要记录蓝色多选框和它所选中的图形的位置信息。
tsif (this.selectGraph?.id === -1 && this.selectGraphs?.length) { if (this.selectGraphs && this.selectGraphs.length > 0) { // 移动多个图形 this.isMoveGraphs = true; // 拷贝信息,如果数量多,不建议使用 JSON.parse 和 JSON.stringify this.mouseDownSelectGraphsInfo = JSON.parse(JSON.stringify(this.selectGraphs)); this.mouseDownBoxGraphInfo = JSON.parse(JSON.stringify(this.selectGraph)); } }
-
当前位置是图形时,我们要记录此图形的位置信息。
tsif(this.selectGraph) { this.boxGraph = undefined; this.selectGraphs = undefined; this.isMoveGraphs = false; this.graphs.forEach(item => item.select = false); this.selectGraph.select = true; this.mouseDownSelectGraphInfo = JSON.parse(JSON.stringify(this.selectGraph)); }
-
当前位置什么也没时,我们要初始化所有信息。
tsthis.isMoveGraphs = false; this.boxGraph = undefined; this.selectGraph = undefined; this.selectGraphs = undefined; this.graphs.forEach(item => item.select = false);
2. 移动图形
在鼠标按下时,如果选中了蓝色的多选框,那在鼠标移动的时候,我们就要来修改蓝色多选框和选中图形的每一个端点,来移动他们。
ts
if (this.isMoveGraphs) {
// 修改蓝色多选框各个端点的位置
this.boxGraph = {
...this.mouseDownBoxGraphInfo,
points: this.mouseDownBoxGraphInfo?.points?.map(point => {
// 新端点的位置 = 鼠标按下时端点的位置 + 鼠标移动的距离
// 鼠标移动的距离 = ( 鼠标当前的位置 - 鼠标按下时的位置 ) / 画布的缩放比例
return {
x: point.x + (event.x - this.mouseDownOffsetX) / this.scale,
y: point.y + (event.y - this.mouseDownOffsetY) / this.scale,
}
})
} as GraphInter;
// 修改选中图形各个端点的位置
this.selectGraphs?.forEach((graph, graphIndex) => {
graph.points = graph.points.map((_point, pointIndex) => {
// 鼠标按下时,选中图形的各个端点的位置
const x = this.mouseDownSelectGraphsInfo?.[graphIndex]?.points?.[pointIndex]?.x as number;
const y = this.mouseDownSelectGraphsInfo?.[graphIndex]?.points?.[pointIndex]?.y as number;
return {
x: x + (event.x - this.mouseDownOffsetX) / this.scale,
y: y + (event.y - this.mouseDownOffsetY) / this.scale,
}
})
})
// 重新渲染画布
this.renderActive(this.selectGraphs);
}