前言
這是之前在玩遊戲的時候,發現平臺爲了這個遊戲做了一個九宮格的抽獎,雖然之前業務上沒有這方面的需求,但本人對這個很感興趣,在完成之後,現在有時間因此整理一下供有需求的同學參考;
簡介
代碼已上傳JQ的一個插件庫,下載地址是:http://www.jq22.com/yanshi22780;(不需要jq幣)
這是一個基於H5的小插件,使用的是jQuery,是移動端的,兼容性方面沒有考慮IE,因爲demo版本,因此沒有使用到圖片,純文字展示獎品(PS:當時因爲時間關係,代碼也沒有做一些優化,因此有些部分寫的還略顯粗糙不夠優雅,請多多見諒…);
演示:
正文開始
流程分析
最終效果是需要達到:點擊“開始”後,從左上角第一個方塊開始,圍繞開始按鈕有一個轉動的動畫,該動畫從開始到結束有一個加速->勻速->減速的過程,最終動畫停止在某個獎品上;
因此,整個過程大致的步驟有:
- 根據傳入的參數,創建9個div,並排列整齊;
- 點擊中間的“開始”按鈕,啓動加速動畫,同時,根據參數篩選出最終獎品;
- 啓動加速動畫,在某個時間段內不斷加速轉動;
- 啓動勻速動畫,在某個時間段內保持勻速轉動;
- 啓動減速動畫,在某個時間段內不斷減速,直至停到到最終獎品上;
創建插件
在正式開始寫抽獎之前,雖然寫的是小demo,但是當然還是要正規一點,功能什麼的還是齊全的,因此在當下當然都是插件化組件化的,因此這個小demo還是儘量做成插件,jq的插件寫法有很多種,我個人一直比較喜歡下面這種寫法:
//用一個立即執行函數將代碼包裹起來,防止變量污染
(function(root,func,plugin){
func(jQuery,plugin)
})(this,function(jQuery,plugin){
//給$的fn添加一個名爲prizeDraw的函數
$.fn[plugin] = function(params) {
}
},'prizeDraw')
PS:關於立即執行函數,閉包等知識點可以查看我的另外一篇文章《JavaScript》JavaScript進階知識點(一)
創建佈局
在這個階段,最終要實現的目標是:接收一個長度爲8的獎品數組爲參數,加上“開始”按鈕,一共9個方塊,創建出3*3的佈局,按鈕在正中間,獎品圍繞按鈕成一圈;
因此這個階段分爲兩個小節:
- 第一個小節:接收數組後,生成對應的獎品div,這些div加入DOM;
- 第二個小節:對加入DOM這些獎品div以及“開始”按鈕進行位置排列,最終達到圖上的顯示效果
創建DIV
(function(root,func,plugin){
func(jQuery,plugin)
})(this,function(jQuery,plugin){
const defaultName = '謝謝惠顧';
//獎品列表上限數量8
const maxListNumber = 8;
const puginID = 'oliverPrizeDraw';
const row = 3;
const column = 3;
let flg=false;
let _this_;
$.fn[plugin] = function(params) {
let _this = this;
if(!isObj(params)) throwError('傳入的參數必須是一個對象');
//此時的createDOMList僅僅是一個數組,尚未插入DOM樹中
let createDOMList = createDomListArr(params.prizeList);
//在createDOM()函數中創建DOM,並返回了中間“開始”按鈕的ID,這樣方便給按鈕添加點擊事件
let btn = createDOM(createDOMList,_this);
}
//根據參數,按順序創建了9個div,保存在了數組中返回
function createDomListArr(arr){
let newArr = [];
arr.forEach((el,index) => {
newArr.push(`<div data-id='${el.id}' index='${index}'>${el.name}</div>`)
})
return newArr;
}
//創建DOM
function createDOM(list,dom){
//創建了一個div,作爲外框的父級元素
let divContainer = `<div id='${puginID+'Container'}'></div>`;
//將外框加入頁面上
dom.append(divContainer);
//創建一個div用來存放8個抽獎元素
let div = `<div id='${puginID+'Box'}'></div>`;
//將用來存放抽獎元素的div加入外框下
$('#'+puginID+'Container').append(div);
//獲得到上面剛剛加入到頁面上用來存放抽獎元素的box
let box = $('#'+puginID+'Box');
//將上面生成的8個抽獎元素統統放進去,此時裏面的抽獎元素還沒有最終排列成需要的形態
box.append(list);
//獲得8個抽獎元素
let children = box.children();
//對每個抽獎元素的大小進行設定,每個方塊的大小都設定成父元素的三分之一
//裏面的-4是因爲需要做間隔
children.css({
'width':box.width()/row-4+'px',
'height':box.height()/column-4+'px',
'line-height':box.height()/column-4+'px',
})
}
//是否是個對象
function isObj(obj){
return Object.prototype.toString.call(obj) === '[object Object]';
}
},'prizeDraw')
當執行到這裏的時候,已經創建好了8個div,並且每個div的大小都已經設置成外框的三分之一大小了(其他具體的樣式,比如間隔,背景色等等都是通過css設置的);
開始按鈕和排列位置
當時的基本思路是,一個父級元素div作爲外框,下面有兩部分:
- 第一部分是中間的開始按鈕;
- 第二部分是一個div,這個div下有按鈕周圍一圈的獎品;
這兩部分一共9格,採用的都是絕對定位,只有絕對定位可以按照意願隨意擺放位置;
//創建DOM
function createDOM(list,dom){
/*
第一小節代碼放這裏
*/
//-----------------分割線--------------//
//對8個元素每個都執行一次位置排放
children.each((index,el)=>{
//第0-1個,也就是第1、第2個方塊
//離頂部的距離是0,每一個方塊相對前面一個方塊離左側的距離是多一個方塊的寬度
if(index >= 0 && index < (row-1)){
$(el).css({
'top':0,
'left':index*(box.width()/row)+'px'
})
}
//第3,第4個方塊,離右側的距離是0,離頂部是逐一多一個方塊的距離
//
else if(index>=row-1 && index<row+column-1){
$(el).css({
'right':0,
'top':(index-2)*(box.height()/column)+'px'
})
}
//第5,第6個方法,離底部的距離是0,離右側的距離逐一多一個方塊的距離
else if(index>=row-1+column&&index<(2*row+column-2)){
$(el).css({
'bottom':0,
'right':(index-(row+column-2))*(box.width()/row)+'px'
})
}
//第7,第8個,離左側的距離是0,距離頂部的距離是處於第二排,第三排
else{
$(el).css({
'top':(index-(2*(row+column)-6))*(box.height()/column)+'px',
'left':0
})
}
})
//創建開始按鈕
let startBtn = `<span id='${puginID+'StartBtn'}' class='start-btn start-btn-able'>開始</span>`;
//將按鈕加入DOM
box.parent().append(startBtn);
給按鈕設置樣式,將其置於正中間
$('#'+puginID+'StartBtn').css({
'width':box.width()/row-4+'px',
'height':box.height()/column-4+'px',
'line-height':box.height()/column-4+'px',
'left':box.width()/row+'px',
'top':box.height()/column+'px',
// 'transform':'translate(-50%,-50%)'
})
//返回按鈕
return {
startBtn:puginID+'StartBtn',
boxId:puginID+'Box'
}
}
篩選獎品和啓動轉動動畫
在這個階段,需要給開始按鈕綁定點擊事件,點擊後可以啓動轉動動畫,當然在啓動之前,必須對獎品數組進行一系列檢測,如果有錯誤進行簡單的處理,最終返回一個我們需要的抽獎數組,這個最終的抽獎數組才上面插入DOM的div,也就是說,實際上對傳入的參數進行檢測這一步應該放在排列位置之前;
有了最終的合法的抽獎數組,那麼就可以根據需求,篩選最終獎品;
檢測參數
$.fn[plugin] = function(params) {
let _this = this;
if(!isObj(params)) throwError('傳入的參數必須是一個對象');
//再創建之前,需要先對傳入的參數做一次檢測,如果參數錯誤,長度錯誤可以及時補齊或拋出異常
params.prizeList = finalList(params.prizeList);
//此時的createDOMList僅僅是一個數組,尚未插入DOM樹中
let createDOMList = createDomListArr(params.prizeList);
let btn = createDOM(createDOMList,_this);
}
//檢測參數
function finalList(arr){
//存在且必須是數組
if(!(arr && Array.isArray(arr))){
let newArr = [];
for(let i = 0 ; i < maxListNumber ; i++){
//數組每一項都有名字,id和概率
newArr.push({
name:defaultName,
id:puginID + i,
percent:100 / maxListNumber
});
}
//返回數組
return newArr;
}
//概率這一項必須是數字
arr.forEach((el)=>{
if(!isNumber(el.percent)){
throwError('獎品列表的percent的值必須是數字類型,當前ID爲:'+el.id+' 的percent值不是數字');
}
})
//獎品列表長度必須是8
if(arr.length === maxListNumber){
//概率的總和必須是100,不能8個方塊的概率加起來超出100了
let percent = resultPro(arr);
if(percent !== 100) throwError('獎品列表的概率和必須是100,當前是:'+percent);
return arr;
}
else{
//假如傳入的參數的長度超過了8,提示
let length = maxListNumber - arr.length;
length = length > 0 ? length : throwError('獎品列表的數量上限是:'+maxListNumber+',當前是:'+arr.length);
//當前概率和
let current = resultPro(arr);
if(current>100) throwError('獎品列表的概率和必須小於100,當前是:'+current);
for(let i = 0 ; i < length ; i++){
arr.push({
name:defaultName,
id:puginID + i,
percent:(100 - current) / length
})
}
//返回打亂的數組,不能是輸入的獎品順序是什麼就是什麼
return arr.sort(randomArr);
}
}
//判斷當前的獎品列表的概率總計
function resultPro(arr){
if(!Array.isArray(arr)) throwError('resultPro的參數必須是數組');
let result = 0;
arr.forEach((el) => {
el.percent && isNumber(el.percent)? result = result + el.percent : '';
})
return result;
}
//打亂數組
function randomArr(a,b){
return Math.random()>.5 ? -1 : 1;
}
//拋出異常
function throwError(val){
throw new Error(val)
}
等檢測完,此時返回的抽獎數組就是一個完整的獎品數組了,在第一步創建div的時候就可以按照這個順序直接創建div並排列;
點擊開始按鈕
到這一階段,就需要給開始按鈕添加點擊事件了,並且點擊後開始按鈕進入disable的狀態,不然連續點擊就會出現問題,並且,點擊“開始”按鈕後,會出現兩種情況:
- 存在指定獎品,也就是俗稱的黑幕,不管誰抽,都是謝謝惠顧之類的;
- 不存在指定獎品,那麼就會按照設定的概率進行抽獎;
$.fn[plugin] = function(params) {
let _this = this;
if(!isObj(params)) throwError('傳入的參數必須是一個對象');
//生成抽獎數組
params.prizeList = finalList(params.prizeList);
let createDOMList = createDomListArr(params.prizeList);
//創建DOM並返回按鈕ID
let btn = createDOM(createDOMList,_this);
let boxChinldren = btn.boxId;
$('#'+btn.startBtn).on('click',function(){
_this_ = this;
//通過flg開關,判斷是否可以點擊
if(!flg){
//添加不可點擊時的樣式;
$(_this_).addClass('start-btn-disable').removeClass('start-btn-able');
flg = true;
//判斷是否有指定獎品ID
//假如有指定獎品,那麼概率抽獎就不會生效,假如沒有,那麼就進行概率抽獎
if(params.finalPrizeID&&isString(params.finalPrizeID)){
//判斷一下設定的最終獎品id存不存在
//不要設定了一個獎品id,結果獎品列表中沒有,或者有2個及2個以上的獎品
let array = [];
params.prizeList.forEach(element => {
if(element.id === params.finalPrizeID){
array.push(element);
}
})
//根據數組長度進行判斷
switch(array.length){
//長度是0,那麼就說明指定的id不存在,那麼將進行概率抽獎
case 0:
console.log('指定的獎品ID在獎品列表中不存在,將按指定概率進行抽獎');
//這個是抽獎函數,下一大節解釋這個函數
targetPrize(params.prizeList,boxChinldren);
break
//長度是1,那麼這個就是正常情況,這個獎品就是最終獎品
case 1:
let name = array[0].name?array[0].name:'默認名字';
console.log('指定獎品爲:'+name);
targetPrize(array[0],boxChinldren);
break
//長度超出了1個,那麼就是存在多個相同id的最終獎品,其實也可以進行概率抽獎
default:
throwError('指定的最終獎品ID在獎品列表中不唯一');
break;
}
}
//沒有指定獎品,那麼就正常進行概率抽獎
else{
console.log('無指定獎品,將按指定概率進行抽獎');
targetPrize(params.prizeList,boxChinldren);
}
}
})
}
抽獎函數
在上一大節中,通過targetPrize函數,進行抽獎,其中第一個參數:
- 如果是一個對象,那麼就代表直接傳遞過來了最終獎品,那麼就不需要通過函數去計算哪個是最終獎品;
- 如果是一個數組,那麼代表使用者並沒有設定最終獎品,需要按照概率進行隨機抽取;
其中,如果是數組,大致上就是將數組中的每一項獎品的概率轉成一個區間,比如:第一個獎品的概率是10,那麼在100中,隨機數0-9指的就是這個獎品,如果第二個獎品的該也是10,那麼轉換後10-19就是這個獎品,第三個獎品的概率是30,那麼它所對應的區間就是20-59,以此類推;
這樣就將獎品的概率平鋪滿了100,到這裏的抽獎只需要隨機數一個0-100的數字,這個數字在哪個區間,就代表抽中了哪個獎品;
//執行抽獎,傳入的正常抽獎或者是指定獎品,dom列表
function targetPrize(params,dom){
//如果是對象,執行指定獎品,如果是數組,執行概率抽獎
isObj(params)?
pointPrize(params,dom):
Array.isArray(params)?
percentPrize(params,dom):
throwError('targetPrize()參數錯誤');
}
//指定獎品
function pointPrize(params,dom){
//指定產品的位置
let ax = targetIndex(params,dom)
console.log(params);
speedUp(dom,ax);
}
//概率抽獎
function percentPrize(params,dom){
//將概率轉成數組區間
let newArr = arrayPercent(params);
//獲得最終獎品
let percent = objPercent(newArr);
console.log(percent);
//指定產品的位置
let ax = targetIndex(percent,dom)
speedUp(dom,ax);
}
//將概率轉成區間數組
function arrayPercent(arr){
let sum = 0;
let newArr=[];
arr.forEach((el)=>{
el.percent&&isNumber(el.percent)?
newArr.push({
name:el.name,
id:el.id,
percent:[sum,(sum+el.percent)===100?(sum=sum+el.percent):(sum=sum+el.percent)-1]
}):'';
})
return newArr;
}
//選出一個隨機數並返回指定區間的對象
function objPercent(arr){
let radomNum = Math.floor(Math.random() * 100);
console.log(radomNum);
for(let el of arr){
if(radomNum >= el.percent[0] && radomNum <= el.percent[1]){
return el;
}
}
}
轉動動畫
轉動動畫是通過div的背景色的順時針的順序切換實現的,通過不斷的順時間變化給人一種轉動的感覺,而啓動動畫就是開始每一個變化的間隔都較長,然後變化的間隔逐漸變短,造成加速的視覺過程;減速同理,也就是間隔變長導致了轉速下降的視覺錯覺;
這是一個選中div的函數,因爲不管是加速,勻速,減速,都是需要選中div改變背景色,因此這個過程抽離了出來,方便在三個速度中調用
//選中dom對象
function runAnimate(dom){
if(i<7){
i++;
}
else{
i=0;
}
$('#'+dom).children().removeClass('selected').eq(i).addClass('selected');
}
加速動畫
//不管是概率抽獎還是指定獎品抽獎,最終都會執行到這個加速動畫
//設定時間
const normalTime = 50,maxTime=300;
//轉動次數
const normal = 2400/normalTime;
let timer,i,t;
let v;
const a=-20;
//執行動畫,加速
function speedUp(dom,index){
//將dom傳遞進去,當然也可以直接將dom寫進選中的函數裏
runAnimate(dom);
//如果間隔已經達到設定的值,那麼就執行勻速動畫函數
if(v<=normalTime){
v=normalTime;
clearTimeout(timer);
speedNormal(dom,index);
}
//else裏面則是間隔還沒有達到設定的要求,還需要繼續循環加速
else{
v = v+a;
timer = setTimeout(()=>{
speedUp(dom,index)
}, v);
}
}
勻速動畫
//執行動畫,勻速
function speedNormal(dom,index){
//選中div
runAnimate(dom);
//執行一段時間的勻速動畫,
timer = setTimeout(() => {
speedNormal(dom,index);
}, v);
//判斷執行勻速動畫的時間有沒有到達設定的值了
//轉動的時間大於或等於設定的值,那麼就代表可以開始執行減速動畫了
if(t >= normal){
clearTimeout(timer);
t=0;
speedDown(dom,index);
}
//還沒到達設定的時間,繼續執行勻速動畫
else{
t++;
}
}
減速動畫
減速動畫最爲複雜,因爲需要將最終的背景色變化停留在最終獎品上
//轉成DOM,減速
function speedDown(dom,index){
//選中dom
runAnimate(dom);
//判斷當前的時間還夠不夠轉一圈,如果夠,那麼轉完一圈再判斷
//當前的時間不夠再轉一圈了
if(v>=maxTime+8*a){
clearTimeout(timer)
timer = setTimeout(()=>{
//判斷i是不是最終的獎品
if(i == index){
//停止循環
clearTimeout(timer);
//可以按鈕可以再次點擊
flg = false;
//最終獎品的背景色變化
$(_this_).removeClass('start-btn-disable').addClass('start-btn-able');
}
//不是最終獎品,繼續執行減速動畫
else{
speedDown(dom,index)
}
},v)
}
//當前的總時間夠一圈
else{
v = v-a;
timer = setTimeout(() => {
speedDown(dom,index);
}, v);
}
}
總結
在這個小demo中,個人認爲的難點有以下幾個:
- 篩選最終獎品,其中需要對傳入的參數有一個預處理,因爲不能保證傳入的參數是完全符合要求的,因此需要做一個判斷,並返回一個最終需要的獎品數組;
- 獎品數組轉成概率區間,需要通過疊加將每個獎品的概率轉成到0-100之間的某一段,這樣可以通過隨機數判斷處於哪一段獎品內;
- 減速動畫,減速動畫需要判斷剩餘的時間,夠不夠再轉一圈,直至不夠轉一圈的時候需要判斷當前的方塊是不是最終獎品的那個方塊,如果是,那麼就需要停止繼續轉動;
如果這個demo有什麼錯誤或者其他疑問歡迎留言,本菜雞隨時等候~