整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

使用javascript中canvas实现拼图小游戏

使用javascript中canvas实现拼图小游戏

篇文章给大家带来的内容是关于使用javascript中canvas实现拼图小游戏 ,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

如果您想要综合使用javascript中canvas、原生拖拽、本地存储等多种技术完成一个有趣的项目,那么这篇文章将非常适合您

1 简介和源码

该项目中的拼图小游戏使用javascript原创,相比于网站上类似的功能,它使用到的技术点更先进丰富,功能更强大,还包含程序开发中更多先进的思想理念,从该项目中您将能学到:

  • FileReader、Image对象的配合canvas对图片进行压缩,切割的技巧。
  • 学习小游戏开发中最常用的碰撞检测、状态监控、刷新保持状态的处理方法。
  • 深入了解拖拽交换元素的细节,学习到动态元素绑定事件、回调函数的处理方式。

项目源码-github

下面是游戏界面的示例图:

2 实现思路

根据游戏界面图我们可以将完成这么一个小游戏分为以下几步来实现:

  • 1.拖拽图片到指定区域,使用FileReader对象读取到图片的base64内容,然后添加到Image对象中
  • 2.当Image对象加载完成后,使用canvas对图片进行等比缩放,然后取到缩略图的base64内容,添加到另外一个缩略图Image对象中,并将该缩略图base64的内容保存到本地存储(localStorage)中
  • 3.当缩略图Image对象加载完成后,再次使用canvas对缩略图进行切割,该游戏中将缩略图切割成3*4一共12等份,使用本地存储保存每份切割缩略图base64内容,将缩略图顺序打乱,使用img标签显示在web页面上
  • 4.当缩略图切片都添加到web界面上以后,为每一份缩略图切片添加注册拖拽事件,使得缩略图切片可以相互交换,在这个过程当中,添加对缩略图切片顺序状态的监控,一旦完成拼图,就直接展示完整的缩略图,完成游戏

从以上对小游戏制作过程的分析来看,第4步是程序功能实现的重点和难点,在以上的每个步骤中都有很多小细节需要注意和探讨,下面我就详细分析一下每个步骤的实现细节,说的不好的地方,欢迎大家留言指正。

3 开发细节详解

3.1 图片内容读取和加载

在游戏开发第1步中,我们将图片拖拽到指定区域后,程序是怎样得到图片内容信息的呢?fileReader对象又是怎样将图片信息转化为base64字符串内容的?Image对象拿到图片的base64内容之后,又是怎样初始化加载的?带着这些疑问,我们来研究一下实现项目中实现了第一步的关键代码。

var droptarget=document.getElementById("droptarget"),

output=document.getElementById("ul1"),

thumbImg=document.getElementById("thumbimg");

//此处省略相关代码........

