Canvas学习:自定义的坐标变换

相对于Web坐标系统而言Canvas里的坐标系统较为复杂一些,除了默认的坐标系统之外还有坐标变换概念。在上一节中,已经了解了如何使用 scale()rotate()translate() 方法来变换坐标系。这三个方法提供了一种简便的手段,用于操作绘图环境对象的变换矩阵(Transformation Matrix)。默认情况下,这个变换矩阵就是 单位矩阵 (Identity Matrix),它并不会影响所要绘制的物体。当调用了 scale()rotate()translate() 方法之后,变换矩阵就会被修改,从而也会影响到所有后续的绘图操作。

在大多数情况之下,这三个方法就足够用了,不过,有些时候可能需要自己直接操作变换矩阵。比方说,如果要对所绘对象进行“ 错切 ”(Shear),那么就没有办法通过组合运用这三个方法来达成此效果。在这种情况下,就必须直接操作变换矩阵了。这一节,我们就一起来了解Canvas中的矩阵变换。

矩阵变换

Canvas的矩阵变换又称为 自定义的坐标变换 。Canvas的绘图环境对象( CanvasRenderingContext2D )提供了两个可以直接操作变换矩阵的方法:

  • CanvasRenderingContext2D.transform :在当前的变换矩阵之上叠加运用另外的变换效果
  • CanvasRenderingContext2D.setTransform :将当前的变换矩阵设置为默认的单位矩阵,然后在单位矩阵之上运用用户指定的变换效果

要点:多次调用 transform() 方法所造成的变换效果是累积的,而每次只要调用 setTransform() 方法,它就会将上一次的变换矩阵彻底清除。

在上一节中,我们了解到 translate()rotate()scale() 这三个方法都是通过操作变换矩阵来实现其功能的,也就是说,也可以直接使用 transform()setTransform() 方法来操作变换矩阵,实现坐标系统的平移、旋转和缩放等效果。直接使用 transformsetTransform() 方法操作变换矩阵有自己的优势,也有自己的劣势。

使用 transform()setTransform() 有两个好处:

  • 可以实现 scale()rotate()translate() 方法所达到的效果,比如错切效果
  • 只需调用一次 transform()setTransform() 方法,就可以做出结合了缩放、旋转、平移及错切等诸多操作的效果

使用 transform()setTransform() 方法的主要缺点则是,这两个方法不像 scale()rotate()translate() 方法那样直观。

上面的内容简单的提到 transform()setTransform() 方法是操作变换矩阵,那么要彻底的理解这两个方法,就得对矩阵有所了解。为了帮助大家更好的理解这两个方法,先来了解一下矩阵相关的知识。如果你对矩阵比较了解,可以忽略这些内容,直接跳到后面你想阅读的部分。

矩阵基础知识

矩阵 是一种非常有用的数学工具,尽管听起来可能有些吓人,不过一旦你理解了它们后,它们会变得非常有用。在讨论矩阵的过程中,我们需要使用到一些数学知识。对于一些愿意多了解这些知识的同学,我会附加一些资源给你们阅读。

在深入了解矩阵之前我们有必要先了解一些相关的概念。这一节的目标就是让大家拥有将来需要的最基础的数学背景知识。如果你发现这节十分困难,尽量尝试去理解它们,当你以后需要它们的时候回过头来复习这些概念。

向量

向量最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)和大小(Magnitude,也称之为长度)。可以把向量想像成一个藏宝图上的指示:“向左走十步,向北走三步,然后向右走五步”;“左”就是方向,“十步”就是向量的长度。那么这个藏宝图的指示一共有三个向量。向量可以在任意维度(Dimension)上,但是我们通常只使用 2~4 维。如果一个向量有两个维度,它表示一个平面的方向(想像一下2D的图像),当它有三个维度的时候,它可以表达一个3D世界的方向。

