前端筆記之Canvas

mufengsm發表於2019-04-18

一、Canvas基本使用

CanvasHTML5的畫布,Canvas算是“不務正業”的物件導向大總結,將物件導向玩極致。

演算法為王!就是說canvas你不會,但是演算法好,不怕寫業務,不怕程式碼量,只要稍微學學API就能出活。

Canvas這裡是HTML5新標籤,直接要了flash的命。

 

1.1 Canvas簡介

MDNCanvas線上手冊:

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API

瞭解:

<canvas>是一個可以使用指令碼(通常為JavaScript)來繪製圖形的 HTML 元素.它可以用於繪製圖表、製作圖片構圖或者製作簡單的(以及不那麼簡單的)動畫. 右邊的圖片展示了一些 <canvas> 的實現示例

歷史:

<canvas> 最早由Apple引入WebKit,用於Mac OS X Dashboard,隨後被各個瀏覽器實現。如今,所有主流的瀏覽器都支援它。

Mozilla 程式從 Gecko 1.8 (Firefox 1.5) 開始支援 <canvas>。它首先是由 Apple 引入的,用於 OS X Dashboard SafariInternet Explorer IE9開始支援<canvas> ,更舊版本的IE可以引入 Google  Explorer Canvas 專案中的指令碼來獲得<canvas>支援。ChromeOpera 9+ 也支援 <canvas>

Canvas相容到IE9


1.2 Canvas入門

canvasHTML5中比較特殊的雙標籤,可以在body中放:

<html>
<head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style type="text/css">
        canvas{border:1px solid #000;}
    </style>
</head>
<body>
    <canvas width="600" height="400"></canvas>
</body>
</html>

不能將widthheightCSS中設定,否則畫布的畫素的會被縮放,畫面質量粗糙了。

 

<canvas>元素可以像任何一個普通的影象一樣(有marginborderbackground等等屬性)被設計。然而,這些樣式不會影響在canvas中的實際影象。

 

畫布沒什麼用,所有操作都要在“上下文”中進行,這裡的上下文是環境的意思,不是物件導向中的this

<script type="text/javascript">
     //得到畫布標籤
     var canvas = document.querySelector("canvas");
     //使用上下文,得到一個2D的畫布
     var ctx = canvas.getContext("2d");
     //畫畫
     ctx.fillRect(100, 100, 300, 100);
</script>

Canvas的本質就是用js來畫畫,所有的繪畫函式,都是ctx的方法。

 

 

canvas馬上開始面對一堆API

<script type="text/javascript">
     //得到畫布標籤
     var canvas = document.querySelector("canvas");
     //使用上下文,得到一個2D的畫布
     var ctx = canvas.getContext("2d");
     //繪製矩形
     ctx.fillStyle = "orange"; //先提供一個顏色的筆
     ctx.fillRect(100, 100, 300, 100); //在根據以上顏色填充

     ctx.fillStyle = "green"; //先提供一個顏色的筆
     ctx.fillRect(100, 200, 300, 200); //在根據以上顏色填充
</script>

 

Canvas的座標系和絕對定位的座標系是一樣的。

 


二、Canvas繪製形狀

2.1繪製形狀路徑

Canvas中有兩種東西:

l stroke路徑【筆觸】,也叫描邊,就是形狀的輪廓

l fill填充,就是裡面的顏色

//得到畫布標籤
var canvas = document.querySelector('canvas');
//使用上下文,得到一個2D畫布
var ctx = canvas.getContext("2d");
//畫畫
ctx.beginPath();     //宣告要開始繪製路徑
ctx.moveTo(100,100); //移動到繪製點,將“畫筆”移動到100,100的位置
ctx.lineTo(250,250); //劃線
ctx.lineTo(500,250); //劃線
ctx.lineWidth = 10;  //線的粗細
ctx.strokeStyle = "red"; //線的顏色
ctx.fillStyle = "blue"; //準備填充的顏色
ctx.closePath();     //閉合路徑(自動補全)
ctx.stroke();        //顯示線(繪製線),可以繪製的路徑顯示出來
ctx.fill();          //填充顏色

只有矩形有快捷方法,比如想繪製多邊形,都要用以上這些組合。

 


2.2繪製矩形

ctx.fillRect(x,y,w,h);    //繪製填充矩形
ctx.strokeRect(x,y,w,h);   //繪製路徑矩形

 

繪製調色盤:

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
for (var i = 0;i < 6;i++){
   for (var j = 0;j < 6;j++){
       ctx.fillStyle = 'rgba('+ Math.floor(255-42.5 * i) +','+ Math.floor(255-42.5 * j) +', 200)';
       ctx.fillRect(i * 25, j * 25, 25, 25);
   }
}

記住一句話:Canvas是不可逆,繪製的元素一旦上了螢幕,是無法針對它再次操作。


2.3繪製弧度

ctx.arc(圓心x, 圓心y, 半徑, 開始的弧度, 結束的弧度, 是否逆時針);

ctx.beginPath();  //開始繪製路徑
// ctx.arc(100, 100, 60, 0, 6.28, false);
ctx.arc(100, 100, 60, 0, Math.PI * 2, false);
ctx.stroke(); //顯示路徑線

切一個圓,讓切下來的弧邊長等於圓的半徑,此時弧對應的角度是57.3度左右,此時角度是固定的。

 

正方向是正右方

canvas中所有涉及角度的座標系有兩點注意的:

l 0弧度的方向是正右方向。

 

弧度的順時針和逆時針:

ctx.arc(100,100,60, 0, 3, false); //繪製圓弧

ctx.arc(100,100,60, 0, 1, true); //繪製圓弧

 

繪製圓形:

ctx.arc(100,100,60, 0, Math.PI * 2, false);
ctx.arc(100,100,60, 0, 6.28, false);
ctx.arc(100, 100, 60, 0, -6.28, true);

注意:xy座標是到圓心的位置,而且圓的大小是半徑,後面繪製的形狀會覆蓋前面的形狀。

 

繪製笑臉

<script type="text/javascript">
     var canvas = document.querySelector("canvas");
     var ctx = canvas.getContext("2d");

     //繪製大臉
     ctx.beginPath(); //開始繪製路徑
     ctx.arc(300,200, 160, 0, 6.28, false); //繪製圓弧
     ctx.stroke(); //顯示路徑線
     ctx.fillStyle = "brown";
     ctx.fill();

     //繪製左眼睛
     ctx.beginPath();
     ctx.arc(230,150, 30, 0, 6.28, false);
     ctx.stroke();
     ctx.fillStyle = "orange";
     ctx.fill();

     //繪製右眼睛
     ctx.beginPath();
     ctx.arc(370,150, 30, 0, 6.28, false);
     ctx.stroke();
     ctx.fillStyle = "blue";
     ctx.fill();

     //繪製嘴巴
     ctx.beginPath();
     ctx.arc(300,160, 120, 0.5, 2.6, false);
     ctx.lineWidth = 10;
     ctx.stroke();
</script>
繪製笑臉


三、使用圖片

3.1圖片基本使用

canvas中不可能所有形狀都自己畫,一定是設計師給我們素材,然後使用。

canvas中使用圖片的方法:注意,必須等img完全載入後才能呈遞圖片。

ctx.drawImage();

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
//建立一個img標籤
var image = new Image()
//設定圖片的路徑
image.src = "images/baby1.jpg";
//當圖片成功載入,就畫圖(上螢幕)
image.onload = function(){
   //顯示圖片的API
   ctx.drawImage(image, 100, 100); //表示x和y座標
}

3.2使用切片

如果2個數字引數,此時表示左上角位置的xy座標:

ctx.drawImage(img,100,100);
ctx.drawImage(img圖片物件,畫布X,畫布Y);
如果4個數字引數,此時表示x、y、w、h:
ctx.drawImage(img圖片物件, 畫布X,畫布Y,圖片W,圖片H);
如果8個數字引數,此時表示:
ctx.drawImage(img,切片X,切片Y,切片W,切片H,畫布X,畫布Y,圖片W,圖片H);
//建立一個img標籤
var image = new Image()
//設定圖片的路徑
image.src = "images/baby1.jpg";
//當圖片成功載入,就畫圖(上螢幕)
image.onload = function(){
   //顯示圖片的API
   // ctx.drawImage(image, 100, 100); //表示x和y座標
   // ctx.drawImage(image, 100, 100, 150, 150); //表示x和y座標
   // ctx.drawImage(img,切片X,切片Y,切片W,切片H,畫布X,畫布Y,圖片W,圖片H);
   ctx.drawImage(image, 108, 200, 145, 120, 100, 100, 145, 120);
}

圖片APIhttps://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images

 


3.3簡易的圖片載入器

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
var R = {
   "0":"images/d1.jpg",
   "1":"images/d2.jpg",
   "2":"images/d3.jpg"
}
var arr = [];
for(var k in R){
   arr[k] = new Image(); //建立img物件
   arr[k].src = R[k];    //設定圖片地址
   // 當圖片成功載入,就畫圖(上螢幕)
   arr[k].onload = function(){
       ctx.drawImage(arr[k], 50, 50)
   }
}

3.4顯示GIF動態圖

 HTML程式碼:

<img id="testImg" src="xxx.gif" width="224" height="126">
<p><input type="button" id="testBtn" value="停止"></p>
if ('getContext' in document.createElement('canvas')) {
    HTMLImageElement.prototype.play = function() {
        if (this.storeCanvas) {
            // 移除儲存的canvas
            this.storeCanvas.parentElement.removeChild(this.storeCanvas);
            this.storeCanvas = null;
            // 透明度還原
            image.style.opacity = '';
        }
        if (this.storeUrl) {
            this.src = this.storeUrl;    
        }
    };
    HTMLImageElement.prototype.stop = function() {
        var canvas = document.createElement('canvas');
        // 尺寸
        var width = this.width, height = this.height;
        if (width && height) {
            // 儲存之前的地址
            if (!this.storeUrl) {
                this.storeUrl = this.src;
            }
            // canvas大小
            canvas.width = width;
            canvas.height = height;
            // 繪製圖片幀(第一幀)
            canvas.getContext('2d').drawImage(this, 0, 0, width, height);
            // 重置當前圖片
            try {
                this.src = canvas.toDataURL("image/gif");
            } catch(e) {
                // 跨域
                this.removeAttribute('src');
                // 載入canvas元素
                canvas.style.position = 'absolute';
                // 前面插入圖片
                this.parentElement.insertBefore(canvas, this);
                // 隱藏原圖
                this.style.opacity = '0';
                // 儲存canvas
                this.storeCanvas = canvas;
            }
        }
    };
}

var image = document.getElementById("testImg"), 
    button = document.getElementById("testBtn");
    
if (image && button) {
    button.onclick = function() {
        if (this.value == '停止') {
            image.stop();
            this.value = '播放';
        } else {
            image.play();
            this.value = '停止';
        }
    };
}
JavaScript程式碼

3.5遊戲圖片資源載入器

//得到畫布
var canvas = document.querySelector("canvas");
// 使用上下文,得到一個2D的畫布
var ctx = canvas.getContext("2d");
//資原始檔
var R = {
   "d1" : "images/d1.jpg",
   "d2" : "images/d2.jpg",
   "d3" : "images/d3.jpg"
}
//遍歷這個物件,將他們的地址變為真實圖片地址
var count = 0; //已成功載入的圖片個數
var length = Object.keys(R).length; //所有圖片的總數
for(var k in R){
   //建立image物件
   var image = new Image();
   //設定src圖片路徑
   image.src = R[k];
   //將R裡面的資原始檔,變為真正的圖片物件
   R[k] = image;
   //當image載入成功後,顯示圖片在畫布上
   image.onload = function(){
       count++; //當某張圖片載入成功,給計數器+1
       ctx.clearRect(0,0,600,600)
       //繪製文字,提升使用者體驗,提示載入的進度
       //填充文字API
       ctx.textAlign = "center";
       ctx.font = "30px 微軟雅黑";
       ctx.fillText("正在載入圖片:" + count + "/" + length, canvas.width / 2,50)
       //當載入完畢,開始遊戲
       if(count == length){
           //開始遊戲的回撥函式
           ctx.clearRect(0,0,600,600)
           start();
       }
   }
}
// 開始遊戲的函式
function start(){
   ctx.drawImage(R["d1"],100,100);
   ctx.drawImage(R["d2"],0,100);
   ctx.drawImage(R["d3"],300,200);
}
遊戲圖片載入器

四、畫布的變形

4.1 translate移動變形

translate()移動畫布,rotate()旋轉畫布。

canvas中不能只移動某一個物件,移動的都是整個畫布。

canvas中不能只旋轉某一個物件,旋轉的都是整個畫布。

但是可以用save()restore()來巧妙設定,實現讓某一個元素進行移動和旋轉。

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
ctx.translate(100, 100); //將畫布移動,座標系就發生變化了
ctx.fillRect(100, 100, 100, 100); //相對於移動後的座標系開始畫畫

 

移動變形、移動的是整個畫布、而不是某個元素,在ctx.translate()之後繪製的語句都將被影響。

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
ctx.translate(100, 100); //將畫布移動,座標系就發生變化了
ctx.fillRect(100, 100, 100, 100); //相對於移動後的座標系開始畫畫
ctx.beginPath();
ctx.arc(100,100, 100, 0, 6.28, false);
ctx.fillStyle = 'skyblue';
ctx.fill();


4.2 save()儲存和restore()恢復

ctx.save()表示儲存上下文的物理性質,ctx.restore()表示恢復最近一次的儲存。

save表示儲存sava函式之前的狀態,restore表示獲取save儲存的狀態。

移動了的元素,會影響不需要移動圓點座標的元素,所以可以使用以上兩個方法儲存起來,可以解決讓某一個元素移動變形不受影響。

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");

ctx.save();
ctx.translate(100, 100); //將畫布移動,座標系就發生變化了
ctx.fillRect(100, 100, 100, 100); //相對於移動後的座標系開始畫畫
ctx.restore();

ctx.beginPath();
ctx.arc(100,100, 100, 0, 6.28, false);
ctx.fillStyle = 'skyblue';
ctx.fill();


4.3 rotate()旋轉變形

旋轉的是整個座標系,座標系以0,0點為中心點進行旋轉。

rotate(1)的引數,是弧度,旋轉的也不是矩形,而是畫布。

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
ctx.rotate(1); //1表示57.3度(1弧度)
ctx.fillRect(100, 100, 100, 100); //相對於旋轉後的座標系開始畫畫

 

如果想旋轉某一個元素,必須將座標軸原點,放到要旋轉的元素身上,然後再旋轉。

ctx.save();
ctx.translate(150,150)
ctx.rotate(1); //1表示57.3度(1弧度)
ctx.fillRect(-50, -50, 100, 100); //相對於旋轉後的座標系開始畫畫
ctx.restore();

座標系移動到物體的中心點,物體以負半寬、半高、為xy繪製。

function Box(){
    this.x = 150;
    this.y = 150;
    this.w = 100;
    this.h = 100;
    this.deg = 0;
}
Box.prototype.render = function(){
    ctx.save()
    ctx.translate(this.x,this.y)
    ctx.rotate(this.deg); 
    ctx.fillRect(-this.w / 2,-this.h / 2,this.w,this.h);
    ctx.restore()
}
Box.prototype.update = function(){
    this.deg += 0.2;
}
var b = new Box();
b.render();
setInterval(function(){
    ctx.clearRect(0,0,600,400)
    b.update();
    b.render();
},20);

globalCompositeOperation

用來設定新影象和老圖形如何“融合”、“裁剪”。

值有以下這些:

新圖形是:source,老圖形是destination

ctx.globalCompositeOperation="destination-over";


五、FlappyBird遊戲

5.1遊戲結構

遊戲採用中介者模式開發,Game類統領全域性,負責讀取資源、設定定時器、維護各種演員的例項,也就是說所有的演員都是Gamenew出來,當做一個子屬性。

也就是,遊戲專案外部就一條語句:

var game = new Game();

其他的所有語句都寫在Game類裡面。

 

需要的類:

Game類:         中介者,讀取資源、設定定時器、維護各種演員的例項
Bird類:         小鳥類,這個類是單例的,例項化一次
Pipe類:         管子類
Land類:         大地類
background類:   背景類
<body>
    <canvas width="414" height="650"></canvas>
</body>
<script type="text/javascript" src="js/lib/underscore-min.js"></script>
<script type="text/javascript" src="js/Game.js"></script>
<script type="text/javascript" src="js/Bird.js"></script>
<script type="text/javascript" src="js/Land.js"></script>
<script type="text/javascript" src="js/Pipe.js"></script>
<script type="text/javascript" src="js/Background.js"></script>

5.2建立Game類:開始介面、載入資源

(function(){
    window.Game = function() {
        this.f = 0; //幀編號
        this.init();//初始化DOM
    }

    Game.prototype.init = function() {
        this.canvas = document.getElementById("canvas");
        this.ctx = this.canvas.getContext("2d");
        //R物件表示資原始檔,圖片總數
        this.R = {
            "bg_day": "images/bg_day.png",
            "land": "images/land.png",
            "pipe_down": "images/pipe_down.png",
            "pipe_up": "images/pipe_up.png",
            "bird0_0": "images/bird0_0.png",
            "bird0_1": "images/bird0_1.png",
            "bird0_2": "images/bird0_2.png",
        }

        var self = this;
        //遍歷物件用for in語句
        //遍歷這個物件,將它們變為真的圖片地址
        var count = 0; //計算載入好的圖片總數(成功載入一張就+1)
        var length = Object.keys(this.R).length; //得到圖片的總數
        for (var k in this.R) {
            //建立一個img標籤,發出圖片的請求,目前img物件是孤兒節點
            var img = new Image();
            //將這個R[k]物件賦值給src設定圖片的路徑
            img.src = this.R[k];
            //將R裡面的資原始檔,改為img真的圖片物件
            this.R[k] = img;
            //當圖片載入完畢,就畫圖上畫布(圖片必須load才能上畫布)
            img.onload = function () {
                count++; //當某張圖片載入完畢,給計數器+1
                //清屏
                self.clear()
                //繪製文字,提升使用者載入到什麼程度了

                //save和restore方法配合使用,防止汙染其他樣式
                self.ctx.save(); //儲存狀態
                self.ctx.textAlign = "center";
                self.ctx.font = "18px 微軟雅黑";
                self.ctx.fillStyle = "blue";
                //填充文字
                self.ctx.fillText(`載入中 ${count} / ${length}`, self.canvas.width / 2, 100);
                self.ctx.restore(); //恢復儲存的狀態
                //當載入完畢的圖片總數==圖片總數時,此時就開始載入圖片並開始遊戲
                if (count == length) {
                    self.start(); //開始遊戲的回撥函式
                }
            }
        }
    }
    //清屏
    Game.prototype.clear = function() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    }
    // 遊戲主迴圈
    Game.prototype.start = function () {
        var self = this;
        this.timer = setInterval(function(){
            self.f++;
            // 清屏
            self.clear();
            //顯示幀率
            self.ctx.font = "16px 微軟雅黑";
            self.ctx.fillText(self.f,10,20);
        },20);
    }
})();
開始介面&載入資源