function handleEvent(event) {

var info="",

reader=new FileReader(),

files, i, len;

EventUtil.preventDefault(event);

localStorage.clear();

if (event.type=="drop") {

files=event.dataTransfer.files;

len=files.length;

if (!/image/.test(files[0].type)) {

alert('请上传图片类型的文件');

}

if (len > 1) {

alert('上传图片数量不能大于1');

}

var canvas=document.createElement('canvas');

var context=canvas.getContext('2d');

var img=new Image(), //原图

thumbimg=new Image(); //等比缩放后的缩略图

reader.readAsDataURL(files[0]);

reader.onload=function (e) {

img.src=e.target.result;

}

//图片对象加载完毕后,对图片进行等比缩放处理。缩放后最大宽度为三百像素

img.onload=function () {

var targetWidth, targetHeight;

targetWidth=this.width > 300 ? 300 : this.width;

targetHeight=targetWidth / this.width * this.height;

canvas.width=targetWidth;

canvas.height=targetHeight;

context.clearRect(0, 0, targetWidth, targetHeight);

context.drawImage(img, 0, 0, targetWidth, targetHeight);

var tmpSrc=canvas.toDataURL("image/jpeg");

//在本地存储完整的缩略图源

localStorage.setItem('FullImage', tmpSrc);

thumbimg.src=tmpSrc;

}

//此处省略相关代码......

EventUtil.addHandler(droptarget, "dragenter", handleEvent);

EventUtil.addHandler(droptarget, "dragover", handleEvent);

EventUtil.addHandler(droptarget, "drop", handleEvent);

}

这段代码的思路就是首先获得拖拽区域目标对象droptarget,为droptarget注册拖拽监听事件。代码中用到的EventUtil是我封装的一个对元素添加事件、事件对象的兼容处理等常用功能的简单对象,下面是其添加注册事件的简单简单代码,其中还有很多其他的封装,读者可自行查阅,功能比较简单。

var EventUtil={

addHandler: function(element, type, handler){

if (element.addEventListener){

element.addEventListener(type, handler, false);

} else if (element.attachEvent){

element.attachEvent("on" + type, handler);

} else {

element["on" + type]=handler;

}

},

//此处省略代......

}

当用户将图片文件拖放到区域目标对象droptarget时,droptarget的事件对象通过event.dataTransfer.files获取到文件信息,对文件进行过滤(限制只能为图片内容,并且最多只能有一张图片)。拿到文件内容以后,使用FileReader对象reader读取文件内容,使用其readAsDataURL方法读取到图片的base64内容,赋值给Image对象img的src属性,就可以等到img对象初始化加载完毕,使canvas对img进行下一步的处理了。这里有一个重点的地方需要说明:一定要等img加载完成后,再使用canvas进行下一步的处理,不然可能会出现图片损坏的情况。原因是:当img的src属性读取图片文件的base64内容时,可能还没有将内容加载到内存中时,canvas就开始处理图片(此时的图片是不完整的)。所以我们可以看到canvas对图片的处理是放在img.onload方法中进行的,程序后边还会有这种情况,之后就不再赘述了。

3.2 图片等比缩放和本地存储

在第一步中我们完成了对拖拽文件的内容读取,并将其成功加载到了Image对象img中。接下来我们使用canvas对图片进行等比缩放,对图片进行等比缩放,我们采取的策略是限制图片的最大宽度为300像素,我们再来看一下这部分代码吧:

img.onload=function () {

var targetWidth, targetHeight;

targetWidth=this.width > 300 ? 300 : this.width;

targetHeight=targetWidth / this.width * this.height;

canvas.width=targetWidth;

canvas.height=targetHeight;

context.clearRect(0, 0, targetWidth, targetHeight);

context.drawImage(img, 0, 0, targetWidth, targetHeight);

var tmpSrc=canvas.toDataURL("image/jpeg");

//在本地存储完整的缩略图源

localStorage.setItem('FullImage', tmpSrc);

thumbimg.src=tmpSrc;

}

确定了缩放后的宽度targetWidth和高度targetHeight之后,我们使用canvas的drawImage方法对图像进行压缩,在这之前我们最好先使用画布的clearRect对画布进行一次清理。对图片等比缩放以后,使用canvas的toDataURL方法,获取到缩放图的base64内容,赋给新的缩放图Image对象thumbimg的src属性,待缩放图加载完毕,进行下一步的切割处理。缩放图的base64内容使用localStorage存储,键名为"FullImage"。浏览器的本地存储localStorage是硬存储,在浏览器刷新之后内容不会丢失,这样我们就可以在游戏过程中保持数据状态,这点稍后再详细讲解,我们需要知道的是localStorage是有大小限制的,最大为5M。这也是为什么我们先对图片进行压缩,减少存储数据大小,保存缩放图base64内容的原因。关于开发过程中存储哪些内容,下一小节会配有图例详细说明。

3.3 缩略图切割

生成缩略图之后要做的工作就是对缩略图进行切割了,同样的也是使用canvas的drawImage方法,而且相应的处理必须放在缩略图加载完成之后(即thumbimg.onload)进行处理,原因前面我们已经说过。下面我们再来详细分析一下源代码吧:

thumbimg.onload=function () {

//每一个切片的宽高[切割成3*4格式]

var sliceWidth, sliceHeight, sliceBase64, n=0, outputElement='',

sliceWidth=this.width / 3,

sliceHeight=this.height / 4,

sliceElements=[];

canvas.width=sliceWidth;

canvas.height=sliceHeight;

for (var j=0; j < 4; j++) {

for (var i=0; i < 3; i++) {

context.clearRect(0, 0, sliceWidth, sliceHeight);

context.drawImage(thumbimg, sliceWidth * i, sliceHeight * j, sliceWidth, sliceHeight, 0, 0, sliceWidth, sliceHeight);

sliceBase64=canvas.toDataURL("image/jpeg");

localStorage.setItem('slice' + n, sliceBase64);

//为了防止图片三像素问题发生,请为图片属性添加 display:block

newElement="<li name=\"" + n + "\" style=\"margin:3px;\"><img src=\"" + sliceBase64 + "\" style=\"display:block;\"></li>";

//根据随机数打乱图片顺序

(Math.random() > 0.5) ? sliceElements.push(newElement) : sliceElements.unshift(newElement);

n++;

}

}

//拼接元素

for (var k=0, len=sliceElements.length; k < len; k++) {

outputElement +=sliceElements[k];

}

localStorage.setItem('imageWidth', this.width + 18);

localStorage.setItem('imageHeight', this.height + 18);

output.style.width=this.width + 18 + 'px';

output.style.height=this.height + 18 + 'px';

(output.innerHTML=outputElement) && beginGamesInit();

droptarget.remove();

}

上面的代码对于大家来说不难理解,就是将缩略图分割成12个切片,这里我给大家解释一下几个容易困惑的地方:

  • 1.为什么我们再切割图片的时候,代码如下,先从列开始循环?

for (var j=0; j < 4; j++) {

for (var i=0; i < 3; i++) {

//此处省略逻辑代码

}

}

这个问题大家仔细想一想就明白了,我们将图片进行切割的时候,要记录下来每一个图片切片的原有顺序。在程序中我们使用 n 来表示图片切片的原有顺序,而且这个n记录在了每一个图片切片的元素的name属性中。在后续的游戏过程中我们可以使用元素的getAttribute('name')方法取出 n 的值,来判断图片切片是否都被拖动到了正确的位置,以此来判断游戏是否结束,现在讲起这个问题可能还会有些迷惑,我们后边还会再详细探讨,我给出一张图帮助大家理解图片切片位置序号信息n:

序号n从零开始是为了和javascript中的getElementsByTagName()选择的子元素坐标保持一致。

  • 2 我们第3步实现的目的不仅是将缩略图切割成小切片,还要将这些图片切片打乱顺序,代码程序中这一点是怎样实现的?
  • 阅读代码程序我们知道,我们每生成一个切片,就会构造一个元素节点: newElement="<li name=\"" + n + "\" style=\"margin:3px;\"><img src=\"" + sliceBase64 + "\" style=\"display:block;\"></li>"; 。我们在是在外部先声明了一个放新节点的数组sliceElements,我们每生成一个新的元素节点,就会把它放到sliceElements数组中,但是我们向sliceElements头部还是尾部添加这个新节点则是随机的,代码是这样的:

(Math.random() > 0.5) ? sliceElements.push(newElement) : sliceElements.unshift(newElement);

我们知道Math.random()生成一个[0, 1)之间的数,所以再canvas将缩略图裁切成切片以后,根据这些切片生成的web节点顺序是打乱的。打乱顺序以后重新组装节点:

//拼接元素

for (var k=0, len=sliceElements.length; k < len; k++) {

outputElement +=sliceElements[k];

}

然后再将节点添加到web页面中,也就自然而然出现了图片切片被打乱的样子了。

  • 3.我们根据缩略图切片生成的DOM节点是动态添加的元素,怎样给这样动态元素绑定事件呢?我们的项目中为每个缩略图切片DOM节点绑定的事件是“拖动交换”,和其他节点都有关系,我们要保证所有的节点都加载后再对事件进行绑定,我们又是怎样做到的呢?

下面的一行代码,虽然简单,但是用的非常巧妙:

(output.innerHTML=outputElement) && beginGamesInit();

有开发经验的同学都知道 && 和 || 是短路运算符,代码中的含义是:只有当切片元素节点都添加到

WEB页面之后,才会初始化为这些节点绑定事件。

3.4 本地信息存储

代码中多次用到了本地存储,下面我们来详细解释一下本游戏开发过程中都有哪些信息需要存储,为什么要存储?下面是我给出的需要存储的信息图示例(从浏览器控制台获取):

浏览器本地存储localStorage使用key:value形式存储,从图中我们看到我们本次存储的内容有:

  • FullImage:图片缩略图base64编码。
  • imageWidth:拖拽区域图片的宽度。
  • imageHeight:拖拽区域图片的高度。
  • slice*:每一个缩略图切片的base64内容。
  • nodePos:保存的是当前缩略图的位置坐标信息。

保存FullImage缩略图的信息是当游戏结束后显示源缩略图时,根据FullImage中的内容展示图片。而imageWidth,imageHeight,slice*,nodePos是为了防止浏览器刷新导致数据丢失所做的存储,当刷新页面的时候,浏览器会根据本地存储的数据加载没有完成的游戏内容。其中nodePos是在为缩略图切片发生拖动时存入本地存储的,并且它随着切片位置的变化而变化,也就是它追踪着游戏的状态,我们在接下来的代码功能展示中会再次说到它。

3.5 拖拽事件注册和监控

接下来我们要做的事才是游戏中最重要的部分,还是先来分析一下代码,首先是事件注册前的初始化工作:

//游戏开始初始化

function beginGamesInit() {

aLi=output.getElementsByTagName("li");

for (var i=0; i < aLi.length; i++) {

var t=aLi[i].offsetTop;

var l=aLi[i].offsetLeft;

aLi[i].style.top=t + "px";

aLi[i].style.left=l + "px";

aPos[i]={left: l, top: t};

aLi[i].index=i;

//将位置信息记录下来

nodePos.push(aLi[i].getAttribute('name'));

}

for (var i=0; i < aLi.length; i++) {

aLi[i].style.position="absolute";

aLi[i].style.margin=0;

setDrag(aLi[i]);

}

}

可以看到这部分初始化绑定事件代码所做的事情是:记录每一个图片切片对象的位置坐标相关信息记录到对象属性中,并为每一个对象都注册拖拽事件,对象的集合由aLi数组统一管理。这里值得一提的是图片切片的位置信息index记录的是切片现在所处的位置,而我们前边所提到的图片切片name属性所保存的信息n则是图片切片原本应该所处的位置,在游戏还没有结束之前,它们不一定相等。待所有的图片切片name属性所保存的值和其属性index都相等时,游戏才算结束(因为用户已经正确完成了图片的拼接),下面的代码就是用来判断游戏状态是否结束的,看起来更直观一些:

//判断游戏是否结束

function gameIsEnd() {

for (var i=0, len=aLi.length; i < len; i++) {

if (aLi[i].getAttribute('name') !=aLi[i].index) {

return false;

}

}

//后续处理代码省略......

}

下面我们还是详细说一说拖拽交换代码相关逻辑吧,拖拽交换的代码如下图所示:

//拖拽

function setDrag(obj) {

obj.onmouseover=function () {

obj.style.cursor="move";

console.log(obj.index);

}

obj.onmousedown=function (event) {

var scrollTop=document.documentElement.scrollTop || document.body.scrollTop;

var scrollLeft=document.documentElement.scrollLeft || document.body.scrollLeft;

obj.style.zIndex=minZindex++;

//当鼠标按下时计算鼠标与拖拽对象的距离

disX=event.clientX + scrollLeft - obj.offsetLeft;

disY=event.clientY + scrollTop - obj.offsetTop;

document.onmousemove=function (event) {

//当鼠标拖动时计算p的位置

var l=event.clientX - disX + scrollLeft;

var t=event.clientY - disY + scrollTop;

obj.style.left=l + "px";

obj.style.top=t + "px";

for (var i=0; i < aLi.length; i++) {

aLi[i].className="";

}

var oNear=findMin(obj);

if (oNear) {

oNear.className="active";

}

}

document.onmouseup=function () {

document.onmousemove=null; //当鼠标弹起时移出移动事件

document.onmouseup=null; //移出up事件,清空内存

//检测是否普碰上,在交换位置

var oNear=findMin(obj);

if (oNear) {

oNear.className="";

oNear.style.zIndex=minZindex++;

obj.style.zIndex=minZindex++;

startMove(oNear, aPos[obj.index]);

startMove(obj, aPos[oNear.index], function () {

gameIsEnd();

});

//交换index

var t=oNear.index;

oNear.index=obj.index;

obj.index=t;

//交换本次存储中的位置信息

var tmp=nodePos[oNear.index];

nodePos[oNear.index]=nodePos[obj.index];

nodePos[obj.index]=tmp;

localStorage.setItem('nodePos', nodePos);

} else {

startMove(obj, aPos[obj.index]);

}

}

clearInterval(obj.timer);

return false;//低版本出现禁止符号

}

}

这段代码所实现的功能是这样子的:拖动一个图片切片,当它与其它的图片切片有碰撞重叠的时候,就和与其左上角距离最近的一个图片切片交换位置,并交换其位置信息index,更新本地存储信息中的nodePos。移动完成之后判断游戏是否结束,若没有,则期待下一次用户的拖拽交换。

下面我来解释一下这段代码中比较难理解的几个点:

  • 1.图片切片在被拖动的过程中是怎样判断是否和其它图片切片发生碰撞的?这就是典型的碰撞检测问题。
  • 程序中实现碰撞检测的代码是这样的:

//碰撞检测

function colTest(obj1, obj2) {

var t1=obj1.offsetTop;

var r1=obj1.offsetWidth + obj1.offsetLeft;

var b1=obj1.offsetHeight + obj1.offsetTop;

var l1=obj1.offsetLeft;

var t2=obj2.offsetTop;

var r2=obj2.offsetWidth + obj2.offsetLeft;

var b2=obj2.offsetHeight + obj2.offsetTop;

var l2=obj2.offsetLeft;

`if (t1 > b2 || r1 < l2 || b1 < t2 || l1 > r2)` {

return false;

} else {

return true;

}

}

这段代码看似信息量很少,其实也很好理解,判断两个图片切片是否发生碰撞,只要将它们没有发生碰撞的情形排除掉就可以了。这有点类似与逻辑中的非是即否,两个切片又确实只可能存在两种情况:碰撞、不碰撞。图中的这段代码是判断不碰撞的情况:if (t1 > b2 || r1 < l2 || b1 < t2 || l1 > r2),返回false, else 返回true。

2.碰撞检测完成了之后,图片切片之间又是怎样寻找左上角定点距离最近的元素呢?

代码是这个样子的:

//勾股定理求距离(左上角的距离)

function getDis(obj1, obj2) {

var a=obj1.offsetLeft - obj2.offsetLeft;

var b=obj1.offsetTop - obj2.offsetTop;

return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));

}

