前言
最近看到一篇博客,实现了拼图游戏并分享了源码。我感觉很好玩,就拿来改了改,实现了一个自己的版本。核心逻辑没有变,主要是优化了样式和交互,增加了些游戏的趣味性。
原文代码实现是用原生 js 面向对象的方式写的,这让习惯了使用 vue 语法糖的我,再次感到了用原生 js 好麻烦。好在,最近也是在重温原生js,打算再把基础学扎实一点,所以我遵循了原作者,也先用原生 js 做优化,后面还是打算改写为 vue 版本,是为了可以打包为移动端的 App,这样就可以在手机上玩了,哈哈。想拼什么图,就拼什么图,完美。
参考博文
https://blog.csdn.net/dkm123456/article/details/111991734#comments_15924279
拼图逻辑分析
1、一张图分割成几块,怎么分割?
定义构造函数,初始化数据。
function Jigsaw(row,boxWidth){
this.row=row;
this.itemWidth= boxWidth/this.row;
this.fragment=[];//拼图碎片的dom数组
this.originalKeys=[];//拼图碎片的下标,记录最初的正确顺序,方便后面对照拼图是否正确完成。
this.keys=[];//拼图碎片的下标,游戏开始时会被打乱顺序。
this.len=this.row*this.row;
this.init();
}
拼图的尺寸是固定的,难度系数,确定了一张图分割为横纵的多少块,这样每块的宽高也就可以计算了。
分割使用背景图 background 属性,定位呈现整张图的固定部分区域。
background-position 属性设置背景图像的起始位置。
以 900px 的拼图大小分割为 3*3 的拼图碎片为例
(x , y)
x 随着每次换行 归零
y 随着每次换行 自增
游戏初始化
//初始化
init:function(){
var fragment=dom.createDocumentFragment();
var url = imgView.src;
for(var i=0;i<this.len;i++){
var div=dom.createElement('div');
div.style.cssText=`
background:url(${url}) no-repeat -${(i%this.row)*this.itemWidth}px -${Math.floor(i/this.row)*this.itemWidth}px;
height:${this.itemWidth}px;
width:${this.itemWidth}px;
`;
this.fragment.push(div);//每个拼图碎片,都对应唯一的keys[i] 和 originalKeys[i]
this.keys.push(i);
this.originalKeys.push(i);
fragment.appendChild(div);
}
box.innerHTML="";
box.appendChild(fragment);
},
完整的拼图就是多个应用了背景图属性的 div 组合而成的。
2、随机位置实现
随机位置实现,首先要打乱拼图碎片的下标数组。
arrayObject.sort(sortby)
排序函数的巧妙使用,用来打乱拼图碎片的下标数组。
sort 一般用于给乱序的数组排序,这里反其道用之,给正序的数组打乱顺序。参数 a b是数组中的元素。新的排序 依据 排序函数的返回值 来决定 a 和 b 在数组中的前后位置。
this.keys.sort(function(a,b){
console.log('a = '+a+' , b = '+b)
//return a-b
//return Math.random()>0.5?1:-1;
})
乱序
this.keys.sort(function(a,b){
console.log("a = "+a+' , b = '+b)
return Math.random()>0.5?1:-1;
})
经测试,这里 sort()不传参数 a,b 也是可以的,应该是sort()方法在源码实现时,做了默认参数处理。
根据拼图碎片的随机下标,获取拼图碎片的随机 dom ,然后依次给拼图碎片应用绝对定位。
以 900px 的拼图大小分割为 3*3 的拼图碎片为例(原理同上使用背景图定位,所以后面代码实现也可以同上优化)
(x , y)
x 随着每次换行 归零
y 随着每次换行 自增
最外层相对定位,拼图碎片绝对定位。
this.item 是存储的拼图碎片 element 数组
也是在此时,给每个拼图碎片绑定鼠标事件。
start:function(){
//随机位置
this.keys.sort(function(a,b){
return Math.random()>0.5?1:-1;
})
var keys = this.keys;//随机打乱的拼图碎片下标
var colNum=0;
var rowNum=0;
box.innerHTML="";
for(var i=0;i<keys.length;i++){
if(i>0){
if(i%this.row===0){
rowNum++;
colNum=0;
}
}
var item = this.fragment[keys[i]];
item.style.position='absolute';
item.style.left=`${colNum++*this.itemWidth}px`;
item.style.top=`${rowNum*this.itemWidth}px`;
item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 index
item.key=keys[i];//key::item数组元素在 keys 数组中对应的值
this.drag(item);
box.appendChild(item);
}
},
start 方法 可以优化
start:function(){
//随机位置
this.keys.sort(function(a,b){
return Math.random()>0.5?1:-1;
})
var keys = this.keys;//随机打乱的拼图碎片下标
box.innerHTML="";
//js % 模运算
//余数指整数除法中被除数未被除尽部分,且余数的取值范围为0到除数之间(不包括除数)的整数。 [1] 例如:27除以6,商数为4,余数为3。
//一个数除以另一个数,要是比另一个数小的话,商为0,余数就是它自己。 [1] 例如:1除以2,商数为0,余数为1;2除以3,商数为0,余数为2。
for(var i=0;i<keys.length;i++){
var item = this.fragment[keys[i]];
item.style.position='absolute';
item.style.left=`${(i%this.row)*this.itemWidth}px`;
item.style.top=`${Math.floor(i/this.row)*this.itemWidth}px`;
item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 index
item.key=keys[i];//key::item数组元素在 keys 数组中对应的值
this.drag(item);
box.appendChild(item);
}
},
3、鼠标右键的点击问题
拼图交互,设置为鼠标左键选中,右键取消选中。
鼠标右键和系统事件冲突。解决方法是自定义鼠标右键 oncontextmenu 事件。
自定义鼠标右键 oncontextmenu 事件
//设置拖动
drag:function(item){
var me=this;//构造函数 Jigsaw
item.onmousedown=function(e){
var e = e||window.event;
//this 当前点击的拼图碎片
var target = me.findTarget(this);
if(e.button===0){//左键(键值0)
if(target){
//前面已有一块拼图碎片,处于已点击 active 状态
me.exchange(this,target);
//单纯点击不增加步数,只有交换后才增加步数。
//重点在于 在 findTarget 方法中,排除两次点击相同的情况
steps++;
stepMsg.innerHTML=steps;
}else{
this.moveFlag=true;
this.className='active';
}
}
e.preventDefault && e.preventDefault();
}
//右键(键值2)取消拼图选中
//oncontextmenu 事件在元素中用户右击鼠标时触发并打开上下文菜单。
//注意:所有浏览器都支持 oncontextmenu 事件, contextmenu 元素只有 Firefox 浏览器支持。
item.oncontextmenu = function(e) {
var e = e||window.event;
//findTarget 传参 undefined 避开同次点击判断的限制
//取消 active 一般都是同拼图碎片 点击
var target = me.findTarget();
if(target){
target.className='';
target.moveFlag=false;
}
e.preventDefault && e.preventDefault();
}
}
4、两块拼图位置交换
exchange:function(from,target){
var fromLeft = from.style.left;
var fromTop = from.style.top;
var fromPos = from.pos;
var fromKey = from.key;
var targetLeft = target.style.left;
var targetTop = target.style.top;
var targetPos = target.pos;//pos:item数组元素在 keys 数组中对应的下标 index
var targetKey = target.key;//key::item数组元素在 keys 数组中对应的值
from.style.left=targetLeft;
from.style.top=targetTop;
from.pos=targetPos;
target.style.left=fromLeft;
target.style.top=fromTop;
target.pos=fromPos;
//交换过后取消 active
target.className='';
target.moveFlag=false;
this.keys.splice(fromPos,1,targetKey);
this.keys.splice(targetPos,1,fromKey);
//如果相等表示已经找好了
if(this.diff(this.originalKeys,this.keys)){
this.tips();
gameMsg.innerHTML="太棒了,成功了!";
//关闭计时器
clearInterval(timer);
}else{
//提示未完成,继续加油
//随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)
var i=Math.floor(Math.random()*10);//0~9
gameMsg.innerHTML=gameMsgArr[i]
}
},
exchange 方法可以优化
//交换两个div位置 既要交换视图上的位置,也要交换在 keys数组 中的位置。
exchange:function(from,target){
//解构语法交换两个变量的值
[from.style.left,target.style.left]=[target.style.left,from.style.left];
[from.style.top,target.style.top]=[target.style.top,from.style.top];
[from.pos,target.pos]=[target.pos,from.pos];
//交换过后取消 active
target.className='';
target.moveFlag=false;
//交换它们在this.keys中的位置
this.keys.splice(target.pos,1,target.key);
this.keys.splice(from.pos,1,from.key);
//如果相等表示已经找好了
if(this.diff(this.originalKeys,this.keys)){
this.tips();
gameMsg.innerHTML="太棒了,成功了!";
//关闭计时器
clearInterval(timer);
}else{
//提示未完成,继续加油
//随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)
var i=Math.floor(Math.random()*10);//0~9
gameMsg.innerHTML=gameMsgArr[i]
}
},
5、是否完成拼图判断。
//比较 this.originalKeys , this.keys 两个数组,如果一模一样 证明顺序对了
diff:function(a,b){
var me=this;
//Array.isArray() 用于确定传递的值是否是一个 Array
var isArrayA = Array.isArray(a);
var isArrayB = Array.isArray(b);
if (isArrayA && isArrayB) {//如果都是数组
return a.every(function (item, index) {//用every和递归来比对a数组和b数组的每个元素,并返回
return me.diff(item, b[index]);
})
}else{
return String(a) === String(b)
}
},
6、总结
相较原文
1、增加了步数统计(有点问题待优化,单纯点击鼠标不应该增加步数)。
2、增加了文字提示(只要拼图没完成,就会随机文字提示,如果有音效就更棒了)。
3、增加了耗时统计,这样就知道拼图的快慢了。
4、顺序图片,改为随机图片,也是为了好玩,图库里的图片越多越好玩,有的简单,有的复杂。
5、难度设置由纵横两个维度,统一为一个维度。这样是为了保证方形的拼图,因为我觉得方形的图片好看。也是为了简化代码逻辑和视图呈现的好看。
6、原文拼图默认宽高是 600*600,我觉得有点小,改成了 900*900。所以,准备的拼图都是900*900的方图。如果是其他尺寸的图片,则要么拼不全,要么有空白。
7、增加了拼图的分割线。
8、部分代码优化。
我的实现
界面预览
源码分享
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;">
<style>
div.game{
display: flex;
flex-direction: row;
justify-content: center;
}
div.left{
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 50px;
background-color: #f7f7f7;
margin-right:10px;
padding:20px;
}
/* 预览图片 */
.imgView{
width: 300px;
box-sizing: content-box;
border: 8px solid #bc6e42;
}
.input{
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
width: 234px;
}
div.btns{
width: 100%;
text-align: center;
border-top:1px solid #999;
margin-top:30px;
padding:10px 0;
}
button{
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0 10px;
transition: .1s;
font-weight: 500;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
}
div.right{
background-color: #f7f7f7;
padding:20px;
margin-left:10px;
}
div.message{
display: flex;
flex-direction: row;
justify-content: space-around;
text-align: center;
line-height: 60px;
font-weight: bold;
}
div.message div{
margin:0 10px;
}
div.message img{
width:24px;
}
div.message span{
padding:0 16px;
color:#F56200;
}
/* 拼图盒子 */
#box{
width: 900px;
height: 900px;
box-sizing: content-box;
position: relative;
}
#box div{
box-sizing: border-box;
border: 1px dotted #fff;
float:left;
cursor:move;
}
/* 消息盒子 */
#box p{
box-sizing: border-box;
line-height:900px;
text-align:center;
font-size:60px;
color:#F56200;
margin:0;
}
.active{
border:8px solid #bc6e42 !important;
}
</style>
</head>
<body>
<div class="container">
<div class="game">
<div class="left">
<div>随机图片:<span id="imgNum">图1</span></div>
<div><img class="imgView" src='./images/1.jpg'/></div>
<div>难度系数:<input type='number' id='difficulty' class='input' min=3 max=9 value='3' onchange="gameInit()"></div>
<div class="btns">
<button onclick='imgChange()'>换一张图</button>
<button onclick='startGame()'>开始游戏</button>
</div>
</div>
<div class="right">
<div class="message">
<div><img src="./images/step.png" alt="img"><span id='stepMsg'>0</span></div>
<div><img src="./images/msg.png" alt="img"><span id='gameMsg'>无</span></div>
<div><img src="./images/time.png" alt="img"><span id='timeMsg'>0</span></div>
</div>
<div id='box'></div>
</div>
</div>
</div>
</body>
<script>
//全局变量
var isFirst=true;//页面第一次加载
var dom=document;
var timer;
var timeMsg=dom.getElementById("timeMsg");
var stepMsg = dom.getElementById("stepMsg");
var gameMsg = dom.getElementById("gameMsg");
var gameMsgArr=[
'继续加油!',
'保持冷静!',
'马上就要完成了!',
'快成功了!',
'还差一点儿!',
'加油啊',
'继续保持',
'行不行啊',
'吁。。。',
'快点快点'
];
var box=dom.getElementById("box");
var imgView=dom.getElementsByClassName("imgView")[0];
var game;
var steps=0;
function Jigsaw(row,boxWidth){
this.row=row;
this.itemWidth= boxWidth/this.row;
this.fragment=[];//拼图碎片的dom数组
this.originalKeys=[];//拼图碎片的下标,记录最初的正确顺序,方便后面对照拼图是否正确完成。
this.keys=[];//拼图碎片的下标,游戏开始时会被打乱顺序。
this.len=this.row*this.row;
this.init();
}
Jigsaw.prototype={
//初始化
init:function(){
var fragment=dom.createDocumentFragment();
var url = imgView.src;
for(var i=0;i<this.len;i++){
var div=dom.createElement('div');
div.style.cssText=`
background:url(${url}) no-repeat -${(i%this.row)*this.itemWidth}px -${Math.floor(i/this.row)*this.itemWidth}px;
height:${this.itemWidth}px;
width:${this.itemWidth}px;
`;
this.fragment.push(div);//每个拼图碎片,都对应唯一的keys[i] 和 originalKeys[i]
this.keys.push(i);
this.originalKeys.push(i);
fragment.appendChild(div);
}
box.innerHTML="";
box.appendChild(fragment);
},
start:function(){
//随机位置
//sort() 方法用于对数组的元素进行排序。
//返回值 对数组的引用。请注意,数组在原数组上进行排序,不生成副本。
//如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。
//如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:
// return a-b
// 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
// 若 a 等于 b,则返回 0。
// 若 a 大于 b,则返回一个大于 0 的值。
// 这里没有用 return a-b ,用的是 return Math.random()>0.5?1:-1; 则 传入的 a,b 根据这个结果排序。
this.keys.sort(function(a,b){
return Math.random()>0.5?1:-1;
})
var keys = this.keys;//随机打乱的拼图碎片下标
box.innerHTML="";
//js % 模运算
//余数指整数除法中被除数未被除尽部分,且余数的取值范围为0到除数之间(不包括除数)的整数。 [1] 例如:27除以6,商数为4,余数为3。
//一个数除以另一个数,要是比另一个数小的话,商为0,余数就是它自己。 [1] 例如:1除以2,商数为0,余数为1;2除以3,商数为0,余数为2。
for(var i=0;i<keys.length;i++){
var item = this.fragment[keys[i]];
item.style.position='absolute';
item.style.left=`${(i%this.row)*this.itemWidth}px`;
item.style.top=`${Math.floor(i/this.row)*this.itemWidth}px`;
item.pos=i;//pos:item数组元素在 keys 数组中对应的下标 index
item.key=keys[i];//key::item数组元素在 keys 数组中对应的值
this.drag(item);
box.appendChild(item);
}
},
//寻找 active moveFlag = true
findTarget:function(current){
//从列表中查找是否已经有选择的目标了
for(var i=0;i<this.fragment.length;i++){
//排除两次点击的一样
if(current!==this.fragment[i]&&this.fragment[i].moveFlag){
return this.fragment[i];
}
}
// findTarget 使用 find 始终返回 undefined ,猜测可能是因为 this.item 数组元素为 element dom 对象,所以不能用 find 方法。
// find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。
// find() 方法为数组中的每个元素都调用一次函数执行:
// 当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。
// 如果没有符合条件的元素返回 undefined
// this.fragment.find(item=>{
// return current!==item&&item.moveFlag;
// })
},
//交换两个div位置 既要交换视图上的位置,也要交换在 keys数组 中的位置。
exchange:function(from,target){
//解构语法交换两个变量的值
[from.style.left,target.style.left]=[target.style.left,from.style.left];
[from.style.top,target.style.top]=[target.style.top,from.style.top];
[from.pos,target.pos]=[target.pos,from.pos];
//交换过后取消 active
target.className='';
target.moveFlag=false;
//交换它们在this.keys中的位置
//arrayObject.splice(index,howmany,item1,.....,itemX)
//返回值:由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。
//index 必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
// howmany 必需。要删除的项目数量。如果设置为 0,则不会删除项目。
// item1, ..., itemX 可选。向数组添加的新项目。
this.keys.splice(target.pos,1,target.key);
this.keys.splice(from.pos,1,from.key);
//如果相等表示已经找好了
if(this.diff(this.originalKeys,this.keys)){
this.tips();
gameMsg.innerHTML="太棒了,成功了!";
//关闭计时器
clearInterval(timer);
}else{
//提示未完成,继续加油
//随机数在范围从0到小于1,也就是说,从0(包括0)往上,但是不包括1(排除1)
var i=Math.floor(Math.random()*10);//0~9
gameMsg.innerHTML=gameMsgArr[i]
}
},
//完成后倒计时的提示
tips:function(){
setTimeout(function(){
var suc=dom.createElement('p');
suc.innerHTML='Bingo!';
box.innerHTML="";
box.appendChild(suc);
},500)
},
//比较 this.originalKeys , this.keys 两个数组,如果一模一样 证明顺序对了
diff:function(a,b){
var me=this;
//Array.isArray() 用于确定传递的值是否是一个 Array
var isArrayA = Array.isArray(a);
var isArrayB = Array.isArray(b);
if (isArrayA && isArrayB) {//如果都是数组
return a.every(function (item, index) {//用every和递归来比对a数组和b数组的每个元素,并返回
return me.diff(item, b[index]);
})
}else{
return String(a) === String(b)
}
},
//设置拖动
drag:function(item){
var me=this;//构造函数 Jigsaw
item.onmousedown=function(e){
var e = e||window.event;
//this 当前点击的拼图碎片
var target = me.findTarget(this);
if(e.button===0){//左键(键值0)
if(target){
//前面已有一块拼图碎片,处于已点击 active 状态
me.exchange(this,target);
//单纯点击不增加步数,只有交换后才增加步数。
//重点在于 在 findTarget 方法中,排除两次点击相同的情况
steps++;
stepMsg.innerHTML=steps;
}else{
this.moveFlag=true;
this.className='active';
}
}
e.preventDefault && e.preventDefault();
}
//右键(键值2)取消拼图选中
//oncontextmenu 事件在元素中用户右击鼠标时触发并打开上下文菜单。
//注意:所有浏览器都支持 oncontextmenu 事件, contextmenu 元素只有 Firefox 浏览器支持。
item.oncontextmenu = function(e) {
var e = e||window.event;
//findTarget 传参 undefined 避开同次点击判断的限制
//取消 active 一般都是同拼图碎片 点击
var target = me.findTarget();
if(target){
target.className='';
target.moveFlag=false;
}
e.preventDefault && e.preventDefault();
}
}
}
//页面初始化
function gameInit(){
//关闭计时器
if(!timer){
timer=null;
}else{
//关闭计时器
clearInterval(timer);
}
if(isFirst){//只执行一次 只在页面第一次初始化时执行
isFirst=false;
//随机的一张图
var index=Math.floor(Math.random()*10)+1;
imgView.src='./images/'+index+'.jpg';
var imgNum=dom.getElementById("imgNum");
imgNum.innerHTML="图"+index;
}
steps=0;
stepMsg.innerHTML=0;
timeMsg.innerHTML=0;
gameMsg.innerHTML='无';
var difficulty=dom.getElementById("difficulty").value;
game = new Jigsaw(difficulty,900);
}
//开始游戏
function startGame(){
game.start();
timeStart();
}
//随机的一张图
function imgChange(){
isFirst=true;
gameInit();
}
function timeStart(){
//防止多次点击开始游戏启动多个计时器
//关闭计时器
if(!timer){
timer=null;
}else{
//关闭计时器
clearInterval(timer);
}
var time=0;
timer=setInterval(function(){
timeMsg.innerHTML=time++;
})
}
//页面初始化
gameInit();
</script>
</html>