5.3建立background.js背景類

(function () {
    window.Background = function () {
       this.image = game.R["bg_day"]; //圖片
       this.x = 0; 
    }
    Background.prototype.render = function () {
        // 畫一個矩形,補充一下天空的顏色
        game.ctx.save()
        game.ctx.fillStyle = "#4ec0ca";
        game.ctx.fillRect(0,0,game.canvas.width,game.canvas.height - 512);
        //第一步:為了不穿幫繪製背景連續放3張圖片讓背景無縫滾動             game.ctx.drawImage(this.image,this.x,game.canvas.height - 512);
        game.ctx.drawImage(this.image,this.x + 288 ,game.canvas.height - 512);
        game.ctx.drawImage(this.image,this.x + 288 * 2 ,game.canvas.height - 512);
        game.ctx.restore();
    }
    Background.prototype.update = function () {
        this.x--;
        if(this.x < -288){
            this.x = 0;
        }
    }
})();
背景類
Game.prototype.start = function() {
    // 遊戲開始主 迴圈
    var self = this;
    this.background = new Background();// new 背景類
    this.land = new Land(); //new例項化大地類
    this.timer = setInterval(function(){
        self.f++;
        self.clear();
        // 渲染 和 更新 背景類
        self.background.render();
        self.background.update();
        // 每隔100幀,例項化一根管子類
        self.f % 100 == 0 && new Pipe();
        // 渲染 和 更新 大地類
        self.land.render();
        self.land.update();
        // 渲染 和 更新所有管子類
        for (var i = 0; i < self.pipeArr.length; i++) {
            self.pipeArr[i].render()
            self.pipeArr[i].update()
        }
        self.ctx.font = "16px 微軟雅黑";
        self.ctx.fillText(self.f,10,20);
    }, 20)
}