下图展示了三个向量,每个向量在2D图像中都用一个箭头 (x,y) 表示。我们在2D图片中展示这些向量,因为这样子会更直观一点。由于向量表示的是方向,起始于何处并不会改变它的值。

数学家喜欢在字母上面加一横表示向量,比如说在 v 的上面加 - 。当用在公式中时它们通常是这样的:

注:把2D向量当做 z 坐标轴为 0 的3D向量。

由于向量是一个方向,所以有些时候会很难形象地将它们用位置表示出来。为了让其更为直观,通常设定这个方向的原点为 (0,0,0) (在2D世界中,这个原点就是 (0,0) ),然后指向一个方向,对应一个点,使其变为 位置向量(Position Vector) 。比如上图中位置向量 (3,2) 在图像中的起点会是 (0,0) ,并会指向 (3,2)

向量与标量运算

标量(Scalar)只是一个数字(或者说是仅有一个分量的向量)。当把一个向量加、减、乘或除一个标量,我们可以简单的把向量的每个分量分别进行该运算。对于加法来说会像这样:

其中的 + 可以是 +-·÷ ,其中 · 是乘号。 注意, -÷ 运算时不能颠倒(标量 -/÷向量),它为颠倒的运算是没有定义的

向量取反

对一个向量取反(Negate)会将其方向逆转。一个向东北的向量取反后就指向西南方向了。我们在一个向量的每个分量前加负号就可以实现取反了(或者说用 -1 数乘该向量):

向量加减

向量的加法可以被定义为是分量的(Component-wise)相加,即将一个向量中的每一个分量加上另一个向量的对应分量:

向量 v = (4, 2)k = (1, 2) 可以直观地表示为:

就像普通数字的加减一样,向量的减法等于加上第二个向量的相反向量:

两个向量的相减会得到这两个向量指向位置的差。这在我们想要获取两点的差会非常有用。

长度

我们使用 勾股定理 (Pythagoras Theorem)来获取向量的长度(Length)/大小(Magnitude)。如果你把向量的 xy 分量画出来,该向量会和 xy 分量为边形成一个 三角形 :

因为两条边( xy )是已知的,如果希望知道斜边 的长度,我们可以直接通过勾股定理来计算:

向量相乘

两个向量相乘是一种很奇怪的情况。普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。但是在相乘的时候我们有两种特定情况可以选择:一个是点乘(Dot Product),记作 v¯⋅k¯ ,另一个是叉乘(Cross Product),记作 v¯×k¯

点乘

两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。可能听起来有点费解,我们来看一下公式:

它们之间的夹角记作 θ 。为什么这很有用?想象如果 都是单位向量,它们的长度会等于 1 。这样公式会有效简化成:

现在点积只定义了两个向量的夹角。你也许记得 90 度的余弦值是 00 度的余弦值是 1 。使用点乘可以很容易测试两个向量是否正交(Orthogonal)或平行(正交意味着两个向量互为直角)。如果你想要了解更多关于正弦或余弦函数的知识,我推荐你看 可汗学院 的基础三角学视频。

所以,我们该如何计算点乘呢?点乘是通过将对应分量逐个相乘,然后再把所得积相加来计算的。两个单位向量的(你可以验证它们的长度都为1)点乘会像是这样:

要计算两个单位向量间的夹角,我们可以使用 反余弦函数 cos−1 ,可得结果是 143.1 度。现在我们很快就计算出了这两个向量的夹角。点乘会在计算光照的时候非常有用。

叉乘

叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。接下来的教程中这会非常有用。下面的图片展示了3D空间中叉乘的样子:

不同于其他运算,如果你没有钻研过线性代数,可能会觉得叉乘很反直觉,所以只记住公式就没问题啦(记不住也没问题)。下面你会看到两个正交向量 AB 叉积:

是不是看起来毫无头绪?不过只要你按照步骤来了,你就能得到一个正交于两个输入向量的第三个向量。

矩阵

