上一節中,我們留下了一個flyin的方法沒有介紹,這裡想單獨寫一篇html5的動畫實現。
在第二節中我們實現了畫一個泡泡,並且成功的擦除了泡泡,但當時也說了別把棋盤的線給擦掉了,所以做了位移量。所以說html5 Canvas還是低級, 沒有圖層的概念,擦掉再想補回來,怎麼補? 答案就是重繪。 沒錯,整個Canvas重繪,這樣就能不用擔心補哪裡了。雖然帶來了效能的損失,但絕對減少的編碼難度。而且電腦的能力也不差這點損失。那麼重繪的話,我們在Canvas是上所有的需要繪製的對象都應該有draw方法。這是必須的。另外,所有的元素都有個上下的概念,所以要先繪製下面的,再繪製上面的。 而這個上下就得靠 子項目的概念,這樣在父元素繪製完畢後遍曆其子項目繪製,就不用擔心掩蓋的問題。
如果想把ready區的3個泡“飛入”棋盤,就需要讓Canvas在泡移動的時候進行重繪,泡泡不動時不需要重繪。泡泡的移動很容易實現,只要改變他的x,y座標即可。如果想達到動畫的效果,就得在改變座標期間,定時重繪,可以使用setInterval來實現。
另外,我們不光飛入的動作需要重繪,遊戲開始後玩家還要點擊移動一個泡泡到另外一個格子,所以這裡也要重繪。那繪製的資訊這麼多,整個重繪工作都要交給game來進行,game控制所有的父元素。
start: function () {this.map.init();this.ready.init();this.draw();this.canvas.onclick = this.onclick;},over: function () {alert("GAME OVER");},draw: function () {this.ctx.clearRect(0, 0, 400, 600);this.ctx.save();this.map.draw();this.ready.draw();this.ctx.restore();},play: function (action, interval) {var me = this;play = setInterval(function () {action();me.draw();}, interval || 1);},stop: function () {clearInterval(play);this.draw();//console.log(this.ready.bubbles.length);},
game.start就是初始化所有的父元素,
game.over自然不必說,只是這裡沒有寫具體代碼,結束時應該無法繼續操作泡泡。
game.draw 繪製所有的父元素
game.play 就是重繪方法,需要重繪時掉用此方法。接收2個參數,第一個是重繪時需要做的動作,interval是繪製的間隔時間。不同的動作可能間隔不一樣。
可能這種實現是野路子,真正的重繪應該是遊戲開始後就不聽的調用重繪方法,而不是具體哪裡調用,只是在具體的精靈(每個元素)Update自己狀態就像我這裡的action。 這裡我們暫且這樣實現,後面如果達不到需求再重構這個重繪的代碼,畢竟核心的代碼不變,只是改改機制 不是大問題。
game.stop 停止重繪,又調用了一次draw,是為了保證最後的繪製沒問題。
接下來考慮下flyin飛入的實現。知道起始和結束的xy座標,飛入的路徑不是問題,無非是x y的加加減減,那麼動畫方面,我們的game.play的action就是來加減ready.bubbles的xy座標。飛入的邏輯並非這麼簡單,首先需要3個沒染色空格,如果不足3個,那就GameOver了,所以map需要提供一個返回3個空格子的方法,另外,飛入之後ready區要重建新的泡泡,而這幾個被飛的泡泡需要刪除,並且map要對3個空格子進行染色。 這就完成了整個飛入效果。
其實還有一個邏輯就是 飛入完畢後檢查是否有哪些泡泡可以被消除,這個我們後面再講。
先看擷取3個空格的方法:
getEmptyBubbles: function () { var empties = []; this.bubbles.forEach(function (row) { row.forEach(function (bubble) { if (!bubble.color) { empties.push(new Bubble(bubble.x, bubble.y)); } }); }); if (empties.length <= 3) { game.over(); return []; } var result = []; var useds = []; for (var i = 0; i < empties.length; i++) { if (result.length == 3) { break; } var isUsed = false; var ra = game.getRandom(empties.length); for (var m = 0; m < useds.length; m++) { isUsed = ra === useds[m]; if (isUsed) break; } if (!isUsed) { result.push(empties[ra]); useds.push(ra); } } console.log(useds); return result; },
擷取3個隨機的空格還是挺多代碼的。。。然後就是flyin的實現了。
首先定一個一個status,來存飛入的狀態。3個都飛完畢才能做後面的邏輯。Bubble對象也為此增加了px和py倆個成員(即Bubble的實際座標),這樣才能控制每個像素的移動。 其實在計算飛入路徑時我寫了很久的代碼,別看現在就這麼幾行,開發過程中還是頗費力。各種詭異的飛行。。。開始是按x++ y++遞增飛行的,這樣就是45°角飛行,但顯然飛行線路(起始到結束的線)的傾斜度不是45°,那就會出現先飛完x或y,再走直線,很傻的。所以要用斜率來計算當前的y座標。而x的座標可以固定常熟移動。我畫了一個斜率的公式,忘記的同學可以看看下。根據長寬的比率,就能計算當前的y值。
flyin: function () {var emptys = game.map.getEmptyBubbles();if (emptys.length < 3) {//GAME OVERgame.over();return;}var me = this;var status = [0, 0, 0];game.play(function () {if (status[0] && status[1] && status[2]) {game.stop();status = [0, 0, 0];me.bubbles = [];me.genrate();return;}for (var i = 0; i < me.bubbles.length; i++) {if (status[i]) {continue;}var target = emptys[i];var x2 = target.px + game.map.startX - me.startX;var y2 = target.py + game.map.startY - me.startY;var current = me.bubbles[i];var tmpWidth = 2;if (current.px < x2) {current.py = ((y2 - current.py) / (x2 - current.px)) * tmpWidth + current.py;current.px += tmpWidth;}else if (current.px > x2) {current.py = ((y2 - current.py) / (current.px - x2)) * tmpWidth + current.py;current.px -= tmpWidth;}else {current.py += tmpWidth;}if (current.py > y2) {current.py = y2;}if (current.px > x2) {current.px = x2;}if (current.px == x2 && current.py == y2) {status[i] = 1;current.x = target.x;current.y = target.y;game.map.addBubble(current);console.log(1);}}});}
既然我們已經實現了動畫效果,那麼接下來順便再實現一個動畫效果,就是點擊泡泡時,泡泡要做出響應(就是忽閃忽閃的),要不然使用者都不知道點了沒有。所以Bubble也要增加一個閃動的action。
代碼的意思是讓間隔50毫秒,重繪一次光照的亮度,亮度值(外圓的半徑)10和30之間來回蕩。這樣就亮了暗,暗了再亮。很有意思:)
Bubble.prototype.play = function () {var me = this;var isUp = true;game.play("light_" + this.x + "_" + this.y, function () {if (isUp) {me.light++;}if (!isUp) {me.light--;}if (me.light == 30) {isUp = false;}if (me.light == 10) {isUp = true;}}, 50);};Bubble.prototype.stop = function () {//this.light = 10;var me = this;game.stop("light_" + this.x + "_" + this.y);game.play("restore_" + this.x + "_" + this.y, function () {if (me.light > 10) {me.light--;}else {me.light = 10;game.stop("restore_" + me.x + "_" + me.y);}}, 50);};
細心的朋友可能會發現,在調用Game.stop的方法的參數上增加了一個參數。 這裡我要說明下。如果沒有參數的情況,game.play和stop會造成問題,因為用的都是一個interval,clear的話會打斷其他的動畫,所以我們把每個action都要傳遞一個name,這樣就能讓game粒度更細的控制每個action了。game的播放代碼也做了響應的調整,如下:
play: function (name, action, interval) { var me = this; this.actions[name] = setInterval(function () { action(); me.draw(); }, interval || 1); }, stop: function (name) { clearInterval(this.actions[name]); this.draw(); },
效果示範地址:http://jsfiddle.net/maddemon/VtMSU/embedded/result/