Canvas鼠标滚轮缩放以及画布拖动(图文并茂版)

news/2025/3/20 7:32:02/

Canvas鼠标滚轮缩放以及画布拖动

本文会带大家认识Canvas中常用的坐标变换方法 translate 和 scale,并结合这两个方法,实现鼠标滚轮缩放以及画布拖动功能。

Canvas的坐标变换

Canvas 绘图的缩放以及画布拖动主要通过 CanvasRenderingContext2D 提供的 translatescale 两个方法实现的,先来认识下这两个方法。

translate 方法

语法:

translate(x, y)

translate 的用法记住一句话:

translate 方法重新映射画布上的(0, 0)位置。

说白了就是把画布的原点移动到了 translate 方法指定的坐标,之后所有图形的绘制都会以该坐标进行参照。

举个例子:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 400;ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 50, 50);ctx.translate(50, 50);ctx.fillStyle = 'green';
ctx.fillRect(50, 50, 50, 50);

开始的时候,Canvas 容器原点和绘图原点重合,绘制一个背景色为红色,原点坐标(50, 50),长宽各为 50 的矩形,接着调用 translate 方法将绘图原点沿水平和纵向各偏移50,再绘制一个背景色是绿色,原点坐标(50, 50),长宽各为 50 的矩形,示意图如下,其中灰色的背景为 Canvas 区域。

translate.png

需要注意的是,如果此时继续调用 translate 方法进行偏移操作,后续的偏移会基于原来偏移的基础上进行的。

ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 50, 50);// 第一次坐标系偏移
ctx.translate(50, 50);ctx.fillStyle = 'green';
ctx.fillRect(50, 50, 50, 50);// 第二次坐标系偏移
ctx.translate(50, 50);ctx.fillStyle = 'blue';
ctx.fillRect(50, 50, 50, 50);

第二次translate.png

因此,如果涉及到多次调用 translate 方法进行坐标变换,很容易将坐标系搞混乱,所以,一般在translate 之前会调用 save 方法先保存下绘图的状态,再调用 translate 后,绘制完图形后,调用 restore 方法恢复之前的上下文,对坐标系进行还原,这样不容易搞乱坐标系。

save方法通过将当前状态压入堆栈来保存画布的整个状态。

保存到堆栈上的图形状态包括:

  • 当前转换矩阵。
  • 当前裁剪区域。
  • 当前的破折号列表。
  • 包含的属性:strokeStyle、ill Style、lobalAlpha、linewidth、lineCap、lineJoin、miterLimit、lineDashOffset、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、global alCompositeOperation、Font、extAlign、extBaseline、Direction、ImageSmoothingEnabled。

restore 方法通过弹出绘制状态堆栈中的顶部条目来恢复最近保存的画布状态。

ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 50, 50);// 保存绘图上下文
ctx.save()ctx.translate(50, 50);
ctx.fillStyle = 'green';
ctx.fillRect(50, 50, 50, 50);// 绘制完成后恢复上下文
ctx.restore()ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 50, 50);

translate3.png

scale 方法

语法:

scale(x, y)

缩放 (scale) 就是将一个图形围绕中心点,然后将宽和高分别乘以一定的因子(sx,sy)

默认情况下,画布上的一个单位正好是一个像素。缩放变换会修改此行为。例如,如果比例因子为0.5,则单位大小为0.5像素;因此,形状的绘制大小为正常大小的一半。类似地,比例因子为2会增加单位大小,使一个单位变为两个像素;从而以正常大小的两倍绘制形状。

举个例子:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');ctx.scale(0.5,2);
ctx.fillStyle="blue";
ctx.fillRect(50,50,100,50);

调用 scale(0.5,2) 将画布水平方向缩小一倍,垂直方向放大一倍,绘制一个坐标原点 (50, 50),宽度 100,高度 50 的矩形。经过缩放变换后,距离原点的实际像素是横轴 25像素,纵轴 100 像素,宽度 50 像素,高度 100 像素。

scale.png

实现鼠标拖动画布

效果

创建Sence类

Sence类:

class Scene {constructor(id, options = {width: 600,height: 400}) {this.canvas = document.querySelector('#' + id)this.width = options.width;this.height = options.height;this.canvas.width = options.width;this.canvas.height = options.height;this.ctx = this.canvas.getContext('2d');}draw() {this.ctx.fillStyle = 'red';this.ctx.fillRect(50, 50, 50, 50);this.ctx.fillStyle = 'green';this.ctx.fillRect(150, 150, 50, 50);}clear() {this.canvas.width = this.width;}paint() {this.clear();this.draw();}
}let scene = new Scene('canvas');scene.draw();

