学了一段时间的Canvas 有必要实战一下,这是一套视频课程中的项目,拿来练练手,毕竟编程只看不写等于不会。就好像学汉字,总共常用的也就那几千个,每个字也都认识,学霸们能够写出大作,学渣们却连一篇像样的作文都写不出来。所以,最主要的是多写,多练,借鉴别人的代码结构,如何巧妙使用,如何构造,自己能写出来,能理解,能运用才算自己。长点心,没有什么学不会。共勉,以上都是说给自己的。 .~_~.
先看下什么是Flappy Bird:Flappy Bird百度百科 , 好像还有个floppy bird ,大概意思就是无精打采的小鸟。先玩两局感受下。
> 1.仅绘制天空
<body>
<canvas id="cvs"> canvas>
<script src="./loadImage.js">
script>
<script>
var cvs = document.querySelector('#cvs');
var ctx = cvs.getContext('2d');
function Sky(ctx, img, step) {
this.ctx = ctx;
this.img = img;
this.width = this.img.width;
this.height = this.img.height;
// 这里是绑定在类上的属性,那么由该类创建的实例共享该属性
Sky.objNum++;
// 该案例总共创建了两个实例,第一个放在画布上,第二个在第一个的右侧,宽高相等
this.x = this.width * (Sky.objNum - 1);
this.y = 0;
// 移动的速度,每隔100ms移动20
this.step = step || 20;
}
Sky.prototype = {
constructor: Sky,
draw: function() {
this.ctx.drawImage(this.img, this.x, this.y);
},
update:function(){
// 两个sky背景每隔一定时间100ms,同时往左移动,当第一个移出画布时(this.x <= -this.width),第一个的x坐标往右移两个宽度(注意,这里创建了两个sky实例)
this.x -= this.step;
if (this.x <= -this.width) {
this.x += this.width *Sky.objNum;
}
}
}
Sky.objNum = 0;
script>
<script>
loadImage({
bird: './img/bird.png',
land: './img/land.png',
pipeDown: './img/pipeDown.png',
pipeUp: './img/pipeUp.png',
sky: './img/sky.png'
}, function(imgObj) {
// console.log(imgObj.bird.src);
// console.log(imgObj.sky.src);
// 画布的大小和背景图片的大小一样大
cvs.width = imgObj.sky.width;
cvs.height = imgObj.sky.height;
var sky1 = new Sky(ctx, imgObj.sky);
var sky2 = new Sky(ctx, imgObj.sky);
setInterval(function() {
sky1.draw();
sky1.update();
sky2.draw();
sky2.update();
}, 100);
})
script>
body>
2.天空中加入bird
这只鸟儿自动下落,点击画布,往上升一点。算法比较简单。
<script>
function Bird(ctx, img, widthFrame, heightFrame, x, y) {
this.ctx = ctx;
this.img = img;
// 给的素材图片每一行有几个小图
this.widthFrame = widthFrame;
// 给的素材图片每一列有几个小图
this.heightFrame = heightFrame;
// 本次给的素材小鸟只有一行三张图片,我们是切换这三张图来动态显示小鸟,这个属性表示当前切换的第几个图
this.currentFrame = 0;
this.width = this.img.width / this.widthFrame;
this.height = this.img.height / this.heightFrame;
this.x = x;
this.y = y;
// 每次移动的步长
this.moveDistance = 2;
// 步长每次加0.5
this.speedPlus = 0.5;
// 点击画布,步长变为负值,表示向上移动
this._bind();
}
Bird.prototype = {
constructor: Bird,
draw: function() {
this.ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
// this.ctx.drawImage(this.img, this.width * this.currentFrame, 0, this.width, this.height, this.x , this.y, this.width, this.height);
},
update: function() {
this.currentFrame = ++this.currentFrame >= this.widthFrame ? 0 : this.currentFrame;
// this.x += this.speedPlus;
// 让小鸟不断下落
// setInterval中,每隔一定时间(100ms)向下移动
this.y += this.moveDistance;
// 给速度一个增量,加速度为speedPlus
this.moveDistance += this.speedPlus;
},
_bind: function() {
var self = this;
this.ctx.canvas.addEventListener('click', function() {
// 点击一下屏幕,小鸟的 每次移动距离变为-4 表示向上移动,但是由于(this.moveDistance += this.speedPlus;)这个值会由 -4 慢慢变大 ,小鸟先上升后下降
self.moveDistance = -4;
});
}
}
script>
<script>
loadImage({
bird: './img/bird.png',
land: './img/land.png',
pipeDown: './img/pipeDown.png',
pipeUp: './img/pipeUp.png',
sky: './img/sky.png'
}, function(imgObj) {
// console.log(imgObj.bird.src);
// console.log(imgObj.sky.src);
// 画布的大小和背景图片的大小一样大
cvs.width = imgObj.sky.width;
cvs.height = imgObj.sky.height;
var sky1 = new Sky(ctx, imgObj.sky);
var sky2 = new Sky(ctx, imgObj.sky);
var bird = new Bird(ctx, imgObj.bird, 3, 1, 10, 10);
setInterval(function() {
sky1.draw();
sky1.update();
sky2.draw();
sky2.update();
bird.draw();
bird.update();
}, 100);
})
script>
3.天空背景+鸟+大地
效果如下,这里可以点击屏幕使小鸟上天,但是录制这个gif的时候,屏幕点击不了,所以就没有理会小鸟,只看下面的大地的左右移动,跟天空类似。
<script>
function Bird(ctx, img, widthFrame, heightFrame, x, y) {
this.ctx = ctx;
this.img = img;
// 给的素材图片每一行有几个小图
this.widthFrame = widthFrame;
// 给的素材图片每一列有几个小图
this.heightFrame = heightFrame;
// 本次给的素材小鸟只有一行三张图片,我们是切换这三张图来动态显示小鸟,这个属性表示当前切换的第几个图
this.currentFrame = 0;
this.width = this.img.width / this.widthFrame;
this.height = this.img.height / this.heightFrame;
this.x = x;
this.y = y;
// 每次移动的步长
this.moveDistance = 2;
// 步长每次加0.5
this.speedPlus = 0.5;
// 点击画布,步长变为负值,表示向上移动
this._bind();
}
Bird.prototype = {
constructor: Bird,
draw: function() {
this.ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
// this.ctx.drawImage(this.img, this.width * this.currentFrame, 0, this.width, this.height, this.x , this.y, this.width, this.height);
},
update: function() {
this.currentFrame = ++this.currentFrame >= this.widthFrame ? 0 : this.currentFrame;
// this.x += this.speedPlus;
// 让小鸟不断下落
// setInterval中,每隔一定时间(100ms)向下移动
this.y += this.moveDistance;
// 给速度一个增量,加速度为speedPlus
this.moveDistance += this.speedPlus;
},
_bind: function() {
var self = this;
this.ctx.canvas.addEventListener('click', function() {
// 点击一下屏幕,小鸟的 每次移动距离变为-4 表示向上移动,但是由于(this.moveDistance += this.speedPlus;)这个值会由 -4 慢慢变大 ,小鸟先上升后下降
self.moveDistance = -4;
});
}
}
script>
<script>
// 这里用混入继承来实现扩展原型
function extend(o1, o2) {
for (var key in o2) {
if (o2.hasOwnProperty(key)) {
o1[key] = o2[key];
}
}
}
function Land(ctx, img, speed) {
this.ctx = ctx;
this.img = img;
this.x = Land.objNum * this.img.width;
// 大地在下方,大地的顶部在画布的高度-大地图片的高度处
this.y = this.ctx.canvas.height - this.img.height;
this.speed = speed || 10;
Land.objNum++;
}
extend(Land.prototype, {
draw: function() {
this.ctx.drawImage(this.img, this.x,this.y);
},
update: function() {
this.x -= this.speed;
if (this.x < -this.img.width) {
// 道理同天空背景的移动,只不过这里用了四个大地,而Sky我们实例化了两个
this.x += Land.objNum * this.img.width;
}
}
});
Land.objNum = 0;
script>
<script>
loadImage({
bird: './img/bird.png',
land: './img/land.png',
pipeDown: './img/pipeDown.png',
pipeUp: './img/pipeUp.png',
sky: './img/sky.png'
}, function(imgObj) {
// console.log(imgObj.bird.src);
// console.log(imgObj.sky.src);
// 画布的大小和背景图片的大小一样大
cvs.width = imgObj.sky.width;
cvs.height = imgObj.sky.height;
var sky1 = new Sky(ctx, imgObj.sky);
var sky2 = new Sky(ctx, imgObj.sky);
// 实例化四个大地,三个不够,可以画个图就知道了
var land = new Land(ctx, imgObj.land, 10);
var land1 = new Land(ctx, imgObj.land, 10);
var land2 = new Land(ctx, imgObj.land, 10);
var land3 = new Land(ctx, imgObj.land, 10);
var bird = new Bird(ctx, imgObj.bird, 3, 1, 10, 10);
setInterval(function() {
sky1.draw();
sky1.update();
sky2.draw();
sky2.update();
land.draw();
land.update();
land1.draw();
land1.update();
land2.draw();
land2.update();
land3.draw();
land3.update();
bird.draw();
bird.update();
}, 100);
})
script>
4.Sky + Bird + Land +Pipe
// 让背景动起来
var timer = setInterval(function() {
/*
* 每次绘制新的游戏画面时,
* 先判断小鸟有没有碰撞,
* 如果碰撞暂停定时器。
* */
var birdCoreX = bird.x + bird.width / 2;
var birdCoreY = bird.y + bird.height / 2;
// 如果小鸟撞向管道,或者上天,或者入地,那么游戏结束
if ( ctx.isPointInPath( birdCoreX, birdCoreY )
|| birdCoreY < 0
|| birdCoreY > (ctx.canvas.height - imgObj.land.height) )
{
clearInterval( timer ); //停止游戏
ctx.fillStyle = 'rgba( 100, 100, 100, 0.8 )';
ctx.fillRect( 0, 0, ctx.canvas.width, ctx.canvas.height );
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'red';
ctx.font = '900 40px 微软雅黑';
ctx.fillText( 'GAME OVER!!!', ctx.canvas.width / 2, ctx.canvas.height / 2 );
return;
}
// 先清除上一次绘制的6个管道路径,
// 然后再按照新的位置绘制新路径
ctx.beginPath();
pipe.draw();
pipe.update();
pipe1.draw();
pipe1.update();
pipe2.draw();
pipe2.update();
pipe3.draw();
pipe3.update();
pipe4.draw();
pipe4.update();
pipe5.draw();
pipe5.update();
}, 50);
5.ctx.save()和ctx.restore是什么意思呢?
我们先看下上图的代码:
当你屏蔽掉ctx.save()和ctx.restore()之后,会发现根本不是想要的样子,至于理解的话,[引用别人写出来:](http://bbs.csdn.net/topics/390866099)
❑ save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
❑ restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
save和restore要配对使用(restore可以比save少,但不能多),如果restore调用次数比save多,会引发Error。
下面分别列出有无save——restore的情况:
**有save——restore**
i=1
执行save保存起点(0,0),图中红色小点,
执行ctx.translate(),起点变为(60*i,0)即(60,0),第一个黑点,
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(90,0)
执行restroe起点回到(0,0),图中红色小点。
i=2
执行save保存起点(0,0)
执行ctx.translate(),起点变为(60*i,0)即(120,0),第二个黑点,
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(150,0)
执行restroe起点回到(0,0)
i=3
执行save保存起点(0,0)
执行ctx.translate(),起点变为(60*i,0)即(180,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(210,0)
执行restroe起点回到(0,0)
看到坐标点的变化了吗?你画的半圆直径是60,所以第一个半圆的起止点在(60,0)和(120,0)第二个半圆的起止点在(120,0)和(180,0),第三个半圆的起止点(180,0)和(240,0)所以这些半圆都是相切的,就像下面那个图一样。
**无save——restore**
i=1
执行ctx.translate(),起点变为(60*i,0)即(60,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(90,0)
i=2
执行ctx.translate(),起点变为(90+60*i,0)即(210,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(240,0)
i=3
执行ctx.translate(),起点变为(240+60*i,0)即(420,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(450,0)
对比坐标,第一个半圆起止点是(60,0)和(120,0),第二个半圆起止点是(210,0)和(270,0),第三个半圆的起止点是(420,0)和(480,0)。看到坐标了吗?第一个半圆的止点与第二个半圆的起点差了90,第二个半圆的止点与第三个半圆的起点差了150,所以你画出的图像就错位了,就像你的上图一样
这就很容易理解了。相当于,笔触每次被挪动后,放回原位,下次别人来找的时候,在原来的地方能找到,而不是去另外的地方找。