//找到距离最近的

function findMin(obj) {

var minDis=999999999;

var minIndex=-1;

for (var i=0; i < aLi.length; i++) {

if (obj==aLi[i]) continue;

if (colTest(obj, aLi[i])) {

var dis=getDis(obj, aLi[i]);

if (dis < minDis) {

minDis=dis;

minIndex=i;

}

}

}

if (minIndex==-1) {

return null;

} else {

return aLi[minIndex];

}

}

因为都是矩形区块,所以计算左上角的距离使用勾股定理,这点相信大家都能明白。查找距离最近的元素原理也很简单,就是遍历所有已经碰撞的元素,然后比较根据勾股定理计算出来的最小值,返回元素就可以了。代码中也是使用了比较通用的方法,先声明一个很大的值最为最小值,当有碰撞元素比其小时,再将更小的值最为最小值,遍历完成后,返回最小值的元素就可以了。

  • 3.图片区块每次交换之后,是怎样监控判断游戏是否已经结束的呢?

答案是回调函数,图片切片交换函数通过回调函数来判断游戏是否已经结束,游戏是否结束的判断函数前面我们已经说过。图片切片交换函数就是通过添加gameIsEnd作为回调函数,这样在每次图片切片移动交换完成之后,就判断一下游戏是否结束。图片切片的交换函数还是比较复杂的,有兴趣的同学可以研究一下,下面是其实现代码,大家重点理解其中添加了回调函数监控游戏是否结束就好了。