现在我们已经讨论了向量的全部内容,是时候看看矩阵了!简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个 2×3 矩阵的例子:

矩阵可以通过 (i, j) 进行索引, i 是行, j 是列,这就是上面的矩阵叫做 2×3 矩阵的原因( 32 行,也叫做矩阵的维度(Dimension))。这与你在索引2D图像时的 (x, y) 相反,获取 4 的索引是 (2, 1) (第二行,第一列)。

矩阵基本也就是这些了,它就是一个矩形的数学表达式阵列。和向量一样,矩阵也有非常漂亮的数学属性。矩阵有几个运算,分别是:矩阵加法、减法和乘法。

矩阵的加减

矩阵与标量之间的加减定义如下:

标量值要加到矩阵的每一个元素上。矩阵与标量的减法也相似:

矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,所以总体的规则和与标量运算是差不多的,只不过在相同索引下的元素才能进行运算。这也就是说加法和减法只对同维度的矩阵才是有定义的。一个 3×2 矩阵和一个 2×3 矩阵(或一个 3×3 矩阵与 4×4 矩阵)是不能进行加减的。我们看看两个 2×2 矩阵是怎样相加的:

矩阵的数乘

和矩阵与标量的加减一样,矩阵与标量之间的乘法也是矩阵的每一个元素分别乘以该标量。下面的例子展示了乘法的过程:

现在我们也就能明白为什么这些单独的数字要叫做标量(Scalar)了。简单来说,标量就是用它的值缩放(Scale)矩阵的所有元素。前面那个例子中,所有的元素都被放大了 2 倍。

到目前为止都还好,我们的例子都不复杂。不过矩阵与矩阵的乘法就不一样了。

矩阵相乘

矩阵之间的乘法不见得有多复杂,但的确很难让人适应。矩阵乘法基本上意味着遵照规定好的法则进行相乘。当然,相乘还有一些限制:

  • 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。
  • 矩阵相乘不遵守交换律(Commutative),也就是说 A⋅B≠B⋅A

我们先看一个两个 2×2 矩阵相乘的例子:

现在你可能会在想了:天哪,刚刚到底发生了什么? 矩阵的乘法是一系列乘法和加法组合的结果,它使用到了左侧矩阵的行和右侧矩阵的列。我们可以看下面的图片:

我们首先把左侧矩阵的行和右侧矩阵的列拿出来。这些挑出来行和列将决定我们该计算结果 2x2 矩阵的哪个输出值。如果取的是左矩阵的第一行,输出值就会出现在结果矩阵的第一行。接下来再取一列,如果我们取的是右矩阵的第一列,最终值则会出现在结果矩阵的第一列。这正是红框里的情况。如果想计算结果矩阵右下角的值,我们要用第一个矩阵的第二行和第二个矩阵的第二列。

计算一项的结果值的方式是先计算左侧矩阵对应行和右侧矩阵对应列的第一个元素之积,然后是第二个,第三个,第四个等等,然后把所有的乘积相加,这就是结果了。现在我们就能解释为什么左侧矩阵的列数必须和右侧矩阵的行数相等了,如果不相等这一步的运算就无法完成了!

结果矩阵的维度是 (n, m)n 等于左侧矩阵的行数, m 等于右侧矩阵的列数。

如果在脑子里想象出这一乘法有些困难,别担心。不断地动手计算,如果遇到困难再回头看这页的内容。随着时间流逝,矩阵乘法对你来说会变成很自然的事。

我们用一个更大的例子来结束对矩阵相乘的讨论。试着使用颜色来寻找规律。作为一个有用的练习,你可以试着自己解答一下这个乘法问题,再将你的结果和图中的这个进行对比(如果用笔计算,你很快就能掌握它们)。

可以看到,矩阵相乘非常繁琐而容易出错(这也是我们通常让计算机做这件事的原因),而且当矩阵变大以后很快就会出现问题。如果你仍然希望了解更多,或对矩阵的数学性质感到好奇,我强烈推荐你看看 可汗学院 的矩阵教程。

