Canvas 画布:图形移动时的辅助线和吸附效果

1,475 阅读5分钟

本文会带大家使用 TypeScript 继续封装构造函数 CanvasContainer。实现直线、矩形、多边形在移动过程中与相邻的图形之间显示 辅助线,来辅助对齐的功能;并实现两个图形靠近后自动贴合的 吸附功能

一、Canvas 画布:拖动和缩放 👉👉👉 代码演示

二、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.verticalthis.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)
        }
    })
})