使用 jQuery(逻辑无关框架,可复用)

1587959622720923.mp4 (993.26KB)

HTML 模板结构

  1. <div class="main play-page">
  2. <div class="main-content">
  3. // 用于游戏计时
  4. <div class="count-time prompt"><i class="prompt-icon"></i><span class="prompt-text">用时 <span class="j-time-number">0</span></span></div>
  5. // 拼图父元素
  6. <div class="puzzle-box">
  7. // 拼图原图,用于拼图九宫格渲染未完成前展示
  8. <img class="puzzle-preview" src="" alt="预览图">
  9. // 拼图容器
  10. <ul class="puzzle-content" style="display: none;">
  11. // 九宫格
  12. <li class="debris"><img src=""></li>
  13. <li class="debris"><img src=""></li>
  14. <li class="debris"><img src=""></li>
  15. <li class="debris"><img src=""></li>
  16. <li class="debris"><img src=""></li>
  17. <li class="debris"><img src=""></li>
  18. <li class="debris"><img src=""></li>
  19. <li class="debris"><img src=""></li>
  20. <li class="debris"><img src=""></li>
  21. </ul>
  22. </div>
  23. // 利用 canvas 生成九宫格
  24. <canvas style="display: none;" id="puzzle-board" style="width:318px; height:318px;"></canvas>
  25. </div>
  26. <div class="prompt"><i class="prompt-icon"></i><span class="prompt-text">点击图片交换位置</span></div>
  27. <div class="quit-game btn j-quit-game">放弃挑战</div>
  28. </div>

