# 幸運輪盤 CSS 分析 Elantris [TOC] --- ## 前言 接到一個活動頁面的製作需求,主體是一個幸運輪盤,有不少定位、動畫的目標要達成。 1. 輪盤外框上有 16 個燈,奇數偶數要間隔 0.3 秒交替閃爍 2. 正中央按鈕上的標題要來回放大縮小 3. 按下按鈕後輪盤要轉動,最後要停在指定的位置上 定位方面大致上都用百分比當單位就能處理,每個元件的相對大小、相對位置,動畫的部分不外乎 `animation` 以及 `transition`,互動的部分就交給 js `setTimeout` 改變 class 即可。 ## 實作解析 ### 定位 整個輪盤的組成根據功能可以分為四個元件: 1. **轉盤**:放在圖層最下方,幸運輪盤中可以轉動的部分 2. **外框、指針**:視覺上的主體,固定不動 3. **燈飾**:因為需要交替閃爍所以拆成奇數燈、偶數燈兩張圖 4. **按鈕**:蓋在最上方可以自己放大縮小 請設計師切圖的時候指定素材尺寸都要是正方形,圓心對準正中間,如此一來定位就能用相同尺寸直接疊起來,後續輪盤在轉動的時候才不會跑版。 ```html <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` 則是瞬間切換 ```css .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; } } ``` ### 來回放大縮小按鈕標題 這個就相對簡單很多,置中、動畫,用圖片或背景實作皆可。 ```css #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 塊用旋轉的方式轉到各自的區塊上就能完成盤面數字的定位。 ```html <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` 實際上是一個重心位於圓心的長方形。 ```css .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 迴圈語法替每個區塊作旋轉。 ```scss @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` ```scss @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 * i` 與 `36 * i` 都是相同的位置,就能銜接下一次轉動動畫。 ## 完整程式碼 [Elantris - Lucky Wheel](https://codepen.io/Elantris/pen/MYYewav)