Sence 类的构造函数中初始化 Canvas,得到 CanvasRenderingContext2D 对象,并设置 Canvas 的宽高属性,draw 方法里面绘制了两个矩形。

在进行下面的工作之前,我们先来了解下 Canvas 的事件机制。

通过 addEventListener 方法可以给 Canvas 绑定一个事件。

this.canvas.addEventListener('mousedown', (event) => {console.log(event.x)
});

事件的回调函数参数的 event 对象中可以获取鼠标点击 Canvas 时的坐标信息,event 对象中经常会用到的坐标有两个,一个是 event.xevent.y,另一个是 event.offsetXevent.offsetY,其中,event.xevent.y 获取的是鼠标点击时相对于屏幕的坐标,而 event.offsetXevent.offsetY 是相对于 Canvas 容器的坐标。

通过下面这张图可以清晰的看出两个坐标的区别,明白这一点对于我们后续的坐标变换非常重要。

事件坐标系.png

在构造函数中添加对 Canvasmousedown 事件监听,记录点击鼠标时相对屏幕的位置 xy

class Scene {x = 0; // 记录鼠标点击Canvas时的横坐标y = 0; // 记录鼠标点击Canvas时的纵坐标constructor(id, options = {width: 600,height: 400}) {this.canvas.addEventListener('mousedown', this.onMousedown);}onMousedown(e) {if (e.button === 0) {// 点击了鼠标左键this.x = x;this.y = y;}}
}

画布拖动的整体思路就是利用前面介绍的 Canvastranslate 方法。画布的整体偏移量记录在 offset.xoffset.y,鼠标触发 mousedown 事件时,记录当前鼠标点击的位置相对于屏幕的坐标 x, 和 y,并且开始监听鼠标的 mousemovemouseup 事件。鼠标触发 mousemove 事件时计算每次移动时整体累加的偏移量:

onMousemove(e) {this.offset.x = this.curOffset.x + (e.x - this.x);this.offset.y = this.curOffset.y + (e.y - this.y);this.paint();
}

其中 curOffset.xcurOffset.y 记录的是鼠标触发 mouseup 时保存的当前的偏移量,便于计算累加的偏移量。每次触发完鼠标 mousemove 事件后,重新进行图形绘制。

onMouseup() {this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;window.removeEventListener('mousemove', this.onMousemove);window.removeEventListener('mouseup', this.onMouseup);
}

Sence 类完整代码如下:

class Scene {offset = { x: 0, y: 0 }; // 拖动偏移curOffset = { x: 0, y: 0 }; // 记录上一次的偏移量x = 0; // 记录鼠标点击Canvas时的横坐标y = 0; // 记录鼠标点击Canvas时的纵坐标constructor(id, options = {width: 600,height: 400}) {this.canvas = document.querySelector('#' + id);this.width = options.width;this.height = options.height;this.canvas.width = options.width;this.canvas.height = options.height;this.ctx = this.canvas.getContext('2d');this.onMousedown = this.onMousedown.bind(this);this.onMousemove = this.onMousemove.bind(this);this.onMouseup = this.onMouseup.bind(this);this.canvas.addEventListener('mousedown', this.onMousedown);}onMousedown(e) {if (e.button === 0) {// 鼠标左键this.x = e.x;this.y = e.ywindow.addEventListener('mousemove', this.onMousemove);window.addEventListener('mouseup', this.onMouseup);}}onMousemove(e) {this.offset.x = this.curOffset.x + (e.x - this.x);this.offset.y = this.curOffset.y + (e.y - this.y);this.paint();}onMouseup() {this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;window.removeEventListener('mousemove', this.onMousemove);window.removeEventListener('mouseup', this.onMouseup);}draw() {this.ctx.fillStyle = 'red';this.ctx.fillRect(50, 50, 50, 50);this.ctx.fillStyle = 'green';this.ctx.fillRect(150, 150, 50, 50);}clear() {this.canvas.width = this.width;}paint() {this.clear();this.ctx.translate(this.offset.x, this.offset.y);this.draw();}
}