//通过class获取元素

function getClass(cls){

var ret=[];

var els=document.getElementsByTagName("*");

for (var i=0; i < els.length; i++){

//判断els[i]中是否存在cls这个className;.indexOf("cls")判断cls存在的下标,如果下标>=0则存在;

if(els[i].className===cls || els[i].className.indexOf("cls")>=0 || els[i].className.indexOf(" cls")>=0 || els[i].className.indexOf(" cls ")>0){

ret.push(els[i]);

}

}

return ret;

}

function getStyle(obj,attr){//解决JS兼容问题获取正确的属性值

return obj.currentStyle?obj.currentStyle[attr]:getComputedStyle(obj,false)[attr];

}

function gameEnd() {

alert('游戏结束!');

}

function startMove(obj,json,fun){

clearInterval(obj.timer);

obj.timer=setInterval(function(){

var isStop=true;

for(var attr in json){

var iCur=0;

//判断运动的是不是透明度值

if(attr=="opacity"){

iCur=parseInt(parseFloat(getStyle(obj,attr))*100);

}else{

iCur=parseInt(getStyle(obj,attr));

}

var ispeed=(json[attr]-iCur)/8;

//运动速度如果大于0则向下取整,如果小于0想上取整;

ispeed=ispeed>0?Math.ceil(ispeed):Math.floor(ispeed);

//判断所有运动是否全部完成

if(iCur!=json[attr]){

isStop=false;

}

//运动开始

if(attr=="opacity"){

obj.style.filter="alpha:(opacity:"+(json[attr]+ispeed)+")";

obj.style.opacity=(json[attr]+ispeed)/100;

}else{

obj.style[attr]=iCur+ispeed+"px";

}

}

//判断是否全部完成

if(isStop){

clearInterval(obj.timer);

if(fun){

fun();

}

}

},30);

}

