运用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 = ""; //依据随机数打乱图片次序 (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:
浏览代码程序我们晓得,我们每生成一个切片,就会结构一个元素节点: newElement = "";
。我们在是在外部先声明了一个放新节点的数组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.onmou搜索引擎优化ver = 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.缓存有的时候也让人很疼痛,就比方说在游戏中有些会员就想要从新开端,而我们的小游戏只要在游戏完成之后才清空缓存,刷新页面,游戏才干够从新开端。这给会员的体验很欠好,我们可以加一个重置游戏按钮,清空缓存并优化游戏完毕后的一些逻辑。
这些功能感乐趣的小同伴可以尝试一下。