上述代码中有几点需要注意:

  1. 事件函数中的this指向问题

细心的同学可能注意到,在 Sence 类的构造函数里有这样几行代码:

constructor(id, options = {width: 600,height: 400}) {this.onMousedown = this.onMousedown.bind(this);this.onMousemove = this.onMousemove.bind(this);this.onMouseup = this.onMouseup.bind(this);}

为什么要使用 bind 函数给事件函数重新绑定this对象呢?

主要的原因在于一个事件有监听就会有移除。假设我们想要销毁 mousemove 事件怎么办呢?

可以调用 removeEventListener 方法进行事件监听的移除,比如上述代码会在 onMouseup 中移除对 mousemove 事件的监听:

onMouseup() {this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;window.removeEventListener('mousemove', this.onMousemove);
}

如果不在构造函数中使用 bind 方法重新绑定 this 指向,此时的 this 指向的就是window,因为 this 指向的是调用 onMouseup 的对象,而 onMouseup 方法是被 window 上的 mouseup 事件调用的,但是实际上我们想要的this指向应该 Sence 实例。为了避免上述问题的出现,最好的解决办法就是在 Sence 类的构造函数中重新绑定 this 指向。

  1. 画布的清空问题

每次鼠标移动的时候会改变 CanvasCanvasRenderingContext2D 偏移量,并重新进行图形的绘制,重新绘制的过程就是先将画布清空,然后设置画布的偏移量(调用 translate 方法),接着绘制图形。其中清空画布这里选择了重新设置Canvas的宽度,而不是调用 clearRect 方法,主要是因为clearRect 方法只在 Canvas 的渲染上下文没有进行过平移、缩放、旋转等变换时有效,如果 Canvas 的渲染上下文已经经过了变换,那么在使用 clearRect 清空画布前,需要先重置变换,否则 clearRect 将无法有效地清除整块画布。

实现鼠标滚轮缩放

效果

实现原理

鼠标滚轮的放大需要结合上面介绍的 Canvastranslatescale 两个方法进行组合变换。

计算放大系数

监听鼠标滚轮的 mousewheel 事件,在事件的回调函数中通过 event.wheelDelta 值的变化来实时计算当前的缩放值,其中 event.wheelDelta > 0 表示放大,反之表示缩小,放大和缩小都有对应的阈值,超过阈值就禁止继续放大和缩小。

改造 Sence 类,添加 onMousewheel 事件:

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

其中,this.scale / this.preScale 计算出来的值就是放大系数,暂且记做 n

在计算放大系数的时候,需要注意两个浮点型数值在计算不能直接相加,否则会出现丢失精度的问题。

缩放原理

在缩放的时候,会调用 scale(n, n) 方法,将坐标系放大 n 倍。假设鼠标滚轮停在 A 点进行放大操作,放大之后得到坐标 A’ 点。

图形放大1.png

可以看到,放大之后,A(x1, y1) 坐标变换到了 A'(x1, y1)A => A' 放大了 n 倍,因此得到 x1 = x * ny1 = y1 * n

这个时候就会存在一个问题,我们在 A 点进行放大,放大后得到的 A' 的位置应该是不变的,所以需要在放大之后需要调整 A’ 点的位置到 A 点。

这里我们采用的策略是在放大前先偏移一段距离,然后进行放大之后就可以保持 A 点和 A‘ 点的重合。

缩放原理图.png

鼠标停留在 A 点对蓝色矩形进行放大,放大系数为 n,蓝色矩形的起点左上角和坐标原点重合,宽度和高度分别是 xy,因此,A点的坐标为 (x, y)

前面我们说过,对 A 点进行放大后得到的 A’点应该和A点重合,这样就需要先把整个坐标系沿着x轴和y轴分别向左和向上偏移 offsetXoffsetY,偏移后得到的 A'点坐标记作 (x1, x2),因为 A 点是经过放大 n 倍后得到的 A' 点,所以得到以下距离关系:

x1 = x * n;
y1 = y * n

进一步就可以得到横纵坐标的偏移量 offsetXoffsetY 的绝对值:

offsetX = x*n-x;
offsetY =x*n - y;

因此,这需要将坐标系经过 translate(-offsetX, -offsetY) 之后,再 scale(n, n),就能确保 A 点 和 A‘ 点重合了。

明白了缩放的基本原理,下面就继续码代码吧😜。