4 补充和总结

4.1 游戏中值得完善的功能

我认为该游戏中值得优化的地方有两个:

  • 1.为拼图小游戏添加缩略图,因为缩略图有利于为玩游戏的用户提供思路。我们又在浏览器本地存储中保存了缩略图的base64内容,所以实现起来也很容易。
  • 2.缓存有的时候也让人很痛苦,就比如说在游戏中有些用户就想要重新开始,而我们的小游戏只有在游戏完成之后才清空缓存,刷新页面,游戏才能够重新开始。这给用户的体验很不好,我们可以加一个重置游戏按钮,清空缓存并优化游戏结束后的一些逻辑。

这些功能感兴趣的小伙伴可以尝试一下。

相关推荐:

用javascript实现web拼图游戏

H5的canvas实现贪吃蛇小游戏

以上就是使用javascript中canvas实现拼图小游戏的详细内容,更多请关注其它相关文章!

更多技巧请《转发 + 关注》哦!

、canvas简介

  1. canvas是HTML5提供的一种新标签,双标签;
  2. HTML5 canvas标签元素用于图形的绘制,通过脚本 (通常是JavaScript)来完成;
  3. canvas标签只是图形容器,必须使用脚本来绘制图形;

Canvas是一个矩形区域的画布,可以用JavaScript在上面绘画;


二、案例目标

我们今天的目标是使用HTML5画布技术制作一款拼图小游戏,要求将图像划分为3*3的9块方块并打乱排序,用户可以移动方块拼成完整图片。

