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

《html5 Canvas画图教程26:用transform来实现位移,缩放,旋转等》有1个想法

发表评论

电子邮件地址不会被公开。