onMousewheel(e) {e.preventDefault();this.mousePosition.x = e.offsetX; // 记录当前鼠标点击的横坐标this.mousePosition.y = e.offsetY; // 记录当前鼠标点击的纵坐标if (e.wheelDelta > 0) {// 放大this.scale = parseFloat((this.scaleStep + this.scale).toFixed(2)); // 解决小数点运算丢失精度的问题if (this.scale > this.maxScale) {this.scale = this.maxScale;return;}} else {// 缩小this.scale = parseFloat((this.scale - this.scaleStep).toFixed(2)); // 解决小数点运算丢失精度的问题if (this.scale < this.minScale) {this.scale = this.minScale;return;}}this.offset.x = this.mousePosition.x - ((this.mousePosition.x -   this.offset.x) * this.scale) / this.preScale;this.offset.y = this.mousePosition.y - ((this.mousePosition.y - this.offset.y) * this.scale) / this.preScale;this.paint(this.ctx);this.preScale = this.scale;this.curOffset.x = this.offset.x;this.curOffset.y = this.offset.y;
}paint() {this.clear();this.ctx.translate(this.offset.x, this.offset.y);this.ctx.scale(this.scale, this.scale);this.draw();
}

总结

本文从基础原理到代码实现,完整给大家讲解了 Canvas 画布绘制中经常会遇到的画布拖动和鼠标滚轮缩放功能,希望对大家有帮助,更多精彩文章欢迎大家关注我的vx公众号:前端架构师笔记。本文完整代码地址:https://github.com/astonishqft/scanvas-translate-and-scale


http://www.ppmy.cn/news/846582.html

相关文章

cad怎样按住鼠标中键拖动图

就是按住中键拖动就行了啊。中键就是滚轮。把滚轮按下去拖动。

不能拖动CAD文件到CAD窗口打开,解决方案

运行regedit HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA设为0 重启

CAD 鼠标中间不能拖动,解决方法

cad 滚轴键不能使用 如果CAD 鼠标中键不能平移&#xff0c;而是弹出下拉菜单&#xff0c;修改系统变量mbuttonpan的值为1&#xff0c;重新打开CAD后鼠标中键还是 一样不能平移&#xff0c;有什么方法能够恢复原来的中键平移功能呢&#xff1f; 大家可以试试下面的方法&…

GoLand导入redis的github包失败

GoLand导入redis依赖失败 网上有下载guryburd和gomodel的&#xff0c;这里按照官网文档安装依赖 以下命令在项目的根目录执行 初始化一个Go模块&#xff1a; go mod init github.com/my/repoTo install go-redis/v9:要安装go-redis/v9&#xff1a; go get github.com/redis/…

java语言学习

java基础知识 文章目录 java基础知识一、预言二、简介三、环境设置四、基本语法五、对象和类六、基本数据类型七、变量类型八、修饰符九、运算符十、循环控制十一、判断十二、 Numbers类十三、Character 类十四、String类十五、数组 一、预言 Java是一种高级编程语言&#xff…

Java基础——环境变量配置、注释、关键字、标识符

目录 01.01_计算机基础知识(计算机概述)01.02_计算机基础知识(软件开发和计算机语言概述)01.03_计算机基础知识(人机交互)01.04_计算机基础知识(键盘功能键和快捷键)01.05_计算机基础知识(如何打开DOS控制台)01.06_计算机基础知识(常见的DOS命令讲解)01.07_Java语言基础(Java语…

Java Study Notes_Design in 2023(Day01~Day14)

文章目录 Day01&#xff1a;Java入门1.1 Java的技术体系1.2 Java快速入门1.2.1 JDK、JRE与JVM1.2.2 JDK环境变量配置 1.3 Java基础语法1.3.1 注释1.3.2 字面量1.3.3 JAVA关键字1.3.4 标志符1.3.5 变量 Day02&#xff1a; 数据类型、运算符2.1 数据的表示详解2.1.1二进制 2.2 数…

JAVA-------计算机基础与准备阶段

JavaSE --计算机基础与准备阶段 计算机基础 dos命令 java语言概述 JDK的下载安装以及环境变量的配置 Hello World的编写与运行 注释 关键字 标识符 常量 变量 数据类型和分类 数据类型转换 字符和字符串参与的运算 算术运算符 赋值运算符 关系运算符计算机基础 计算机&#x…