html5 Canvas画图教程26:用transform来实现位移,缩放,旋转等

本文属于《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,比如画个矩形,如果之前有变化过,那么我画出来肯定就不对了,那么,我怎么才能保证我画出来的就是我想要的呢?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值