Canvas 画布:拖动和缩放

1,051 阅读4分钟

本文会带大家使用 TypeScript 封装一个构造函数 CanvasContainer。用来实现 Canvas 画布的 拖动平移 和 鼠标滚动缩放 两个常用的功能。

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

二、Canvas 画布:绘制直线、矩形、多边形 👉👉👉 代码演示

三、Canvas 画布:图形的选中和移动 (上) 👉👉👉 代码演示

四、Canvas 画布:图形的选中和移动 (下) 👉👉👉 代码演示

五、Canvas 画布:修改图形各端点的位置 👉👉👉 代码演示

六、Canvas 画布:图形移动时的辅助线和吸附效果 👉👉👉 代码演示

1. 创建一个 CanvasContainer 构造函数

第一步:创建一个构造函数 CanvasContainer,它接受一个参数:canvas 画布。并初始化一些会用到的变量。

class CanvasContainer {
    /** 画布的宽度 */
    public width = 0;
    /** 画布的高度 */
    public height = 0;
    /** 画布的上下文 */
    public ctx = null as unknown as CanvasRenderingContext2D;
    
    private mouseDownOffsetX = 0;              // 鼠标按下时,鼠标的位置
    private mouseDownOffsetY = 0;
    private offsetX = 0;                       // 当前正在拖动的偏移量
    private offsetY = 0;
    private preOffsetX = 0;                    // 上一次移动结束后的偏移量
    private preOffsetY = 0;
    private mouseWheelOffsetX = 0;             // 鼠标滚轮滚动时,鼠标的位置
    private mouseWheelOffsetY = 0;

    /** 缩放比例 */
    public scale = 1;
    private preScale = 1;                      // 上一次的缩放比例
    private scaleStep = 0.1;                   // 每次缩放的间隔
    private maxScale = 5;                      // 最大缩放比例
    private minScale = 0.2;                    // 最小缩放比例

    /**
     * 创建 Canvas 画布
     * @param canvas canvas标签
     */
    constructor(public canvas: HTMLCanvasElement) {}
}

2. 初始化 Canvas 画布

第二步:对画布进行初始化。

  1. 获取到 canvas 画布的上下文。
  2. 设置 canvas 画布的宽高。canvas有两个宽高:
    • 一个是 canvas 画布在显示屏上的宽高,一般称画布尺寸。
    • 一个是 canvas 画布自身绘图时的宽高,一般称画板尺寸。 1715328788068.jpg
  3. 为避免图形变形失真,在设备的 devicePixelRatio 为 1 时,要保持两个宽高相同;在不为 1 时,要将画板的尺寸缩放。
/** 初始化 Canvas */
private initCanvas = () => {
    this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;

    const { width, height } = this.canvas.getBoundingClientRect();
    this.width = width;
    this.height = height;

    /** 避免图形变形失真 */
    this.ratio = window.devicePixelRatio || 1;
    /** 设置画布绘制图形区域的尺寸,默认为 300 * 150 */
    this.canvas.width = this.width * this.ratio;
    this.canvas.height = this.height * this.ratio;
    this.ctx.scale(this.ratio, this.ratio);
}

3. 在 Canvas 画布上渲染一些图形

第三步:在画布上渲染一些图形,以便于演示。

  1. 绘制一个 矩形 和 圆形。
    • 矩形:this.ctx.rect(300, 100, 200, 100);
    • 圆形:this.ctx.arc(100, 100, 30, 0, Math.PI * 2);
  2. 在每次渲染之前要先清空画布。
/** 渲染函数 */
render = () => {
    /** 清空画布 */
    this.ctx.clearRect(0, 0, this.width * this.ratio, this.height * this.ratio);
    /** 保存画布状态 */
    this.ctx.save();
    /** 平移画布 */
    this.ctx.translate(this.offsetX , this.offsetY);
    /** 缩放画布 */
    this.ctx.scale(this.scale, this.scale);
    /** 渲染图形 */
    this.ctx.beginPath();
    this.ctx.rect(300, 100, 200, 100);
    this.ctx.arc(100, 100, 30, 0, Math.PI * 2);
    this.ctx.closePath();
    this.ctx.fill();
    /** 移除画布状态 */
    this.ctx.restore();
}

4. 添加鼠标的按下、移动、抬起事件,实现画布的移动

第四步:给画布添加鼠标的按下、移动、抬起、离开事件,用来移动画布。 1715351938792.jpg

