美文网首页
用 60 行代码实现一个高性能的圣诞抽抽乐 H5 小游戏 (含源

用 60 行代码实现一个高性能的圣诞抽抽乐 H5 小游戏 (含源

作者: 趣谈前端_徐小夕 | 来源:发表于2020-12-14 11:30 被阅读0次

    今天圣诞节,先预祝大家节日快乐.既然是圣诞节,那我们就来学点有意思的,用几十行代码来实现一个高性能的抽奖小游戏.也基于此,来巩固我们的javascript基础,以及前端一些基本算法的应用.

    效果展示

    image

    你将收获

    • 防抖函数的应用
    • 用css实现九宫格布局
    • 生成n维环形坐标的算法
    • 如何实现环形随机轨道运动函数
    • 实现加速度动画
    • 性能分析与优化

    设计思路

    image

    具体实现

    由于目前已有很多方案可以实现九宫格抽奖动画,比如使用动态active实现边框动画,用随机算法和定时器设置在何处停止等等.
    为了进一步提高性能,本文介绍的方法,将使用坐标法,将操作dom的成本降低,完全由js实现滑块的路径的计算,滑块元素采用绝对定位,让其脱离文档流,避免其他元素的重绘等等,最后点击按钮我们会使用防抖函数来避免频繁执行函数,造成不必要的性能损失.

    1. 九宫格布局实现

    为了让大家更加熟悉dom结构,这里我就不用js动态生成了.如下html结构:

    <div class="wrap">
        <div class="title">圣诞抽抽乐</div>
        <div class="box">
            <div class="item">我爱你</div>
            <div class="item">你爱我</div>
            <div class="item">我不爱你</div>
            <div class="item">你爱我</div>
            <div class="item start">开始</div>
            <div class="item">你爱我</div>
            <div class="item">再见</div>
            <div class="item">谢谢惠顾</div>
            <div class="item">你爱我</div>
            <div class="spin"></div>
        </div>
    </div>
    

    九宫格布局我们使用flex来实现,核心代码如下:

    .box {
        display: flex;
        flex-wrap: wrap;
        width: 300px;
        height: 300px;
        position: relative;
        .item {
            box-sizing: border-box;
            width: 100px;
        }
        // 滑块
        .spin {
            box-sizing: border-box;
            position: absolute;
            left: 0;
            top: 0;
            display: inline-block;
            width: 100px;
            height: 100px;
            background-color: rgba(0,0,0,.2);
        }
    }
    

    由上可知容器box采用flex布局,要想让flex子元素换行,我们这里要设置flex-wrap: wrap;此时九宫格布局就实现了. 滑块采用绝对定位,至于具体如何去沿着环形轨道运动,请继续看下文介绍.

    2.生成n维环形坐标的算法

    image

    由上图我们可以知道,一个九宫格的4条边,可以用以上8个坐标收尾连接起来,那么我们可以基于这个规律.来生成环形坐标集合.代码如下:

    /**
     * 生成n维环形坐标
     * @param {number} n 维度
     * @param {number} cell 单位坐标长度
     */
    function generateCirclePath(n, cell) {
      let arr = []
      for(let i=0; i< n; i++) {
          arr.push([i*cell, 0])
      }
      for(let i=0; i< n-1; i++) {
          arr.push([(n-1)*cell, (i+1)*cell])
      }
      for(let i=0; i< n-1; i++) {
          arr.push([(n-i-2)*cell, (n-1)*cell])
      }
      for(let i=0; i< n-2; i++) {
          arr.push([0, (n-i-2)*cell])
      }
      return arr
    }
    

    如果是单位坐标,那么cell为1,cell设计的目的就位为了和现实的元素相结合,我们可以手动设置单元格的宽度来实现不同大小的n维环形坐标集.

    3.实现环形随机轨道运动函数

    由抽奖动画分析可知,我们滑块运动的轨迹,其实就是环形坐标集合,所以我们只要让滑块的顶点(默认左上角)沿着环形坐标集合一步步变化就好了.

    function run(el, path, n = 1, i = 0, len = path.length) {
        setTimeout(() => {
            if(n > 0) {
              if(len <= i) {
                  i = n === 1 ? len : 0
                  n--
              }
              el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
              run(el, path, n, ++i, len)
            }
        }, 300)   
    }
    

    这样就能实现我们的滑块按照九宫格边框运动的动画了,当然以上函数只是基本的动画, 还没有实现在随机位置停止, 以及滑块的加速度运动,这块需要一定的技巧和js基础知识比如闭包.

    3.1 加速度运动

    加速度运动其实很简单,比如每转过一圈将setTimeout的延迟时间改变即可.代码如下:

    function run(el, path, n = 1, speed = 60, i = 0, len = path.length) {
        setTimeout(() => {
            if(n > 0) {
              if(len <= i) {
                  i = n === 1 ? len : 0
                  n--
                  speed += (300 - speed) / n
              }
              el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
              run(el, path, n, speed, ++i, len)
            }
        }, speed)   
    }
    
    3.2 随机停止实现

    随机停止这块主要是用了Math.random这个API, 我们在最后一圈的时候, 根据随机返回的数值来决定何时停止,这里我们在函数内部实现随机数值,完整代码如下:

    /**
    * 环形随机轨道运动函数
    * @param {element} el 运动的dom元素
    * @param {array} path 运动的环形坐标集合
    * @param {number} speed 运动的初始速度
    * @param {number} i 运动的初始位置
    * @param {number} len 路径的长度
    * @param {number} random 中奖坐标
    */
    function run(el, path, n = 1, speed = 60, i = 0, len = path.length, random = Math.floor(Math.random() * len)) {
        setTimeout(() => {
            if(n > 0) {
              // 如果n为1,则设置中奖数值
              if(n === 1) {
                len = random
              }
              if(len <= i) {
                  i = n === 1 ? len : 0
                  n--
                  speed += (300 - speed) / n
              }
              el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
              run(el, path, n, speed, ++i, len, random)
            }
        }, speed)   
    }
    

    4.实现点击开始的防抖函数以及应用

    防抖函数实现:

    // 防抖函数,避免频繁点击执行多次函数
    function debounce(fn, interval = 300) {
      let timeout = null
      return function () {
          clearTimeout(timeout)
          timeout = setTimeout(() => {
              fn.apply(this, arguments)
          }, interval)
      }
    }
    

    那么我们点击时,代码应该长这样:

    // 点击开始按钮,开始抽奖
    $('.start').on('click',debounce(() => { run($('.spin'), generateCirclePath(3, 100), 3) }))
    

    延伸

    在文章发布之后,有热心的小伙伴们提出了几个建议,综合如下:

    • 抽奖动画结束后提供回调来通知页面以便处理其他逻辑
    • 处理多次点击时,虽然加了防抖,但是用户在动画没结束时点击了开始按钮,又会执行动画导致动画越来越快,发生混乱.

    综合以上问题,我在之前基础上做了进一步扩展,来解决以上提到的问题.

    1. 添加动画结束时回调:
    /**
    * 环形随机轨道运动函数
    * @param {element} el 运动的dom元素
    * @param {array} path 运动的环形坐标集合
    * @param {func} cb 动画结束时回调
    * @param {number} speed 运动的初始速度
    * @param {number} i 运动的初始位置
    * @param {number} len 路径的长度
    * @param {number} random 中奖坐标
    */
    function run(el, path, n = 1, cb, speed = 60, i = 0, len = path.length, random = Math.floor(Math.random() * len)) {
        setTimeout(() => {
            if(n > 0) {
              // 如果n为1,则设置中奖数值
              if(n === 1) {
                len = random
              }
              if(len <= i) {
                  i = n === 1 ? len : 0
                  n--
                  speed += (300 - speed) / n
              }
              el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
              run(el, path, n, cb, speed, ++i, len, random)
            }else {
              cb && cb()
            }
        }, speed)
    }
    
    1. 处理多次点击时,虽然加了防抖,但是用户在动画没结束时点击了开始按钮,又会执行动画导致动画越来越快,发生混乱.
    // 1. 点击开始按钮,开始抽奖
    $('.start').on('click',debounce(() => {
        // 点击开始后禁用点击
        $('.start').css('pointer-events', 'none')
        run($('.spin'), generateCirclePath(3, 100), 3, () => {
          // 动画结束后开启按钮点击
          $('.start').css('pointer-events', 'auto')
          alert('抽奖结束')
        }) 
    }))
    

    谢谢各位认真的建议,继续优化吧.

    总结

    该实现方式的好处是支持n维环形坐标的抽奖,基于坐标法的应用还有很多,尤其是游戏和图形领域,在实现过程中一定要考虑性能和可扩展性,这样我们就可以在不同场景使用同一套方法论,岂不乐哉?本文完整源码我会放在github上,欢迎交流学习~

    最后

    如果想了解更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在微信搜索《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

    相关文章

      网友评论

          本文标题:用 60 行代码实现一个高性能的圣诞抽抽乐 H5 小游戏 (含源

          本文链接:https://www.haomeiwen.com/subject/ymhvgktx.html