不管怎样,现在我们知道如何进行矩阵相乘了,我们可以开始学习好东西了。

矩阵与向量相乘

目前为止,通过这些教程我们已经相当了解向量了。我们用向量来表示位置,表示颜色,甚至是纹理坐标。让我们更深入了解一下向量,它其实就是一个 N×1 矩阵, N 表示向量分量的个数(也叫 N 维(N-dimensional)向量)。如果你仔细思考一下就会明白。向量和矩阵一样都是一个数字序列,但它只有 1 列。那么,这个新的定义对我们有什么帮助呢?如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的 N×1 向量,因为这个矩阵的列数等于向量的行数,所以它们就能相乘。

但是为什么我们会关心矩阵能否乘以一个向量?好吧,正巧,很多有趣的2D/3D变换都可以放在一个矩阵中,用这个矩阵乘以我们的向量将变换(Transform)这个向量。如果你仍然有些困惑,我们来看一些例子,你很快就能明白了。

单位矩阵

最简单的变换矩阵就是 单位矩阵 (Identity Matrix)。单位矩阵是一个除了对角线以外都是 0N×N 矩阵。在下式中可以看到,这种变换矩阵使一个向量完全不变:

向量看起来完全没变。从乘法法则来看就很容易理解来:第一个结果元素是矩阵的第一行的每个元素乘以向量的每个对应元素。因为每行的元素除了第一个都是 0 ,可得: 1⋅1+0⋅2+0⋅3+0⋅4=1 ,向量的其他 3 个元素同理。

理解Canvas中的transform、setTransform

为了能更好的理解Canvas中的 transform()setTransform() 方法,我们花了很大的篇幅介绍了一些基础知识。如果你理解了上述的内容,接下来的内容就能更好的理解。那么我们接下来就开始来理解这两个方法。

  • CanvasRenderingContext2D.transform() 是Canvas 2D API使用矩阵多次叠加当前变换的方法,矩阵由方法的参数进行描述。可以实现缩放、旋转、移动和倾斜等效果。
  • CanvasRenderingContext2D.setTransform() 是Canvas 2D API使用单位矩阵重新设置(覆盖)当前的变换并调用变换的方法,此变换由方法的变量进行描述。

这两个方法都接受六个参数:

ctx.transform(a, b, c, d, e, f);
ctx.setTransform(a, b, c, d, e, f);

其中这六个参数可以使用一个变换矩阵来描述:

其具体代表的是:

  • a : 表示水平缩放 scaleX
  • b : 表示水平倾斜 skewX
  • c : 表示垂直倾斜 skewY
  • d : 表示垂直缩放 scaleY
  • e : 表示水平移动 translateX
  • f : 表示垂直移动 translateY

该方法使用一个新的变化矩阵与当前变换矩阵进行乘法运算,该变换矩阵的形式如下:

你可以忽略最后一行,因为你不需要也不能修改它的值。最重要的是第一行和第二行,其中包含的数字值对应画布中使用的 a~f 。如上图所示,每一个数字值都对应一种特定的变形。

在坐标变换一切中,我们得知: 除平移 translate() 之外,旋转 rotate() 、缩放 scale() 都可以围绕一个中心点来进行,如果不指定,在默认情况下是围绕 (0,0) 原点进行相应的变换

而前面也说过, transform()setTransform() 可以帮助我们实现 translate()scale()rotate() 等变形。那接下来我们来看看如何实现。

平移

通过前面的知识,我们知道,不管 transform() 还是 setTransform() 都是矩阵变换。那先来看看平移对应的阵。

假定有一个点的坐标是 P(x0,y0) ,将移动到 P(x,y) 位置处,再假定在 x 轴和 y 轴方向移动的大小分别为:

如下图所示:

不难知道:

如果用矩阵来表示的话,就可以写成:

前面也介绍了矩阵怎么相乘。

