本文属于《html5 Canvas画图系列教程》,本文有些长.
前面我讲过在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)是不是就表示旋转呢?很抱歉不是,他们是表示斜切.什么是斜切?把一个矩形的任一条边用力一拉,变成平行四边形,这就是斜切.
我们保持其他的不变,单独来试一下斜切效果:
1
2 3 |
ctx.
arc
(
200
,
50
,w
/
2
,
0
,
Math.
PI
*
2
)
. fillRect ( 200 , 100 , 50 , 50 ) . stroke ( ) |
以上代码是初始没有斜切时的,其效果如图:
现在我们加上tranform的斜切:
1
2 3 4 |
ctx.
transform
(
1
,
Math.
tan
(
Math.
PI
/
180
*
30
)
,
0
,
1
,
0
,
0
)
. arc ( 200 , 50 ,w / 2 , 0 , Math. PI * 2 ) . fillRect ( 200 , 100 , 50 , 50 ) . stroke ( ) |
效果:
可以看到矩形X轴产生了斜切效果.
另外,代码中我们可以看到使用了一个tan函数.为什么?
不为什么!我知道也不告诉你,更何况我也不知道.我只知道,如果你要用斜切,比如想斜切30度,那么就必须用tan把30度包起来,x/y轴都是如此.
结合前面所讲,矩阵的参数所指实际上是:
ctx.transform(scaleX,skewX,skewY,scaleY,transX,transY);
现在我们意外的实现了斜切,但旋转效果还没实现呢,可参数都已经占完了…
不用怕,因为旋转的效果是斜切配合缩放实现的.比如,其他的都不变,只把图形旋转30度,那么我们要这么做:
1
2 3 4 5 |
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),
sin(30*deg),
-sin(30*deg),
cos(30*deg)
这就是简单的旋转30度的方法—-看起来完全没有直观的rotate方法好懂啊!
不过大家记住,反正30度这个值是不会有变化的,我们只是要记住cos与sin的顺序.这篇文章里说我们可以这么记:CS-SC=初三-上床,我觉得很直观所以就直接推荐给你们了.
不要忘了那个-负号.
现在,单独的位移缩放旋转斜切我们都知道怎么做了,那么就来玩个大的,综合运用一把试试:
实现x轴放大至1.5倍Y轴不变,旋转30度,然后位移(111,111).
使用translate等直观方法的代码:
1
|
ctx.
scale
(
1.5
,
1
).
rotate
(
30
*deg
).
translate
(
111
,
111
)
|
如果你切实的使用过translate等方法,你就会知道,先旋转再位移与先位移再旋转得到的结果差别很大,所以,他们的先后顺序是很重要的.
而transform的矩阵有个最大的问题:如果我只用一句transform就同时实现旋转位移,那么transform是会先旋转还是先位移呢?
如果更进一步:我要先旋转再斜切,那transform的矩阵该如何计算?
最好我们能把所有计算都放在transform中,这样很节约代码—-虽然那样会让transform变得很长,且难以读懂;
另外我们还可以每次变化就写一句transform,旋转写一个,斜切写一个,这样也能轻松的控制先后顺序;
还有就是,找到旋转,斜切等变化的矩阵计算公式.
前面说了,我数学和几何都很差,连记个三角函数都困难,所以我跑去SO上问了这些变化的公式:so上的问题.然后我根据这些公式写了个Matrix类:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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类可以链式调用,如:
1
|
arr.
scale
(
2
,
1
).
rotate
(
30
*deg
).
translate
(
111
,
111
)
;
|
这样,我们就有顺序了.
粗看一下这些公式,你就会发现他们的计算过程和我前面讲的完全不一样!!不过我并没有坑你们,前面的分析都是针对单一效果的,比如只旋转,只位移,而其他的保持默认.如果你们把公式代入某个单一变化,会发现虽然公式很不同,但得到的结果就是前面的结果.
比如我们来个默认的矩阵先:[1,0,0,1,0,0].
我们使用公式来计算一下位移(111,111)的结果,公式如下:
1
2 3 4 |
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[5] += 0 * x + 1 * y;
即:
this[4] += x;
this[5] += y;
与前文结论完全一致.
个人看来transform使用起来不是很方便—其实是矩阵的计算就很不方便.使用transform,起不到节约代码的作用;但有些效果必须使用transform才能实现,比如斜切,canvas可没有一个叫skew方法.其他更复杂的变化就别提了.
前面提到我写的那什么Matrix类,变化计算后怎么使用呢?要知道transform需要的参数可是一个一个的,而Matrix生成的却是个数组.我一般是这样用的:
1
|
ctx.
transform.
apply
(ctx
,Matrix
)
|
你看懂了吗?
在最后,必须要提一下setTransform方法—-这个方法一看就是和transform一样的啦.不过他的作用是直接把矩阵设为你传给他的值,会清空前面所有的transform造成的效果;也就是说,transform的每次变化,都是在以前的矩阵上进行的(如果有的话).
setTransform用来干什么呢?我问大家一个问题:我不知道之前我的canvas是否有过translate,rotate,skew等操作,我也没有save过,但我现在要操作canvas,比如画个矩形,如果之前有变化过,那么我画出来肯定就不对了,那么,我怎么才能保证我画出来的就是我想要的呢?