Puzzle Game:Vue实现一个拼图游戏

前言

  会作这个 Puzzle Game,仍是应前几天 lightyears 的一次提议,模仿的是鹰脚网络首页左下角那个拼图小游戏。那天晚上睡觉的时候在床上想了一下,大体 get 到了它内部实现的原理,因而就干脆动手实践一番,如今也顺道写一篇博客记录下实现思路和中间遇到的一些问题。 css

实现

介绍

  Puzzle Game 的游戏过程为:用户上传图片后选择要分割的碎片数量,一颗星表明 2 * 2 = 4 个碎片, 两颗星表明 3 * 3 = 9 个碎片,以此类推八颗星则是 81 个碎片。经过拖拽碎片进行拼图,当每一个碎片和其相邻的碎片间隔都不超过阈值时,则提示拼图成功。html

  传送门, Go to play?vue

  GitHub,喜欢的话就给个 star 鼓励鼓励吧😊。git

首先要解决的两个难点

  Puzzle Game 的难点主要是两个:一是如何对上传的图片进行切割成各个碎片,二是如何判断是否拼图成功。鹰脚网络那个拼图由于是固定的,就只有那一张图片和四个图片碎片,因此大能够事先把图片分割成 4 块碎片再写进代码里。但 Puzzle Game 使用的图片和碎片数量取决于用户选择,因此就得另辟蹊径了。github

  这里我利用到的是精灵图,根据用户选择的碎片数量,生成 N 个<div>(表明每个碎片),将用户上传的图片设置为每个<div>的背景图片,再使用background-position把每个碎片都定位到图片相应的位置,这样就能够实现把图片切割成各个碎片啦。Puzzle Game 仍是挺简单的,每个碎片都是等大的,并且是 n * n 的碎片数量,相对容易实现不少。不过即便是 n * m 也是同样的,只要改变一下碎片的宽高和background-position的定位就能够了,问题不大。但若是每个碎片都不等大的话就麻烦了,目前没想到实现思路。api

  至于判断拼图是否成功,我想到的办法是:先设置好一个阈值,在每次拖拽完毕后就遍历每个碎片,判断它和相邻碎片(上下左右四个碎片,对于边界碎片再行判断)之间的间隔是否不超过这个阈值。若是每个碎片都不超过,则拼图成功;如有一个碎片和相邻某一个碎片之间的间隔超过了阈值,则直接结束判断过程。此处判断碎片间的距离使用的是element.getBoundingClientRect(),四个属性值leftrighttopbottom统一是相对于浏览器视口来计算的。(具体代码实现能够看这里浏览器

遇到的一些问题

拖拽后计算碎片的位置

  拖拽碎片进行移动使用到的 api 有三个:mousedownmousemovemouseup(移动端则是对应的touchstarttouchmovetouchend。不过 Puzzle Game 没有适应移动端,由于我以为经过拖拽来实现拼图并且碎片数量仍是不肯定的,这须要大屏幕才方便操做,移动端屏幕过小不适合)。在mousemove的过程当中实时更新碎片的位置,计算方法有两个:缓存

  1. 鼠标拖拽过程当中移动的距离 + 碎片原先离父元素的左 / 上边距
let x = e.clientX - startX + px;
let y = e.clientY - startY + py;
复制代码

  其中e.clientX是鼠标松开时鼠标的 x 坐标,startX 是一开始鼠标按下时的 x 坐标,px 是碎片原先离父元素的左边距,由targetEle.offsetLeft获得。 2. 鼠标松开时的 x / y 坐标 - 碎片自身宽 / 高的一半bash

let x = e.clientX - targetEle.clientWidth / 2;
let y = e.clientY - targetEle.clientHeight / 2;
复制代码

  这两个计算方法自己没有问题,但和后面的targetEle.style.left = `${x}px`; targetEle.style.top = `${y}px`;合用的时候就产生了一个参照物的问题。方法一使用到的targetEle.offsetLefttargetEle.offsetTop是相对于它的offsetParent而言的,也就是它第一个设置有定位的父元素,若是它的父级元素都没有定位则为 body。而碎片设置的lefttop属性也是相对于它第一个设有定位的父级元素而言的,因此无论碎片的父元素如何定位(默认也好,绝对 / 相对 / 固定定位也罢),得出的 x 和 y 值以及lefttop都是相对于其父元素而言的,是统一的网络

  但使用方法二算出来的 x 和 y 是相对于浏览器视口左上角而言的,和lefttop的参照物可能不同,因此使用方法二有时候就会出现拖拽图片但图片却偏离到右下角去了的状况。所以要准确地使用方法二是有个前提条件的,就是碎片的父元素必须得是相对于浏览器左上角定位的。这和父元素是否设置了定位无关,由于若是父元素没有设置定位,那么父元素天然是相对于浏览器左上角来定位的(即便父元素前面已经有其余的元素了)。而若是父元素设置了定位,只要父元素位于浏览器左上角从而让碎片仍是相对于浏览器左上角定位那也是能够的,好比父元素设置了定位但lefttop为 0 而且前面没有其余的元素,或者父元素前面的元素都脱离了文档流。只有知足这几个条件,才能正确使用方法二,不然拖拽后图片的位置会出现异常,例以下面图三所示。

  接下来咱们先用代码和结果图进一步验证,看客能够戳这里查看具体代码自行验证,这里就只放效果图不放代码啦。

  • 父元素没有设置定位

  • 父元素设置了定位(relative / absolute / fixed 定位都行),但父元素位于浏览器左上角的位置(lefttop为 0 且前面没有元素,或是前面的元素都脱离了文档流)。

  • 父元素设置了定位,但设置了非零的lefttop,或是前面已有在文档流中占位的元素。

  考虑到使用方法二会有一些限制条件,因此仍是推荐使用方法一的好,比较健壮适应性也好。若是使用方法二的话,则要注意上述的这些坑点,省得跳坑里了。(咦,若是不跳一次坑哪来的这篇博客??)

拖拽速度过快

  在监听事件的时候,咱们一般都是把监听函数绑定到相应的目标元素上的,不多把监听函数绑定到documentbody上(监听页面滚动和利用事件委托等除外)。因此当我把mousedownmousemovemouseup这三个监听函数绑定到碎片上时,mousedown没什么问题,问题就出在了mousemovemouseup上。若是点击碎片后拖拽的速度过大,就会形成鼠标移动过快而碎片来不及响应移动。要知道,mousemove触发频率是很高的,相应的监听函数也会被高频率调用。这很容易形成碎片的移动速度跟不上鼠标的移动速度,结果就是鼠标移出了碎片后,即便鼠标没有松开但由于监听函数失去了目标因此碎片也不会再跟着移动。而由于鼠标松开后也没能触发相应的监听函数,因此此时标记鼠标移动是否开始的变量仍是为true,又形成了当鼠标移动到碎片上时即便没有按住鼠标碎片也仍是能够跟着鼠标移动。具体代码和效果能够戳这里

  我使用的解决办法很简单,直接把mousemovemouseup这两个监听函数绑定到document或者body上就好了。这样即便鼠标移动速度过快离开了碎片,也仍是能触发到相应的监听函数让碎片也跟着移动。这里还有一个注意点,计算获得 x 和 y 的值要修改碎片的lefttop属性时,不能使用e.target来获取碎片自身了。由于鼠标移动过快离开碎片后此时的e.target便成document了,因此**须要事先使用一个变量来缓存e.target**才行。

  接下来再说说把mousemovemouseup这两个监听函数绑定到documentbody的区别。document包括了整个浏览器视图,而body只包括了网页正文(脱离文档流的元素还不算在body的宽高上)。因此若是body没有达到整个浏览器视图的大小,那把监听函数绑定到body上和原先绑定在碎片上没有啥区别。若是body的宽高已经和浏览器视图同样大小了,好比手动设置为100vw100vh,此时绑定到documentbody上的区别在于二者对边界状况的处理不一样。绑定到document上时即便鼠标移到了页面正文外(好比浏览器的工具栏和桌面的任务栏)碎片也仍是能跟着继续移动,而绑定到body上若是鼠标移出了页面正文碎片就不会跟着移动了,只有鼠标再移回页面正文碎片的位置才会继续跟着响应。(若绑定到window或者html上则跟绑定到document上是同样的。)

  看效果图会更直观点,下图一是mousemovemouseup监听函数绑定在document上,下图二是绑定在body上(固然代码中可不能直接写body,得写成document.body才行)。

  不知道看客有没有想到一个问题,把mousemovemouseup的监听函数绑定到document上会不会形成事件触发频率太高的问题?毕竟监听对象从原来的几个碎片扩大成了整个document啊。不可避免地事件触发频率会高不少,但没办法,我想不出其余的解决方案啊,只能去尽可能避免过多触发到监听函数了。好比把mousemovemouseup的监听写在mousedown的监听函数里,这和前面使用一个变量标记鼠标移动是否开始差很少。但主要的是mouseup的监听函数里把document上的mousemovemouseup监听事件清除掉。好比document.onmousemove = null; document.onmouseup = null;。这样只有在点击碎片的时候才会监听事件而且点击完毕后就立刻清除掉了,触发监听函数的次数少了不少。

上传的图片过大

  若是用户上传的图片过大,甚至超过了浏览器视窗大小,那必然得对图片进行压缩后,不然图片都铺满了整个浏览器视窗还怎么进行拼图。这里我采用的方法也很简单,上传完图片后对图片的大小进行判断,若是超过了限定值(我设置的是浏览器宽度的一半)则将图片的宽度缩小为这个限定值,再根据原始的宽高比例和压缩后的图片宽度计算出压缩后的图片高度就好了。这里我还遇到两个小问题。

  1. 我使用imageEle.naturalWidthimageEle.naturalHeight来获取图片大小,但若是直接读取的话你会发现获得的图片宽高都是 0。这是由于 imageEle 的 src 属性是依赖于用户上传的图片来动态赋值的,须要等图片加载完成后才能获取到它的宽高,图片尚未加载完成就去获取获得的天然就是 0 了。解决方法是使用setTimeout(callback, 0)来异步获取图片宽高,若是想要更及时获取到图片宽高也能够开一个setInterval隔一段时间就去判断获取到的图片宽高是否非 0,是的话则表明图片已经加载完成就能够结束setInterval了。我原本觉得使用Vue.$nextTick在下次 DOM 更新时再获取图片宽高也是能够的,但发现不行,估计是下次 DOM 更新了图片也可能没有加载完毕吧,这点不是很清楚。

  2. 另外一个问题是,缩小图片尺寸后页面会有一瞬间图片从大变小的过程。解决方法也很简单,**事先给图片设置一个max-width**就好了,这个max-width也就是前面提到的限定值,这样图片就不会有一个大小的瞬间变化了。不过这得在 js 代码里去设置,在 css 里无法获取到浏览器宽高。

$refs 的获取

  由于要分割成的碎片数量取决于用户的选择,因此全部的碎片都是动态加载的。$refs不是响应式的,只能在组件渲染完成后才能获取到,全部动态加载的模板更新时$refs都没法相应地及时变化可见官方文档)。因此须要等 DOM 更新后才能获取到$refs,能够利用Vue.$nextTick(callback)实现。

