先来预览,咳咳,这个比上次那个地鼠会好看点……
代码是可以设置难度的,3 就是 9,9 就是 81……
相比来说,此程序难度可是远远高过打地鼠的,希望小伙伴能跟上~
header 好理解,注意其中的“新标签打开图片”相当于过关福利,平常是隐藏的。
之所以内容这么少,是因为主逻辑这一块的 html 代码许多属性都是动态的,所以写死没有价值,需要在 js 里面动态生成与删除,所以基本都移到 js 里面了,这里只要看到几个容器就行。其中 #cut-imgs 是下面游戏的容器。
.cut-img {
position: absolute;
top: 0;
left: 0;
border: 0;
padding: 0;
transition: transform .3s linear;
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
}
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
header,
main,
footer {
width: 50%;
}
header {
display: flex;
justify-content: space-between;
}
main {
position: relative;
height: auto;
}
.game-area {
position: relative;
height: auto;
}
#background-img {
max-width: 100%;
max-height: 100%;
vertical-align: top;
opacity: 0;
}
#cut-imgs {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-wrap: wrap;
width: 100%;
height: 100%;
}
button:focus {
outline: none;
}
.selected {
border: 1px solid blue;
}
#download {
display: none;
}
css 里面注意动画的设置,还有切片图像的处理。
这里相信第一反应下面是把一整张图切 9 份。其实不然,不过是 9 个容器(本例用的是 button)分别展示了不同图片的一部分,然后控制相关的容器即可。
所有容器的位置都是左上角,设置偏移量使其在各个位置上,具体设置方法在 js 里面。
const Game = {
// 重新开始游戏
restart() {
// 清空已有数据,重置按钮
this.reset()
const level = this.config.level
// 计算 position 的参数
const positionParam = 1 / (level - 1) * 100
const imgUrl = this.config.imgUrl = `https://h5games-dom.oss-cn-hangzhou.aliyuncs.com/puzzle/${~~(Math.random() * 5)}.png`
const backgroundImg = document.querySelector('#background-img')
backgroundImg.src = imgUrl
// 获取样式表
const styleSheet = this.config.imgCutStyle = document.styleSheets[0]
// 如果添加过自定义则删除
let firstRule = styleSheet.rules[0]
if (firstRule.selectorText === '.custom') styleSheet.deleteRule(0)
let scale = 1 / this.config.level * 100 + '%'
styleSheet.insertRule(`.custom {
width: ${scale};
height: ${scale};
background: url(${imgUrl}) no-repeat;
background-size: ${this.config.level * 100}%; }`, 0)
backgroundImg. = () => {
for (let i = 0, j = Math.pow(this.config.level, 2); i < j; i++) {
this.config.cutImgsCountArray.push(i)
}
// DOM字符串
let cutImgsStr = ''
this.getInitialSort()
this.config.cutImgsCountArray.forEach((num, index) => {
// 保存正确的变化,做判断是否获胜的基础
this.config.trueTransforms.push(`translate(${index % level * 100}%, ${~~(index / level) % level * 100}%)`)
// 这里设置会变动的 style
const transform = `transform: translate(${num % level * 100}%, ${~~(num / level) % level * 100}%);`
const backgroundPosition = `background-position: ${index % level * positionParam}% ${~~(index / level) % level * positionParam}%;`
// 全部在左上初始位置,设置偏移量即可
cutImgsStr += ``
})
document.querySelector('#cut-imgs').innerHTML = cutImgsStr
this.instance.cutImgs = document.querySelectorAll('.cut-img')
}
},
// 点击图片
click(e) {
const index = e.target.dataset.index
// 第一次点击直接结束
if (this.tool.currentIndex === -1) {
this.getCutImg(index).classList.add('selected')
this.tool.currentIndex = index
return
}
const oldCutImg = this.getCutImg(this.tool.currentIndex)
// 如果点击不是同一个再走逻辑
if (this.tool.currentIndex === index) {
this.getCutImg(index).classList.remove('selected')
this.tool.currentIndex = -1
} else {
const newCutImg = this.getCutImg(index)
const [a, b] = [newCutImg.style.transform, oldCutImg.style.transform]
oldCutImg.style.transform = a
newCutImg.style.transform = b
this.tool.currentIndex = -1
setTimeout(() => {
download.style.display = 'none'
oldCutImg.classList.remove('selected')
newCutImg.classList.remove('selected')
if (this.checkNoWin()) console.log('NoWin')
else {
download.style.display = 'block'
alert('win')
}
}, 500);
}
},
// 获取实例
getCutImg(index) {
return this.instance.cutImgs[index]
},
// 获取初始的正确排序
getInitialSort() {
const cal = arr => {
let length = arr.length
let reverse = 0
for (let i = 0; i < length - 1; i++) {
let n = arr[i]
for (let j = i + 1; j < length; j++) {
let m = arr[j]
if (n > m) reverse += 1
}
}
return reverse
}
// 数组随机排序
const randomSort = (a, b) => Math.random() > 0.5 ? -1 : 1
// 循环直到获取可还原的排序
while (1) {
if (cal(this.config.cutImgsCountArray.sort(randomSort)) % 2 === 0) return
}
},
// 检查是否还没胜利
checkNoWin() {
let cutImgs = this.instance.cutImgs
let trueTransforms = this.config.trueTransforms
for (let i = 0, j = this.instance.cutImgs.length; i < j; i++) {
if (cutImgs[i].style.transform !== trueTransforms[i]) return true
}
},
// 清空已有数据
reset() {
let resetParam = this.resetParam
this.config = this.deepCopy(resetParam.config)
this.instance = this.deepCopy(resetParam.instance)
this.tool = this.deepCopy(resetParam.tool)
download.style.display = 'none'
},
deepCopy(obj) {
return JSON.parse(JSON.stringify(obj))
},
// 打开图片
openImage() {
window.open(this.config.imgUrl)
},
// 重置时候的初始化参数
resetParam: {
// 配置
config: {
level: 3,
cutImgsCountArray: [],
trueTransforms: [],
imgCutStyle: {},
imgUrl: '',
},
// 实例
instance: {
// 所有图片的实例
cutImgs: [],
},
// 记录工具
tool: {
currentIndex: -1
},
}
}
Game.restart()
js 就麻烦许多许多了,逻辑和功能匹配,还要用到一些冷门的知识,比如 styleSheets 相关知识,一直用框架,都快忘光了。
说起来简单,就是把前后选中的容器进行 transform 的替换。但是需要注意是基础的业务逻辑:
第一次和下一次点击的是同一个,那么是要取消选中。
交换后,需要两个都取消选中。
重置游戏需要情况上一轮的样式,重新排版。
游戏过关的业务逻辑。
游戏难易度配置。
过关奖励,嘿嘿嘿。
等等等等。
注意里面有个生成可还原的排序,具体见我之前文章:逆序数,拼图游戏必备知识
具体基本逻辑都在代码里面,相关注释也有加上,喜欢喜欢的小伙伴仔细看看,试试手,练一练。
在这里就不长篇赘述了。
祝你玩的开心。
GitHub 源码
在线试玩