变换

在本教程的前面,我们学习了有关 canvas 网格 和 **坐标空间** 的知识。到目前为止,我们只使用了默认网格,并根据需要调整整个 canvas 的大小。使用变换,我们可以更强大地将原点移动到不同的位置、旋转网格甚至缩放网格。

保存和恢复状态

在我们查看变换方法之前,让我们先看看另外两种方法,一旦你开始生成越来越复杂的图形,它们就必不可少。

save()

保存 canvas 的整个状态。

restore()

恢复最近保存的 canvas 状态。

Canvas 状态存储在堆栈中。每次调用 save() 方法时,当前绘图状态都会被推入堆栈。绘图状态包含:

你可以根据需要多次调用 save() 方法。每次调用 restore() 方法时,最后一个保存的状态都会从堆栈中弹出,并且所有保存的设置都会被恢复。

一个 saverestore canvas 状态的例子

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");

  ctx.fillRect(0, 0, 150, 150); // Draw a Black rectangle with default settings
  ctx.save(); // Save the original default state

  ctx.fillStyle = "#09F"; // Make changes to saved settings
  ctx.fillRect(15, 15, 120, 120); // Draw a Blue rectangle with new settings
  ctx.save(); // Save the current state

  ctx.fillStyle = "#FFF"; // Make changes to saved settings
  ctx.globalAlpha = 0.5;
  ctx.fillRect(30, 30, 90, 90); // Draw a 50%-White rectangle with newest settings

  ctx.restore(); // Restore to previous state
  ctx.fillRect(45, 45, 60, 60); // Draw a rectangle with restored Blue setting

  ctx.restore(); // Restore to original state
  ctx.fillRect(60, 60, 30, 30); // Draw a rectangle with restored Black setting
}

第一步是使用默认设置绘制一个大矩形。接下来,我们保存此状态并更改填充颜色。然后我们绘制第二个更小的蓝色矩形并保存状态。再次更改一些绘图设置并绘制第三个半透明的白色矩形。

到目前为止,这与我们在前面几节中所做的非常相似。但是,一旦我们调用第一个 restore() 语句,最上面的绘图状态就会从堆栈中删除,并且设置会被恢复。如果我们没有使用 save() 保存状态,则需要手动更改填充颜色和透明度才能返回到先前状态。对于两个属性来说这很容易,但如果我们有不止两个属性,我们的代码会很快变得很长。

当第二个 restore() 语句被调用时,原始状态(我们在第一次调用 save 之前设置的状态)会被恢复,最后一个矩形再次被绘制为黑色。

平移

我们首先要看的是变换方法中的 translate()。此方法用于将 canvas 及其原点移动到网格中的不同点。

translate(x, y)

将 canvas 及其原点移动到网格上的位置。x 指示要移动的水平距离,y 指示要垂直移动网格的距离。

The canvas is pushed down and to the right, or translated, from its origin point on the grid by 'x' units horizontally and 'y' units vertically.

在执行任何变换之前保存 canvas 状态是一个好主意。在大多数情况下,调用 restore 方法比手动反向平移以返回到原始状态更容易。此外,如果你在循环中执行平移并且没有保存和恢复 canvas 状态,你可能会错过部分图形,因为这些图形绘制在 canvas 边缘之外。

一个 translate 的例子

此示例演示了平移 canvas 原点的一些好处。如果没有 translate() 方法,所有矩形都将在相同位置 (0,0) 绘制。translate() 方法还使我们能够在 canvas 上的任意位置放置矩形,而无需手动调整 fillRect() 函数中的坐标。这使它更容易理解和使用。

draw() 函数中,我们使用两个 for 循环九次调用 fillRect() 函数。在每个循环中,canvas 被平移,矩形被绘制,canvas 被返回到其原始状态。注意 fillRect() 的调用每次使用相同的坐标,依赖 translate() 来调整绘制位置。

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      ctx.save();
      ctx.fillStyle = `rgb(${51 * i} ${255 - 51 * i} 255)`;
      ctx.translate(10 + j * 50, 10 + i * 50);
      ctx.fillRect(0, 0, 25, 25);
      ctx.restore();
    }
  }
}

旋转

第二个变换方法是 rotate()。我们用它来绕当前原点旋转 canvas。

rotate(angle)

将 canvas 绕当前原点顺时针旋转 angle 个弧度。

The default origin point is at the top left, 0 degrees is horizontal and to the right. The rotation point starts from the origin point and goes clockwise.

旋转中心点始终是 canvas 原点。若要更改中心点,我们需要使用 translate() 方法移动 canvas。

一个 rotate 的例子

