前端笔记之Canvas
一、canvas基本使用
canvas是html5的画布,canvas算是“不务正业”的面向对象大总结,将面向对象玩极致。
算法为王!就是说canvas你不会,但是算法好,不怕写业务,不怕代码量,只要稍微学学api就能出活。
canvas这里是html5新标签,直接要了flash的命。
1.1 canvas简介
mdn的canvas在线手册:
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 和 safari。internet explorer 从ie9开始支持<canvas> ,更旧版本的ie可以引入 google 的 explorer canvas 项目中的脚本来获得<canvas>支持。chrome和opera 9+ 也支持 <canvas>。
canvas兼容到ie9。
1.2 canvas入门
canvas是html5中比较特殊的双标签,可以在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>
不能将width、height在css中设置,否则画布的像素的会被缩放,画面质量粗糙了。
<canvas>元素可以像任何一个普通的图像一样(有margin,border,background等等属性)被设计。然而,这些样式不会影响在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);
注意:x和y坐标是到圆心的位置,而且圆的大小是半径,后面绘制的形状会覆盖前面的形状。
绘制笑脸
<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个数字参数,此时表示左上角位置的x和y坐标:
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); }
图片api:https://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 = '停止'; } }; }
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();
坐标系移动到物体的中心点,物体以负半宽、半高、为x,y绘制。
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类统领全局,负责读取资源、设置定时器、维护各种演员的实例,也就是说所有的演员都是game类new出来,当做一个子属性。
也就是,游戏项目外部就一条语句:
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界面
l 场景管理器(scenemanager)的好处就是可以管理零碎的东西。
l game类现在不再直接管理bird、background、pipe、land了。而是只管理场景管理器。
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; } }
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中是行不通的。
下一篇: 使用CSS3来实现滚动视差效果的教程