转换

在本教程的早期,我们学习了画布网格坐标空间。到目前为止,我们只使用了默认的网格,并根据我们的需要更改了整个画布的大小。通过变换,有更强大的方法可以将原点平移到不同的位置、旋转网格甚至缩放网格。

保存和恢复状态

在查看变换方法之前,让我们先看另外两种方法,一旦你开始生成更复杂的绘图,这两种方法将是不可或缺的。

save()

保存画布的整个状态。

restore()

恢复最近保存的画布状态。

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

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

保存和恢复画布状态的示例

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 = "#0099ff"; // 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 = "white"; // 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()。此方法用于将画布及其原点移动到网格中的不同点。

translate(x, y)

在网格上移动画布及其原点。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.

在进行任何变换之前保存画布状态是一个好主意。在大多数情况下,调用 restore 方法比执行反向平移以返回原始状态要容易得多。此外,如果您在循环中进行平移而不保存和恢复画布状态,您可能会丢失部分绘图,因为它绘制到了画布边缘之外。

translate 示例

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

draw() 函数中,我们使用两个 for 循环调用 fillRect() 函数九次。在每个循环中,画布都会被平移,绘制矩形,然后画布会恢复到其原始状态。请注意 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()。我们使用它围绕当前原点旋转画布。

rotate(angle)

围绕当前原点以 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.

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

rotate 示例

在此示例中,我们将使用 rotate() 方法,首先围绕画布原点旋转一个矩形,然后借助 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);
}

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

缩放

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

scale(x, y)

在水平方向上按 x,在垂直方向上按 y 缩放画布单位。这两个参数都是实数。小于 1.0 的值会减小单位大小,大于 1.0 的值会增大单位大小。值为 1.0 时单位大小不变。

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

默认情况下,画布上的一个单位正好是一个像素。如果我们应用,例如,缩放因子 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)

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

[acebdf001]\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);
}