乐趣区

关于canvas:Canvas实现以鼠标当前位置为原点缩放及画布拖动矩阵变换

Canvas 实现以鼠标以后地位为原点缩放及画布拖动(矩阵变换)

前言

在之前的 Canvas 鼠标滚轮缩放以及画布拖动 (图文并茂版) 一文中我已经介绍过一种实现鼠标滚轮缩放及画布拖动的办法,这种形式利用的是 Canvas 的 api 进行缩放和拖动,并且实现原理了解起来也比拟形象。

本文将介绍一种更加便捷、通用的形式来实现鼠标滚轮缩放及画布拖动的形式,这就是 矩阵变换

矩阵变换

矩阵变换 就是一种坐标系的转换,因而在图形学中,就会应用矩阵变换来进行图形的变动,比方平移、缩放、旋转。接下来我会重点介绍本文所波及到的平移和缩放变换。

平移

假如有一个点 P(x, y),平移到点 P'(x1, y1),在程度方向位移 dx,垂直方向的位移 dy,那么就能够失去如下公式:

$$
x1 = x + dx \\
y1 = y + dy
$$

如果将上述变换公式转换为矩阵变换的模式能够失去如下矩阵变换公式:

$$
\left[
\begin{matrix}
1 & 0 & dx \\
0 & 1 & dy \\
0 & 0 & 1
\end{matrix}
\right]
\left[
\begin{matrix}
x \\ y \\ 1
\end{matrix}
\right]= \left[
\begin{matrix}
x+dx \\
y+dy \\
1 \\
\end{matrix}
\right]
$$

在坐标系中,一个点就相当于一个 向量,从 P 点到 P' 点的变换能够通过:

$$
变换矩阵 * P 点 = P’ 点
$$

的模式来表白。

$$
A = \left[
\begin{matrix}
1 & 0 & dx \\
0 & 1 & dy \\
0 & 0 & 1
\end{matrix}
\right]
$$

A 矩阵就是平移的变换矩阵。

三维矩阵

下面的变换矩阵咱们采纳的是一个三维矩阵,很多人应该会有疑难:

既然是表白一个二维坐标变换,为什么不必二维矩阵来表白呢?

答案是: 二维矩阵无奈表白

举个例子:

假如咱们想把 (0, 0) 这个点挪动到 (2, 3),如果用二维矩阵来表白,是这样的:

$$
\left[
\begin{matrix}
1 & 2 \\
0 & 3 \\
\end{matrix}
\right]
\left[
\begin{matrix}
0 \\ 0
\end{matrix}
\right]= \left[
\begin{matrix}
0 \\
0 \\
\end{matrix}
\right]
$$

发现无论如何也得不到咱们想到的后果,因为 0 与任何数相乘都为 0。

如果想表白二维坐标的变换,至多用三维矩阵来表白,同样,如果在三维坐标系中(WebGL),那么至多须要四维矩阵能力表白。

缩放

假如有一个点 P(x, y),沿程度方向缩放 m 倍,沿垂直方向缩放 n 倍之后,失去点 P'(x1, y1),那么就能够失去如下公式:

$$
x1 = x * m \\
y1 = y * n
$$

如果将上述变换公式转换为矩阵变换的模式能够失去如下矩阵变换公式:

$$
\left[
\begin{matrix}
m & 0 & 0 \\
0 & n & 0 \\
0 & 0 & 1
\end{matrix}
\right]
\left[
\begin{matrix}
x \\ y \\ 1
\end{matrix}
\right]= \left[
\begin{matrix}
m*x \\
n*y \\
1 \\
\end{matrix}
\right]
$$

P 点到 P' 点的变换能够通过:

$$
变换矩阵 * P 点 = P’ 点
$$

的模式来表白。

$$
A = \left[
\begin{matrix}
m & 0 & 0 \\
0 & n & 0 \\
0 & 0 & 1
\end{matrix}
\right]
$$

A 矩阵就是缩放的变换矩阵。

平移 & 缩放

后面咱们别离探讨了平移和缩放的场景下,矩阵的变换形式,那如果想要实现本文的指标——以鼠标以后地位为原点缩放,就须要同时用到平移和缩放变换了。

如果一个二维坐标系存在两个坐标点,A(-2,0)B(2,0)

如果沿程度方向放大 2 倍,即 AB 的程度坐标值都乘以放大系数2,失去新的坐标A'(-4,0)B'(4,0)

如果想要以 B 点放大核心,确保放大后的 B'B放弃重合,须要将放大后的图形整体向左挪动一段距离,间隔为 BB‘之间程度间隔,依据数学公式中两点之间的间隔公式,BB‘之间程度间隔为 B‘ 的程度坐标减去 B 的程度坐标。

进一步形象计算公式:

如果 B(x,0),沿程度方向放大 n 倍之后,失去 B’(n*x,0),那么两点间的间隔为 n*x-x,又因为是向左挪动,上述间隔须要取反,即 x-n*x,提取公因式之后失去最终程度方向的偏移值为 x(1-n)。此公式同样实用于垂直方向的缩放。

