前面我讲过在canvas中实现图形的变换,这是比较简单的,因为都是用的直观的函数.今天我还是要实现同样的图形变化效果,但不同的是我要用一个看起来就让人心碎的方法,就是transform,也就是矩阵matrix.
其实我对Matrix的认识只限于他是一部很好看的电影(即黑客帝国),在没看此电影前,我根本不知道有矩阵这个名字,而且矩阵这名字又不霸气,我听了除了不明白为什么要叫这么个怪名字之外没什么感觉;看了电影,然后又知道矩阵是个数学上的东西后,我就知道要糟,作为一个数学白痴的我希望永远不要和矩阵打上交道.
无奈我居然做了程序员!
不说这些伤心事了.我要提前告诉大家,虽然前面讲的scale,tranlate,rotate是独立的方法,但实际上他们之所以能产生变化,都是因为他们操作了矩阵.而canvas的transform,就是直接操作矩阵,所以理论上效率还比前面说的这些方法要高.
代码如下 | 复制代码 |
ctx.transform(a,b,c,d,e,f); |
开始之前我还要提一个问题:图形都有矩阵,那一个图形的默认矩阵是什么样子的?
答案是:(1,0,0,1,0,0)
很奇怪这里面居然有两个1,怎么不是全都是0呢?
一个图形,在没有缩放,旋转,位移…什么的时候,他也会有一个属性会是1,就是—-缩放!因为在没有缩放的情况下,图形的缩放其实是原大小的1倍.所以,这个默认的矩阵里面才会有两个1.
而正如你所想,位置1上的1(即参数a),是表示x轴上的缩放,位置4上的1(即参数d)是表示y轴上的缩放!
所以要用矩阵来实现scale的效果就很简单了!
代码如下 | 复制代码 |
ctx.transform(scaleX,0,0,scaleY,0,0); |
看到这里你肯定希望能举一反三,既然a,d是表示缩放,那肯定有分别表示旋转,位移的数字吧?
没错!矩阵中的最后两位参数就是表示位移距离的数字(没有位移的情况下当然就是0了).即:
代码如下 | 复制代码 |
ctx.transform(scaleX,0,0,scaleY,transX,transY); |
那么剩下的两个数字(b,c)是不是就表示旋转呢?很抱歉不是,他们是表示斜切.什么是斜切?把一个矩形的任一条边用力一拉,变成平行四边形,这就是斜切.
我们保持其他的不变,单独来试一下斜切效果:
代码如下 | 复制代码 |
ctx.arc(200,50,w/2,0,Math.PI*2) .fillRect(200,100,50,50) .stroke() |
以上代码是初始没有斜切时的,其效果如图:
现在我们加上tranform的斜切:
代码如下 | 复制代码 |
ctx.transform(1,Math.tan(Math.PI/180*30),0,1,0,0) |
效果:
可以看到矩形X轴产生了斜切效果.
另外,代码中我们可以看到使用了一个tan函数.为什么?
不为什么!我知道也不告诉你,更何况我也不知道.我只知道,如果你要用斜切,比如想斜切30度,那么就必须用tan把30度包起来,x/y轴都是如此.
结合前面所讲,矩阵的参数所指实际上是:
代码如下 | 复制代码 |
ctx.transform(scaleX,skewX,skewY,scaleY,transX,transY); |
现在我们意外的实现了斜切,但旋转效果还没实现呢,可参数都已经占完了…
不用怕,因为旋转的效果是斜切配合缩放实现的.比如,其他的都不变,只把图形旋转30度,那么我们要这么做:
代码如下 | 复制代码 |
var deg = Math.PI/180; ctx.transform(Math.cos(30*deg),Math.sin(30*deg),-Math.sin(30*deg),Math.cos(30*deg),0,0) .arc(200,50,w/2,0,Math.PI*2) .fillRect(200,100,50,50) .stroke() |
大家看看transform里面的参数,真长,吓死个人了!依次是:
代码如下 | 复制代码 |
cos(30*deg), |
这就是简单的旋转30度的方法—-看起来完全没有直观的rotate方法好懂啊!
不过大家记住,反正30度这个值是不会有变化的,我们只是要记住cos与sin的顺序.这篇文章里说我们可以这么记:CS-SC=初三-上床,我觉得很直观所以就直接推荐给你们了.
不要忘了那个-负号.
现在,单独的位移缩放旋转斜切我们都知道怎么做了,那么就来玩个大的,综合运用一把试试:
实现x轴放大至1.5倍Y轴不变,旋转30度,然后位移(111,111).
使用translate等直观方法的代码:
代码如下 | 复制代码 |
ctx.scale(1.5,1).rotate(30*deg).translate(111,111) |
如果你切实的使用过translate等方法,你就会知道,先旋转再位移与先位移再旋转得到的结果差别很大,所以,他们的先后顺序是很重要的.
而transform的矩阵有个最大的问题:如果我只用一句transform就同时实现旋转位移,那么transform是会先旋转还是先位移呢?
如果更进一步:我要先旋转再斜切,那transform的矩阵该如何计算?
最好我们能把所有计算都放在transform中,这样很节约代码—-虽然那样会让transform变得很长,且难以读懂;
另外我们还可以每次变化就写一句transform,旋转写一个,斜切写一个,这样也能轻松的控制先后顺序;
还有就是,找到旋转,斜切等变化的矩阵计算公式.
前面说了,我数学和几何都很差,连记个三角函数都困难,所以我跑去SO上问了这些变化的公式:so上的问题.然后我根据这些公式写了个Matrix类:
代码如下 | 复制代码 |
function Matrix() { var x = (arguments.length>0) ? Array.prototype.slice.call(arguments) : [1,0,0,1,0,0]; for(var p in x)this[p]=x[p]; this.length=x.length; } Matrix.prototype = { rotate:function (r) { var cos = Math.cos(r), sin = Math.sin(r), mx = this, a = mx[0] * cos + mx[2] * sin, b = mx[1] * cos + mx[3] * sin, c = -mx[0] * sin + mx[2] * cos, d = -mx[1] * sin + mx[3] * cos; this[0] = a; this[1] = b; this[2] = c; this[3] = d; return this; }, skew: function(x,y) { var tanX=Math.tan(x), tanY=Math.tan(y), mx0=this[0], mx1=this[1]; this[0] += tanY*this[2]; this[1] += tanY*this[3]; this[2] += tanX*mx0; this[3] += tanX*mx1; return this; }, translate: function(x,y) { this[4] += this[0] * x + this[2] * y; this[5] += this[1] * x + this[3] * y; return this; }, scale:function (x,y) { var mx = this; this[0] *= x; this[1] *= x; this[2] *= y; this[3] *= y; return this; } } |
公式也在此类中.此Matrix类可以这么用:
代码如下 | 复制代码 |
var arr=new Matrix(); |
这样会建一个默认矩阵;也可以传一个矩阵给他,则会建一个你传的矩阵:
代码如下 | 复制代码 |
var arr=new Matrix(0.5,0.334,0,1,111,111); |
这个Matrix类可以链式调用,如:
代码如下 | 复制代码 |
arr.scale(2,1).rotate(30*deg).translate(111,111); |
这样,我们就有顺序了.
粗看一下这些公式,你就会发现他们的计算过程和我前面讲的完全不一样!!不过我并没有坑你们,前面的分析都是针对单一效果的,比如只旋转,只位移,而其他的保持默认.如果你们把公式代入某个单一变化,会发现虽然公式很不同,但得到的结果就是前面的结果.
比如我们来个默认的矩阵先:[1,0,0,1,0,0].
我们使用公式来计算一下位移(111,111)的结果,公式如下:
代码如下 | 复制代码 |
function translate(x,y) { this[4] += this[0] * x + this[2] * y; this[5] += this[1] * x + this[3] * y; } |
其中的this是一个矩阵.调用:translate(111,111),然后我们代入默认矩阵,则:
代码如下 | 复制代码 |
this[4] += 1 * x + 0 * y; 即: this[4] += x; |
与前文结论完全一致.
个人看来transform使用起来不是很方便—其实是矩阵的计算就很不方便.使用transform,起不到节约代码的作用;但有些效果必须使用transform才能实现,比如斜切,canvas可没有一个叫skew方法.其他更复杂的变化就别提了.
前面提到我写的那什么Matrix类,变化计算后怎么使用呢?要知道transform需要的参数可是一个一个的,而Matrix生成的却是个数组.我一般是这样用的:
代码如下 | 复制代码 |
ctx.transform.apply(ctx,Matrix) |
你看懂了吗?
在最后,必须要提一下setTransform方法—-这个方法一看就是和transform一样的啦.不过他的作用是直接把矩阵设为你传给他的值,会清空前面所有的transform造成的效果;也就是说,transform的每次变化,都是在以前的矩阵上进行的(如果有的话).
setTransform用来干什么呢?我问大家一个问题:我不知道之前我的canvas是否有过translate,rotate,skew等操作,我也没有save过,但我现在要操作canvas,比如画个矩形,如果之前有变化过,那么我画出来肯定就不对了,那么,我怎么才能保证我画出来的就是我想要的呢?