效果如下所示:


三、程序流程

3.1 HTML静态页面布局

<div id="container">
            <!--页面标题-->
            <h3>HTML5画布综合项目之拼图游戏</h3>
            <!--水平线-->
            <hr />
            <!--游戏内容-->
            <!--游戏时间-->        
            <div id="timeBox">
                共计时间:<span id="time">00:00:00</span>
            </div>
            <!--游戏画布-->
            <canvas id="myCanvas" width="300" height="300" style="border:1px solid">
                对不起,您的浏览器不支持HTML5画布API。
            </canvas>
            <!--游戏按钮-->
            <div>
                <button onclick="restartGame()">
                    重新开始
                </button>
            </div>  
</div>

效果如下所示:

我们可以看到页面的大致结构是已经显现出来了,就是骨架已经搭建好了,现在我们要使用css强化样式;


3.2 CSS打造页面样式

整体背景设置

body {
    background-color: silver;/*设置页面背景颜色为银色*/
}

游戏界面样式设置

#container {
    background-color: white;
    width: 600px;   
    margin: auto;
    padding: 20px;
    text-align: center; 
    box-shadow: 10px 10px 15px black;
}

游戏时间面板样式设置

#timeBox {
    margin: 10px 0;
    font-size: 18px;
}

游戏按钮样式设置

button {
    width: 200px;
    height: 50px;
    margin: 10px 0;
    border: 0;
    outline: none;
    font-size: 25px;
    font-weight: bold;
    color: white;  
    background-color: lightcoral;
}

鼠标悬浮时的按钮样式设置

button:hover {
    background-color: coral;
}

设置好界面整体样式之后我们得到完整的界面,如下所示:

可以看到整体的静态界面已经搭建出来了


3.3 js构建交互效果

3.3.1 对象的获取以及图片的设置

目标对象的获取

var c=document.getElementById('myCanvas'); //获取画布对象
var ctx=c.getContext('2d'); //获取2D的context对象

声明拼图的图片素材来源

var img=new Image();
img.src="image/pintu.jpg";
                
img.onload=function() { //当图片加载完毕时
    generateNum(); //打乱拼图的位置
    drawCanvas(); //在画布上绘制拼图
}

3.3.2 初始化拼图

  • 需要将素材图片分割成3行3列的9个小方块,并打乱顺序放置在画布上;
  • 为了在游戏过程中便于查找当前的区域该显示图片中的哪一个方块,首先为原图片上的9个小方块区域进行编号;


定义初始方块位置

var num=[[00, 01, 02], [10, 11, 12], [20, 21, 22]];


打乱拼图的位置

function generateNum() { //循环50次进行拼图打乱    
         for (var i=0; i < 50; i++) {
      //随机抽取其中一个数据
            var i1=Math.round(Math.random() * 2);
            var j1=Math.round(Math.random() * 2);
      //再随机抽取其中一个数据
            var i2=Math.round(Math.random() * 2);
            var j2=Math.round(Math.random() * 2);
      //对调它们的位置
            var temp=num[i1][j1];
            num[i1][j1]=num[i2][j2];
            num[i2][j2]=temp;
   }
}


绘制拼图

自定义名称的drawCanvas()方法用于在画布上绘制乱序后的图片;

function drawCanvas() {
    //清空画布
    ctx.clearRect(0, 0, 300, 300);
    //使用双重for循环绘制3x3的拼图
    for (var i=0; i < 3; i++) {
        for (var j=0; j < 3; j++) {
            if (num[i][j] !=22) {
                //获取数值的十位数,即第几行
                var row=parseInt(num[i][j] / 10);
                //获取数组的个位数,即第几列
                var col=num[i][j] % 10;
                //在画布的相关位置上绘图
                ctx.drawImage(img, col * w, row * w, w, w, j * w, i * w, w, w); // w:300 / 3=100(小图宽度)
            }
        }
    }
}

如下所示:

3.3.3 事件绑定

监听鼠标监听事件

c.onmousedown=function(e) {
    var bound=c.getBoundingClientRect(); //获取画布边界
    
    var x=e.pageX - bound.left; //获取鼠标在画布上的坐标位置(x,y)
    var y=e.pageY - bound.top;


    var row=parseInt(y / w); //将x和y换算成几行几列
    var col=parseInt(x / w);


    
    if (num[row][col] !=22) { //如果当前点击的不是空白区域
        detectBox(row, col); //移动点击的方块
        drawCanvas(); //重新绘制画布
        var isWin=checkWin(); //检查游戏是否成功
        
        if (isWin) { //如果游戏成功
            clearInterval(timer); //清除计时器
            ctx.drawImage(img, 0, 0); //绘制完整图片
            ctx.font="bold 68px serif"; //设置字体为加粗、68号字,serif
            ctx.fillStyle="red"; //设置填充色为红色
            ctx.fillText("游戏成功!", 20, 150); //显示提示语句
        }
    }
}