了解了以某一个点进行缩放的原理之后,咱们就能够失去以下矩阵变换公式:

$$
\left[
\begin{matrix}
m & 0 & x(1-m) \\
0 & m & y(1-m) \\
0 & 0 & 1
\end{matrix}
\right]
\left[
\begin{matrix}
x \\ y \\ 1
\end{matrix}
\right]= \left[
\begin{matrix}
m*x+x(1-m) \\
m*y+y(1-m) \\
1 \\
\end{matrix}
\right]
$$

变换矩阵 A 即为以某一个点进行缩放的变换矩阵。

$$
A=\left[
\begin{matrix}
m & 0 & x(1-m) \\
0 & m & y(1-m) \\
0 & 0 & 1
\end{matrix}
\right]
$$

glMatrix

在矩阵变换过程中会频繁波及到矩阵的一些操作,特地是矩阵的乘法,能够应用 glMatrix 库,帮忙咱们简化矩阵的计算过程。

如果计算两个矩阵相乘:

const out = new Float32Array([
  0, 0, 0,
  0, 0, 0, 
  0, 0, 0,
]);

const o = new Float32Array([
  1, 0, 0,
  0, 1, 0,
  0, 0, 1,
]);

const t = new Float32Array([
  3, 0, 0,
  0, 3, 0,
  0, 0, 3,
]);

const nv = mat3.multiply(out, t, o);

应用 glMatrix 库进行矩阵计算的时候有一点要留神:

This may lead to some confusion when referencing OpenGL documentation, however, which represents out all matricies in column-major format

glMatrix 文档中强调,glMatrix 是以列为主的格局。也就是说一个矩阵,在 glMatrix 中会变成矩阵的转置进行计算。

$$
A=\left[
\begin{matrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
x & y & z & 0 \\
\end{matrix}
\right]
$$

在输出到 glMatrix 中会转换为 A 的转置进行计算:

$$
A=\left[
\begin{matrix}
1 & 0 & 0 & x \\
0 & 1 & 0 & y \\
0 & 0 & 1 & z \\
0 & 0 & 0 & 0 \\
\end{matrix}
\right]
$$

代码实现

实现拖拽

e.movementXe.movementY 记录了鼠标程度方向和垂直方向两次挪动之间的间隔。t 是变换矩阵,留神这里的矩阵 tFloat32Array 类型示意,并且输出到 glMatrix 中参加计算时须要变为 t 的转置(前文曾经介绍,glMatrix 是以列为主的格局)。

onMousemove(e) {const { movementX, movementY} = e;
  const t = new Float32Array([
    1, 0, 0,
    0, 1, 0,
    movementX, movementY, 1,
  ]);
  this.matrix = this.refresh(this.matrix, t);
}

每次挪动之后,调用 refresh 办法,利用 glMatrix.mat3.multiply 办法从新计算挪动之后的矩阵值,通过 ctx.transform 办法来更新视图。

refresh(o, t) {
  const out = new Float32Array([
    0, 0, 0,
    0, 0, 0,
    0, 0, 0,
  ]);
  const calc = glMatrix.mat3.multiply(out, t, o);
  this.ctx.save();
  this.ctx.clearRect(0, 0, this.width, this.height);
  this.ctx.transform(calc[0], calc[3], calc[1], calc[4], calc[6], calc[7]);
  this.draw();
  this.ctx.restore();
  return calc;
}

实现以鼠标以后地位为原点缩放

鼠标滚轮事件触发时,通过 event.deltaY 值来判断是放大还是放大,放大超过最大值或者放大低于最小值时则进行缩放。

以鼠标以后地位为原点缩放的要害是在缩放的同时须要思考偏移量,程度方向的偏移量为 clientX * (1 - zoom),垂直方向的偏移量为 clientY * (1 - zoom)

onMousewheel(e) {e.preventDefault();
  const {clientX, clientY, deltaY} = e;
  const zoom = 1 + (deltaY < 0 ? this.scaleStep : -this.scaleStep);
  this.scale = parseFloat((this.scale * zoom).toFixed(2));

  if (this.scale < this.minScale) {
    this.scale = this.minScale;
    return;
  } else if(this.scale > this.maxScale) {
    this.scale = this.maxScale;
    return;
  }

  const x = clientX * (1 - zoom);
  const y = clientY * (1 - zoom);
  const t = new Float32Array([
    zoom, 0, 0,
    0, zoom, 0,
    x, y, 1,
  ]);
  this.matrix = this.refresh(this.matrix, t);
}

总结

通过矩阵的形式用 Canvas 实现以鼠标以后地位为原点缩放及画布拖动,了解起来更加容易(当然前提是要有肯定的数学根底,起码理解过矩阵🤣),大大减少了代码量,同时缩放和拖拽的逻辑能够复用,不仅是在 Canvas 中,一般的 div 拖拽和放大也是一样的代码逻辑。

更多精彩文章欢送大家关注我的 vx 公众号:前端架构师笔记。本文残缺代码地址:https://github.com/astonishqft/canvas-matrix

退出移动版