画布的平移使用到了画布的API translate,将画布的原点移动到指定位置。

  1. 给画布添加鼠标按下事件,用于记录元素开始移动时鼠标的位置。
  2. 给画布添加鼠标抬起事件,用于记录元素结束移动时鼠标的位置。
  3. 给画布添加鼠标移动事件,用于记录元素的当前位置。
    • 元素当前的位置 = 当前鼠标的位置 - 鼠标开始移动时的位置 + 元素上一次移动结束后的位置
  4. 在鼠标离开画布时要取消鼠标的事件。
/** 添加事件函数 */
private addEvent = () => {
    /** this.canvas.addEventListener('wheel', this.onMouseWheel, { passive: true }); */
    this.canvas.addEventListener('mousedown', this.onMouseDown);
    this.canvas.addEventListener('mouseleave', this.onMouseUp);
    this.canvas.addEventListener('mouseleave', this.onMouseLeave);
}

/** 鼠标按下事件 */
private onMouseDown = (event: MouseEvent) => {
    // 鼠标左键
    if (event.button === 0) {
        // 记录鼠标按下时,鼠标的位置
        this.mouseDownOffsetX = event.x;
        this.mouseDownOffsetY = event.y;
        // 添加事件
        this.canvas.addEventListener('mousemove', this.onMouseMove);
        this.canvas.addEventListener('mouseup', this.onMouseUp);
    }
}

/** 鼠标按下移动事件 */
private onMouseMove = (event: MouseEvent) => {
    // 鼠标按下后的移动偏移量 = 当前鼠标的位置 - 鼠标按下的位置
    // 当前拖动偏移量 = 上一次的偏移量 + 鼠标按下后的移动偏移量
    this.offsetX = this.preOffsetX + (event.x - this.mouseDownOffsetX);
    this.offsetY = this.preOffsetY + (event.y - this.mouseDownOffsetY);
    // 重新渲染
    this.render();
}

/** 鼠标抬起事件 */
private onMouseUp = () => {
    // 记录鼠标抬起时,鼠标的位置
    this.preOffsetX = this.offsetX;
    this.preOffsetY = this.offsetY;
    // 移除事件
    this.canvas.removeEventListener('mousemove', this.onMouseMove);
    this.canvas.removeEventListener('mouseup', this.onMouseUp);
}

/** 鼠标离开事件 */
private onMouseLeave = () => {
    this.onMouseUp();
}

5. 添加鼠标滚轮的滚动事件,实现画布的缩放

第五步:给画布添加鼠标滚轮的滚动事件,用于画布的缩放。

1715411046257.jpg 画布的缩放使用的是画布的API scale,将画布缩放为 x, y。

  1. 记录滚轮滚动时,鼠标的位置。this.mouseWheelOffsetX
  2. 计算当前的缩放比例,并求出缩放比。zoomRatio
  3. 计算缩放之前,鼠标到画布原点的距离。this.mouseWheelOffsetX - this.offsetX
  4. 计算缩放之后,鼠标到画布原点的距离。(this.mouseWheelOffsetX - this.offsetX) * zoomRatio
  5. 计算新的原点的位置:鼠标的位置 - 画布缩放之后鼠标到原点的距离。
    • this.mouseWheelOffsetX - (this.mouseWheelOffsetX - this.offsetX) * zoomRatio
  6. 将当前的缩放比例,画布原点位置,记录为上一次的缩放比例,画布原点位置。
/** 鼠标滚轮滚动事件 */
private onMouseWheel = (event: WheelEvent) => {
    // 获取鼠标滚轮滚动时,鼠标的位置
    this.mouseWheelOffsetX = event.offsetX;
    this.mouseWheelOffsetY = event.offsetY;

    if (event.deltaY < 0) {
        // 画布放大
        if (this.scale >= this.maxScale) return;
        // 解决小数点运算丢失精度的问题
        this.scale = parseFloat((this.scale + this.scaleStep).toFixed(2));
    } else {
        // 画布缩小
        if (this.scale <= this.minScale) return;
        // 解决小数点运算丢失精度的问题
        this.scale = parseFloat((this.scale - this.scaleStep).toFixed(2));
    }

    // 缩放比
    const zoomRatio = this.scale / this.preScale;
    // 鼠标当前的位置 - 当前拖动偏移量
    this.offsetX = this.mouseWheelOffsetX - (this.mouseWheelOffsetX - this.offsetX) * zoomRatio;
    this.offsetY = this.mouseWheelOffsetY - (this.mouseWheelOffsetY - this.offsetY) * zoomRatio;

    this.render();

    // 将当前的缩放比例保存为 上一次的缩放比例
    this.preScale = this.scale;
    // 记录鼠标滚轮停止时,鼠标的位置
    this.preOffsetX = this.offsetX;
    this.preOffsetY = this.offsetY;
}

结束