5.4建立Land.js大地類

(function () {
    window.Land = function () {
        this.image = game.R["land"]; //圖片
        this.x = 0;
    }
    Land.prototype.render = function () {
        game.ctx.drawImage(this.image, this.x, game.canvas.height - 112);
        game.ctx.drawImage(this.image, this.x + 336, game.canvas.height - 112);
        game.ctx.drawImage(this.image, this.x + 336 * 2, game.canvas.height - 112);
    }
    Land.prototype.update = function () {
        this.x--;
        if (this.x < -336) {
            this.x = 0;
        }
    }
})();
大地類

5.5建立Pipe.js管子類:

(function () {
    window.Pipe = function () {
        this.pipeDown = game.R["pipe_down"]; //上管子
        this.pipeUp = game.R["pipe_up"]; //下管子

        this.pipeDownH = _.random(50,300); //隨機一個上面管子的高度(因)
        this.space = 120; //上下管子之間的空隙(因)
        //下面管子的高度隨之而定了(果),高度-大地高-上管子高-空隙
        this.pipeUpH = game.canvas.height - 112 - this.pipeDownH - this.space; 
        this.x = game.canvas.width; //讓管子在螢幕右側外面就緒
        game.pipeArr.push(this); //將自己存進陣列

    }
    Pipe.prototype.render = function () {
        //兩根管子在畫布的位置(image物件, 切片X, 切片Y, 切片W,切片H,畫布X,畫布Y,圖片W,圖片H)
        //渲染上面的管子
        game.ctx.drawImage(this.pipeDown, 0, 400 - this.pipeDownH, 52, this.pipeDownH, this.x, 0, 52, this.pipeDownH);
        //下面的管子
        game.ctx.drawImage(this.pipeUp, 0, 0, 52, this.pipeUpH, this.x, this.pipeDownH + this.space, 52, this.pipeUpH);
    }
    Pipe.prototype.update = function () {
       this.x -= 2;//更新管子(讓管子移動)
       if(this.x < -52){
           this.goDie(); //超過螢幕左側-52的位置(刪除管子)
       }
    }
    Pipe.prototype.goDie = function () {
        for(var i = game.pipeArr.length - 1; i >= 0; i--){
             if (game.pipeArr[i] == this){
                 game.pipeArr.splice(i,1);
             }
        }
    }
})();
管子類

