本文会带大家使用 TypeScript 继续封装构造函数 CanvasContainer。实现直线、矩形、多边形在移动过程中与相邻的图形之间显示 辅助线
,来辅助对齐的功能;并实现两个图形靠近后自动贴合的 吸附功能
。
二、Canvas 画布:绘制直线、矩形、多边形 👉👉👉 代码演示
三、Canvas 画布:图形的选中和移动 (上) 👉👉👉 代码演示
四、Canvas 画布:图形的选中和移动 (下) 👉👉👉 代码演示
五、Canvas 画布:修改图形各端点的位置 👉👉👉 代码演示
六、Canvas 画布:图形移动时的辅助线和吸附效果 👉👉👉 代码演示
一、优化图形的数据结构
我们需要对图形的数据结构进行扩展,并定义几个用到的变量。
// 给图形新增两个属性:isVertical 表示竖向吸附的锁,isHorizontal 表示横向吸附的锁
interface GraphInter {
......
isVertical: boolean;
isHorizontal: boolean;
}
class CanvasContainer {
......
/** 画布竖向的辅助线 */
public vertical = new Set<number>();
/** 画布横向的辅助线 */
public horizontal = new Set<number>();
}
二、辅助线
在移动图形的过程中,有的时候我们需要一种功能:让移动的图形与其他图形的 一个端点
或 一条边
或 边的中心点
,或与 画布可视区的中心点
对齐。为了实现这个功能,我们一般会以辅助线的形式来帮助使用者观察图形是否对齐。
1. 创建辅助线
- 我们创建的辅助线有两种方向:一种横向,一种竖向。
- 因为图形的类型不一样,所以辅助线的数量也不一样。对不同类型的图形的辅助线我们有以下规定:
- 直线:两个端点和直线的中心点,横向和竖向的辅助线各一条,共 6 条。
- 矩形:两对平行的边和平行边的中点,横向和竖向的辅助线各一条,共 6 条。
- 多边形:每一个端点,横向和竖向的辅助线各一条,共 2n 条。
const vertical = new Set<number>(); // 竖向辅助线
const horizontal = new Set<number>(); // 横向辅助线
this.graphs.forEach(graphs => {
// 选中的图形不绘制辅助线
if (graphs.id !== this.selectGraph?.id) {
switch(graphs.type) {
case 'line':
case 'rect':
// 竖向
vertical.add(graphs.points[0].x) // 开始点
vertical.add((graphs.points[0].x + graphs.points[1].x) / 2) // 中心点
vertical.add(graphs.points[1].x) // 结束点
// 横向
horizontal.add(graphs.points[0].y) // 开始点
horizontal.add((graphs.points[0].y + graphs.points[1].y) / 2) // 中心点
horizontal.add(graphs.points[1].y) // 结束点
break;
case 'polygon':
// 遍历所有端点
graphs.points.forEach(point => {
vertical.add(point.x)
horizontal.add(point.y)
})
break;
}
}
})
2. 筛选辅助线
上面一步,让我们得到了除选中图形外所有其他图形的辅助线。但是,我们并不需要将所有的辅助线都绘制到画布上,我们只绘制距离移动的图形的各个端点 小于 10
的辅助线。
// 清除过期的辅助线
this.vertical.clear();
this.horizontal.clear();
// 遍历移动图形的各个端点
this.selectGraph.points.forEach((graphPoint) => {
vertical.forEach(point => {
// 记录距离小于 10 的竖向辅助线
if (Math.abs(graphPoint.x - point) < 10) {
this.vertical.add(point)
}
})
horizontal.forEach(point => {
// 记录距离小于 10 的横向辅助线
if (Math.abs(graphPoint.y - point) < 10) {
this.horizontal.add(point)
}
})
})
3. 绘制辅助线
上面一步,我们得到了要绘制的辅助线的列表 this.vertical
和 this.horizontal
,现在我们要以 虚线
的形式绘制两条辅助线:
this.vertical.forEach(point => {
this.ctx.beginPath()
this.ctx.setLineDash([5]);
// | 竖向的辅助线,X 轴坐标一致,Y 坐标:起始(偏移量Y / 缩放比例)、结束(画布的高度 / 缩放比例)
this.ctx.moveTo(point, -this.offsetY / this.scale)
this.ctx.lineTo(point, this.height / this.scale)
this.ctx.closePath()
this.ctx.stroke()
})
this.horizontal.forEach(point => {
this.ctx.beginPath()
this.ctx.setLineDash([5]);
// —— 横向的辅助线,Y 轴坐标一致,X 坐标:起始(偏移量X / 缩放比例)、结束(画布的宽度 / 缩放比例)
this.ctx.moveTo(-this.offsetX / this.scale, point)
this.ctx.lineTo(this.width / this.scale, point)
this.ctx.closePath()
this.ctx.stroke()
})
三、吸附效果
在上面,我们成功绘制出了辅助线,在实际的使用中,根据辅助线做到丝毫不差的对齐,是一件不容易的事情。为了解决这个问题,我们还需要完成一个功能:吸附。这个功能的作用是:在两个图形距离非常近的时候,自动将两个图形对齐,不需要我们一像素一像素的去手动对齐,避免差错和节约时间。
1. 实现吸附效果
this.selectGraph.points.forEach((graphPoint) => {
vertical.forEach(point => {
if (Math.abs(graphPoint.x - point) < 10) this.vertical.add(point);
// 当移动图形的端点到辅助线的绝对距离 小于 3 时,将图形自动移动到辅助线的位置
if (Math.abs(graphPoint.x - point) < 3) {
(this.selectGraph as GraphInter).points = this.selectGraph?.points.map(p => {
// 将图形的所有坐标的 X 值,向辅助线移动相同距离
return {
x: p.x - (graphPoint.x - point),
y: p.y,
}
}) as PointsType;
}
})
horizontal.forEach(point => {
if (Math.abs(graphPoint.y - point) < 10) this.horizontal.add(point);
if (Math.abs(graphPoint.y - point) < 3) {
(this.selectGraph as GraphInter).points = this.selectGraph?.points.map(p => {
return {
x: p.x,
y: p.y - (graphPoint.y - point),
}
}) as PointsType;
}
})
})
2. 优化吸附效果
上面一步,我们实现了吸附效果,但是吸附的时候会出现跳动的现象。出现这个现象的原因是:只要图形到辅助线的绝对距离 小于 3
都会触发吸附,也就是在 -2,-1,0,1,2 这几个距离都会重复触发,所以才出现了这个现象。
现在我们来设置一个 锁
,使图形到辅助线之间的距离 第一次小于 3
的时候才会触发吸附效果,并且只有当图形到辅助线之间的距离 再次大于 3
时,才能再一次触发吸附效果。
this.selectGraph.points.forEach((graphPoint) => {
vertical.forEach(point => {
if (Math.abs(graphPoint.x - point) < 10) this.vertical.add(point);
// 只有当绝对距离小于 3,并且解锁的状态
if (Math.abs(graphPoint.x - point) < 3 && this.selectGraph?.isVertical) {
(this.selectGraph as GraphInter).points = this.selectGraph?.points.map(p => {
return {
x: p.x - (graphPoint.x - point),
y: p.y,
}
}) as PointsType;
// 上锁
this.selectGraph && (this.selectGraph.isVertical = false)
}
// 当绝对距离大于 3,并是上锁状态时
if (Math.abs(graphPoint.x - point) > 3 && !this.selectGraph?.isVertical) {
// 解锁
this.selectGraph && (this.selectGraph.isVertical = true)
}
})
horizontal.forEach(point => {
if (Math.abs(graphPoint.y - point) < 10) {
this.horizontal.add(point)
}
if (Math.abs(graphPoint.y - point) < 3 && this.selectGraph?.isHorizontal) {
(this.selectGraph as GraphInter).points = this.selectGraph?.points.map(p => {
return {
x: p.x,
y: p.y - (graphPoint.y - point),
}
}) as PointsType;
this.selectGraph && (this.selectGraph.isHorizontal = false)
}
if (Math.abs(graphPoint.y - point) > 3 && !this.selectGraph?.isHorizontal) {
this.selectGraph && (this.selectGraph.isHorizontal = true)
}
})
})