抽獎動畫 - 大轉盤抽獎

nd發表於2021-07-29

1.需求

抽獎是各類營銷活動中最常見的一種形式,本產品需求大致如下:轉盤周圍跑馬燈交替閃爍,點選抽獎,大轉盤旋轉,呼叫介面獲取抽獎結果,大轉盤指標指向對應的獎品。高保如下圖1

圖1-高保

2.整體思路

本需求要求跑馬燈交替閃爍,那四周的跑馬燈就不能是死的圖片了,要用動畫來實現,並且第奇數,偶數交替變換,這個使用vue中的動態屬性可以實現。

其次小燈泡分佈在四周,首先想到的是 transform: rotate(); 然後獎品圖片也是分佈在圓形四周,這個也可以用 transform: rotate()。

最後點選立即抽獎,包含獎品圖片的整個dom旋轉,這個使用animation+transform: rotate();可以實現。

3.實現過程

各位看官請注意,這裡只介紹了關鍵實現過程,中間涉及到的佈局是大多使用absolute定位來實現的,中獎彈框是另外一個元件,因為不是關鍵,所以沒有具體介紹,也沒有貼出程式碼。

3.1背景

這個背景一般是UI給個圖片出來,雖然使用css可以實現複雜的圖形,但是要花很長時間得不償失,一般給個背景圖片就可以了。注意這裡高保給的有問題,後面獎品的dom會用另外一張背景覆蓋。如下圖2

圖2-轉盤背景

3.2轉盤跑馬燈

跑馬燈是一個一個的燈泡,放在轉盤周圍,這個要根據高保尺寸來寫,不然距離有差,沒法落在四周邊緣的位置。還有小燈泡明暗交替放置,不然也沒有效果。html程式碼如下:

<!-- 轉盤周圍跑馬燈 -->
<div class="lightWrap">
  <div
    v-for="i in 20"
    :key="i"
    :style="setLightRotate(i)"
    class="lightItem">
    <img
      v-if="i % 2 == 0"
      :class="{active: !lightChange}"
      :src="lightChange ? lightGrey : lightActive"
      alt=""/>
    <img
      v-else
      :class="{active: lightChange}"
      :src="!lightChange ? lightGrey : lightActive"
      alt=""/>
  </div>
</div>

注意周圍有20個燈泡,所以使用v-for="i in 20",然後setLightToatate(i)是一個方法,用來設定每個燈泡的傾斜度,如下:

//20個燈泡設定傾斜
setLightRotate(index) {
  let lightRotate = (360 / 20) * index
  return {
    transform: 'rotate(' + lightRotate + 'deg)'
  }
}

這個很容易理解了,整個圓是360度,除以20,是每兩個圓之間的間隔角度,再乘以陣列20的下標,就是每個燈泡的偏移角度。

小燈泡的顯示就需要交替顯示了,計算奇偶使用表示式i%2 == 0,然後使用一個變數lightChange來判斷當前這個燈泡是否是點亮,來區分顯示不同的圖片。這個變數是通過questAnimationFrame遞迴呼叫來修改。方法如下:

//瀏覽器播放跑馬燈動畫
setTimeLine() {
  this.lightCount += 1
  // 瀏覽器渲染頻率 60幀/s 約等於 16.66ms 一次,取20的倍數,就是約300ms切換跑馬燈一次
  if ((this.lightCount % 20) == 0) {
    this.lightChange = !this.lightChange
  }
  requestAnimationFrame(this.setTimeLine)
}

在mounted鉤子裡呼叫一次setTimeLine()方法,然後在方法裡呼叫requestAnimationFrame(),但是在requestAnimation()的回撥函式裡又呼叫了自己。注意這種方式類似遞迴呼叫,但是不是遞迴呼叫,瀏覽器的渲染評率是60幀每秒,也就是requestAnimationFrame()的回撥函式在1000毫秒/60=16.66毫秒,也就是每16.66毫秒就執行一次setTimeLine()方法,在方法裡lightChange自增1,判斷lightCount是20的倍數切換lightChange變數,然後16.66毫秒*20=33.33毫秒切換一次。

lightChange變數還切換了當前燈泡的樣式,點亮後還會設定燈泡變大一點。css如下:

.lightWrap {
  width: $turntableWrap_size;
  height: $turntableWrap_size;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: calc(#{$turntableWrap_size} / -2);
  margin-top: calc(#{$turntableWrap_size} / -2);
  .lightItem {
    width: 22px;
    height: calc(#{$turntableWrap_size} / 2);
    position: absolute;
    left: 50%;
    top: 0%;
    transform-origin: 0 calc(#{$turntableWrap_size} / 2);

    img {
      width: 22px;
      height: 22px;
      position: absolute;
      top: 10px;
      left: 0;
    }
    img.active {
      width: 40px;
      height: 40px;
      position: absolute;
      top: 1px;
      left: -9px;
    }
  }
}

最終效果如下圖3

圖3

3.3獎品圖片

接下來是要把獎品圖片放在轉盤上,並且分佈在轉盤四周,原理還是使用transform: rotate();方法來設定傾斜。html程式碼如下:

<!-- 獎品圖片 -->
<div class="circleMax" :class="{ani: runningLock}">
  <div
    v-for="(item, i) in actPrizeList"
    :key="i"
    :style="setRotate(i)"
    class="spin">
    <div :style="setSpinInner(i)" class="spinInner">
      <div :style="spinCntDocObj" class="spinCntDoc">
        <div class="spinImg">
          <img :src="item.imgUrl" alt="" srcset="">
        </div>
      </div>
    </div>
  </div>
</div>

actPrizeList就是獎品資訊了,這個是從介面獲取,裡面有配置好的獎品圖片連線。這裡還是使用了一個方法setTotate(i)來動態設定樣式,方法如下:

/* 獎品圖片傾斜 */
setRotate(index) {
  let spinRotate = this.jiaodu * index
  return {
    transform: 'rotate(' + spinRotate + 'deg)'
  }
}

setSpinInner()方法的功能類似,如下:

setSpinInner(index) {
  return {
    transform: 'rotate(-' + this.jiaodu + 'deg)',
    borderLeft: 0
  }
}

這裡的變數jiaodu是45,是根據360度/8個獎品的規則來的,用來調整獎品傾斜度。這裡還有一個spinCntDocObj物件,用來設定獎品圖片容器的尺寸,這個是為了在某些需求不是顯示獎品圖片,而是獎品名稱的時候,或者即顯示獎品名稱,又顯示獎品圖片的時候佈局方便,當然在這裡只顯示了一個獎品圖片。如下圖4

圖4

具體設定方法根據內圈直徑計算容器寬度,程式碼如下:

/* 圖片旋轉 */
setSpinCntDoc() {
  let spinCntDocWidth = (Math.sin((this.jiaodu / 2) * (Math.PI / 180)) * this.tableInnerSize) / 70
  this.spinCntDocObj.width = spinCntDocWidth + 'rem'
  this.spinCntDocObj.textAlign = 'center'
  this.spinCntDocObj.transform = 'rotate(' + (this.jiaodu / 2) + 'deg)'
}

3.4背景整體旋轉

跑馬燈有了,獎品也有了,剩下就是要背景整體旋轉起來了。細心的話,你會發現在所有獎品容器上有一個動態樣式:class="{ani: runningLock}",變數runningLock這個變數是用來控制大轉盤旋轉的,大轉盤中所有獎品容器如下圖5:

圖5

css類anti中包含一個animation動畫,如下:

  .ani {
    animation: circle 3s ease forwards;
  }

因為最後要根據中獎的獎品來計算大轉盤具體傾斜的角度,所以這個關鍵幀circle需要在請求介面之後,通過js程式碼動態載入到頁面上,下面講抽獎按鈕的時候會具體的說明。

3.5抽獎按鈕

接下來需要把抽獎按鈕放在大轉盤正中間,還是使用相對定位absolute來實現,html程式碼如下:

<!-- 抽獎按鈕 -->
<div
  class="arrowBtn"
  :class="{btnShake: btnShakeShow}"
  @click="startClick">
  <img src="../assets/images/summer/btn-draw.png" alt=""/>
</div>

css程式碼如下

.arrowBtn {
  width: 216px;
  height: 260px;
  border-radius: 95px;
  text-align: center;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: -108px;
  margin-top: -152px;
  img {
    width: 100%;
  }
}

在點選按鈕的時候也有一個動畫,就是按鈕會變大然後變小,看起開是彈了一下,這個就簡單了,使用animation動畫就好,這裡通過btnShakeShow變數來控制,css程式碼如下:

.btnShake {
  animation: btnShakeAni 0.5s ease-out forwards;
}
@keyframes btnShakeAni {
  0% {
    transform: scale(1);
  }
  10% {
    transform: scale(1.1);
  }
  30% {
    transform: scale(0.9);
  }
  50% {
    transform: scale(1.1);
  }
  70% {
    transform: scale(0.9);
  }
  90% {
    transform: scale(1.1);
  }
  100% {
    transform: scale(1);
  }
}

最後效果如下圖6

圖6

3.6點選抽獎

上面3.4講到在css類ani中使用animation動畫circle來控制整個大轉盤旋轉,並且要根據介面返回的抽獎結果計算旋轉角度動態設定rotate角度。程式碼如下

/* 點選抽獎播放動畫 */
startClick() {
  //大轉盤旋轉
  this.runningLock = true
  //抽獎按鈕彈一下
  this.btnShakeShow = true
  //調介面
  let data = {actCode: actCode}
  coc2.drawLottery(data).then(res => {
    if (res.code == 0) {
      if (res.data) {
        this.prizeNum = this.actPrizeList.findIndex((item, index) => item.pid == res.data.pid)
        this.prizeName = res.data.prizeName
        this.prizeImgSrc = res.data.litimgUrl
      }
    }
    //動態載入動畫關鍵幀
    let targetDeg = 360 * this.defaultRunTimes + (this.spinNum - this.prizeNum) * this.jiaodu + this.jiaodu * 0.5
    let runkeyframes = `@keyframes circle{ 0% {transform: rotate(0deg);} 100% {transform: rotate(${targetDeg}deg); }`
    document.getElementById('mystyle').innerHTML = runkeyframes
    setTimeout(() => {
      this.$refs.refAlert.show('getPrize')
    }, 3500)
  })
}

注意spinNum是轉盤中所有獎品個數,prizeNum是中獎獎品在整個獎品中陣列中的下標,二者做減法,然後頭部加上一個轉盤預設要轉圈數,尾部加上一個偏移(360度/8=45度)就可以定位到相應的位置的角度。隨後就是用這個角度拼接關鍵幀,最後動態設定這個css關鍵幀。注意要在index.html中加上一個id為mystyle的style元素,html如下:

圖7

整個動畫部分已經完成,來看看整體效果是怎麼樣的,如下圖7

圖7

3.7動畫復原&中獎彈框

最後還有一個問題,抽獎之後需要無論是否中獎都需要將轉盤復原到初始狀態,這個動作的觸發時機在中獎彈框彈出之後,這樣方便下一次抽獎。實現這個功能需要在點選獎品彈框的時候使用回撥的方式。最後中獎的獎品圖片和獎品名稱也需要通過屬性賦值傳遞給獎品彈框。上面程式碼中有的this.prizeName = res.data.prizeName;this.prizeImgSrc = res.data.litimgUrl就是在做這個事情。下面的html程式碼。

<!-- 中獎彈框 -->
<dialog-alert
  ref="refAlert"
  :prize-img-src="prizeImgSrc"
  :prize-name="prizeName"></dialog-alert>

在dialog-alert元件中,會有一個事件回撥,這裡使用的是eventbus,原因是這個元件在多個地方呼叫,這個和本文的主題關係不大 ,只簡單提一下。元件中回撥方法如下:

EventBus.$emit("turntableReset")

當前抽獎元件中監聽方法如下:

mounted() {
  // 彈窗關閉 重置大轉盤
  EventBus.$on("turntableReset", () => this.turnTableReset())
},
methods{
  //轉盤資料重置
  turnTableReset() {
    this.startLock = false
    this.runningLock = false
    this.btnShakeShow = false
  }
}

最後看看整體效果,如下圖8

圖8

4.總結

本功能還涉及到其他的功能,本功能實現的有些倉促,還有很多可以改進的地方。例如在播放動畫之前先請求了介面,等後端有了響應才開始播放動畫,這個不太合理,應該是先播放一個動畫,等有結果之後,再播放第二個動畫,讓指標指向中獎獎品。可以使用jquery動畫,或者tween.js,下次有時間再研究。

 

相關文章