点击方块移动

function detectBox(i, j) {
    //如果点击的方块不在最上面一行
    if (i > 0) {
        //检测空白区域是否在当前方块的正上方
        if (num[i-1][j]==22) {
            //交换空白区域与当前方块的位置
            num[i-1][j]=num[i][j];
            num[i][j]=22;
            return;
        }
    }
    //如果点击的方块不在最下面一行
    if (i < 2) {
        //检测空白区域是否在当前方块的正下方
        if (num[i+1][j]==22) {
            //交换空白区域与当前方块的位置
            num[i+1][j]=num[i][j];
            num[i][j]=22;
            return;
        }
    }
    //如果点击的方块不在最左边一列
    if (j > 0) {
        //检测空白区域是否在当前方块的左边
        if (num[i][j - 1]==22) {
            //交换空白区域与当前方块的位置
            num[i][j - 1]=num[i][j];
            num[i][j]=22;
            return;
        }
    }
    //如果点击的方块不在最右边一列
    if (j < 2) {
        //检测空白区域是否在当前方块的右边
        if (num[i][j + 1]==22) {
            //交换空白区域与当前方块的位置
            num[i][j + 1]=num[i][j];
            num[i][j]=22;
            return;
        }
    }
}


3.3.4 游戏计时

  • 自定义函数getCurrentTime()用于进行游戏计时;
  • function getCurrentTime() {
    s=parseInt(s);
    //将时分秒转换为整数以便进行自增或赋值
    m=parseInt(m);
    h=parseInt(h);
    s++;
    //每秒变量s先自增1

    if (s==60) {
    s=0;
    //如果秒已经达到60,则归0
    m++;
    //分钟自增1
    }
    if (m==60) {
    m=0;
    //如果分钟也达到60,则归0
    h++;
    //小时自增1
    }


    //修改时分秒的显示效果,使其保持两位数
    if (s < 10)
    s="0" + s;
    if (m < 10)
    m="0" + m;
    if (h < 10)
    h="0" + h;
    time.innerHTML=h + ":" + m + ":" + s;
    //将当前计时的时间显示在页面上
    }
  • 在JavaScript中使用setInterval()方法每隔1秒钟调用getCurrentTime()方法一次,以实现更新效果;var timer=setInterval("getCurrentTime()", 1000);


3.3.5 游戏成功与重新开始

游戏成功判定与显示效果的实现

  • 自定义函数checkWin()用于进行游戏成功判断;
function restartGame() {
    clearInterval(timer);  //清除计时器
    s=0; //时间清零
    m=0;
    h=0;
    getCurrentTime();  //重新显示时间
    timer=setInterval("getCurrentTime()", 1000);
 
    generateNum(); //重新打乱拼图顺序
    drawCanvas(); //绘制拼图
    
}
  • 如果成功则使用clearInterval()方法清除计时器。然后在画布上绘制完整图片,并使用fillText()方法绘制出“游戏成功”的文字图样;if (isWin) { //如果游戏成功
    clearInterval(timer);
    //清除计时器
    ctx.drawImage(img, 0, 0);
    //绘制完整图片
    ctx.font="bold 68px serif";
    //设置字体为加粗、68号字,serif
    ctx.fillStyle="red";
    //设置填充色为红色
    ctx.fillText("游戏成功!", 20, 150);
    //显示提示语句
    }

3.4 最终效果演示

静态效果如上所示,至于游戏成功这里伙计们可以自行操作;


四、总结

本次案例我们使用HTML5的新特性canvas画布标签打造了简单的9宫格拼图游戏,总体来说没有特别的复杂,主要是图片的分割方块移动事件的绑定,以及重新游戏的初始化操作,明确了游戏逻辑之后其实代码的编写其实不难。感兴趣的小伙伴可以去尝试一下。

TML5 CSS3 3D魔方拼图在线益智小游戏网页开发。一款基于HTML5和CSS3的3D立方体拼图应用,一共有8个小立方体组成的3D拼图,我们可以点击立方体或者方向键完成拼图,同时我们也可以让立方体保持旋转。采用响应式设计,自适应手机移动端,用户体验友好。

ps:推荐一下我的微细公众号:webqiand,学习前端有不懂的(学习方法,学习路线,如何学习有效率的问题)可以关一下,公众号有不错的学习教程,开发工具、电子书籍分享。

部分源码展示,由于代码有点多,超出头条字数,发布不了

