~~~ L O A D I N G ~~~~~ L O A D I N G ~~~~~ L O A D I N G ~~~

JS地下城 - 幸運輪盤

Jan 14, 2020 Max Lee

本篇為六角學院 - JS地下城攻略文 GithubDemo


確認需求

  • 根據資料產生畫面
  • 抽獎機率需考慮獎項數量而非種類
  • 輪盤指針的旋轉角度
  • 抽中的獎品減少並不再被抽到

這次比較需要思考的應該會是機率的計算以及指針旋轉的度數計算。


解題攻略

# 扇形切版

純 CSS 要做出扇型一直都是蠻麻煩的,網上其實都查得到很多作法,這邊提供我的結構以及樣式。

<div class="fan"><div class="inner"></div></div>
.fan {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  overflow: hidden;
  clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%)
}
.inner {
  width: 50%;
  height: 100%;
  background: #5858B9;
  transform-origin: right center;
  transform: rotate(60deg);
}

我是使用 clip-path 搭配 rotate 來做到這樣的效果,旋轉幾度這個扇形就會是幾度,最後只要根據需求來跑相對應的迴圈次數,為 .fan 也加上 rotate 就可以將扇形拼成一個圓了。


# 資料管理

網站其實就是一堆的資料的顯示與互動。以本題目為例,每年老闆都會提供一份「尾牙獎品清單」這樣的資料,而我們的工作便是確保畫面與資料的一致性,所以每當有獎品增減,我們都要去改動畫面並且確認功能沒有因此而壞掉,這樣其實是非常繁複沒有效率的,所以我們要設計成由「資料驅動畫面」,未來就算資料更動了,我們也不用特地去調整畫面。

首先要做的第一步就是資料管理,而一般網頁上最常見的資料格式就是 JSON,我們可以把獎品清單整理成以下格式:

[
  {
    "text": "Movie",
    "icon": "<i class='material.icons'>movie_filter</i>",
    "num": 0
  },
  {
    "text": "Wish",
    "icon": "<i class='material.icons'>cake</i>",
    "num": 5
  },
  {
    "text": "Anything",
    "icon": "<i class='material.icons'>star</i>",
    "num": 5
  },
  {
    "text": "Child",
    "icon": "<i class='material.icons'>child_care</i>",
    "num": 4
  },
  {
    "text": "Flight",
    "icon": "<i class='material.icons'>flight</i>",
    "num": 1
  },
  {
    "text": "wifi",
    "icon": "<i class='material.icons'>wifi</i>",
    "num": 5
  },
]

接著把 JSON 資料存成 .json 檔,然後再用 AJAX 去取得資料。

let data;
$.get('db.json', function(res) { 
  data = res 
};

JSON 資料會是一個陣列,所以我們可以使用一些陣列的方法來處理它,像是用 forEach 迴圈來新增元素:

// 根據資料長度計算每個扇型要占多少度數
let preAngle = 360 / data.length;
let wheel = $('.wheel');

// 跑forEach迴圈
data.forEach((item, index) => {
  let fan = $('<div class="fan"></div>');
  let inner = $('<div class="inner"></div>');

  fan.css('transform', `rotate(${preAngle * index}deg)`);
  inner.css('transform', `rotate(${preAngle}deg)`);

  fan.append(inner);
  wheel.append(fan);
});

現在程式就會根據資料來製作輪盤了,不管資料是多是少都能自動計算並產生畫面,當然我裡面省略的很多,例如文字和圖示的顯示以及扇型起始角度的調整,不過相信各位勇者都能舉一反三的 !


# 機率處理

再來是處理機率的問題,我們要先知道「抽獎機率需考慮獎項數量而非種類」是甚麼意思,假設獎項種共有 6 種,但是數量是 25 個,那每個獎品被抽到的機率應該是 1/25 而不是 1/6,所以數量越多的獎品應該要越容易被抽中。

為了有一個符合自然機率的抽獎系統,我們需要製作一個虛擬的抽獎箱,把所有的獎項給放進去。

let preAngle = 360 / data.length;
let wheel = $('.wheel');
// 宣告一個陣列
let giftBox = []

data.forEach((item, index) => {
  // 在forEach中加上迴圈,讓每個獎品根據其數量往giftBox添加
  for (let i = 0; i < item.num; i++) {
   giftBox.push(index)
  }

  let fan = $('<div class="fan"></div>');
  let inner = $('<div class="inner"></div>');

  fan.css('transform', `rotate(${preAngle * index}deg)`);
  inner.css('transform', `rotate(${preAngle}deg)`);

  fan.append(inner);
  wheel.append(fan);
});

迴圈跑完後的 giftBox 應該會長這個樣子...

console.log(giftBox)
// [1,1,1,1,1,2,2,2,2,2,3,3,3,3,4,5,5,5,5,5]

之後我們可以設計成,按下按鈕後就從陣列 pop 出一個獎品就可以了。 不過現在的獎項排列太整齊了,沒有隨機性,所以我們可以在每次抽獎前先打亂順序,就像是把抽獎箱搖一搖:

let shuffle = function(a, b) {
  return Math.random() > 0.5 ? -1 : 1;
};
$("button").click(function(){
  let giftIndex = giftBox.sort(shuffle).pop();
});

這樣抽獎功能就符合自然機率了,而且因為 pop 會實際改動陣列本身,所以被抽走的獎品自然就從陣列裡移除不會再被抽到囉。


# 指針旋轉

抽出獎項後我們要讓指針轉動,沒錯,先抽獎再轉指針,畫面只是一種氣氛啦!

還記得我們剛剛亂數抽出的 giftIndex 嗎?它代表的是 JSON 資料裡的索引,只要拿去陣列裡查詢就可以知道抽到的是什麼獎品,而且 giftIndex 乘以先前計算的 preAngle 就可以知道指針要轉多少角度了。

let gift;
let startAngle;
$("button").click(function(){
  // 舉個例子:假如我們亂數抽出的gift是2
  // 那可以知道抽出的是data[2]的獎品,也就是Anything
  let giftIndex = giftBox.sort(shuffle).pop();
  gift = data[giftIndex];

  // 計算指針要旋轉多少角度才會指到Anything
  let goAngle = giftIndex * preAngle;
  // 為了讓使用者有臨場感,刻意多加幾圈
  goAngle = goAngle + 360 * 5;
  // 為了讓指針不必每次都歸零才旋轉,所以我們加上前一次旋轉的度數
  goAngle = goAngle + startAngle

  // 把這次的角度給記下來,
  startAngle = giftIndex * preAngle;

  // 最後為指針設定樣式
  $('.hand').css({transform: `rotate(${goAngle}deg)`})
});

這樣主要的幾個重點需求就都完成了,過程中我有省略了一些部分,如果有問題的話再可以看看完整的 程式碼。 相信各位勇者都是非常聰慧的!加油!