理论有了,咱们来写实例。先来看 translate() 平移的效果:

ctx.fillStyle = '#f36';
ctx.translate(100, 100);
ctx.fillRect(0,0,100,100);

效果如下:

接下来我们换成 transform()setTransform() 方法来实现:

ctx.fillStyle = '#f36';
ctx.transform(1, 0, 0, 1, 100, 100);
ctx.fillRect(0,0,100,100);

根据前面的矩阵,可以很轻易得到 ctx.transform(1,0,0,1,100,100) 。效果如下:

效果和前面的是一样的,也就是说: ctx.transform(1, 0, 0, 1, dx, dy);ctx.translate(dx, dy) 是等效的。

注:其中 ctx.transform(1, 0, 0, 1, dx, dy) 等同于 ctx.transform(0, 1, 1, 0, dx, dy)

上面看到的效果是 transform() 实现的平移,其实也可以直接将 transform() 替换成 setTransform()

缩放

理论上而言,一个点是不存在什么缩放变换的,但考虑到所有图像都是由点组成,因此,如果图像在 x 轴和 y 轴方向分别放大 k1k2 倍的话,那么图像中的所有点的 x 坐标和 y 坐标均会分别放大 k1k2 倍。

用公式表示就是: (x y) 代表原坐标的点, (x',y') 代表新坐标的点。

如果用矩阵来表示就是:

我们分别进行两个测试, X 方向测试矩阵如下:

ctx.fillStyle = '#f36';
ctx.transform(.5, 0, 0, 1, 0, 0);
ctx.fillRect(0,0,100,100);

效果如下:

可以看到,正方形在 X 方向上进行了缩放,变成了之前的 0.5 倍。再进行 Y 方向的矩阵测试:

ctx.fillStyle = '#f36';
ctx.transform(1, 0, 0, .5, 0, 0);
ctx.fillRect(0,0,100,100);

效果如下:

没有问题, Y 方向变成原来的 0.5 倍。

注:从上面的示例可以看出来,不管是 X 轴还是 Y 的缩放都是基于原点进行的。如果需要基于元素自身中心点做缩放,那需要在 transform()setTransform() 之前先做 translate() 操作。当然也可以使用 transform()setTransform() 来替代 translate()

旋转

前面我们看到了怎么通过 transform()setTransform() 方法来实现 translate()scale() 的效果,接下来看怎么实现 rotate() 效果。因为旋转涉及到角度的问题,这里有一个数学知识很有必要先进行了解。

假定有一个点 P(x0,y0) 相对坐标原点顺时针旋转 θ 到达点 P(x,y) ,同时假定 P 点离坐标原点的距离为 r ,如下图所示:

那么要计算出 P 点的坐标,就需要运用到两角和公式:

sin(α + θ) = sin(α)*cos(θ) + cos(α)*sin(θ)
cos(α + θ) = cos(α)*cos(θ) - sin(α)*sin(θ)

除了有两角和公式之外,还有对应的两角差公式:

sin(α - θ) = sin(α)*cos(θ) - cos(α)*sin(θ)
cos(α - θ) = cos(α)*cos(θ) + sin(α)*sin(θ)

根据上图我们可以推导出, P0 点以半径 r 旋转一定的角度 α ,然后位置在 (x0,y0) 位置处,并且从 (x0,y0) 处继续围绕原点旋转 θ 度到达 P(x,y) 处。根据前面的两角和公式,可以计算出 P(x,y) 的值:

x = r * cos(α + θ) = r*cos(α)*cos(θ) - r*sin(α)*sin(θ)
y = r * sin(α + θ) = r*sin(α)*cos(θ) + r*cos(α)*sin(θ)

x0 = r*cos(α); y0=r*sin(θ) ,那么:

x = r * cos(α + θ) = r*cos(α)*cos(θ) - r*sin(α)*sin(θ) = x0*cos(θ) - y0*cos(θ)
y = r * sin(α + θ) = r*sin(α)*cos(θ) + r*cos(α)*sin(θ) = y0*cos(θ) + x0*sin(θ)

