【CSON原创】js对几何变换的简单封装
如果是涉及到游戏或动画的编程,我们很可能会用到几何变换。如果在大学过线性代数的话,我们就会知道,无论是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坐标轴下的几何变换部分):
/** * *矩阵 * **/ 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; });
参考资料:计算机图形学