主要实现功能:
- 图片拖动后位置的变换效果
- 拼图成功进入更难一级游戏
- 游戏计时
哔哩哔哩演示
技术栈:
- jQuery、面向对象方式编程
实现原理:
-
在多个子元素上利用backgroundPosition来实现多张图片拼成一张完整的图片。
-
根据生成的子元素的数组索引来渲染出一张完整的图片,根据子元素数组的索引乱序来渲染出一张打乱的图片。(根据子元素的left和top值打乱)。
-
子元素发生拖拽,当鼠标在棋盘外,元素不交换位置,并回到原位置;当鼠标在棋盘内,将拖动的元素和当前鼠标位置上的元素的位置发生交换。
具体讲解:
-
初始化页面
- 根据传进来的行数与列数来生成一个棋盘,第一个for循环可以理解为生成行(i)的空间(没有元素生成),第二个for循环理解为每一行的(每一小列或者说每一个元素(j));然后保存当前子元素在父级上(oImgArea)的索引。(因为sort方法改变原数组,所以存了两个,一个是标准的,用于复原图片,一个是用于乱序的,用于打乱图片)。
// 初始化
init() {
randerGameTime();
this.cellHW = this.getCellWH();
let count = 0;
this.oImgArea.text('');
// 生成棋盘
for (let i = 0; i < this.tr; i++) {
for (let j = 0; j < this.td; j++) {
// 创建元素
let cell = $('<div class="imgCell"></div>');
// 根据行与列的关系生成棋盘,并移动每个子元素上背景图片的位置
$(cell).css({
'width': this.cellHW.W + 'px',
'height': this.cellHW.H + 'px',
'left': this.cellHW.W * j,
'top': this.cellHW.H * i,
'backgroundPosition': (-this.cellHW.W) * j + 'px ' + (-this.cellHW.H) * i + 'px',
});
// 将当前的这个子元素插入到页面中
this.oImgArea.append(cell);
// 保存当前子元素在父级上的索引
this.origArr.push(count);
this.randArr.push(count);
count++
}
}
// 调用开始游戏方法
this.startGame();
}
// 生成随机数
randomNum() {
this.randArr.sort(function () {
return Math.random() - 0.5;
})
// 如果检测到数组没有被打乱就再次调用打乱数组方法,但要限定数组长度较短时才去检测,数组较长检测会有调用栈溢出情况
if (this.randArr.toString() == this.origArr.toString() && this.tr == 2) {
this.randomNum();
}
}
-
游戏开始
- 实现点击按钮的变换,给所有子元素实现拖拽;在鼠标点击到子元素时,记录当前点击到的子元素的在标准数组里的索引;
- 鼠标抬起时,记录鼠标相对于父元素(oImgArea)的坐标,取消事件;
- 根据上面得到的坐标,调用获取拖拽运动的子元素在鼠标抬起时,鼠标该位置上子元素的在乱序数组里的索引
- 得到的索引与鼠标点击到子元素的索引相同,就直接调用子元素运动方法,让子元素运动到原来的位置上
- 得到的索引与鼠标点击到子元素的索引不同,就调用子元素位置交换方法
// 开始游戏
startGame() {
this.oImgCellArr = $('.imgCell');
let That = this;
// 给点击按钮绑定click事件
$('.start').on('click', () => {
// 点击按钮的切换效果
if (this.play) {
this.randomNum()
$('.start').text('复原').css('backgroundColor', '#0ff');
this.play = false;
// 以乱序数组来渲染子元素的位置
this.cellRander(this.randArr);
// 给每个子元素绑定事件
this.oImgCellArr.on('mousedown', function (e) {
// 获取当前点击到的元素在父级里的索引
let indexDown = $(this).index();
// 获取鼠标点击的位置到父级元素左顶点位置的距离(x, y)
let leftDown = e.pageX - That.oImgCellArr.eq(indexDown).offset().left;
let topDown = e.pageY - That.oImgCellArr.eq(indexDown).offset().top;
$(document).on('mousemove', function (event) {
That.oImgCellArr.eq(indexDown).css({
'z-index': '40',
// 获取鼠标这一时刻的位置相较于上一时刻的位置的变化量
'left': event.pageX - leftDown - That.oImgArea.offset().left + 'px',
'top': event.pageY - topDown - That.oImgArea.offset().top + 'px'
})
}).on('mouseup', function (event) {
// 保存鼠标松开时该拖拽子元素的位置
let topUp = event.pageY - That.oImgArea.offset().top;
let leftUp = event.pageX - That.oImgArea.offset().left;
// 获取拖拽运动的元素在鼠标抬起时,该位置上元素的在乱序数组里的索引
let indexUp = That.getIndexUp(leftUp, topUp, indexDown);
if (indexUp == indexDown) {
// 鼠标越界,子元素回弹
That.cellMove(That.randArr, indexDown, indexDown);
} else {
// 以修改乱序数组里的索引来达到让元素替换位置的效果
That.cellsPositionChange(indexDown, indexUp);
}
$(document).off('mousemove').off('mouseup');
})
})
} else {
$('.start').text('打乱').css('backgroundColor', '#f0f');
this.cellRander(this.origArr);
this.play = true;
$(this.oImgCellArr).off('mousemove').off('mouseup').off('mousedown');
}
})
}
-
获取拖拽运动的鼠标抬起时该位置上元素的乱序数组里的索引
- 鼠标的位置越出棋盘
- 直接返回拖拽的子元素在标准数组里的索引
- 鼠标的位置在棋盘里
- 根据鼠标当前相对于父级元素(oImgArea)左顶点为原点的坐标值求出鼠标下面的子元素的索引(未乱序),再根据这个索引求出该子元素在乱序数组里的索引
- 梨子: 标准[0, 1, 2,3, 4] --求–>[4, 1, 0, 2, 3]
- 主要是根据两个数组里的数字是一样的,只是索引的位置不同,假如传进来的索引是3,求法可理解为[4, 1, 0, 2, 3]里第几个的值是3;多理解几遍就知道了。
- 鼠标的位置越出棋盘
// 获取当前鼠标抬起时的位置下的元素的索引
getIndexUp(x, y, index) {
if (x < 0 || x > (this.td * this.cellHW.W) || y < 0 || y > (this.tr * this.cellHW.H)) {
return index;
} else {
// 行 = 鼠标当前相对于父级元素(oImgArea)左顶点为原点的坐标y值 / 子元素的高度
let row = Math.floor(y / this.cellHW.H);
// 列 同上
let col = Math.floor(x / this.cellHW.W);
// 求出该元素在标准数组里的索引(可理解为:一行子元素的个数 * 行数 + 最后一行的子元素的个数)
let l = row * this.td + col;
// 根据该元素在标准数组里的索引求出在乱序数组里索引
let i = 0;
let len = this.randArr.length;
while ((i < len) && this.randArr[i] !== l) {
i++;
}
return i;
}
}
-
子元素运动函数
-
这个方法是自己封装的,三个参数分别代表:依照正序数组还是乱序数组进行运动、拖动的元素运动到乱序数组里索引为to的元素上,乱序数组里索引为to的元素远动到乱序数组里索引为from的元素上;
-
规则就是将数组里的索引转换为行与列,然后再根据子元素的宽和高来求出需要运动到的left和top的值,(注意:在标准数组里index = 标准数组[index],乱序数组里是index = 乱序数组[i]),即: 第(index - index % this.td) / this.td 行,第(index % this.td)列,然后top = 子元素的高 * 行数,left = 子元素 * 列数
-
如果子元素有位置的调换,就需要将拖动的子元素在乱序数组的索引传入from,然后调换也需要在乱序数组里将这两个值进行调换;如果没有元素位置的调换,那么第三参数的值与第二是参数的值一样,即让拖动的元素回到元素回到原来的位置。
-
// 子元素运动函数
cellMove(arr, to, from) {
this.oImgArea.find('.imgCell').eq(to).animate({
'top': (this.cellHW.H) * (arr[from] - (arr[from] % this.td)) / this.td + 'px',
'left': (this.cellHW.W) * (arr[from] % this.td) + 'px'
}, 600, function () {
// 修改层级
$(this).css('z-index', '10')
})
}
// 获取当前游戏时间
let firstTime = new Date().getTime();
let randerGameTime = function () {
let currTime = new Date().getTime();
let time = (currTime - firstTime) / 1000;
let str, hours, minute, second;
if (time > 3600) {
let num = parseInt(parseInt(time / 3600));
hours = num >= 10 ? num + ':' : '0' + num + ':';
minute = (time - num * 3600) / 60 >= 10 ? parseInt((time - num * 3600) / 60) + ':' : '0' + parseInt((time - num * 3600) / 60) + ':';
} else {
hours = parseInt(time / 3600) > 0 ? (parseInt(time / 3600) >= 10 ? (parseInt(time / 3600) + ':') : ('0' + parseInt(time / 3600) + ':')) : '00:';
minute = parseInt(time / 60) >= 10 ? (parseInt(time % 3600) >= 59 ? (parseInt(parseInt(time % 3600) / 60) + ':') : (parseInt(time / 60) + ':')) : ('0' + parseInt(time / 60) + ':');
}
second = parseInt(time % 60) >= 10 ? parseInt(time % 60) : '0' + parseInt(time % 60);
// 更新上次保留的时间
str = hours + minute + second;
$('.time').find('span').text(str)
requestAnimationFrame( randerGameTime );
}
// 检测所有子元素是否能拼成一张完整图片
chack() {
if (this.randArr.toString() == this.origArr.toString()) {
// 延迟执行,使得图片运动完成后再触发
setTimeout(() => {
alert('恭喜你!拼图成功!!请进入下一关');
// 清空数组,防止干扰
this.origArr = [];
this.randArr = [];
// 以增加行与列的方式来增加游戏难度
this.tr += 1;
this.td += 1;
let game = new Game(this.tr, this.td);
game.init();
$('.start').text('打乱').css('backgroundColor', '#f0f');
}, 1000)
}
}