<!DOCTYPE html>
<html lang="en">
 
 <head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>HTML5 CSS3 3D魔方拼图在线益智小游戏网页开发</title>
 <meta name="keywords" content="HTML5,CSS3,3D魔方,拼图,在线益智小游戏,网页开发" />
 <meta name="description" content="HTML5 CSS3 3D魔方拼图在线益智小游戏网页开发。一款基于HTML5和CSS3的3D立方体拼图应用,一共有8个小立方体组成的3D拼图,我们可以点击立方体或者方向键完成拼图,同时我们也可以让立方体保持旋转。采用响应式设计,自适应手机移动端,用户体验友好。" /> 
 <link rel='stylesheet prefetch' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
 <link rel="stylesheet" href="css/style.css">
 </head>
 
 <body>
 <div class="body-wrapper">
 <div class="cubetwo-help-component">
 <div class="cubetwo-row">
 <div class="cubetwo-device-info">
 <i class="material-icons">
 touch_app
 </i>
 <div>
 tap or swipe with fingers
 </div>
 </div>
 <div class="cubetwo-device-info">
 <i class="material-icons">
 mouse
 </i>
 <div>
 click or swipe with mouse
 </div>
 </div>
 <div class="cubetwo-device-info cubetwo-device-info--keyboard">
 <i class="material-icons">
 keyboard
 </i>
 <div>
 keyboard keys
 </div>
 <div class="cubetwo-device-info-groups">
 <div class="cubetwo-device-info-group">
 <div>
 <i class="material-icons">
 keyboard_tab
 </i>
 <i class="material-icons">
 keyboard_arrow_up
 </i>
 </div>
 <div>
 <i class="material-icons">
 keyboard_arrow_left
 </i>
 <i class="material-icons">
 keyboard_arrow_down
 </i>
 <i class="material-icons">
 keyboard_arrow_right
 </i>
 </div>
 </div>
 <div class="cubetwo-device-info-group">
 <div>
 <span>
 q
 </span>
 <span>
 w
 </span>
 <span>
 e
 </span>
 </div>
 <div>
 <span>
 a
 </span>
 <span>
 s
 </span>
 <span>
 d
 </span>
 </div>
 <div>
 <span>
 x
 </span>
 <span>
 y
 </span>
 <span>
 z
 </span>
 </div>
 </div>
 </div>
 </div>
 </div>
 <a href="https://github.com/kunukn/cube-two" target="_blank" class="cubetwo-github-link">
 github project
 </a>
 </div>
 <div class="cubetwo-menu-component">
 <div class="cubetwo-row">
 <button class="cubetwo-js cubetwo-btn cubetwo-btn-scramble">
 scramble
 </button>
 <button class="cubetwo-js cubetwo-btn cubetwo-btn-spin">
 spin
 </button>
 <button class="cubetwo-js cubetwo-btn cubetwo-btn-solve">
 solve
 </button>
 </div>
 </div>
 <div class="cubetwo-component" id="cubetwo-component-1">
 <div class="cubetwo-rotation-view">
 <div class="cubetwo-cube-group cubetwo-cube-group--1">
 <div class="cubetwo-cube-1" tabindex="0" data-type="cubetwo">
 <div class="cubetwo-cube" data-type="cubetwo-display" data-index="1">
 <div data-type="front">
 <div>
 front
 </div>
 </div>
 <div data-type="up">
 <div>
 up
 </div>
 </div>
 <div data-type="right">
 <div>
 right
 </div>
 </div>
 <div data-type="back">
 <div>
 back
 </div>
 </div>
 <div data-type="down">
 <div>
 down
 </div>
 </div>
 <div data-type="left">
 <div>
 left
 </div>
 </div>
 </div>
 <div class="cubetwo-cube" data-type="cubetwo-touch">
 <div data-type="front">
 touch front
 </div>
 <div data-type="up">
 touch up
 </div>
 <div data-type="left">
 touch left
 </div>
 </div>
 </div>
 <div class="cubetwo-cube-2" tabindex="0" data-type="cubetwo">
 <div class="cubetwo-cube" data-type="cubetwo-display" data-index="2">
 <div data-type="front">
 <div>
 front
 </div>
 </div>
 <div data-type="up">
 <div>
 up
 </div>
 </div>
 <div data-type="right">
 <div>
 right
 </div>
 </div>
 <div data-type="back">
 <div>
 back
 </div>
 </div>
 <div data-type="down">
 <div>
 down
 </div>
 </div>
 <div data-type="left">
 <div>
 left
 </div>
 </div>
 </div>
 <div class="cubetwo-cube" data-type="cubetwo-touch">
 <div data-type="front">
 touch front
 </div>
 <div data-type="up">
 touch up
 </div>
 <div data-type="right">
 touch right
 </div>
 </div>
 </div>

代码运行效果截图:

需要这个项目css、js代码、图片的可以找我免费领取。如果大家不怕麻烦可以关注我后私信我“前端学习资料”几个字 找我领取 24小时在线!