对 background-position 理解出错

  说到这个就有些尴尬了,我以前觉得background-position的两个属性值 x 和 y 表明的是在这张图片上定位到(x, y)这个坐标的位置上再显示背景图片,结果致使使用background-position定位到的碎片显示的图片位置错位。又由于我总是觉得是我中间哪里出问题了因此一直没有想到是我本身对background-position理解出错上去(Orz),最后单独起了个 demo 实验才发现问题所在。原来background-position的两个属性值 x 和 y 表明的是将这张图片向右移动和向下移动指定的距离后再显示背景图片。和我以前理解的偏偏相反,并且由于我以前没有设置background-repeat: no-repeat因此若是只有四个碎片的话是不会表现出什么问题的。知道了background-position的真正意思后要解决就很容易了,只要把 x 和 y 都变成负值就能够啦。

后记

  Puzzle Game 写起来仍是挺简单的,主要是一时兴起练练手吧。中午去食堂的时候才想起来我在写一个Web Project的时候应该边写边记录的才对啊。像这个 Puzzle Game 就是边写代码的过程当中边三言两语记录下遇到的问题和解决方案,这样过后才能够总结成一篇博客,记录本身遇到的坑点和盲点。否则就跟以前写的cloud music同样,没有边写代码边记录遇到的问题,因此等写完代码后都差很少忘记中间遇到的不少问题了,写成的博客也就很空淡。好吧,吸取经验,下次就要记得正确的学习姿式了!

  花了三天的时间写完 Puzzle Game 和这篇博客,五一假期就只剩下这半天了啊。我仍是滚回去写个人数据结构实验了,挥挥。

相关文章
相关标签/搜索