如果用矩阵,就可以表示为:

我们来进行一个 90 度旋转的测试, cos(90)=0,sin(90)=1 ,所以矩阵应该写成:

将上面的矩阵通过 transform() 来表示就是 transform(0,1,-1,0,0,0) 。来看在Canvas中的效果:

ctx.fillStyle = '#f36';
ctx.transform(0,1,-1,0,0,0);
ctx.fillRect(0,0,200,100);

如果就这样的话,大家在Canvas的画布中看不到任何的图形,有可能还以为是出错了,其实并非如此。为什么呢?因为如果从 (0,0) 点绘制一个旋转的图,那么肯定已经转到屏幕外面了,所以我们给它加一个偏移试试(位移前面介绍过了):

ctx.fillStyle = '#f36';
ctx.transform(0,1,-1,0,200,50);
ctx.fillRect(0,0,200,100);

这下效果就出来了:

斜切

斜切变换Skew在数学上又称为Shear Mapping或者Transvection,它是一种比较特殊的线性变换。大家不知道是否还记得,在上一节或者说本节内容前面,我们提到Canvas自定的变换有 rotate()scale()translate() ,就是没有看到 skew() 这样的方法。虽然没有自带 skew() 这样的方法,但值得庆幸的是,可以通过 transform() 或者说 setTransform() 方法来实现。接下来我们来看看怎么实现斜切变换。

斜切变换的效果就是让所有点的 x 坐标(或者 y 坐标)保持不变,而对应的 y 坐标(或者 x 坐标)按比例发生平移,且平移的大小和该点到 x 轴(或 y 轴)的垂直距离成正比。斜切变换,属于面积变换,即一个形状在斜切变换的前后,其面积是相等的。

比如下图,各点的 y 坐标保持不变,但其 x 坐标则按比例发生了平移。这种情况将发生水平斜切:

下图各点的 x 坐标保持不变,但其 y 坐标则按比例发生了平移。这种情况将发生垂直斜切:

与旋转变换相比,旋转变换是旋转坐标系中的点,而斜切内里是转坐标轴,坐标轴旋转之后,要保持各点的坐标值不变,所有各点的位置就变了,如下图:

变换后,点在新坐标系中的位置不变,在原坐标系中的位置是:

f(x) = x * tan(α)
f(y) = y * tan(α)

计算如下:

回到Canvas当中来,假定有一个点 P0(x0,y0) 经过斜切变换后得到 P(x,y) ,对于水平斜切,它们之关的关系是:

x = x0 + k*y0
y = y0

用矩阵表示就是:

扩展到 3 x 3 的矩阵就是下面这样的形式:

同理,对于垂直斜切,可以有:

在数学上严格的斜切变换就是上面这样,但在Canvas中除了有上面说的情况之外,还可以同时进行水平、垂直斜切,那么矩阵就变成:

根据上面的总结,我们使用 transform()setTransform() 可以实现斜切效果:

// 水平斜切
ctx.transform(1, 0, k, 1, 0, 0) 或者 ctx.setTransform(1, 0, k, 1, 0, 0)
// 垂直斜切
ctx.transform(1, k, 0, 1, 0, 0) 或者 ctx.setTransform(1, k, 0, 1, 0, 0)
// 水平和垂直方向斜切
ctx.transform(1, k1, k2, 1, 0, 0) 或者 ctx.setTransform(1, k1, k2, 1, 0, 0)

注意,其中 k 或者 k1k2 是弧度值,来看个实例:

ctx.fillStyle = '#f36';
ctx.transform(1, Math.PI * 30 / 180, Math.PI * 30 / 180, 1, 0, 0);
ctx.fillRect(0,0,100,100);

上面的示例相当于CSS中的 transform: skew(30deg, 30deg) :