5.6建立Bird.js小鳥類

(function () {
    window.Bird = function () {
        this.img = [game.R["bird0_0"], game.R["bird0_1"], game.R["bird0_2"]]; //小鳥
        this.x = 100;
        this.y = 100;
        //位置,這裡的x,y不是小鳥的左上角位置,而是小鳥的中心點
        // this.x = game.width / 2 * 0.618;
        this.dy = 0.2; //下降的增量,每幀的恆定變
        this.deg = 0; //旋轉
        this.wing = 0; //拍打翅膀的編號
    }

    //渲染小鳥
    Bird.prototype.render = function() {
        game.ctx.save();
        game.ctx.translate(this.x,this.y);
        game.ctx.rotate(this.deg);
        //減去24是因為x、y是中心點位置(減去寬度和高度的一半)
        game.ctx.drawImage(this.img[this.wing],-24,-24);
        game.ctx.restore()
    }
    //更新小鳥
    Bird.prototype.update = function () {
        // 下降的增量0.88,變化的量也在變,這就是自由落體
        this.dy += 0.88;
        //旋轉角度的增量
        this.deg += 0.08;
        this.y += this.dy;
        //每2幀拍打一次翅膀
        game.f % 2 && this.wing++;
        if(this.wing > 2){
            this.wing = 0;
        }
    }
    //小鳥飛
    Bird.prototype.fly = function() {
        //小鳥只要有一個負的dy此時就會向上飛,因為this.y += 一個數
        this.dy = -10;
        this.deg = -1;
    }
})();
小鳥類

5.7碰撞檢測

小鳥的碰撞檢測,使用AABB盒方法,就是把小鳥看作是一個矩形,去判斷有沒有碰撞。

碰撞公式:
鳥的X2 >管子的X1 && (鳥的Y1 < 管子的Y1 || 鳥的Y2 > 管子的Y2) && 鳥的X1 < 管子的X2

AABB盒,軸對齊包圍盒也稱為矩形盒,要自己會除錯,將關鍵的資料fillText()渲染到介面上。

碰撞檢測寫在管子身上,因為管子很多,只需要檢測有沒有碰撞到那唯一的小鳥即可,沒有for迴圈。

如果寫在鳥身上,要用for迴圈遍歷所有管子,一一檢測。


六、場景管理器

遊戲是有各種場景的:

① 歡迎介面

② 教學介面

③ 遊戲介面

④ GameOver介面

 

場景管理器(SceneManager)的好處就是可以管理零碎的東西。

l Game類現在不再直接管理BirdBackgroundPipeLand了。而是隻管理場景管理器。

l 場景管理器負責管理其他類的例項化、更新渲染。

 

刪除Game類所有的例項化、更新、渲染,然後建立場景管理器,並例項化場景管理器

繼續引入圖片資源:

this.R = {
   ...
   "title" : "images/title.png",
   "button_play" : "images/button_play.png",
   "text_ready" : "images/text_ready.png",
   "tutorial" : "images/tutorial.png",
   "gameoverbg": "images/gameoverbg.png",
   "b0" : "images/b0.png",
   "b1" : "images/b1.png",
   "b2" : "images/b2.png",
   "b3" : "images/b3.png",
   "b4" : "images/b4.png",
   "b5" : "images/b5.png",
   "b6" : "images/b6.png",
   "b7" : "images/b7.png",
   "b8" : "images/b8.png",
   "b9" : "images/b9.png",
   "b10" : "images/b10.png",
   "b11" : "images/b11.png"
}
圖片資源

 

3號場景:

window.SceneManager = function () {
    //當前場景的編號
    this.smNumber = 1;
    // 初始化場景編號的方法
    this.init(1);
    this.bindEvent();
}


