加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

CANVAS 3D钻石 360旋转版

(2012-04-06 15:11:47)
标签:

javascript

钻石

旋转

3d

立体

三维

投影

顶点

坐标

原型

填充

stroke

样式

it

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

CANVAS <wbr>3D钻石 <wbr>360旋转版

这是一颗钻石(这句是废话),为什么标题写着360旋转版,而它不会转,因为它是截图...

你可以通过下面这个链接下载我的源代码:《CANVAS 3D钻石 360旋转版

为方便下载,提供本人网盘帐号密码,请不要弄乱里面的页面,以方便其它人下载,谢谢。

帐号:287019674@qq.com

密码:123456


源代码是基于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方向分量的距离求出投影缩放比例,将三维的坐标二维化,这里我起初蒙了很久,这个怎么就给转成二维的,不理解,应该有一套固定的投影原理,我没有搜到,不过我以自己的理解画个图:

CANVAS <wbr>3D钻石 <wbr>360旋转版


假如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,那么应该怎么实现? 首先应该算出当前点与旋转中心点的角度,然后再加上对应的角度进行旋转得出新坐标再赋值。
那怎么求这个角度呢,好吧,又有点抽象了,上图片:

CANVAS <wbr>3D钻石 <wbr>360旋转版

根据这张图,相信大家都一目了然了,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,顺序对应的结构是这样的:

CANVAS <wbr>3D钻石 <wbr>360旋转版



以一个六边为例,依据图上数字一次push进VertexArray数组里面,最后那个尖尖的底部就放在数组末尾。因为获取数组的时候比较复杂,所以我抽出来独立一个函数(其实是写了一半,发现把这么一大堆参数整进Diamond里面的话,好丑,才独立出来的),用来获取钻石对应的顶点数组:

function getDiamond(c3d,pointNum,hpos,height,sRadius,bRadius){}

这个才c3d就是上图的中心点0的三维坐标,pointNum表示顶点的个数,你可以设置钻石的边数,sRadius表示小半径,即中心点o到内圈顶点的距离,bRadius表示大半径,即中心点o到外圈顶点的距离,然后就看横切面图吧,我不知道怎么表达得清晰:

CANVAS <wbr>3D钻石 <wbr>360旋转版

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方法,把路径根据设定的样式给描出来,画在画布上,就可以看到一颗这样的钻石:

CANVAS <wbr>3D钻石 <wbr>360旋转版


我代码里面还设置了一个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);
        diamond.draw(vertexArray.pointNum);
    },50);

这一段用来设置钻石的旋转动画,可以通过修改angle来改变钻石的旋转角度方向和大小,当然也可以在这里单独改diamond.rotate(angle,angle,angle),注意在计时动画循环里面,在旋转rotate后我还有投影projection,然后清空画布再绘制draw,要是不清空的话,画布上面会残留上一个钻石的身影,就像EX一样,挥之不去,要吧它擦掉,才会有新的更好的现在,这样表述够形象生动吧...

基本到这里就完成了对程序的分析,这只是一个初学者的实验性作品,如果你有好的想法或者意见,欢迎给我留言反馈,感激不尽,对了,最后要附上我看的那些关于3D的文章博客地址,做人要懂得分享哈:

Flash与3D编程探秘(一)- Flash与3D空间

用JavaScript玩转计算机图形学(一)光线追踪入门

很强悍的两个博客,我是看了上面那个博客的文章才有些体会的,虽然是flash,但是学东西要学原理,原理弄懂了,做出来的就是自己的东西,以后也许会写出更好的创意,不弄清楚原理只会copy的最后只沦为别人尾后的跟随者,一旦没有给你跟随的人,那么将没有个人的价值。我也是理解原理后把立方体转成钻石的,只要理解了原理,你甚至可以做出钻戒...

本文涉及的数学知识与空间思维比较多,重点还是在立体翻转投影方面,如果我分析的不够透彻的话,可以百度数学原理公式自行理解,毕竟这些东西不是三言两语就说的清的...

虽然我现在没钱给你买钻石,不过我可以画出一颗给你,希望你能喜欢...






0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有