对称变换

除了上面讲到的四中基本变换外,事实上,我们还可以利用矩阵,进行对称变换。所谓对称变换,就是经过变化后的图像和原图像是关于某个对称轴是对称的。比如,某点 P0(x0,y0) 结过对称变换后得到 P(x,y) 。如果对称轴是 x 轴,那么:

x = x0
y = -y0

用矩阵表示就是:

如果对称轴是 y 轴,那么:

x = -x0
y = y0

对矩阵表示就是:

如果对称轴是 y = x ,如图:

那么:

很容易可以解得:

x = x0
y = y0

用矩阵表示,就是:

同样的道理,如果对称轴是 y = -x ,那么用矩阵表示就是:

特殊地,如果对称轴是 y = kx ,如下图所示:

那么:

可以解得:

用矩阵表示:

k=0 时,即 y=0 ,也就是对称轴为 x 轴的情况;当 k 趋于无穷大时,即 x=0 ,也就是对称轴为 y 轴的情况;当 k=1 时,即 y=x ,也就是对称轴为 y=x 的情况;当 k=-1 时,即 y=-x ,也就是对称轴为 y=-x 的情况。不难验证,这和我们前面说到的四种具体情况是相吻合的。

如果对称轴是 y = kx + b 这样的情况,只需要在上面的基础上增加两次平移变换即可,即先将坐标原点移动到 (0,b) ,然后做上面的关于 y = kx 的对称变换,再然后将坐标原点移回到原来的坐标原点即可。用矩阵表示大致是这样的:

特别需要注意:在实际编程中,我们知道屏幕的 y 坐标的正向和数学中 y 坐标的正向刚好是相反的,所以在数学上 y=x 和屏幕上的 y=-x 才是真正的同一个东西,反之亦然。也就是说,如果要使图片在屏幕上看起来像按照数学意义上 y=x 对称,那么需要用这种转换:

要使用图片在屏幕上看起来像按照数学意义上 y=-x 对称,那么需要使用这种转换:

关于对称轴为 y=kxy=kx+b 的情况,同样需要考虑这方面的问题。

对称变换在实际中,可以帮助我们实现图像的镜像效果,比如下面的这个小示例:

ctx.strokeStyle = '#f36';
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(100, 100);
ctx.lineTo(50, 150);
ctx.closePath();
ctx.stroke();

ctx.setTransform(-1, 0, 0, -1, 220, 200);
ctx.strokeStyle = '#00f';
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(100, 100);
ctx.lineTo(50, 150);
ctx.closePath();
ctx.stroke();

效果如下:

前面介绍了怎么使用矩阵实现不同的变换。从前面的示例当中,不难发现,像旋转变换,缩放变换都是围绕坐标原点来做的。但实际使用当中,我们很多时候需要的围绕某个点做旋转和缩放。比如说让一个正方形围绕其自身中心点做旋转。那这样的怎么使用矩阵来表示呢?接下来我们简单的来看一下。

如果是围绕某个点 (xp,yp) 顺时针旋转 θ ,那么可以用矩阵表示为:

可以简化为:

这里涉及到了矩阵的计算,这也就是为什么我要在前面花很大的篇幅来介绍矩阵的基础知识。如果你仔细看了上面的内容,那对于这样的矩阵计算就不是什么难度了。其实在整个过程,我们红历了三步。

第一步,下面的矩阵:

实现了将坐标原点移动到点 (xp,yp) 后, P(x0,y0) 的新坐标。接下来下面的矩阵是将第一步变换后的 P(x0,y0) 围绕新的坐标原点顺时针旋转 θ

结过第二步的旋转变换之后,再将坐标原点移回到原来的坐标原点,用矩阵可以像下面这样表示:

所以围绕某一点进行旋转变换,可以分成上面三个步骤,即: 首先将坐标原点移至该点,然后围绕新的坐标原点进行旋转变换,再然后将坐标原点移回到原先的坐标原点 。上面是以旋转变换为例,其实对于缩放变换,原理是一致的,这里就不多阐述。