(function () {    
    //場景初始化方法
    SceneManager.prototype.init = function(number) {
        switch(number){
            case 1:
                // 1號場景
                break;
            case 2:
                // 2號場景
                break;
            case 3:
                // 3號場景:遊戲的主場景
                this.background = new Background(); //例項化背景類
                this.land = new Land(); //例項化大地類
                this.bird = new Bird(); //例項化小鳥類
                break;
        }
    }
    //場景渲染方法
    SceneManager.prototype.render = function () {
        //這裡才是真正的渲染方法,可以寫動畫,因為game類裡面render此方法了
        switch (this.smNumber) {
            case 1:
                break;
            case 2:
                break;
            case 3:
                // 渲染 和 更新背景
                this.background.render();
                this.background.update();

                // 每間隔100幀,例項化一根管子
                game.f % 100 == 0 && new Pipe(); //例項化管子類
                // 迴圈遍歷管子陣列,更新和渲染管子類
                for(var i = 0; i < game.pipeArr.length;i++){
                    game.pipeArr[i].render();
                    game.pipeArr[i].update();
                }
                // 渲染 和 更新大地類
                this.land.render();
                this.land.update();

                // 渲染 和 更新小鳥類
                this.bird.render();
                this.bird.update();
                break;
        }
    }
    
})();
三號場景
修改碰撞檢測:
if(game.sm.bird.x2 > this.x1 && ( game.sm.bird.y1 < this.y1 || game.sm.bird.y2 > this.y2 ) && game.sm.bird.x1 < this.x2) { }

 

3號場景事件:

SceneManager.prototype.bindEvent = function(){
    var self = this;
    game.canvas.onmousedown = function(e){
        //新增事件監聽,要根據當前場景是幾號,觸發對應的場景事件
        switch(self.smNumber){
            case 1:
                break;
            case 2:
                break;
            case 3:
                self.bird.fly();
                break;
            case 4:
                break;
        }
    }
}

以上是全是3號場景業務,都已經完成。


1號場景:

//場景初始化方法
//不管什麼時候來到這個場景,此時都有一個預設就位狀態
//我們動畫是可以重複的,但是這個函式不是每幀執行。
SceneManager.prototype.init = function(number) {
    //init中只有一個初始化引數,不要涉及到運動
    switch(number){
        case 1:
            // 1號場景:遊戲封面和開始按鈕場景的初始化
            this.background = new Background(); //例項化背景類
            this.land = new Land();     //例項化大地類
            this.titleY = -48;         //初始化title位置
            this.titleYTarget = 120; //title停留的位置
            this.buttonY = game.canvas.height; //初始化按鈕的位置
            this.buttonYTarget = 360;          //按鈕停留的位置
            
            this.birdY = 180;         //初始化小鳥的位置
            this.birdD = "down";     //小鳥的運動方向
            break;
        case 2:
            // 2號場景
            break;
        case 3:
            // 3號場景:遊戲的主場景
            this.background = new Background(); //例項化背景類
            this.land = new Land(); //例項化大地類
            this.bird = new Bird(); //例項化小鳥類
            break;
    }
}

//場景渲染方法
SceneManager.prototype.render = function () {
    //這裡才是真正的渲染方法,可以寫動畫,因為game類裡面render此方法了
    switch (this.smNumber) {
        case 1:
            // 1號場景:遊戲封面和開始按鈕場景的更新和渲染
            // 渲染 和 更新背景
            this.background.render();
            this.background.update();
            // 渲染 和 更新大地類
            this.land.render();
            this.land.update();
            //渲染title圖片
           game.ctx.drawImage(game.R["title"],(game.canvas.width-178)/2,this.titleY);
               
game.ctx.drawImage(game.R["button_play"],(game.canvas.width-116)/2,this.buttonY);
            game.ctx.drawImage(game.R["bird1_2"], (game.canvas.width - 48) / 2, this.birdY);
            //title下降運動到目標位置
            this.titleY += 2;
            if(this.titleY > this.titleYTarget){
                this.titleY = this.titleYTarget;
            }
            //按鈕上升運動到目標位置
            this.buttonY -= 5;
            if (this.buttonY < this.buttonYTarget) {
                this.buttonY = this.buttonYTarget;
            }
            //小鳥不停的上下運動
            if(this.birdD == "down") {
                this.birdY += 2;
                if (this.birdY > 260){
                    this.birdD = "up";
                }
            } else if (this.birdD == "up"){
                this.birdY -= 2;
                if (this.birdY < 170) {
                    this.birdD = "down";
                }
            }
            break;
        case 2:
            break;
        case 3:
            ...
            break;
    }
}

//事件監聽方法
SceneManager.prototype.bindEvent = function () {
    //根據當前場景觸發事件
    var self = this;
    game.canvas.onmousedown = function(e) {
        //滑鼠點選的座標位置
        var x = e.offsetX;
        var y = e.offsetY;
        switch (self.smNumber) {
            case 1:
                //1號場景:遊戲封面和開始按鈕場景的初始化
                //得到按鈕的上下左右包圍盒
                var left  = (game.canvas.width - 116) / 2
                var right = (game.canvas.width - 116) / 2 + 116;
                var up    = self.buttonYTarget;
                var down  = self.buttonYTarget + 60;
                if(x >= left && x <= right && y <= down && y >= up){
                    //點選進去2號場景
                    self.smNumber = 2;
                    self.init(2);
                }
                break;
            case 2:
                break;
            case 3:
                break;
        }
    }
}

以上,1號場景完成。


2號場景:

2號場景init初始化:

SceneManganer.prototype.init = function(number) {
    switch (number) {
        case 1:
            ...
            break;
        case 2:
            // 教學 場景
            this.background = new Background();
            this.land = new Land();
            this.readyY = -62; //2號場景的ready圖片
            // 修改 tutorial 的透明度
            this.tutorial = 1
            this.tutorialD = "A"
            break;
        case 3:
            ...
            break;
        case 4:
            ...
            break;
    }
};

