抽獎動畫 – lao虎機抽獎

本文介紹一個lao虎機抽獎動畫的實現,lao虎機抽獎在各類商家營銷活動中非常常見,這裡主要介紹動畫的實現過程,其他細節不做詳細分析。

ps:lao虎機是敏感詞,部落格園的富文本和markdown編輯器都限制不允許出現,所有老用拼音。

1. 需求

UI給到的藍湖如下截圖1
image
圖1

  • 三欄圖片,每欄圖片是一樣的,都包含所有的獎品圖片。
  • 點擊抽獎三欄圖片從左到右依次開始上下滾動,從慢到塊,滾動幾輪後根據抽獎結果固定圖片位置。
  • 如果中獎三欄顯示同一張獎品圖片,否則隨機顯示三張獎品圖片。最後彈出抽獎結果彈框。

2. 整體思路

2.1 滾動

說到滾動,首先想到的是scroll,但是scroll會有滾動條出現。並且需求要求先滾動幾輪,這個使用scroll的話我暫時想不出什麼好的辦法。
然後想到可以使用background-img結合background-repeat,background-position-y來實現這個功能,簡單說就不斷地修改背景圖片的background-position-y,並且設置背景重複顯示,這樣看起來就是獎品圖片在滾動了。

2.2 jquery動畫

雖然可以使用css中的animation動畫來讓背景滾動,但是這裡有個問題,在開始滾動圖片同要去請求介面,在介面有了結果之後要根據結果來固定圖片位置,使用keyframe的話要動態設置關鍵幀,這也很麻煩。
其實jquery提供了響應的api來修改元素的尺寸資訊,和尺寸相關的都可以使用jquery動畫。參考jquery文檔如下:

所有用於動畫的屬性必須是數字的,除非另有說明;這些屬性如果不是數字的將不能使用基本的jQuery功能。(例如,width, height或者left可以執行動畫,但是background-color不能,除非使用jQuery.Color()插件。)屬性值的單位像素(px),除非另有說明。單位em 和 %需要指定使用。

另外,背景開始滾動的時候是空轉的,然後等到有抽獎結果之後在原來的background-position-y的基礎上加上一個值,意思再滾動一個距離,固定在獎品圖片上。這個需求和jquery動畫屬性中「相對值」的概念不謀而合。參考下面的jquery引文。

動畫屬性也可以是一個相對值。如果提供一個以+= 或 -=開始的值,那麼目標值就是以這個屬性的當前值加上或者減去給定的數字來計算的。

等待介面有響應之後還要播放第二個動畫固定獎品,這時就要再播放一個動畫,jquery已經想到了這個問題,所有提供一個done回調方法,如下:

done
Type: Function( Promise animation, Boolean jumpedToEnd )
在動畫完成時執行的函數。 (他的Promise對象狀態已完成). (version added: 1.8)..

2.3 尺寸問題

這個動畫中尺寸問題至關重要,因為要對準獎品圖片,尺寸稍有差別,就不容易設置好位置。還有UI給到我們的需求一般都是px,我們的vue項目中使用到「postcss-plugin-px2rem」插件會將px修改成相對的尺寸單位rem。postcss-plugin-px2rem配置如下:

'postcss-plugin-px2rem': {
  rootValue: 75,
  unitPrecision: 8,
  propWhiteList: [],
  propBlackList: [],
  selectorBlackList: [],
  ignoreIdentifier: false,
  replace: true,
  mediaQuery: false,
  minPixelValue: 3,
  exclude: /vant/i
}

這裡最關鍵的資訊是rootValue,設計稿給到的螢幕寬度是750px,這個值被換算成rem是750px/75=10rem,我們程式碼中所有的尺寸都會按照這個公式換算成rem。
然後我們動畫中計算background-position-y的時候也要像這樣換算一下,不然也有可能對不準獎品圖片。

還有每個獎品圖片在整張背景圖中的次序也要先弄清楚,這裡記在一個數組中,最後固定獎品圖片的時候用到。如下:

prizeList: [
  {pid: 3251, order: 4, code: "HW-AM115", title: "華為半入耳式耳機AM115"},
  {pid: 3231, order: 3, code: "iphone-12", title: "蘋果12 64G綠色"},
  {pid: 3261, order: 2, code: "PMC_iphone12_bhk", title: "浦諾菲-蘋果12水晶保護殼"},
  {pid: 3271, order: 6, code: "PMC-18C", title: "浦諾菲_PMC-18C PD雙口充電器"},
  {pid: 3241, order: 5, code: "SLY_RPB-N16", title: "絲蘭雅_RPB-N16移動電源"},
  {pid: 3221, code: "lost", title: "離大獎就差一點點啦~"}
]

