Canvas简介
Canvas是HTML5中的标签,用以生成图像。这里的生成图像有点类似于我们自己在纸面上绘画,需要指定渲染位置以及大小等参数,同时,这也使得一旦元素被绘制出来,便再也无法编辑,只能擦除后重新绘制。
Canvas支持的浏览器
除IE8及更早版本外,其余浏览器如IE9、Edge、Chrome、FireFox、 Safari等支持Canvas。
预期结果
其中,(1)为游戏新加载时显示的图像,有两个大小为80px*100px的矩形,一个半径为15px的红色小球;(2)当鼠标左键被按下时,木棍增长,最高不超过Canvas页面外;(3)松开鼠标左键,木棍倒下,如果木棍顶端正好位于另外一个矩形顶部范围内,游戏继续,清除页面内容,开始生成下一个矩形;(4)如果木棍顶端没有到达或超出矩形顶部范围,游戏结束,弹出“重新开始”按钮。
项目结构
包含HTML主页面stickGrow.html、css样式文件stick.css和JavaScript文件stick.js。
HTML主页面:stickGrow.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Horizon Project</title>
<link rel="stylesheet" href="./stick.css">
</head>
<body>
<div class="con">
<div class="score">0</div>
<canvas width="400px" height="600px" id="cvs"></canvas>
<button class="restart">重新开始</button>
</div>
</body>
</html>
<script src="./stick.js"></script>
Canvas使用<Canvas>标签创建,并直接向其赋值:宽width和高height。创建用来显示得分的div--”score“,其内部赋初始值0。最后是“重新开始”按钮。运行后得到如下结果:
按照正常使用习惯,我们接下来要将得分和按钮移动进Canvas内,且暂时隐藏“重新开始”按钮,因此需要使用css来完成。
css样式文件:stick.css
body{
margin: 0;
padding: 0;
background-color: black;
}
.con{
width: 400px;
height: 660px;
margin-top: 50px;
margin-left: auto;
margin-right: auto;
}
.score{
width: 180px;
height: 60px;
text-align: center;
margin-left: auto;
margin-right: auto;
color: #eb4b16;
font-size: 60px;
position: relative;
top: 120px;
text-shadow: 5px 5px 10px #b18253;
}
首先给Canvas的父级div--“con”赋宽高值。此处使用position: relative;移动了分数的位置,但是HTML还是认为score在Canvas上方占据60px。因此con的高 = Canvas的高 + score的高,为660px。同时将margin-left和margin-right设为auto使其始终位于页面中央。
.restart{
color: #fef7ec;
border: 0;
border-radius: 15px;
background-color: #b16145;
font-size: 25px;
width: 120px;
height: 40px;
position: relative;
top: -300px;
left: 140px;
display: none;
box-shadow: 3px 3px 10px #865746;
}
.restart:hover{
background-color: #8a4831;
cursor: pointer;
}
canvas{
background-color: #fef7ec;
border-radius: 15px;
}
这里设置“重新开始”按钮display: none;,只有在JavaScript判定游戏结束时才会使它重现。运行后我们可以看到如下结果:
JavaScript文件stick.js
var cvs = document.getElementById("cvs");
var ctx = cvs.getContext("2d");
var sc = document.getElementsByClassName("score");
var btns = document.getElementsByTagName("button");
var scores = 0;
首先取出Canvas,Canvas只是一张画布,没有提供绘图能力和相关函数。为了使用JavaScript进行绘制,我们需要对Canvas进行getContext("2d");操作得到ctx对象,这样ctx对象就可以调用getContext()里的属性和方法了。接下来取出得分和按钮,注意他们的取出方法都是getElements,因此即使只有一个元素返回的也是一个列表,我们在使用时需要添加索引如sc[0]、btns[0]。最后定义全局变量scores,scores运算结束后可把值赋给页面上的得分div框中。
function init(){
ctx.fillStyle = "rgb(190, 23, 47)";
ctx.beginPath();
ctx.arc(50, 485, 15, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillStyle = "rgb(38, 54, 69)";
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 4;
ctx.shadowColor = "rgba(140, 140, 140, 0.6)"
ctx.fillRect(0, 500, 80, 100); //x, y, wid, hei
}
定义函数init(),用来生成角色--“红色小球”,其中绘制圆弧函数ctx.arc()中传入参数分别为:(相对于屏幕左上角的原点)横坐标,纵坐标,圆弧的半径,起始角度,终止角度,顺逆时针(true逆时针,false顺时针,不过此处绘制的是正圆,因此无需考虑顺逆时针)。Math.PI表示π,即Math.PI * 2 = 2π = 360度。绘制的圆弧为空心,使用ctx.fill();填充内部。
绘制角色所在的矩形块,为了使其更立体,我们为其赋上阴影,不过如果想要清除图形,要连带其X轴Y轴上的阴影一同清除,不然就会留下一条明显的阴影。矩形使用ctx.fillRect();函数绘制,传入参数分别为:(相对于屏幕左上角的原点)横坐标,纵坐标,矩形宽,矩形高。
特别需要注意的一点是,在绘制前需要先指明它的填充颜色ctx.fillStyle();,这就像选择画笔一样,且可在后面修改,定义颜色后可开始绘制。同样的,指定阴影样式后绘制的图形都会带上阴影,在上面的代码中我不想要角色带阴影,所以将角色定义于阴影样式之前。
window.onload = function(){
init();
}
定义页面加载函数,在页面打开或刷新时调用init(),运行得到以下结果:
function stageGen(){
var x = 80 + 20 + Math.floor(Math.random()*10*22);
ctx.fillStyle = "rgb(38, 54, 69)";
ctx.fillRect(x, 500, 80, 100);
return x;
}
接下来定义函数stageGen(),以生成新的目标方块,与小球所在方块大小一致,为80px宽、100px高。它在页面X轴的位置需要满足:不能与小球所在方块重叠或太近、不能超出Canvas显示范围。在这里,为了防止两方块距离过近,设置至少间距为20,因此它的X轴位置(左上角的点的位置)在80+20 到 400-80之间取随机数,最终返回其横坐标。
function start(){
init();
res = stageGen();
console.log(res);
}
新定义start()函数,将stageGen()与init()函数封装,并在控制台输出新生成的方块的X轴坐标。
window.onload = function(){
start();
}
直接修改页面加载执行函数window.onload调用封装好的start()函数,运行可看到另一块矩形成功生成,可刷新页面,随着页面刷新,其位置也会发生变化,同时在控制台输出其X轴坐标(不是它相对于左边方块空出来的距离),如下图:
function stickDraw(){
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
stickY = 0;
val = setInterval(function(){
if (stickY >= 500 ) {
clearInterval(val);
}else{
stickY++;
ctx.clearRect(70, 500, 50, -500);
ctx.fillStyle = "rgb(73, 36, 21)";
ctx.fillRect(70, 500, 10, -stickY);
}
},10);
}
定义木棒增长函数stickDraw(),为了防止木棒在绘制中出现错误,我们将它的阴影完全去除,并定义一个全局变量stickY记录棒长。然后使用周期调用函数setInterval();实现每10毫秒让木棒长度自增1px,当木棒长度达到Canvas纵向最高点时,为500px,将木棒旋转也早已超过Canvas横向最大值,因此设定当stickY大于等于500px时停止setInterval()函数。特别的,在下一帧绘制前,我们要将上一帧绘制的木棒清除,所以使用ctx.clearRect();来清除,它传入的参数与ctx.fillRect()函数类似,分别为:(相对于屏幕左上角的原点)横坐标,纵坐标,要清除的矩形宽,要清除的矩形高。这里直接清除了木棒上方全部的内容。
function handleMouseDown() {
stickDraw();
console.log('down' + stickY);
}
cvs.addEventListener('mousedown', handleMouseDown);
定义函数handleMouseDown(),再为Canvas页面绑定鼠标按下事件,因此当鼠标按下时,木棒会向上增长,同时控制台输出down + 木棒起始长度。此时运行,木棒会持续向上增长,如下:
function rotateBlock(){
ctx.translate(80, 500);
ctx.clearRect(0, 0, -10, -500);
ctx.save();
ctx.rotate(90 * Math.PI / 180);
ctx.fillStyle = "rgb(73, 36, 21)";
ctx.fillRect(0, 0, -10, -stickY);
ctx.restore();
}
木棒已经出现,现在要当松开鼠标时停止木棒增长并使其顺时针旋转90度。首先处理木棒旋转函数rotateBlock();,这里选择了木棒右下角(即小球所在方块右上角)为旋转原点,因此使用ctx.translate();将原点设为(80, 500),在这之后的坐标均以此为原点(0, 0)。使用ctx.save();保存当前木棒状态,调用旋转函数ctx.rotate();,此处使其顺时针旋转90度即π/2后重新填充木棒,并使用ctx.restore();来还原之前保存的状态。
function handleMouseUp() {
if (val){
clearInterval(val);
}
rotateBlock();
console.log('up'+stickY);
//judgeOver();
}
cvs.addEventListener('mouseup', handleMouseUp);
定义函数handleMouseUp(),setInterval()函数会有返回值,在前面我们将此返回值存入val全局变量中,当调用函数时,该周期调用函数会被关闭,以此达到木棒长度不再增加,同时调用rotateBlock()函数旋转木棒,并输出此时木棒的长度。向Canvas页面添加鼠标按键抬起触发器,当鼠标抬起,执行以上操作。运行程序,长按鼠标左键,数秒后松开,可得以下结果:
function judgeOver(){
if(stickY < (res-80) || stickY > res){
console.log("游戏结束");
ctx.translate(-80, -500);
cvs.removeEventListener('mousedown', handleMouseDown);
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 4;
ctx.shadowColor = "rgba(140, 140, 140, 0.6)"
ctx.fillStyle = "rgb(89, 109, 143)";
ctx.font = '60px Verdana';
ctx.textAlign = 'center';
ctx.fillText("游戏结束", 200, 200);
cvs.removeEventListener('mouseup', handleMouseUp);
btns[0].style.display = "inline";
}else{
console.log("继续");
scores++;
sc[0].innerHTML = scores;
stickY = 0;
setTimeout(function(){
ctx.translate(-80, -500);
ctx.clearRect(0, 0, 400, 600);
start();
},500);
}
}
最后我们要处理的是游戏是否结束,如果未结束,清除木棒并重新生成方块,分数+1;如果结束,则清除鼠标按下释放事件,弹出“游戏结束”提示和“重新开始”按钮。
判断的条件是木棒长度小于两矩形间距(即res - 80px)或大于左边矩形右上角到右边矩形右上角的距离(即res本身的值)为游戏失败。此时清除鼠标的所有事件,防止游戏结束后的误操作,并使用ctx.fillText();绘制文字“游戏结束”,通过设置btns[0].style.display属性使得“重新开始”按钮显现。
如果木棒落在了矩形上方,游戏继续,令scores自增1后赋值给显示分数的div的innerHTML,分数可实时更新显示出来。再通过延时函数setTimeout();设置0.5秒后还原原点位置至左上角、清除屏幕上所有内容并调用start()函数绘制下一轮的图像。
function handleMouseUp() {
if (val){
clearInterval(val);
}
rotateBlock();
console.log('up'+stickY);
judgeOver();//上面此行被注释掉了,删除注释即可
}
在handleMouseUp()中封装judgeOver()函数,此时程序也可正常运行了:
btns[0].onclick = function(){
location.reload();
}
“重新开始”按钮简单粗暴地刷新页面达到重新开始的效果。
未解决的点
- 全局无动画。最初设想给木棒旋转、小球移动和新方块生成添加动画,但是能力有限,最终没有完成动画的制作。
- 少数情况下会有bug存在。在多次点击鼠标或其他极端情况下程序在方块生成、移除上有bug出现,目前未找到原因。
源码
Hori03/HTML+JavaScript+Canvas编写2D小游戏 - 码云 - 开源中国 (gitee.com)