2號場景render渲染方法:
//場景渲染方法
SceneManager.prototype.render = function () {
    //這裡才是真正的渲染方法,可以寫動畫,因為game類裡面render此方法了
    switch (this.smNumber) {
        case 1:
            ...
            break;
        case 2:
            // 2號場景:教學場景
            this.background.render();
            this.background.update();
            // 渲染 和 更新大地類
            this.land.render();
            this.land.update();
            //渲染title圖片
           game.ctx.drawImage(game.R["text_ready"],(game.canvas.width-196)/2,this.readyY)
           game.ctx.drawImage(game.R["bird0_1"],100,180);
            
            this.readyY += 2;
            if (this.readyY > this.readyYTarget) {
                this.readyY = this.readyYTarget;
            }
            game.ctx.save();
            //讓一個物體閃爍
            if(this.tutorialD == "A"){
                this.tutorial -= 0.04;
                if(this.tutorial < 0.1){
                    this.tutorialD = "B"
                }
             }else if(this.tutorialD == "B"){
                this.tutorial += 0.04;
                if(this.tutorial > 1){
                    this.tutorialD = "A"
                }
            }
            // ctx.globalAlpha改變透明度的API
            game.ctx.globalAlpha = this.tutorialOpacity;
            game.ctx.drawImage(game.R["tutorial"], (game.canvas.width - 114) / 2, 250);
            game.ctx.restore();
            break;
        case 3:
           ...
            break;
        case 4:
           ...
            break;
    }
}

2號場景bindEvent監聽事件方法:
//事件監聽方法
SceneManager.prototype.bindEvent = function () {
    //根據當前場景觸發事件
    var self = this;
    game.canvas.onmousedown = function(e) {
        //滑鼠點選的座標位置
        var x = e.offsetX;
        var y = e.offsetY;
        switch (self.smNumber) {
            case 1:
          
                break;
            case 2:
                //2號場景
                var left = (game.canvas.width - 114) / 2
                var right = (game.canvas.width - 114) / 2 + 114;
                var up = 250;
                var down = 350;
                if (x >= left && x <= right && y <= down && y >= up) {
                    //點選進去2號場景
                    self.smNumber = 3;
                    self.init(3);
                }
                break;
            case 3:
                //3號場景:遊戲的主場景
                self.bird.fly(); 
                break;
        }
}
}

4號場景:

4號場景init方法

SceneManager.prototype.init = function(number) {
    //init中只有一個初始化引數,不要涉及到運動
    switch(number){
        case 1:
            break;
        case 2:
            break;
        case 3:
            break;
        case 4:
            //4號場景:死亡場景
            //紅色邊框圖片的透明度
            this.GameOverBg = 1;
            //小鳥落地死亡的爆炸動畫初始化圖片編號
            this.boom = 0;
            break;
    }
}
碰撞檢測死亡,進入4號場景:
//碰撞檢測
if (game.sm.bird.x1 < this.x2 && game.sm.bird.x2 > this.x1 && (game.sm.bird.y1 < this.y1 || game.sm.bird.y2 > this.y2)){
    //死亡之後,進入4號場景(小鳥下墜)
    document.getElementById("die").play();
    game.sm.smNumber = 4;
    game.sm.init(4);
    return;
}else if(!this.isScore && game.sm.bird.x1 > this.x2){
    // 這裡是記分,條件就是把是否加分的true或false給管子身上
    this.isScore = true;
    game.sm.score++;
    document.getElementById("score").play();
}
4號場景render渲染方法:
//場景渲染方法
SceneManager.prototype.render = function () {
    switch (this.smNumber) {
        case 1:
            ...
            break;
        case 2:
            break;
        case 3:
           ...
            break;
        case 4:
        // 讓所有的物體靜止,只渲染,不更新(update不用呼叫了)
        this.background.render();
        this.land.render();
        for (var i = 0; i < game.pipeArr.length; i++) {
            game.pipeArr[i].render();
        }
        this.bird.render();
        // 讓鳥急速下降
        this.bird.y += 10;
        //播放聲音
        document.getElementById("down").play();
        // 保證鳥頭朝下掉
        this.bird.deg += 0.5;
        if (this.bird.deg > 1.57){
            this.bird.deg = 1.57;
        }
        
        //撞擊地面產生爆炸動畫,並且小鳥飛昇
        if(this.bird.y > game.canvas.height - 112 - 17){
            //小鳥撞地停留在原位
            this.bird.y = game.canvas.height - 112 - 17;
            game.f % 2 == 0 && this.boom++;
            if (this.boom >= 11) {
                //清空管子陣列和分數,為下一回合準備
                game.pipeArr = [];
                this.score = 0;
                //回到1號場景
                this.smNumber = 1;
                this.init(1);
                this.boom = 5;
                // clearInterval(game.timer); //停止遊戲主迴圈
            }
            //渲染爆炸動畫
            game.ctx.drawImage(game.R["b" + this.boom], this.bird.x - 50, this.bird.y - 100);
            
        }
        //渲染紅色邊框
        this.GameOverBg -= 0.03;
        if(this.GameOverBg < 0){
            this.GameOverBg = 0;
        }
        game.ctx.save()
        game.ctx.globalAlpha = this.GameOverBg;
        game.ctx.drawImage(game.R["gameoverbg"],0,0,game.canvas.width,game.canvas.height)
        game.ctx.restore();
        break;
      }    
}
4號場景render渲染方法
Bird.prototype.update = function(){
    this.y += this.dy; //下降的速度
    this.deg += 0.08;
    if(this.deg > 1.57){
        this.deg = 1.57;
}
//掉地死亡
    if(this.y > game.canvas.height - 112){
        document.getElementById('die').play();
        game.sm.smNumber = 4;
        game.sm.init(4);
}
}

