声明:本文为原创文章,如需转载,请注明来源WAxes,谢谢!
我记得很早之前就看过这个DEMO,是岑安大大博客里看到的:
就是这个数字时钟,当时觉得这个创意不错,但是也没去折腾。直到昨天同事又在网上看到这个案例,他觉得很酷炫,就跑过来问我,这个是怎么实现的,然后我大概想了一下实现方法后也来了点兴趣,就花了一点时间模仿做出来了一个。不同的是,岑安用的是div来做的。而我就是用canvas来实现的。用canvas来做性能方面会更好,因为就单单操控每个点的运动,用js控制dom的style属性跟用js控制canvas绘图相比性能方面肯定是有所欠缺的。
先上个我做的DEMO吧,然后再简述一下做这个的方法: 看DEMO请戳我 。
做这个思路很简单,就是通过字符串保存各个数字的位置:
var numData = [ "1111/1001/1001/1001/1001/1001/1111", //0 "0001/0001/0001/0001/0001/0001/0001", //1 "1111/0001/0001/1111/1000/1000/1111", //2 "1111/0001/0001/1111/0001/0001/1111", //3 "1010/1010/1010/1111/0010/0010/0010", //4 "1111/1000/1000/1111/0001/0001/1111", //5 "1111/1000/1000/1111/1001/1001/1111", //6 "1111/0001/0001/0001/0001/0001/0001", //7 "1111/1001/1001/1111/1001/1001/1111", //8 "1111/1001/1001/1111/0001/0001/1111", //9 "0000/0000/0010/0000/0010/0000/0000", //: ]
0代表没像素,1代表有像素,/是为了更好看些,就是分行嘛,简单说起来:比如0就是:
1 1 1 1
1 0 0 1
1 0 0 1
1 0 0 1
1 0 0 1
1 0 0 1
1 1 1 1 这样就很清楚了吧。从0到9还有一个:号都用字符串表示好。
然后就写个粒子对象,也就是像素点:
var P_radius = 8,Gravity = 9.8; var Particle = function(){ this.x = 0; this.y = 0; this.vx = 0; this.vy = 0; this.color = ""; this.visible = false; this.drop = false; } Particle.prototype = { constructors:Particle, paint:function(){ //绘制自身 ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x,this.y,P_radius,0,2*Math.PI); ctx.fill(); }, reset:function(x,y,color){ //重置 this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.color = color; this.visible = true; this.drop = false; }, isDrop:function(){ //落下 this.drop = true; var vx = Math.random()*20+15 this.vx = Math.random()>=0.5?-vx : vx; }, update:function(time){ //每一帧的动作 if(this.drop){ this.x += this.vx*time; this.y += this.vy*time; var vy = this.vy+Gravity*time; if(this.y>=canvas.height-P_radius){ this.y = canvas.height-P_radius vy = -vy*0.7; } this.vy = vy; if(this.x<-P_radius||this.x>canvas.width+P_radius||this.y<-P_radius||this.y>canvas.height+P_radius){ this.visible = false; } } } }
粒子对象的属性比较简单,就位置,速度,以及是否可视化。方法的话,paint是绘制方法,reset是重置(因为粒子要循环利用的,提升性能),isDrop是粒子落下方法,update就是每一帧更新粒子的动作,update中当粒子运动超出canvas的绘制区域时,就把它的可视化置为false,在粒子容器中保存起来等待下一次调用。
写好粒子对象后,就要考虑如何让粒子按照位置画上去,同时当粒子不需要用时能够让他做自由落体的动画了。
先画背景(也就是那没有像素的白点):
function drawBg(){ var tx = (canvas.width-((P_radius*2+X_J)*4*8+7*xjg))/2; for(var i=0;i<8;i++){ var ty = (canvas.height-((P_radius+yjg)*6))/2; for(var j=0;j<numData[0].length;j++){ var tt = numData[0].charAt(j); if(tt==="/"){ ty+=yjg; }else { var x = tx+j%5*(P_radius*2+X_J), y = ty; bgctx.fillStyle = "#FFF"; bgctx.beginPath(); bgctx.arc(x,y,P_radius,0,2*Math.PI); bgctx.fill(); } } tx+=xjg+4*(P_radius*2+X_J); } }
先把背景画到一个离屏canvas中缓存起来,接下来每一帧重画的时候就不需要逻辑计算了,直接把那个离屏canvas画上去就行了。上面的逻辑应该不难理解,就是通过两个循环,循环8个数字,然后再对每个数字一个点一个点进行绘制,当遇到“/”时,就说明要换行了,把绘制的ty加个换行间隔,再把tx重置,再进行绘制。就这样,点就可以都画出来了。效果图如下:
背景画好了,就开始根据每一秒的时间,画数字像素吧。方法主要是这个:
function setTime(time){ var h = time.getHours()+"", m = time.getMinutes()+"", s = time.getSeconds()+""; h = h.length===1?"0"+h:h; m = m.length===1?"0"+m:m; s = s.length===1?"0"+s:s; var nowdate = h+":"+m+":"+s; var tx = (canvas.width-((P_radius*2+X_J)*4*8+7*xjg))/2,color = ""; for(var i=0;i<nowdate.length;i++){ var n = nowdate.charAt(i)===":"?10:parseInt(nowdate.charAt(i)), text = numData[n]; var ty = (canvas.height-((P_radius+yjg)*6))/2; switch(i){ case 0:color = "#4DCB74";break; case 2:color = "#4062E0";break; case 3:color = "#D65050";break; case 5:color = "#4062E0";break; case 6:color = "#797C17";break; } for(var j=0;j<text.length;j++){ var tt = text.charAt(j); if(tt==="/"){ ty+=yjg; }else{ var x = tx+j%5*(P_radius*2+X_J), y = ty, pp = null, usefullp = null; particles.forEach(function(p){ if(p.visible&&p.x===x&&p.y===y){ pp = p; }else if(!p.visible&&usefullp===null){ usefullp = p; } }); if(pp!==null&&tt==="0"){ pp.isDrop(); }else if(pp===null&&tt==="1"){ usefullp.reset(x , y , color); } } } tx+=xjg+4*(P_radius*2+X_J); } }
原理也不难,也是跟上面画背景差不多,遍历所有点,然后根据当前时间的数字转换成的字符串来判断,当前点是否应该有像素,如果有像素就再判断当前这个点是否已经有粒子对象在了,如果已经有粒子对象在了,就直接跳出不处理,如果没有粒子对象在,就再粒子容器中找一个没有被使用的粒子reset到这个位置。还有一种情况,就是当前这个点是不应该有像素的,但是却有粒子,那就获取这个粒子,让这个粒子进行自由落体。
时间设置也写好了,就可以写舞台更新的代码了:
var timeCount_0 = 0,timeCount_1 = 0,particles = []; function initAnimate(){ for(var i=0;i<200;i++){ var p = new Particle(); particles.push(p); } timeCount_0 = new Date(); timeCount_1 = new Date(); drawBg(); setTime(timeCount_0) animate(); } function animate(){ ctx.clearRect(0,0,canvas.width,canvas.height); ctx.drawImage(bgcanvas,0,0); var timeCount_2 = new Date(); if(timeCount_1-timeCount_0>=1000){ setTime(timeCount_1); timeCount_0 = timeCount_1; } particles.forEach(function(p){ if(p.visible){ p.update((timeCount_2-timeCount_1)/70); p.paint(); } }); timeCount_1 = timeCount_2; RAF(animate) }
在initAnimate进行动画初始化,初始化也就是先实例化两百个粒子对象放到粒子容器中保存起来,再更新时间戳,缓存背景,设置当前时间,然后调用animate动画循环主体开始动画。
animate中的逻辑也很简单了,获取时间戳,如果两个时间戳之间的时间差大于或等于1秒,就进行setTime。而再下面的就是对粒子容器里的所有可视化的粒子进行遍历循环重绘了。
然后就做好啦:。当然这个效果还是有很多可以优化的地方的,因为时钟和分钟都是动的比较少的,所以可以把这两个缓存起来,当没有动作的时候就直接将缓存数据画上去就行了,这样就可以减少舞台每一帧的绘图API调用量,肯定是能提高性能的。不过现在毕竟粒子不多,两三百个粒子对象也就够用了,如果不去做优化,动画也还是能很流畅的运行的。所以楼主就偷个小懒啦。
源码地址:https://github.com/whxaxes/canvas-test/blob/gh-pages/src/Funny-demo/coolClock/index.html