CANVAS 3D钻石 360旋转版

标签:
javascript钻石旋转3d立体三维投影顶点坐标原型填充stroke样式it |
分类: Canvas |
打从看了那本《HTML5
CANVAS基础教程》之后,就想写个关于canvas的东西,但是苦无创意,而且这段时间能真正给我写东西的时间不多,因为要Dota...开玩笑的...因为没入职之前,面了十来家公司左右,笔试面试复试什么的比较忙,基本都是闲的时候翻开看看,也没研究得多深入。直到签了卖身契之后,时间稳定了,所以多些时间看博客帖子,这段时间对3d原理比较感兴趣,所以大致都在看这方面的文章,大部分都是百度关键字搜出来的,发现博客园里面很多关于这方面的,而且含金量很重,受益匪浅,于是乎有了自己的一些想法,应该也算入了门吧,所以写了下面这个作品,代码是用纯Javascript写的,给个效果图先:

这是一颗钻石(这句是废话),为什么标题写着360旋转版,而它不会转,因为它是截图...
你可以通过下面这个链接下载我的源代码:《CANVAS 3D钻石 360旋转版》
源代码是基于Canvas的,不能在IE打开,请在Firefox、Chrome、Safari下打开
下面是对钻石的原理分析:
一、Point
Point2d、Point3d、Vertex是我程序里面最要的点类:
function
Point2d(x,y){
this.x=x;
this.y=y;
}
Point2d.prototype.copy=function(){
return new
Point2d(this.x,this.y);
}
Point2d.zero=new Point2d(0,0);
定义一个具有平面X、Y双向坐标的二维平面点类,然后它有一个copy的复制方法(这个是后来补上的),然后再定义多一个zero的静态量,Point3d的基本一致,只不过多了一个Z坐标,代表的是三维立体点类,这里就不贴代码了。重点是Vertex这个点类,比较复杂:
二、Vertex
function Vertex(p2d,p3d,c3d){
this.p2d=p2d;
this.p3d=p3d;
this.c3d=c3d;
this.scale=1;
this.xpos=0;
this.ypos=0;
this.zpos=0;
}
Vertex是一个三维坐标的顶点,拥有很强大的立体性质(个人觉得),跟point3d不可同日而语。
一个Vertex实例会拥有一个平面投影点类p2d,一个实体三维坐标点p3d,一个投影中心点c3d,一个投影缩放比例scale,以及实体三维坐标点p3d到投影中心点c3d各个分量的距离xpos、ypos、zpos。
同时它拥有以下方法:获取投影分量距离getPos、投影projection、旋转rotate
Vertex.prototype.getPos=function(){
this.xpos=this.p3d.x-this.c3d.x;
this.ypos=this.p3d.y-this.c3d.y;
this.zpos=this.p3d.z-this.c3d.z;
}
获取投影分量距离getPos,只是把两个三维p3d做差后赋值。
Vertex.prototype.projection=function(){
this.scale=focusLength/(focusLength+this.zpos);
this.p2d.x=this.p3d.x+(this.xpos)*this.scale;
this.p2d.y=this.p3d.y+(this.ypos)*this.scale;
}
投影projection,就是根据当前焦距与当前偏离中心点Z方向分量的距离求出投影缩放比例,将三维的坐标二维化,这里我起初蒙了很久,这个怎么就给转成二维的,不理解,应该有一套固定的投影原理,我没有搜到,不过我以自己的理解画个图:

假如AB为焦距focusLength,CD为实体,当然也有可能C为负,在AB之间,那么根据中学的公式(叫什么公式来的我忘记了哈)AB/AC=BE/CD,而AB/AC就是所求出的scale,而BE就是所要求的CD的投影线段,线段是由点组成的,那么要求BE这条线段,只要求出B点跟E点就可以了。这是在XZ平面的一个线条的模型,把它加多两个面,放到三维里面,需要一点空间建模能力,可能有点抽象,不过原理是一样的要把BE想象成一个面,然后CD是一个立体的面往BE上面投影,毕竟Canvas也是平面的,不可能变成3D-MAX,所以计算的时候是立体的思维,但是绘制的时候,需要把模型“拍扁”,压在canvas上面呈现给用户,其实是伪3D的2D绘图。
Vertex.prototype.rotate=function(angleX,angleY,angleZ){
if(angleX!=0){
var
distance=Math.sqrt(this.ypos*this.ypos+this.zpos*this.zpos);
var
angle=Math.atan2(this.ypos,this.zpos);
this.zpos=distance*Math.cos(angle+angleX);
this.ypos=distance*Math.sin(angle+angleX);
this.p3d.y=this.c3d.y+this.ypos;
this.p3d.z=this.c3d.z+this.zpos;
}
if(angleY!=0){}
if(angleZ!=0){}
}
这是关于顶点旋转的函数,因为angleX、angleY、angleZ基本一致,所以我只贴angleX的。
假如以X轴方向为轴心旋转,那么应该明确的是:X坐标是不变的,变的是Y跟Z。那么假如要旋转angleX=10,那么应该怎么实现? 首先应该算出当前点与旋转中心点的角度,然后再加上对应的角度进行旋转得出新坐标再赋值。
那怎么求这个角度呢,好吧,又有点抽象了,上图片:

根据这张图,相信大家都一目了然了,ypos和zpos已知,那么可以根据tan来求angle,同时算出顶点与中心点距离distance,也就是圆的半径PO,绿色那条。然后加上对应的变换角度,即angle+angleX,因为P做的是圆周运动,所以distance是恒定的,由此可以根据新得到的角度,利用sin、cos得到新的xpos跟zpos,注意这里得到的是顶点与中心点的距离,不是坐标,得到之后要对应加上中心点的坐标,才能得到顶点变换后的坐标。
这是根据X轴转的,根据Y轴Z轴的同理,不过是用if(){}、if(){}并列写的,而不是if(){}、else if(){}写,因为绕X轴转的同时也可以绕其它轴转。结果rotate之后,就可以得到原顶点旋转某角度后的新顶点三维坐标。
三、颜色
这个类是因为黑色太单调写上去的,拥有RGBA的基本属性以及事先定义好的静态量
四、Diamond
压轴的东西出场啦,其实这个说白了也就是根据顶点Vertex扩展开来的,包含着一个顶点数组VertexArray,旋转中心点c3d,钻石的颜色color。
它拥有以下方法:获取投影分量距离getPos、投影projection、旋转rotate,坑爹啊,怎么跟Vertex一样,都说是从它扩展来的咯...呵呵...不同的还有设置中心点setCenter、初始化init、以及跟Canvas关系密切的绘制draw。
名字一样的就不说了,因为都是做一个循环,循环出VertexArray里面的顶点元素进行操作,调用顶点对应的方法而已。设置中心点setCenter这个是因为钻石是一个物体,所以组成物体的顶点的旋转中心应该统一起来;init是我觉得一句句重复写烦,就把那些获取初始化数据的方法丢进去而已;绘制draw相对来说比较有含金量,这里涉及到一个Canvas路径的描边与填充的操作。
哦,对了,这里要说的是,旋转的角度是我们平常熟悉的0-360,而不是0-Math.PI*2,我在里面进行了简单的转化,要不然Math.PI的概念不大容易理解。先说下VertexArray的顶点存放结构:
VertexArray里面存放着构成钻石的顶点,数据类型是Vertex,顺序对应的结构是这样的:

以一个六边为例,依据图上数字一次push进VertexArray数组里面,最后那个尖尖的底部就放在数组末尾。因为获取数组的时候比较复杂,所以我抽出来独立一个函数(其实是写了一半,发现把这么一大堆参数整进Diamond里面的话,好丑,才独立出来的),用来获取钻石对应的顶点数组:
function getDiamond(c3d,pointNum,hpos,height,sRadius,bRadius){}
这个才c3d就是上图的中心点0的三维坐标,pointNum表示顶点的个数,你可以设置钻石的边数,sRadius表示小半径,即中心点o到内圈顶点的距离,bRadius表示大半径,即中心点o到外圈顶点的距离,然后就看横切面图吧,我不知道怎么表达得清晰:

hpos是两个面的距离,height是钻石的高度,应该从图片可以很容易的理解。
var angle_r=Math.PI*2/pointNum;
var angle=0;
for(var i=0;i<pointNum;i++){
pointArray.push(new
Point3d(c3d.x+sRadius*Math.cos(angle),c3d.y,c3d.z+sRadius*Math.sin(angle)));
angle+=angle_r;
}
这段代码是用来获取是钻石小多边形的坐标点,用Math.PI*2/pointNum得到每个坐标点的角度,然后再通过sin、cos获取对应的坐标,存入pointArray中,注意这是一个p3d的数组,不是Vertex数组。然后同样获取到大多边形的顶点,同样push进去,再push多一个底部尖顶点,再循环一次,产生对应的vertex顶点,还要计算初始化它的Vertex一些属性,所以先给它们Point2d.zero跟Point3d.zero,但是不能直接给,因为直接这样写的话:
new Vertex(Point2d.zero,pointArray[i],Point3d.zero)
赋值的是一个引用,初始化的时候,所有顶点的p2d,p3d都是的坐标都是一样的,所以我前面才会写多一个复制copy的方法。最后就把VertexArray返回,完成VertexArray的生成工作。
下面还是重点对绘制draw进行分析:
ctx.strokeStyle="rgba("+this.color.r+","+this.color.g+","+this.color.b+","+this.color.a+")";
这句是设定画布的描边样式,样式根据diamond.color来设置,下面填充样式fillStyle基本一致。
ctx.beginPath();
beginPath开始路径的绘制;
ctx.moveTo(this.Vertexs[0].p2d.x,this.Vertexs[0].p2d.y);
for(var i=1;i<pointNum;i++){
ctx.lineTo(this.Vertexs[i].p2d.x,this.Vertexs[i].p2d.y);
}
ctx.lineTo(this.Vertexs[0].p2d.x,this.Vertexs[0].p2d.y);
把起始点移到Vertexs[0]的坐标位置,注意这里用的是Vertexs[0].p2d.x,二维的,上面所说的绘制的时候,需要把模型“拍扁”,压在canvas上面呈现给用户,所以这里用的是拍扁后的二维坐标,也就是说rotate的时候用的是三维的p3d来计算实际的位置,绘制前要投影projection,也就是拍扁成二维再draw移动到Vertexs[0]的坐标后就用循环lineTo连接小多边形的顶点,最后要返回到Vertexs[0],这样小多边形的路径就画好了,接着同理画出大多边形的路径;然后就该画钻石的棱,多边形的边是横向的,那么棱就是纵向的那些,比如上上图中0-6-12、1-7-12这些,依次画出6条对应的路径,关闭路径,到此钻石的骨架path基本完成,然后调用Canvas的stroke方法,把路径根据设定的样式给描出来,画在画布上,就可以看到一颗这样的钻石:
我代码里面还设置了一个fill变量,用来判断是否要填充颜色的操作,至于怎么填充颜色,这里就不细讲,只不过不能像stroke那么放荡,有规律的就用循环,但是还是要一个面一个面独立path的绘制,注意path的闭合。
里面设置的一些变量你下载源代码后可以自行改变,比如说:
var vertexArray=getDiamond(new Point3d(200,200,200),8,20,80,40,60);
这句是生成钻石顶点数组的,可以调整它的位置,边数,半径大小等;
var fill=false;
这句设置填充钻石表面与否;
var diamond=new Diamond(vertexArray,new Point3d(200,220,200),Color.blue);
这句用来设置钻石的旋转中心坐标与描边,填充颜色;
var angle=0.5;//setInterval setTimeout
setInterval(function(){
diamond.rotate(angle,angle,angle);
diamond.projection();
ctx.clearRect(0,0,canvas.width,canvas.height);

这是一颗钻石(这句是废话),为什么标题写着360旋转版,而它不会转,因为它是截图...
你可以通过下面这个链接下载我的源代码:《CANVAS 3D钻石 360旋转版》
为方便下载,提供本人网盘帐号密码,请不要弄乱里面的页面,以方便其它人下载,谢谢。
密码:123456
源代码是基于Canvas的,不能在IE打开,请在Firefox、Chrome、Safari下打开
下面是对钻石的原理分析:
一、Point
Point2d、Point3d、Vertex是我程序里面最要的点类:
定义一个具有平面X、Y双向坐标的二维平面点类,然后它有一个copy的复制方法(这个是后来补上的),然后再定义多一个zero的静态量,Point3d的基本一致,只不过多了一个Z坐标,代表的是三维立体点类,这里就不贴代码了。重点是Vertex这个点类,比较复杂:
二、Vertex
function Vertex(p2d,p3d,c3d){
Vertex是一个三维坐标的顶点,拥有很强大的立体性质(个人觉得),跟point3d不可同日而语。
一个Vertex实例会拥有一个平面投影点类p2d,一个实体三维坐标点p3d,一个投影中心点c3d,一个投影缩放比例scale,以及实体三维坐标点p3d到投影中心点c3d各个分量的距离xpos、ypos、zpos。
同时它拥有以下方法:获取投影分量距离getPos、投影projection、旋转rotate
Vertex.prototype.getPos=function(){
获取投影分量距离getPos,只是把两个三维p3d做差后赋值。
Vertex.prototype.projection=function(){
投影projection,就是根据当前焦距与当前偏离中心点Z方向分量的距离求出投影缩放比例,将三维的坐标二维化,这里我起初蒙了很久,这个怎么就给转成二维的,不理解,应该有一套固定的投影原理,我没有搜到,不过我以自己的理解画个图:

假如AB为焦距focusLength,CD为实体,当然也有可能C为负,在AB之间,那么根据中学的公式(叫什么公式来的我忘记了哈)AB/AC=BE/CD,而AB/AC就是所求出的scale,而BE就是所要求的CD的投影线段,线段是由点组成的,那么要求BE这条线段,只要求出B点跟E点就可以了。这是在XZ平面的一个线条的模型,把它加多两个面,放到三维里面,需要一点空间建模能力,可能有点抽象,不过原理是一样的要把BE想象成一个面,然后CD是一个立体的面往BE上面投影,毕竟Canvas也是平面的,不可能变成3D-MAX,所以计算的时候是立体的思维,但是绘制的时候,需要把模型“拍扁”,压在canvas上面呈现给用户,其实是伪3D的2D绘图。
Vertex.prototype.rotate=function(angleX,angleY,angleZ){
这是关于顶点旋转的函数,因为angleX、angleY、angleZ基本一致,所以我只贴angleX的。
假如以X轴方向为轴心旋转,那么应该明确的是:X坐标是不变的,变的是Y跟Z。那么假如要旋转angleX=10,那么应该怎么实现? 首先应该算出当前点与旋转中心点的角度,然后再加上对应的角度进行旋转得出新坐标再赋值。
那怎么求这个角度呢,好吧,又有点抽象了,上图片:

根据这张图,相信大家都一目了然了,ypos和zpos已知,那么可以根据tan来求angle,同时算出顶点与中心点距离distance,也就是圆的半径PO,绿色那条。然后加上对应的变换角度,即angle+angleX,因为P做的是圆周运动,所以distance是恒定的,由此可以根据新得到的角度,利用sin、cos得到新的xpos跟zpos,注意这里得到的是顶点与中心点的距离,不是坐标,得到之后要对应加上中心点的坐标,才能得到顶点变换后的坐标。
这是根据X轴转的,根据Y轴Z轴的同理,不过是用if(){}、if(){}并列写的,而不是if(){}、else if(){}写,因为绕X轴转的同时也可以绕其它轴转。结果rotate之后,就可以得到原顶点旋转某角度后的新顶点三维坐标。
三、颜色
这个类是因为黑色太单调写上去的,拥有RGBA的基本属性以及事先定义好的静态量
四、Diamond
压轴的东西出场啦,其实这个说白了也就是根据顶点Vertex扩展开来的,包含着一个顶点数组VertexArray,旋转中心点c3d,钻石的颜色color。
它拥有以下方法:获取投影分量距离getPos、投影projection、旋转rotate,坑爹啊,怎么跟Vertex一样,都说是从它扩展来的咯...呵呵...不同的还有设置中心点setCenter、初始化init、以及跟Canvas关系密切的绘制draw。
名字一样的就不说了,因为都是做一个循环,循环出VertexArray里面的顶点元素进行操作,调用顶点对应的方法而已。设置中心点setCenter这个是因为钻石是一个物体,所以组成物体的顶点的旋转中心应该统一起来;init是我觉得一句句重复写烦,就把那些获取初始化数据的方法丢进去而已;绘制draw相对来说比较有含金量,这里涉及到一个Canvas路径的描边与填充的操作。
哦,对了,这里要说的是,旋转的角度是我们平常熟悉的0-360,而不是0-Math.PI*2,我在里面进行了简单的转化,要不然Math.PI的概念不大容易理解。先说下VertexArray的顶点存放结构:
VertexArray里面存放着构成钻石的顶点,数据类型是Vertex,顺序对应的结构是这样的:

以一个六边为例,依据图上数字一次push进VertexArray数组里面,最后那个尖尖的底部就放在数组末尾。因为获取数组的时候比较复杂,所以我抽出来独立一个函数(其实是写了一半,发现把这么一大堆参数整进Diamond里面的话,好丑,才独立出来的),用来获取钻石对应的顶点数组:
function getDiamond(c3d,pointNum,hpos,height,sRadius,bRadius){}
这个才c3d就是上图的中心点0的三维坐标,pointNum表示顶点的个数,你可以设置钻石的边数,sRadius表示小半径,即中心点o到内圈顶点的距离,bRadius表示大半径,即中心点o到外圈顶点的距离,然后就看横切面图吧,我不知道怎么表达得清晰:

hpos是两个面的距离,height是钻石的高度,应该从图片可以很容易的理解。
var angle_r=Math.PI*2/pointNum;
var angle=0;
for(var i=0;i<pointNum;i++){
}
这段代码是用来获取是钻石小多边形的坐标点,用Math.PI*2/pointNum得到每个坐标点的角度,然后再通过sin、cos获取对应的坐标,存入pointArray中,注意这是一个p3d的数组,不是Vertex数组。然后同样获取到大多边形的顶点,同样push进去,再push多一个底部尖顶点,再循环一次,产生对应的vertex顶点,还要计算初始化它的Vertex一些属性,所以先给它们Point2d.zero跟Point3d.zero,但是不能直接给,因为直接这样写的话:
new Vertex(Point2d.zero,pointArray[i],Point3d.zero)
赋值的是一个引用,初始化的时候,所有顶点的p2d,p3d都是的坐标都是一样的,所以我前面才会写多一个复制copy的方法。最后就把VertexArray返回,完成VertexArray的生成工作。
下面还是重点对绘制draw进行分析:
ctx.strokeStyle="rgba("+this.color.r+","+this.color.g+","+this.color.b+","+this.color.a+")";
这句是设定画布的描边样式,样式根据diamond.color来设置,下面填充样式fillStyle基本一致。
ctx.beginPath();
beginPath开始路径的绘制;
ctx.moveTo(this.Vertexs[0].p2d.x,this.Vertexs[0].p2d.y);
for(var i=1;i<pointNum;i++){
}
ctx.lineTo(this.Vertexs[0].p2d.x,this.Vertexs[0].p2d.y);
把起始点移到Vertexs[0]的坐标位置,注意这里用的是Vertexs[0].p2d.x,二维的,上面所说的绘制的时候,需要把模型“拍扁”,压在canvas上面呈现给用户,所以这里用的是拍扁后的二维坐标,也就是说rotate的时候用的是三维的p3d来计算实际的位置,绘制前要投影projection,也就是拍扁成二维再draw移动到Vertexs[0]的坐标后就用循环lineTo连接小多边形的顶点,最后要返回到Vertexs[0],这样小多边形的路径就画好了,接着同理画出大多边形的路径;然后就该画钻石的棱,多边形的边是横向的,那么棱就是纵向的那些,比如上上图中0-6-12、1-7-12这些,依次画出6条对应的路径,关闭路径,到此钻石的骨架path基本完成,然后调用Canvas的stroke方法,把路径根据设定的样式给描出来,画在画布上,就可以看到一颗这样的钻石:

我代码里面还设置了一个fill变量,用来判断是否要填充颜色的操作,至于怎么填充颜色,这里就不细讲,只不过不能像stroke那么放荡,有规律的就用循环,但是还是要一个面一个面独立path的绘制,注意path的闭合。
里面设置的一些变量你下载源代码后可以自行改变,比如说:
var vertexArray=getDiamond(new Point3d(200,200,200),8,20,80,40,60);
这句是生成钻石顶点数组的,可以调整它的位置,边数,半径大小等;
var fill=false;
这句设置填充钻石表面与否;
var diamond=new Diamond(vertexArray,new Point3d(200,220,200),Color.blue);
这句用来设置钻石的旋转中心坐标与描边,填充颜色;
var angle=0.5;//setInterval setTimeout