聲明:本文為原創文章,如需轉載,請注明來源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