新增鍵盤事件和聲音、分數:

<audio src="music/die.ogg" id="die"></audio>   
<audio src="music/down.ogg" id="down"></audio>   
<audio src="music/fly.ogg" id="fly"></audio>   
<audio src="music/score.ogg" id="score"></audio>   
<canvas id="canvas" width="414" height="650" tabindex="1"></canvas>
時間和聲音

寫在bindEvent裡:

ame.canvas.onkeydown = function(e){
    switch(self.smNumber){
        case 3:
            if(e.keyCode == 32){
                self.bird.fly();
            }
            break;
    }
}
game.canvas.focus();

七、Canvas動畫

讓元素在canvas上運動,需要使用定時器。

canvas使用了一個特殊的模式,上畫布的元素,立刻被畫素化。也就是說,上畫布的元素,你將得不到這個“物件”的引用。比如,一個圓形畫到了畫布上面,此時就是一堆的畫素點,不是一個整體的物件了,你沒有任何變數能夠得到這個物件,改變這個物件的屬性。也就是說,這種改變的思路在canvas中是行不通的。

要實現動畫,必須:每幀重新畫一個。

 

所以,canvas的畫圖原理是:

清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →清屏 → 重繪 →

清屏 → 重繪 →...

 

注意:千萬不要將得到的Canvasctx放到init中,因為new一個球,來一個定時器得到一個Canvas畫布。


 

7.1小球反彈

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
function Ball(x,y,r){
   //傳入小球的屬性,x和y表示小球left和top的位置
   this.x = x;
   this.y = y;
   this.r = r;
   //當增量的dx、dy都為0的時候再隨機一次
   do{
       this.dx = parseInt(Math.random() * 18) - 9;
       this.dy = parseInt(Math.random() * 18) - 9;
   }while(this.dx == 0 || this.dy == 0);
   //隨機顏色
   var colors = ['#f90','#ff0','#09c','#c06','#F99','#9c3','#6cc','#9cc'];
   this.color = colors[parseInt(Math.random() * colors.length)]
   arr.push(this); //將小球的例項放進陣列
}
//渲染方法,畫一個小球
Ball.prototype.render = function(){
   ctx.beginPath();
   ctx.arc(this.x, this.y, this.r, 0,Math.PI * 2 ,false);
   ctx.fillStyle = this.color;
   ctx.fill();
}
Ball.prototype.update = function(){
   this.x += this.dx;
   this.y += this.dy;
   if(this.x >= canvas.width - this.r || this.x <= this.r){
       this.dx = -this.dx
   }
   if(this.y >= canvas.height - this.r || this.y <= this.r){
       this.dy = -this.dy;
   }
}
var arr = [];
var f = 0;
//開始定時器,每一幀清屏、更新、渲染所有小球
setInterval(function(){
   f++;
   //清屏
   ctx.clearRect(0, 0, canvas.width, canvas.height);
   //渲染
   for(var i = 0;i < arr.length;i++){
       arr[i].render();
       arr[i].update();
   }
   ctx.font = "18px 微軟雅黑";
   ctx.fillStyle = "blue";
   ctx.fillText(f, 10, 20)
},20);
var count = 60;
while(count--){
   new Ball(100,100,20);
}
反彈球

 


7.2炫彩小球

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
canvas.width = document.documentElement.clientWidth;
canvas.height = document.documentElement.clientHeight;
function Ball(x,y,r){
   //傳入小球的屬性,x和y表示小球left和top的位置
   this.x = x;
   this.y = y;
   this.r = r;
   //當增量的dx、dy都為0的時候再隨機一次
   do{
       this.dx = parseInt(Math.random() * 18) - 9;
       this.dy = parseInt(Math.random() * 18) - 9;
   }while(this.dx == 0 || this.dy == 0);
   //隨機顏色
   var colors = ['#f90','#ff0','#09c','#c06','#F99','#9c3','#6cc','#9cc'];
   this.color = colors[parseInt(Math.random() * colors.length)]
   arr.push(this); //將小球的例項放進陣列
}
//渲染方法,畫一個小球
Ball.prototype.render = function(){
   ctx.beginPath();
   ctx.arc(this.x, this.y, this.r, 0,Math.PI * 2 ,false);
   ctx.fillStyle = this.color;
   ctx.fill();
}
Ball.prototype.update = function(){
   this.x += this.dx;
   this.y += this.dy;
   this.r--;
   if(this.r <= 0){
       this.goDie();
   }
   if(this.x >= canvas.width - this.r || this.x <= this.r){
       this.dx = -this.dx
   }
   if(this.y >= canvas.height - this.r || this.y <= this.r){
       this.dy = -this.dy;
   }
}
Ball.prototype.goDie = function(){
   for(var i = arr.length-1; i >= 0; i--){
       if(arr[i] == this){
           arr.splice(i,1); //從陣列第i項,刪除一項
       }
   }
}
var arr = [];
var f = 0;
setInterval(function(){
   f++;
   //清屏
   ctx.clearRect(0, 0, canvas.width, canvas.height);
   //渲染
   for(var i = 0;i < arr.length;i++){
       arr[i].render();
       arr[i].update();
   }
   ctx.font = "18px 微軟雅黑";
   ctx.fillStyle = "blue";
   ctx.fillText(f, 10, 20)
},20);

canvas.onmousemove = function(e){
   new Ball(e.offsetX,e.offsetY, 30);
}

 


 

相關文章