3.實現過程

3.1 布局

這裡頁面布局的時候要和UI溝通一個細節,就是背景圖中上下兩個獎品的間隔是最上面一個獎品和頂部中間間隔的兩倍。如下圖2
image
圖2
同理,背景圖中上下兩個獎品的間隔是最下面一個獎品和底部中間間隔的兩倍。如下圖3
image
圖3
最後整張背景圖片如下圖4
image
圖4
這樣滾動起來看上去是一張整體的圖片,而不會出現偏差。三欄布局使用flex來實現,html程式碼如下:

<div class="session">
  <div class="lottory-box">
    <div class="top-fill"></div>
    <div class="tiger tiger-first"></div>
    <div class="tiger tiger-second center"></div>
    <div class="tiger tiger-thired"></div>
    <div class="bottom-fill"></div>
  </div>
  <img src="../assets/images/btn-lottery/btn-draw-lottery.gif" alt="" class="dray-lottery" @click="lotteryClick">
</div>

css程式碼如下:

.box {
  background: #FFBA76;
  width: 702px;
  margin: 50px auto;
  border-radius: 0px 0px 8px 8px;
  .session {
    padding-top: 22px;
    .lottory-box {
      position: relative;
      width: 664px;
      height: 341px;
      margin: 0 auto;
      background: no-repeat url("../assets/images/bg-lottery-box.png") center/664px 341px;
      border-radius: 8px;
      @include flex(center, center, nowrap, row);
      .tiger-first, .tiger-second, .tiger-thired {
        width: 191px;
        height: 341px;
      }
      //背景圖是同一個圖片,y軸位置不同
      .tiger-first {
        background: url("../assets/images/img-prizelist-border.png") center 62px/191px auto;
      }
      .tiger-second {
        background: url("../assets/images/img-prizelist-border.png") center -167px/191px auto;
      }
      .tiger-thired {
        background: url("../assets/images/img-prizelist-border.png") center -396px/191px auto;
      }
      .center {
        margin: 0 20px;
      }
      .top-fill, .bottom-fill {
        position: absolute;
      }
      .top-fill {
        top: 0;
        width: 660px;
        height: 49px;
        background: linear-gradient(180deg, #D15000 0%, rgba(241, 92, 0, 0) 100%);
        border-radius: 5px 5px 1px 1px;
      }
      .bottom-fill {
        bottom: 0;
        width: 660px;
        height: 49px;
        background: linear-gradient(180deg, rgba(241, 92, 0, 0) 0%, #D15000 100%);
        border-radius: 1px 1px 7px 7px;
      }
    }
    img.dray-lottery {
      width: 549px;
    }
  }
}

注意初始狀態下,tiger-first,tiger-second,tiger-thired三張背景圖片的定位已近寫在css裡面,可以根據情況調整。最後介面效果如下圖5:
image
圖5

3.2 動畫

布局有了就可以讓它動起來了,首先讓三張背景圖勻速運動起來,最後一起停止。程式碼如下:

lotteryClick() {
  let u = 1145                             //整個背景高度
  let that = this
  //播放動畫
  jQuery(".tiger").each(function(index) {
    let currNum = jQuery(this)
    currNum.animate({backgroundPositionY: "+=" + (u * 3)/75 + 'rem'},
      {easing: "easeInOutCirc", duration: 4000 })
  })
}

變數u是整個背景圖片的高度,先讓背景滾動3次,然後再除以75得到先對單位rem,這個75就是上面提到的rootValue,這裡用到的是’+=’,也就是在原有的backgroundPositionY的基礎上再加上一個相對的位移,duration:4000,讓這個動畫整個執行4秒鐘時間。效果如下圖6
image
圖6

需求要求三張圖片從左到有先後滾動,這個可以使用setTimeout(fn, time);來實現,程式碼如下:

//播放動畫
jQuery(".tiger").each(function(index) {
  let currNum = jQuery(this)
  setTimeout(() => {
    currNum.animate({backgroundPositionY: "+=" + (u * 3)/75 + 'rem'},
      {easing: "easeInOutCirc", duration: 4000 })
  }, index * 300)
})

利用jquery中each的參數index,代表當前元素的下標,乘以300,這樣第一個立即執行,第二個300毫秒後執行,第三個600毫秒後執行,效果如下圖7:
image
圖6

為了使效果看起來更加逼真,可以讓每個圖片滾動的時間有所差異,第一個最短,最後一個最長,這樣看起來效果更逼真。方法是給一個延遲參數wast,加在配置參數duration上,程式碼如下:

lotteryClick() {
  let u = 1145                             //整個背景高度
  let waste = 800                          //調整動畫時間
  //播放動畫
  jQuery(".tiger").each(function(index) {
    let currNum = jQuery(this)
    setTimeout(() => {
      currNum.animate({backgroundPositionY: "+=" + (u * 3)/75 + 'rem'},
        {easing: "easeInOutCirc", duration: 4000 + index * waste })
    }, index * 300)
  })
}

效果如下圖7
image
圖7

3.3 請求介面&再動畫

動畫有了,現在要開始從介面中拿數據來定位獎品了。請求介面和上面的動畫一起執行,這裡假定介面響應的時間一定是少於2 * 300 + 4000 + 2 * 800 = 6200ms,一般來說這個時間足夠了,這段時間內動畫空轉。拿到結果後再播放第二個動畫來固定獎品圖片。程式碼如下:

//避免重複點擊
let that = this
if (this.disabled) {
  return
}
this.disabled = true
//抽獎
that.pid = -1
coc2.drawLottery({actId: actId.lottery}).then(res => {
  // 臨時抽獎
  // res = {"code": "0","data":{"pid":3221}}
  if (res.code == 0) {
    let data = res.data
    this.pid = data.pid
  } else if (res.code == 102002) {
    this.pid = 3221
  } else if (res.code == 303) {
    //拉起登錄
    pullLogin()
  } else {
    this.lotteryMsg = res.message
  }
})

這裡用一個變數pid記住獎品id,然後播放第二個動畫,這時jquery動畫提供了一個done方法執行動畫完成之後的後續操作。程式碼如下:

//播放動畫
jQuery(".tiger").each(function(index) {
  let currNum = jQuery(this)
  setTimeout(() => {
    currNum.animate({backgroundPositionY: "+=" + (u * 3)/75 + 'rem'},
      {easing: "easeInOutCirc", duration: 4000 + index * waste, done: function() {
        snapToGrid(currNum, index)
      }})
  }, index * 300)
})

snapToGrid方法就是執行第二個動畫了,程式碼如下:

//對齊獎品圖片
function snapToGrid(domObj, index) {
  let prizeNumber = 0
  //謝謝惠顧
  if ([3221, -1].includes(that.pid)) {
    let result = that.numRand()
    let numArr = (result + '').split('')
    prizeNumber = parseInt(numArr[index])
  } else {
    let prize = that.prizeList.find(p => p.pid == that.pid)
    prizeNumber = prize.order
  }
  domObj.animate({backgroundPositionY:  "+=" + (prizeH * prizeNumber) / 75 + "rem"},
    {easing: "linear", duration: prizeNumber * waste, done: function() {
      if (index == 2) {
        if (that.pid > -1) {
          that.$refs.dialogPrize.popUp(that.pid)
        } else {
          that.$toast(that.lotteryMsg)
        }
        that.disabled = false
      }
    }
    })
}

這裡這裡還有個邏輯,如果未中獎(謝謝惠顧),要生成三個不相等的隨機數來讓獎品圖片固定,就是上面的that.numRand(),用它在0,1,2,3,4中隨機選三個來固定獎品圖片。具體方法如下:

//生成隨機順序
numRand() {
  let arr = ["0", "1", "2", "3", "4"], res = ""
  for (let i = 0; i < 3; i++) {
    let rnd = Math.floor(Math.random() * arr.length)
    res += arr[rnd]
    arr.splice(rnd, 1)
  }
  return res
}

還有個地方要注意,一開始的時候默認顯示的獎品圖片是沒有對齊的,在動畫空轉3圈結束後讓然不會對齊,等介面有結果後需要對齊獎品,而prizeList變數中紀錄的order是按照從上到下的次序來的,最後固定到獎品圖片時不可能對準。所以在開始動畫之前要將每張背景圖片固定到起始的位置,程式碼如下:

jQuery(".tiger").css('backgroundPositionY', 0)

最後看一下整體抽獎動畫效果,如下圖8
image
圖8

4.總結

jquery動畫提供了豐富的功能,能靈活的控制動畫參數和想要的效果,在vue中雖然也提供了動畫功能,但是處理一些複雜的操作用起來不是太理想。