乐趣区

关于canvas:Canvas鼠标滚轮缩放以及画布拖动图文并茂版

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

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

<div align=center>

<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf6d1ae14ebe42d0b31cb9b6542943d4~tplv-k3u1fbpfcp-watermark.image?" alt="" />

</div>

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 办法进行偏移操作,后续的偏移会基于原来偏移的根底上进行的。

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 办法进行坐标变换,很容易将坐标系搞凌乱,所以,个别在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);

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 像素。

实现鼠标拖动画布

成果

<div align=center>
<img alt=”” src=”https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43ce6fc1008245c38e9f3f6b8d3ea296~tplv-k3u1fbpfcp-watermark.image?” />
</div>

创立 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 容器的坐标。

通过上面这张图能够清晰的看出两个坐标的区别,明确这一点对于咱们后续的坐标变换十分重要。

在构造函数中增加对 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.y
      window.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 将无奈无效地革除整块画布。

实现鼠标滚轮缩放

成果

<div align=center>

<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf6d1ae14ebe42d0b31cb9b6542943d4~tplv-k3u1fbpfcp-watermark.image?" alt="" />

</div>

实现原理

鼠标滚轮的放大须要联合下面介绍的 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’ 点。

能够看到,放大之后,A(x1, y1) 坐标变换到了 A'(x1, y1)A => A' 放大了 n 倍,因而失去 x1 = x * ny1 = y1 * n

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

这里咱们采纳的策略是在放大前先偏移一段距离,而后进行放大之后就能够放弃 A 点和 A‘ 点的重合。

鼠标停留在 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 画布绘制中常常会遇到的画布拖动和鼠标滚轮缩放性能,心愿对大家有帮忙。

本文残缺代码地址:https://github.com/astonishqf…

更多精彩文章欢送关注我的公众号[前端架构师笔记]

退出移动版