Try   HackMD

React 幸運輪盤

Elantris


前言

接到一個活動頁面的製作需求,主體是一個幸運輪盤,有不少定位、動畫的目標要達成。

  1. 輪盤外框上有 16 個燈,奇數偶數要間隔 0.3 秒交替閃爍
  2. 正中央按鈕上的標題要來回放大縮小
  3. 按下按鈕後輪盤要轉動,最後要停在指定的位置上

定位方面大致上都用百分比當單位就能處理,每個元件的相對大小、相對位置,動畫的部分不外乎 animation 以及 transition,互動的部分就交給 js setTimeout 改變 class 即可。

實作解析

定位

整個輪盤的組成根據功能可以分為四個元件:

  1. 轉盤:放在圖層最下方,幸運輪盤中可以轉動的部分
  2. 外框、指針:視覺上的主體,固定不動
  3. 燈飾:因為需要交替閃爍所以拆成奇數燈、偶數燈兩張圖
  4. 按鈕:蓋在最上方可以自己放大縮小

請設計師切圖的時候指定素材尺寸都要是正方形,圓心對準正中間,如此一來定位就能用相同尺寸直接疊起來,後續輪盤在轉動的時候才不會跑版。

<div class="wrapper">
  <div class="plate"></div>
  <div class="frame"></div>
  <div class="light-1"></div>
  <div class="light-2"></div>
  <button id="go-button"></button>
</div>

交替閃爍的燈

要能反覆執行的動畫特效自然會用到 animation 以及 @keyframes 語法:

  1. 透過改變燈飾的 opacity 來達到開關燈的效果
  2. animation-delay 交替兩種燈飾的顯示
  3. animation-timing-function 決定漸變模式,如果用 linear 可以做到呼吸燈的效果、用 steps 則是瞬間切換
.light-1,
.light-2 {
  opacity: 0;
  animation-name: sparkling;
  animation-duration: 0.6s;
  animation-iteration-count: infinite;
  animation-timing-function: steps(2, jump-none);
}

.light-2 {
  animation-delay: 0.3s;
}

@keyframes sparkling {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

來回放大縮小按鈕標題

這個就相對簡單很多,置中、動畫,用圖片或背景實作皆可。

#go-button {
  all: unset;
  position: absolute;
  top: 51%;
  left: 50%;
  width: 20%;
  aspect-ratio: 88/58;
  background-image: url("https://i.imgur.com/8u1k7lO.png");
  background-size: contain;
  background-repeat: no-repeat;
  animation-name: zoom-in-out;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
  cursor: pointer;
}

@keyframes zoom-in-out {
  0% {
    transform: translate(-50%, -50%) scale(1);
  }
  50% {
    transform: translate(-50%, -50%) scale(1.5);
  }
  100% {
    transform: translate(-50%, -50%) scale(1);
  }
}

輪盤區塊定位

盤面上的數字是由 API 取得,所以轉盤上的定位就顯得格外重要,仔細觀察這個轉盤分為 10 個區塊,指針的位置在正右方,所以我的策略是把第一個區塊定位之後再將其餘 9 塊用旋轉的方式轉到各自的區塊上就能完成盤面數字的定位。

<div class="plate">
  <div class="sector-anchor">
    <div class="sector-0">0</div>
    <div class="sector-1">1</div>
    <div class="sector-2">2</div>
    <div class="sector-3">3</div>
    <div class="sector-4">4</div>
    <div class="sector-5">5</div>
    <div class="sector-6">6</div>
    <div class="sector-7">7</div>
    <div class="sector-8">8</div>
    <div class="sector-9">9</div>
  </div>
</div>

因為要用 transform: rotate() 如果 transform-origin 直接在圖片的中心就能節省很多麻煩,所以 .sector-anchor 實際上是一個重心位於圓心的長方形。

.sector-anchor {
  position: absolute;
  top: 50%;
  width: 100%;
  aspect-ratio: 10/2;
  transform: translateY(-50%) rotate(0deg);
  
  > * {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: end;
    padding-right: 22%;
    color: white;
    font-size: 20px;
    font-weight: bold;
    line-height: normal;
    font-family: Inter;
    text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.5);
  }
}

借用 SASS 迴圈語法替每個區塊作旋轉。

@for $i from 0 through 9 {
  .sector-#{$i} {
    transform: rotate(#{$i * -36}deg);
  }
}

輪盤轉動動畫

最後是轉盤轉動的特效,我的做法是

  1. 先知道轉盤結果要轉到哪一個區塊,就能得出需要轉多少角度
  2. 用 js 在 .plate 上增加一個 class .spinning-${i},指定 transition 以及 transform
  3. 一段時間後用 js 將 .spinning-${i} 替換為 .rotate-to-${i},此時不需要 transition 只需要 transform
@for $i from 0 through 9 {
  .plate.spinning-#{$i} {
    transition: transform 5s ease-in-out;
    transform: rotate(36deg * $i + 360deg * 5);
  }
  .plate.rotate-to-#{$i} {
    transform: rotate(36deg * $i);
  }
}
  1. 獲得 .spinning-${i} 後轉盤會花 5 秒的時間從當下的狀態轉動到指定的角度,也就是 5 圈 + 36 * i
  2. 移除 .spinning-${i}、獲得 .rotate-to-${i} 的瞬間因為沒有 transition 的關係,轉盤會直接跳到指定的角度,而視覺上 5 圈 + 36 * i36 * i 都是相同的位置,就能銜接下一次轉動動畫。

完整程式碼

Elantris - Lucky Wheel