Canvas 画布:图形的选中和移动 (下)

本文会带大家使用 TypeScript 继续封装构造函数 CanvasContainer。以实现 同时 选中移动 多个 直线、矩形、多边形的功能。

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

二、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 函数,这个函数用来判断当前鼠标按下的位置是在那个图形内。此时它还没有判断是不是在蓝色的矩形框内,我们要新增这个功能。

    ts 复制代码
    if (
        this.boxGraph?.points &&
        this.isPointInGraph(
            point,
            this.normalizationPoint('rect', this.boxGraph?.points as PointsType)
        )
    ) {
        return this.boxGraph as GraphInter;
    }
  • 当前位置是蓝色多选框时,我们要记录蓝色多选框和它所选中的图形的位置信息。

    ts 复制代码
    if (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));
        }
    }
  • 当前位置是图形时,我们要记录此图形的位置信息。

    ts 复制代码
    if(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));
    }
  • 当前位置什么也没时,我们要初始化所有信息。

    ts 复制代码
    this.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);
}
相关推荐
PineappleCoder2 天前
SVG 适合静态图,Canvas 适合大数据?图表库的场景选择
前端·面试·canvas
德育处主任3 天前
p5.js 用 cylinder() 绘制 3D 圆柱体
前端·数据可视化·canvas
蛋蛋_dandan5 天前
Fabric.js从0到1实现图片框选功能
canvas
wayhome在哪7 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任7 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任8 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任10 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼10 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲10 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户25191624271113 天前
Canvas之画图板
前端·javascript·canvas