在此示例中,我们将使用 rotate() 方法首先从 canvas 原点旋转矩形,然后借助 translate() 从矩形的中心旋转矩形。

**注意:**角度以弧度为单位,而不是度数。若要转换,我们使用:radians = (Math.PI/180)*degrees

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");

  // left rectangles, rotate from canvas origin
  ctx.save();
  // blue rect
  ctx.fillStyle = "#0095DD";
  ctx.fillRect(30, 30, 100, 100);
  ctx.rotate((Math.PI / 180) * 25);
  // grey rect
  ctx.fillStyle = "#4D4E53";
  ctx.fillRect(30, 30, 100, 100);
  ctx.restore();

  // right rectangles, rotate from rectangle center
  // draw blue rect
  ctx.fillStyle = "#0095DD";
  ctx.fillRect(150, 30, 100, 100);

  ctx.translate(200, 80); // translate to rectangle center
  // x = x + 0.5 * width
  // y = y + 0.5 * height
  ctx.rotate((Math.PI / 180) * 25); // rotate
  ctx.translate(-200, -80); // translate back

  // draw grey rect
  ctx.fillStyle = "#4D4E53";
  ctx.fillRect(150, 30, 100, 100);
}

若要绕矩形自身中心旋转矩形,我们将 canvas 平移到矩形的中心,然后旋转 canvas,然后将 canvas 平移回 0,0,然后绘制矩形。

缩放

下一个变换方法是缩放。我们使用它来增加或减小 canvas 网格中的单位。这可以用来绘制缩小或放大的形状和位图。

scale(x, y)

将 canvas 单位水平缩放 x 倍,垂直缩放 y 倍。这两个参数都是实数。小于 1.0 的值会减小单位大小,大于 1.0 的值会增加单位大小。1.0 的值会保持单位大小不变。

使用负数可以进行轴镜像(例如,使用 translate(0,canvas.height); scale(1,-1); 你将拥有众所周知的笛卡尔坐标系,原点位于左下角)。

默认情况下,canvas 上的一个单位正好是一个像素。如果我们应用,例如,0.5 的缩放因子,那么得到的单位将变成 0.5 个像素,因此形状将被绘制为一半大小。类似地,将缩放因子设置为 2.0 会增加单位大小,一个单位现在变成两个像素。这会导致形状被绘制为两倍大小。

一个 scale 的例子

在此最后一个示例中,我们将绘制具有不同缩放因子的形状。

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");

  // draw a simple rectangle, but scale it.
  ctx.save();
  ctx.scale(10, 3);
  ctx.fillRect(1, 10, 10, 10);
  ctx.restore();

  // mirror horizontally
  ctx.scale(-1, 1);
  ctx.font = "48px serif";
  ctx.fillText("MDN", -135, 120);
}

变换

最后,以下变换方法允许直接修改变换矩阵。

transform(a, b, c, d, e, f)

将当前变换矩阵与由其参数描述的矩阵相乘。变换矩阵由下式描述

[ a c e b d f 0 0 1 ] \left[ \begin{array}{ccc} a & c & e \\ b & d & f \\ 0 & 0 & 1 \end{array} \right]

如果任何参数为 Infinity,则变换矩阵必须被标记为无限,而不是方法抛出异常。

此函数的参数为:

a (m11)

水平缩放。

b (m12)

水平倾斜。

c (m21)

垂直倾斜。

d (m22)

垂直缩放。

e (dx)

水平移动。

f (dy)

垂直移动。

setTransform(a, b, c, d, e, f)

将当前变换重置为单位矩阵,然后使用相同的参数调用 transform() 方法。这基本上是撤消当前变换,然后设置指定的变换,所有操作都在一步完成。

resetTransform()

将当前变换重置为单位矩阵。这与调用 ctx.setTransform(1, 0, 0, 1, 0, 0); 相同。

transformsetTransform 的示例

js
function draw() {
  const ctx = document.getElementById("canvas").getContext("2d");

  const sin = Math.sin(Math.PI / 6);
  const cos = Math.cos(Math.PI / 6);
  ctx.translate(100, 100);
  let c = 0;
  for (let i = 0; i <= 12; i++) {
    c = Math.floor((255 / 12) * i);
    ctx.fillStyle = `rgb(${c} ${c} ${c})`;
    ctx.fillRect(0, 0, 100, 10);
    ctx.transform(cos, sin, -sin, cos, 0, 0);
  }

  ctx.setTransform(-1, 0, 0, 1, 100, 100);
  ctx.fillStyle = "rgb(255 128 255 / 50%)";
  ctx.fillRect(0, 50, 100, 100);
}