使用 jQuery(逻辑无关框架,可复用)
HTML 模板结构
<div class="main play-page">
<div class="main-content">
// 用于游戏计时
<div class="count-time prompt"><i class="prompt-icon"></i><span class="prompt-text">用时 <span class="j-time-number">0</span> 秒</span></div>
// 拼图父元素
<div class="puzzle-box">
// 拼图原图,用于拼图九宫格渲染未完成前展示
<img class="puzzle-preview" src="" alt="预览图">
// 拼图容器
<ul class="puzzle-content" style="display: none;">
// 九宫格
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
<li class="debris"><img src=""></li>
</ul>
</div>
// 利用 canvas 生成九宫格
<canvas style="display: none;" id="puzzle-board" style="width:318px; height:318px;"></canvas>
</div>
<div class="prompt"><i class="prompt-icon"></i><span class="prompt-text">点击图片交换位置</span></div>
<div class="quit-game btn j-quit-game">放弃挑战</div>
</div>
CSS 样式
.prompt {
display: flex;
justify-content: center;
align-items: center;
margin-top: .28rem;
&-icon {
display: inline-block;
margin-right: .06rem;
width: .22rem;
height: .22rem;
background: url(https://img1.dxycdn.com/2020/0424/126/3409583174073156416-2.png) no-repeat center center;
background-size: contain;
}
&-text {
font-size:.24rem;
font-family:PingFangSC-Regular,PingFang SC;
font-weight:400;
color:rgba(255,224,189,1);
}
}
.play-page {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: url(https://img1.dxycdn.com/2020/0424/066/3409601700414728802-2.png) no-repeat center 0;
background-size: contain;
background-color: #9d2729;
.main-content {
margin: 2.28rem auto 0;
border-radius: .12rem;
width: 6.86rem;
height: 7.24rem;
background:rgba(255,232,232,1);
.count-time {
padding: .1rem 0;
.prompt-icon {
width: .24rem;
height: .24rem;
background-image: url(https://img1.dxycdn.com/2020/0424/785/3409601623105175531-2.png);
}
.prompt-text {
font-size: .28rem;
color:rgba(51,51,51,1);
}
}
.puzzle-box {
margin: 0 auto;
width: 318px;
height: 318px;
.puzzle-content {
position: relative;
display: flex;
flex-wrap: wrap;
width: 100%;
height: 100%;
.debris {
position: absolute;
border-radius: .08rem;
border: .02rem solid transparent;
width: 106px;
height: 106px;
transition: left 0.5s ease, top 0.5s ease;
&:nth-of-type(1) {
top: 0;
left: 0;
}
&:nth-of-type(2) {
top: 0;
left: 106px;
}
&:nth-of-type(3) {
top: 0;
left: 212px;
}
&:nth-of-type(4) {
top: 106px;
left: 0;
}
&:nth-of-type(5) {
top: 106px;
left: 106px;
}
&:nth-of-type(6) {
top: 106px;
left: 212px;
}
&:nth-of-type(7) {
top:212px;
left: 0;
}
&:nth-of-type(8) {
top: 212px;
left: 106px;
}
&:nth-of-type(9) {
top: 212px;
left: 212px;
}
&.active{
z-index: 99;
border-color: #9d2628;
}
img {
border-radius: 4px;
width: 100%;
height: 100%;
}
}
}
}
}
.quit-game {
margin: .53rem auto 0;
border-radius: 1rem;
border: .02rem solid rgba(199,164,105,1);
width: 3.4rem;
height: .8rem;
line-height: .8rem;
text-align: center;
font-size: .32rem;
font-family: PingFangSC-Medium,PingFang SC;
font-weight: 500;
color: rgba(207,171,113,1);
letter-spacing: .02rem;
}
}
JavaScript 游戏逻辑
import '@/css/play.less'
import $ from 'jquery'
const H5App = {
// 获取 cavans DOM 对象
puzzleBoardCanvas: null,
// 2d 绘画上下文
ctx: null,
// 拼图正确顺序
passArr: [1, 2, 3, 4, 5, 6, 7, 8, 9],
// 保存九宫格碎片打乱后的顺序
imgArr: [1, 2, 3, 4, 5, 6, 7, 8, 9],
// 经过碎片图位置交换后的最新顺序
resArr: [],
// 原图地址
originImgUrl: '',
// 九宫格拼图元素集合
debrisSet: null,
// 位置交换元素中转站
transferStation: [],
// 游戏完成结束回调
gamePassedCallback: () => {},
// 放弃挑战回调
quitGameCallback: () => {},
// 游戏中回调
inTheGame: () => {},
// 游戏时间
gameTime: 0,
// 游戏计时定时器
gameTimer: null,
/**
* 给九宫格碎片图绑定 touch 事件
*/
bind () {
const self = this
self.debrisSet.on('click', function (evt) {
evt.preventDefault()
// 处理当前碎片的信息
const $this = $(this)
$this.addClass('active')
// 当前碎片信息对象 => 下标 && 位置信息
const currentDebrisMsg = {}
// index
const selfIndex = $this.index()
currentDebrisMsg.index = selfIndex
// position
currentDebrisMsg.position = {
x: $this.position().left,
y: $this.position().top
}
// 将信息对象推入中转站数组中
self.transferStation.push(currentDebrisMsg)
// 当中转站有两个碎片时,开始交换位置
if (self.transferStation.length === 2) {
/**
* 交换数据信息 position 信息互换
* origin
* [
* 0: {index: 0, position: {x: 0, y: 0}},
* 1: {index: 4, position: {x: 106, y: 106}}
* ]
* transfer
* [
* 0: {index: 0, position: {x: 106, y: 106}},
* 1: {index: 4, position: {x: 0, y: 0}}
* ]
*/
// 调换位置 position 信息
let tempPosition = {}
tempPosition = self.transferStation[0].position
self.transferStation[0].position = self.transferStation[1].position
self.transferStation[1].position = tempPosition
// 排序 imgArr
let tempSeq = ''
tempSeq = self.imgArr[self.transferStation[0].index]
self.imgArr[self.transferStation[0].index] = self.imgArr[self.transferStation[1].index]
self.imgArr[self.transferStation[1].index] = tempSeq
// 绝对定位,css 交换位置
// 并且重置激活状态的碎片
self.transferStation.map((item) => {
$(self.debrisSet[item.index]).css({
top: item.position.y,
left: item.position.x
}).removeClass('active')
})
// 清空中转站,重新开始
self.transferStation.length = 0
console.log(self.imgArr)
// 判断是否通关
self.checkPass()
}
})
$('.j-quit-game').on('click', function (evt) {
evt.preventDefault()
clearInterval(self.gameTimer)
self.quitGameCallback()
})
},
/**
* 游戏开始
*/
gameStart () {
this.gameTimer = setInterval(() => {
this.inTheGame(this.gameTime++)
}, 1000)
},
/**
* 判断拼图游戏是否通关
*/
checkPass () {
if (this.imgArr.join('') === this.passArr.join('')) {
clearInterval(this.gameTimer)
this.gamePassedCallback(this.gameTime - 1)
}
},
/**
* 九宫格渲染完成
*/
renderfinished () {
$('.puzzle-preview').css('display', 'none')
$('.puzzle-content').css('display', 'block')
// 游戏计时开始
this.gameStart()
},
/**
* 打乱九宫格碎片图
*/
randomImage () {
this.imgArr.sort(function (a, b) {
return Math.random() - Math.random()
})
},
/**
* 切割原图形成碎片图
* @param {object} img - 图片元素 dom 对象
*/
splitImage (img) {
this.randomImage()
let index = 1
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
this.ctx.drawImage(img, 318 * j, 318 * i, 318, 636, 0, 0, 318, 318)
this.debrisSet.eq(this.imgArr[index - 1] - 1)
.find('img')
.attr('src', this.puzzleBoardCanvas.toDataURL('image/jpeg'))
index++
}
}
// 九宫格绘制完成
this.renderfinished()
},
/**
* 绘制九宫格图片
*/
render () {
const self = this
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = function () {
self.splitImage(img)
}
img.src = this.originImgUrl
},
/**
* 数据初始化
* @param {object} opts - 九宫格游戏初始化配置项
* --> @param {object} puzzleBoardCanvas - canvas 容器 dom 对象
* --> @param {string} originImgUrl - 九宫格原图地址
* --> @param {array} debrisSet - 九宫格格子碎片 dom 数组集合
*/
init (opts) {
if (opts.puzzleBoardCanvas) {
this.puzzleBoardCanvas = opts.puzzleBoardCanvas
} else {
console.error('puzzleBoardCanvas 参数错误')
}
if (opts.originImgUrl !== '') {
this.originImgUrl = opts.originImgUrl
$('.puzzle-preview').attr('src', this.originImgUrl)
} else {
console.error('originImgUrl 参数错误')
}
if (opts.debrisSet && opts.debrisSet.length === 9) {
this.debrisSet = opts.debrisSet
} else {
console.error('debrisSet 参数错误')
}
if (opts.quitGameCallback && typeof opts.quitGameCallback === 'function') {
this.quitGameCallback = opts.quitGameCallback
} else {
console.error('quitGameCallback 参数错误')
}
if (opts.inTheGame && typeof opts.inTheGame === 'function') {
this.inTheGame = opts.inTheGame
} else {
console.error('inTheGame 参数错误')
}
if (opts.gamePassedCallback && typeof opts.gamePassedCallback === 'function') {
this.gamePassedCallback = opts.gamePassedCallback
} else {
console.error('gamePassedCallback 参数错误')
}
},
/**
* H5 游戏启动
*/
startDraw (opts) {
// 九宫格配置初始化
this.init(opts)
// 判断浏览器是否支持 canvas
if (this.puzzleBoardCanvas.getContext) {
// 访问 2d 绘画上下文
this.ctx = this.puzzleBoardCanvas.getContext('2d')
this.render()
} else {
alert('您的浏览器不支持 Canvas,请换一个浏览器再尝试')
}
// 绑定事件
this.bind()
}
}
const debrisSet = $('.puzzle-box .puzzle-content .debris')
const originImgUrl = 'https://img1.dxycdn.com/2020/0423/836/3409374155194655434-2.jpeg'
const puzzleBoardCanvas = document.getElementById('puzzle-board')
H5App.startDraw({
puzzleBoardCanvas,
originImgUrl,
debrisSet,
inTheGame: function (countTime) {
$('.j-time-number').text(countTime)
},
quitGameCallback: function () {
window.open('/index', '_self')
},
gamePassedCallback: function (countTime) {
setTimeout(() => {
window.open(`/survey?time=${countTime}`, '_self')
}, 1500)
}
})