上面很多都涉及到矩阵,整了好多天,很多地方都是晕晕的,下面有几篇文章帮助大家更好的理解:

transform()和setTransform()区别

如果你坚持阅读到这里,或许你和我也有一个同样的问题想问: transform()setTransform() 都是矩阵变换,都同样接受六个参数,都是一样的矩阵计算。那他们是不是一样的呢?其实是不一样的:

  • transfrom() 不会重置前面的矩阵,做的是一种累加计算。每次调用 transform() 都会在前一个变换矩阵上构建。
  • setTransform() 他是一种重置计算。每次调用 setTransform() 都会重置前一个变换矩阵,然后构建新的矩阵。

还是通过一个简单的示例来演示他们之间的区别吧。

ctx.globalAlpha = 1;
ctx.fillStyle = 'orange';
ctx.fillRect(50, 50, 100, 100);

// 缩小和移动坐标
ctx.transform(.8, 0, 0, .8, 50, 50);
ctx.globalAlpha = .8;
ctx.fillStyle = 'green';
ctx.fillRect(0, 0, 100, 100);

// 再做一次缩小和移动
ctx.transform(.8, 0, 0, .8, 50, 50);
ctx.globalAlpha = .8;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);

// 将坐标重置回原点0,0
ctx.resetTransform();
ctx.globalAlpha = 1;
ctx.fillStyle = 'orange';
ctx.fillRect(350, 50, 100, 100);

// 缩小和移动坐标
ctx.setTransform(.8, 0, 0, .8, 350, 50);
ctx.globalAlpha = .8;
ctx.fillStyle = 'green';
ctx.fillRect(0, 0, 100, 100);

// 再做一次缩小和移动
ctx.setTransform(.8, 0, 0, .8, 350, 50);
ctx.globalAlpha = .8;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);

效果如下:

上面示例一下子就说明了两者的区别。具体的区别可以看代码体会出来。

有了变换,我们就可以通过他们来做一些事情,比如下面的一个简单的示例,制作一个万花筒效果:

resetTransform

在Canvas的 CanvasRenderingContext2D 对象中,除了有 transform()setTransform() 方法之外,还有一个 resetTransform() 方法。这个方法是使用单位矩阵重新设置当前变形的方法。它和 ctx.setTransform(1, 0, 0, 1, 0, 0); 效果等同。简单点讲就是对 ctx.setTransform(1, 0, 0, 1, 0, 0); 的封装。

HTML5 Transformation Matrix

在网上看到一个HTML5 Transformation Matrix的小工具,我将其扒下来了,这个Demo实现了Canvas中的缩放、位移和斜切效果,但没有旋转效果。感兴趣的同学可以看看:

当然有兴趣的同学,可以将旋转的功能添加进来。

总结

这篇文章花了很大的篇幅介绍了Canvas中的自定义的坐标变换,其实也就是矩阵变换。在Canvas中包括了 transform()setTransform()resetTransform() 方法,其中 transform()setTransform() 可以通过矩阵实现旋转、缩放、位移、斜切和对称等坐标变换。他们都是通过矩阵的变换来实现对应的功能,而且它们都接受六个参数。不同之处是: transform() 是基于上一个坐标变换做变换,是一种累积;setTransform()是重置前一个变换矩阵,然后构建新的矩阵。 最后 resetTransform() 是setTransform(1, 0, 0, 1, 0, 0);`的封装。

这篇文章还涉及了很多数学知识,从而再次说明数学在图形学中是多么的重要,后面的课程依旧会不断的在文章中补允对应的数学知识,帮助我们更好的在Canvas中绘制图形。如果您对此系列教程感兴趣,欢迎持续关注相关更新;如果文章有不对之处,还请各位大婶多多指正。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《 图解CSS3:核心技术与案例实战 》。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章