如果是涉及到游戲或動畫的編程,我們很可能會用到幾何變換。如果在大學過線性代數的話,我們就會知道,無論是2d還是3d的幾何變換,矩陣都是實現線性變換的一個重要工具。任意線性變換都可以用矩陣表示為易於計算的一致形式,並且多個變換也可以很容易地通過矩陣的相乘連接在一起。本文章主要對如下的變換進行簡單的封裝,並簡單闡述其中的變換原理:
1.平移變換:只改變圖形的位置,不改變大小。
2.旋轉變換:保持圖形各部分之間的關系,變換后形狀不變。
3.比例變換:可改變圖形大小和形狀。
4.錯切變換:引起圖形角度關系的改變,甚至導致圖形發生畸變。
5.對稱變換:使圖形對稱於x軸或y軸或y=x或y=-x的變換。
程序中,我們將定義一個矩陣類Matrix,其中的matrix屬性保存以二維數組表示的矩陣形式,當我們初始化矩陣對象時,需要傳入該矩陣的具體數據:
var mtx= new matrix({ matrix:[ [1,0,0], [0,1,0], [0,0,1] ] });
由於在變換過程中需要用到矩陣操作的一些通用方法,因此我們可以先在矩陣類中添加這些實例方法:
/** *矩陣相加 **/ add:function(mtx){ var omtx=this.matrix; var newMtx=[]; if(!mtx.length||!mtx[0].length||mtx.length!=omtx.length||mtx[0].length!=omtx[0].length){//如果矩陣為空,或行或列不相等,則返回 return; } for(var i=0,len1=omtx.length;i<len1;i++){ var rowMtx=omtx[i]; newMtx.push([]); for(var j=0,len2=rowMtx.length;j<len2;j++){ newMtx[i][j]=rowMtx[j]+mtx[i][j]; } } this.matrix=newMtx; return this; }, /** *矩陣相乘 **/ multiply:function(mtx){ var omtx=this.matrix; var mtx=mtx.matrix; var newMtx=[]; //和數字相乘 if(cg.core.isNum(mtx)){ for(var i=0,len1=omtx.length;i<len1;i++){ var rowMtx=omtx[i]; newMtx.push([]); for(var j=0,len2=rowMtx.length;j<len2;j++){ omtx[i][j]*=mtx; } } return new matrix({matrix:newMtx}); } //和矩陣相乘 var sum=0; for(var i=0,len1=omtx.length;i<len1;i++){ var rowMtx=omtx[i]; newMtx.push([]); for(var m=0,len3=mtx[0].length;m<len3;m++){ for(var j=0,len2=rowMtx.length;j<len2;j++){ sum+=omtx[i][j]*mtx[j][m]; } newMtx[newMtx.length-1].push(sum); sum=0; } } this.matrix=newMtx; return this; },
每個操作完成后,返回對象本身,這樣的鏈式調用有利於我們對矩陣進行多次連續的操作,實現一些列的圖形變換。
2D坐標系下的變換:
2d坐標系下,我們使用如下的變換矩陣:

其中,左上區域(abde)負責圖形的縮放、旋轉、對稱、錯切等變換,cf負責平移變換。
2d平移:
使點平移一定位移,我們使用的變換矩陣是:

使x,y點平移Tx,Ty的位移,則使[x,y,1]矩陣與上面的矩陣相乘,具體的封裝如下:
/** *2d平移 **/ translate2D:function(x,y){ var changeMtx= new matrix({ matrix:[ [1,0,0], [0,1,0], [x,y,1] ] }); return this.multiply(changeMtx); },
2d縮放:
2d坐標軸下以原點為參考點時,縮放變換使用的變換矩陣如下:

使[x,y,1]矩陣與上面的矩陣相乘,則可得出相對於原點縮放后的坐標值。
但是很多情況下,我們可能需要相對於任意點進行縮放而不單單限制於相對於原點的縮放,因此我們這里最好封裝相對於坐標軸上任意點的縮放方法。相對於任意點的縮放其實也是由相對於原點的縮放轉換而來,相對於點(x1,y1)的縮放計算具體步驟如下:
把坐標原點平移至(x1,y1)-->進行相對於原點的變換-->把坐標原點平移回去
矩陣計算:

因此對任意點縮放的方法封裝如下:
/** *2d縮放 **/ scale2D:function(scale,point){//縮放比,參考點 var sx=scale[0],sy=scale[1],x=point[0],y=point[1]; var changeMtx= new matrix({ matrix:[ [sx,0,0], [0,sy,0], [(1-sx)*x,(1-sy)*y,1] ] }); return this.multiply(changeMtx); },
2d對稱變換:
2d對稱變換使用的變換矩陣如下:

不同的對稱變換,通過abde的值的改變來實現:
相對於x軸的變換:b=d=0 a=1 e=-1
相對於y軸的變換:b=d=0 a=-1 e=1
相對於原點的變換:b=d=0 a=-1 e=-1
相對於y=x的變換:b=d=1 a=e=0
現對於y=-x的變換:b=d=-1 a=e=0
因此乘以不同的變換矩陣,就可以得到不同的變換效果:
/** *2d對稱變換 **/ symmet2D:function(axis){//對稱軸 var changeMtx; axis=="x"&&(changeMtx= new matrix({//相對於x軸對稱 matrix:[ [1,0,0], [0,-1,0], [0,0,1] ] })); axis=="y"&&(changeMtx= new matrix({//相對於y軸對稱 matrix:[ [-1,0,0], [0,1,0], [0,0,1] ] })); axis=="xy"&&(changeMtx= new matrix({//相對於原點對稱 matrix:[ [-1,0,0], [0,-1,0], [0,0,1] ] })); axis=="y=x"&&(changeMtx= new matrix({//相對於y=x對稱 matrix:[ [0,1,0], [1,0,0], [0,0,1] ] })); axis=="y=-x"&&(changeMtx= new matrix({//相對於y=-x對稱 matrix:[ [0,-1,0], [-1,0,0], [0,0,1] ] })); return this.multiply(changeMtx); },
2d錯切變換:
所謂的錯切變換,就是圖形發生傾斜等的畸變,例如下圖中分別是x方向和y方向下的錯切變換:

錯切變換下的變換矩陣為:

[x,y]變換后的坐標為:[x+by,dx+y,1],可以看出,當b不為0,d為0時,y值不變,x軸根據b的值作線性增長,因此可以得出上面的a圖。當b為0,d不為0時,x值不變,y軸根據d的值作線性增長,因此可以得出上面的b圖。
錯切變換的封裝:
/** *2d錯切變換 **/ shear2D:function(kx,ky){ var changeMtx= new matrix({ matrix:[ [1,kx,0], [ky,1,0], [0,0,1] ] }); return this.multiply(changeMtx); },
2d旋轉變換:
旋轉變換和縮放變換一樣,同樣有相對於原點變換和相對於任意點變換這兩種情況,因此這里也直接封裝相對於任意點變換的方法。
相對於任意點(x1,y1)旋轉=把坐標原點平移到(x1,y1)-->相對於原點旋轉 -->把原點平移回去
相對於原點的旋轉變換矩陣為:

因此相對於任意點旋轉的變換矩陣為:

所以封裝方法如下:
/** *2d旋轉 **/ rotate2D:function(angle,point){ var x=point[0],y=point[1]; var cos=Math.cos; var sin=Math.sin; var changeMtx= new matrix({ matrix:[ [cos(angle),sin(angle),0], [-sin(angle),cos(angle),0], [(1-cos(angle))*x+y*sin(angle),(1-cos(angle))*y-x*sin(angle),1] ] }); return this.multiply(changeMtx); },
由於3D坐標軸下的變換和2D坐標軸下的沒有太大差異,所以這里就不作詳述,下面的完整源碼也包含了3d坐標軸下幾何變換的部分,感興趣的童鞋也可以看看~
另外大家可能會覺得dom或canvas的環境下,並不存在真正意義上的z坐標,因此3d坐標軸下的幾何變換並沒有多大意義。其實,雖然實現不了真正意義上的3D,但是實現偽3D還是可以的,視覺上,我們可以把元素z軸的坐標值通過xy的坐標值以及元素尺寸的變化來表示,關於這方面的內容可以參考hongru的rotate3D系列文章。
最后提供所有源碼(包括3D坐標軸下的幾何變換部分):
View Code
/** * *矩陣 * **/ cnGame.register("cnGame", function(cg) { var matrix=function(options){ if (!(this instanceof arguments.callee)) { return new arguments.callee(options); } this.init(options); }; matrix.prototype={ /** *初始化 **/ init:function(options){ this.matrix=[]; this.setOptions(options); }, /** *設置參數 **/ setOptions: function(options) { cg.core.extend(this, options); }, /** *矩陣相加 **/ add:function(mtx){ var omtx=this.matrix; var newMtx=[]; if(!mtx.length||!mtx[0].length||mtx.length!=omtx.length||mtx[0].length!=omtx[0].length){//如果矩陣為空,或行或列不相等,則返回 return; } for(var i=0,len1=omtx.length;i<len1;i++){ var rowMtx=omtx[i]; newMtx.push([]); for(var j=0,len2=rowMtx.length;j<len2;j++){ newMtx[i][j]=rowMtx[j]+mtx[i][j]; } } this.matrix=newMtx; return this; }, /** *矩陣相乘 **/ multiply:function(mtx){ var omtx=this.matrix; var mtx=mtx.matrix; var newMtx=[]; //和數字相乘 if(cg.core.isNum(mtx)){ for(var i=0,len1=omtx.length;i<len1;i++){ var rowMtx=omtx[i]; newMtx.push([]); for(var j=0,len2=rowMtx.length;j<len2;j++){ omtx[i][j]*=mtx; } } return new matrix({matrix:newMtx}); } //和矩陣相乘 var sum=0; for(var i=0,len1=omtx.length;i<len1;i++){ var rowMtx=omtx[i]; newMtx.push([]); for(var m=0,len3=mtx[0].length;m<len3;m++){ for(var j=0,len2=rowMtx.length;j<len2;j++){ sum+=omtx[i][j]*mtx[j][m]; } newMtx[newMtx.length-1].push(sum); sum=0; } } this.matrix=newMtx; return this; }, /** *2d平移 **/ translate2D:function(x,y){ var changeMtx= new matrix({ matrix:[ [1,0,0], [0,1,0], [x,y,1] ] }); return this.multiply(changeMtx); }, /** *2d縮放 **/ scale2D:function(scale,point){//縮放比,參考點 var sx=scale[0],sy=scale[1],x=point[0],y=point[1]; var changeMtx= new matrix({ matrix:[ [sx,0,0], [0,sy,0], [(1-sx)*x,(1-sy)*y,1] ] }); return this.multiply(changeMtx); }, /** *2d對稱變換 **/ symmet2D:function(axis){//對稱軸 var changeMtx; axis=="x"&&(changeMtx= new matrix({//相對於x軸對稱 matrix:[ [1,0,0], [0,-1,0], [0,0,1] ] })); axis=="y"&&(changeMtx= new matrix({//相對於y軸對稱 matrix:[ [-1,0,0], [0,1,0], [0,0,1] ] })); axis=="xy"&&(changeMtx= new matrix({//相對於原點對稱 matrix:[ [-1,0,0], [0,-1,0], [0,0,1] ] })); axis=="y=x"&&(changeMtx= new matrix({//相對於y=x對稱 matrix:[ [0,1,0], [1,0,0], [0,0,1] ] })); axis=="y=-x"&&(changeMtx= new matrix({//相對於y=-x對稱 matrix:[ [0,-1,0], [-1,0,0], [0,0,1] ] })); return this.multiply(changeMtx); }, /** *2d錯切變換 **/ shear2D:function(kx,ky){ var changeMtx= new matrix({ matrix:[ [1,kx,0], [ky,1,0], [0,0,1] ] }); return this.multiply(changeMtx); }, /** *2d旋轉 **/ rotate2D:function(angle,point){ var x=point[0],y=point[1]; var cos=Math.cos; var sin=Math.sin; var changeMtx= new matrix({ matrix:[ [cos(angle),sin(angle),0], [-sin(angle),cos(angle),0], [(1-cos(angle))*x+y*sin(angle),(1-cos(angle))*y-x*sin(angle),1] ] }); return this.multiply(changeMtx); }, /** *3d平移 **/ translate3D:function(x,y,z){ var changeMtx= new matrix({ matrix:[ [1,0,0,0], [0,1,0,0], [0,0,1,0], [x,y,z,1] ] }); return this.multiply(changeMtx); }, /** *3d縮放 **/ scale3D:function(scale,point){//縮放比數組,參考點數組 var sx=scale[0],sy=scale[1],sz=scale[2],x=point[0],y=point[1],z=point[2]; var changeMtx= new matrix({ matrix:[ [sx,0,0,0], [0,sy,0,0], [0,0,sz,0], [(1-sx)*x,(1-sy)*y,(1-sz)*z,1] ] }); return this.multiply(changeMtx); }, /** *3d旋轉 **/ rotate3D:function(angle,axis){ var cos=Math.cos; var sin=Math.sin; var changeMtx; axis=="x"&&(changeMtx=new matrix({ matrix:[ [1,0,0,0], [0,cos(angle),sin(angle),0], [0,-sin(angle),cos(angle),0], [0,0,0,1] ] })); axis=="y"&&(changeMtx=new matrix({ matrix:[ [cos(angle),0,-sin(angle),0], [0,1,0,0], [sin(angle),0,cos(angle),0], [0,0,0,1] ] })); axis=="z"&&(changeMtx=new matrix({ matrix:[ [cos(angle),sin(angle),0,0], [-sin(angle),cos(angle),0,0], [0,0,1,0], [0,0,0,1] ] })); return this.multiply(changeMtx); }, }; this.Matrix=matrix; });
參考資料:計算機圖形學