CSS 样式

  1. .prompt {
  2. display: flex;
  3. justify-content: center;
  4. align-items: center;
  5. margin-top: .28rem;
  6. &-icon {
  7. display: inline-block;
  8. margin-right: .06rem;
  9. width: .22rem;
  10. height: .22rem;
  11. background: url(https://img1.dxycdn.com/2020/0424/126/3409583174073156416-2.png) no-repeat center center;
  12. background-size: contain;
  13. }
  14. &-text {
  15. font-size:.24rem;
  16. font-family:PingFangSC-Regular,PingFang SC;
  17. font-weight:400;
  18. color:rgba(255,224,189,1);
  19. }
  20. }
  21. .play-page {
  22. position: relative;
  23. width: 100%;
  24. height: 100%;
  25. overflow: hidden;
  26. background: url(https://img1.dxycdn.com/2020/0424/066/3409601700414728802-2.png) no-repeat center 0;
  27. background-size: contain;
  28. background-color: #9d2729;
  29. .main-content {
  30. margin: 2.28rem auto 0;
  31. border-radius: .12rem;
  32. width: 6.86rem;
  33. height: 7.24rem;
  34. background:rgba(255,232,232,1);
  35. .count-time {
  36. padding: .1rem 0;
  37. .prompt-icon {
  38. width: .24rem;
  39. height: .24rem;
  40. background-image: url(https://img1.dxycdn.com/2020/0424/785/3409601623105175531-2.png);
  41. }
  42. .prompt-text {
  43. font-size: .28rem;
  44. color:rgba(51,51,51,1);
  45. }
  46. }
  47. .puzzle-box {
  48. margin: 0 auto;
  49. width: 318px;
  50. height: 318px;
  51. .puzzle-content {
  52. position: relative;
  53. display: flex;
  54. flex-wrap: wrap;
  55. width: 100%;
  56. height: 100%;
  57. .debris {
  58. position: absolute;
  59. border-radius: .08rem;
  60. border: .02rem solid transparent;
  61. width: 106px;
  62. height: 106px;
  63. transition: left 0.5s ease, top 0.5s ease;
  64. &:nth-of-type(1) {
  65. top: 0;
  66. left: 0;
  67. }
  68. &:nth-of-type(2) {
  69. top: 0;
  70. left: 106px;
  71. }
  72. &:nth-of-type(3) {
  73. top: 0;
  74. left: 212px;
  75. }
  76. &:nth-of-type(4) {
  77. top: 106px;
  78. left: 0;
  79. }
  80. &:nth-of-type(5) {
  81. top: 106px;
  82. left: 106px;
  83. }
  84. &:nth-of-type(6) {
  85. top: 106px;
  86. left: 212px;
  87. }
  88. &:nth-of-type(7) {
  89. top:212px;
  90. left: 0;
  91. }
  92. &:nth-of-type(8) {
  93. top: 212px;
  94. left: 106px;
  95. }
  96. &:nth-of-type(9) {
  97. top: 212px;
  98. left: 212px;
  99. }
  100. &.active{
  101. z-index: 99;
  102. border-color: #9d2628;
  103. }
  104. img {
  105. border-radius: 4px;
  106. width: 100%;
  107. height: 100%;
  108. }
  109. }
  110. }
  111. }
  112. }
  113. .quit-game {
  114. margin: .53rem auto 0;
  115. border-radius: 1rem;
  116. border: .02rem solid rgba(199,164,105,1);
  117. width: 3.4rem;
  118. height: .8rem;
  119. line-height: .8rem;
  120. text-align: center;
  121. font-size: .32rem;
  122. font-family: PingFangSC-Medium,PingFang SC;
  123. font-weight: 500;
  124. color: rgba(207,171,113,1);
  125. letter-spacing: .02rem;
  126. }
  127. }

JavaScript 游戏逻辑

  1. import '@/css/play.less'
  2. import $ from 'jquery'
  3. const H5App = {
  4. // 获取 cavans DOM 对象
  5. puzzleBoardCanvas: null,
  6. // 2d 绘画上下文
  7. ctx: null,
  8. // 拼图正确顺序
  9. passArr: [1, 2, 3, 4, 5, 6, 7, 8, 9],
  10. // 保存九宫格碎片打乱后的顺序
  11. imgArr: [1, 2, 3, 4, 5, 6, 7, 8, 9],
  12. // 经过碎片图位置交换后的最新顺序
  13. resArr: [],
  14. // 原图地址
  15. originImgUrl: '',
  16. // 九宫格拼图元素集合
  17. debrisSet: null,
  18. // 位置交换元素中转站
  19. transferStation: [],
  20. // 游戏完成结束回调
  21. gamePassedCallback: () => {},
  22. // 放弃挑战回调
  23. quitGameCallback: () => {},
  24. // 游戏中回调
  25. inTheGame: () => {},
  26. // 游戏时间
  27. gameTime: 0,
  28. // 游戏计时定时器
  29. gameTimer: null,
  30. /**
  31. * 给九宫格碎片图绑定 touch 事件
  32. */
  33. bind () {
  34. const self = this
  35. self.debrisSet.on('click', function (evt) {
  36. evt.preventDefault()
  37. // 处理当前碎片的信息
  38. const $this = $(this)
  39. $this.addClass('active')
  40. // 当前碎片信息对象 => 下标 && 位置信息
  41. const currentDebrisMsg = {}
  42. // index
  43. const selfIndex = $this.index()
  44. currentDebrisMsg.index = selfIndex
  45. // position
  46. currentDebrisMsg.position = {
  47. x: $this.position().left,
  48. y: $this.position().top
  49. }
  50. // 将信息对象推入中转站数组中
  51. self.transferStation.push(currentDebrisMsg)
  52. // 当中转站有两个碎片时,开始交换位置
  53. if (self.transferStation.length === 2) {
  54. /**
  55. * 交换数据信息 position 信息互换
  56. * origin
  57. * [
  58. * 0: {index: 0, position: {x: 0, y: 0}},
  59. * 1: {index: 4, position: {x: 106, y: 106}}
  60. * ]
  61. * transfer
  62. * [
  63. * 0: {index: 0, position: {x: 106, y: 106}},
  64. * 1: {index: 4, position: {x: 0, y: 0}}
  65. * ]
  66. */
  67. // 调换位置 position 信息
  68. let tempPosition = {}
  69. tempPosition = self.transferStation[0].position
  70. self.transferStation[0].position = self.transferStation[1].position
  71. self.transferStation[1].position = tempPosition
  72. // 排序 imgArr
  73. let tempSeq = ''
  74. tempSeq = self.imgArr[self.transferStation[0].index]
  75. self.imgArr[self.transferStation[0].index] = self.imgArr[self.transferStation[1].index]
  76. self.imgArr[self.transferStation[1].index] = tempSeq
  77. // 绝对定位,css 交换位置
  78. // 并且重置激活状态的碎片
  79. self.transferStation.map((item) => {
  80. $(self.debrisSet[item.index]).css({
  81. top: item.position.y,
  82. left: item.position.x
  83. }).removeClass('active')
  84. })
  85. // 清空中转站,重新开始
  86. self.transferStation.length = 0
  87. console.log(self.imgArr)
  88. // 判断是否通关
  89. self.checkPass()
  90. }
  91. })
  92. $('.j-quit-game').on('click', function (evt) {
  93. evt.preventDefault()
  94. clearInterval(self.gameTimer)
  95. self.quitGameCallback()
  96. })
  97. },
  98. /**
  99. * 游戏开始
  100. */
  101. gameStart () {
  102. this.gameTimer = setInterval(() => {
  103. this.inTheGame(this.gameTime++)
  104. }, 1000)
  105. },
  106. /**
  107. * 判断拼图游戏是否通关
  108. */
  109. checkPass () {
  110. if (this.imgArr.join('') === this.passArr.join('')) {
  111. clearInterval(this.gameTimer)
  112. this.gamePassedCallback(this.gameTime - 1)
  113. }
  114. },
  115. /**
  116. * 九宫格渲染完成
  117. */
  118. renderfinished () {
  119. $('.puzzle-preview').css('display', 'none')
  120. $('.puzzle-content').css('display', 'block')
  121. // 游戏计时开始
  122. this.gameStart()
  123. },
  124. /**
  125. * 打乱九宫格碎片图
  126. */
  127. randomImage () {
  128. this.imgArr.sort(function (a, b) {
  129. return Math.random() - Math.random()
  130. })
  131. },
  132. /**
  133. * 切割原图形成碎片图
  134. * @param {object} img - 图片元素 dom 对象
  135. */
  136. splitImage (img) {
  137. this.randomImage()
  138. let index = 1
  139. for (let i = 0; i < 3; i++) {
  140. for (let j = 0; j < 3; j++) {
  141. this.ctx.drawImage(img, 318 * j, 318 * i, 318, 636, 0, 0, 318, 318)
  142. this.debrisSet.eq(this.imgArr[index - 1] - 1)
  143. .find('img')
  144. .attr('src', this.puzzleBoardCanvas.toDataURL('image/jpeg'))
  145. index++
  146. }
  147. }
  148. // 九宫格绘制完成
  149. this.renderfinished()
  150. },
  151. /**
  152. * 绘制九宫格图片
  153. */
  154. render () {
  155. const self = this
  156. const img = new Image()
  157. img.crossOrigin = 'anonymous'
  158. img.onload = function () {
  159. self.splitImage(img)
  160. }
  161. img.src = this.originImgUrl
  162. },
  163. /**
  164. * 数据初始化
  165. * @param {object} opts - 九宫格游戏初始化配置项
  166. * --> @param {object} puzzleBoardCanvas - canvas 容器 dom 对象
  167. * --> @param {string} originImgUrl - 九宫格原图地址
  168. * --> @param {array} debrisSet - 九宫格格子碎片 dom 数组集合
  169. */
  170. init (opts) {
  171. if (opts.puzzleBoardCanvas) {
  172. this.puzzleBoardCanvas = opts.puzzleBoardCanvas
  173. } else {
  174. console.error('puzzleBoardCanvas 参数错误')
  175. }
  176. if (opts.originImgUrl !== '') {
  177. this.originImgUrl = opts.originImgUrl
  178. $('.puzzle-preview').attr('src', this.originImgUrl)
  179. } else {
  180. console.error('originImgUrl 参数错误')
  181. }
  182. if (opts.debrisSet && opts.debrisSet.length === 9) {
  183. this.debrisSet = opts.debrisSet
  184. } else {
  185. console.error('debrisSet 参数错误')
  186. }
  187. if (opts.quitGameCallback && typeof opts.quitGameCallback === 'function') {
  188. this.quitGameCallback = opts.quitGameCallback
  189. } else {
  190. console.error('quitGameCallback 参数错误')
  191. }
  192. if (opts.inTheGame && typeof opts.inTheGame === 'function') {
  193. this.inTheGame = opts.inTheGame
  194. } else {
  195. console.error('inTheGame 参数错误')
  196. }
  197. if (opts.gamePassedCallback && typeof opts.gamePassedCallback === 'function') {
  198. this.gamePassedCallback = opts.gamePassedCallback
  199. } else {
  200. console.error('gamePassedCallback 参数错误')
  201. }
  202. },
  203. /**
  204. * H5 游戏启动
  205. */
  206. startDraw (opts) {
  207. // 九宫格配置初始化
  208. this.init(opts)
  209. // 判断浏览器是否支持 canvas
  210. if (this.puzzleBoardCanvas.getContext) {
  211. // 访问 2d 绘画上下文
  212. this.ctx = this.puzzleBoardCanvas.getContext('2d')
  213. this.render()
  214. } else {
  215. alert('您的浏览器不支持 Canvas,请换一个浏览器再尝试')
  216. }
  217. // 绑定事件
  218. this.bind()
  219. }
  220. }
  221. const debrisSet = $('.puzzle-box .puzzle-content .debris')
  222. const originImgUrl = 'https://img1.dxycdn.com/2020/0423/836/3409374155194655434-2.jpeg'
  223. const puzzleBoardCanvas = document.getElementById('puzzle-board')
  224. H5App.startDraw({
  225. puzzleBoardCanvas,
  226. originImgUrl,
  227. debrisSet,
  228. inTheGame: function (countTime) {
  229. $('.j-time-number').text(countTime)
  230. },
  231. quitGameCallback: function () {
  232. window.open('/index', '_self')
  233. },
  234. gamePassedCallback: function (countTime) {
  235. setTimeout(() => {
  236. window.open(`/survey?time=${countTime}`, '_self')
  237. }, 1500)
  238. }
  239. })