國際慣例先來看一下最後的實現效果~
專案背景(扯淡)
世界盃期間公司拉了一批啤酒的贊助,有一個邀請(pian)好友來領(mai)會員灌啤酒的分享活動,達到8瓶酒就可以得一箱。聽起來很是誘人,畢竟世界上最愜意的事情莫過於看看球,喝喝酒,撩撩妹(並沒有)。
準備工作
我的任務呢就是把灌酒的這個操作弄的炫酷一點,畢竟我也是個有藝術細菌的人,立馬就構思好了搖晃和酒上升的動畫。這點小事難不倒我,擼起袖子,反手就開啟了 codepen,搜尋關鍵字 "beer shaking", "beer pouring",經過一番苦苦搜尋,終於找到了一個滿意的例子。emmmmm我可真是個小機靈鬼,這下改改程式碼就好了。
轉折
正在我洋洋得意之時,拿到了對應的設計稿。WTF ???這酒瓶是個圖?這個泡沫還要動?這圖裡面的酒還要能上升?來來來,你行你上。。。這下之前所有的美好幻想都泡湯了,由於酒瓶是定製的圖片,無法自定義酒的顏色和高度,就算採用絕對定位也很難和諧,這還得做動畫,我做!@#¥%……&*不行,我得冷靜一下,我能想出辦法的!
實現
終於迎來了正文(我真的不是個話癆),經過我的一番思索,發現難點主要在這三個方面:
1.啤酒泡沫動畫
2.啤酒上升的動畫
3.一個酒瓶的上升動畫完成之後才能開始下一個啤酒的動畫
前兩個問題由於這次的酒瓶是定製的,所以不可能用 CSS 自己畫出來,只能用替換圖片(GIF)的方式,即動態替換 img 標籤的 src,實踐下來,這樣實現的效果不錯,看不出切換的痕跡,當然如果嫌網速慢的話,可以提前預載入要替換的所有圖片進行快取,這樣切換起來更加流暢。
至於最後一個問題,仔細想想是不是很像一個東西?在一件事情完成之後再去做另一個,這tm不就是 Promise 嗎?,廢話不說直接上程式碼:
// 首先定義10張圖片的地址
const pics = [
'1.gif',
'2.gif',
'3.gif',
'4.gif',
'5.gif',
'6.gif',
'7.gif',
'8.gif',
'9.gif',
'10.png'
];
const pouredQuantity = 2.1; // 已經灌了的酒瓶數量
const pourQuantity = 0.2; // 本次灌了的酒的數量
const max = Math.ceil(pouredQuantity);
const startIndex = max === pouredQuantity ? max : max - 1; // 開始灌酒的酒瓶序號
/**
* 灌酒遞迴方法
* @param {Number} index 當前啤酒序號
* @param {Number} leftQuantity 本次還剩多少可以灌的酒
* @param {Number} total 總共酒量
*/
function recursion(index, leftQuantity, total) {
if (leftQuantity === 0) {
// 可以執行動畫完成後的回撥
return;
}
const decimal = total - index === 0 ? 0 : calc(total, -index);
new Promise(resolve => {
const start = decimal === 0 ? 1 : decimal * 10 + 1;
const end =
decimal + leftQuantity >= 1
? pics.length
: calc(decimal, leftQuantity) * 10;
pourAnimation($bottles[index], start, end, resolve);
}).then(() => {
index++;
const left =
decimal + leftQuantity > 1 ? calc(leftQuantity, -calc(1, -decimal)) : 0;
recursion(index, left, calc(total, calc(leftQuantity, -left)));
});
}
recursion(startIndex, pourQuantity, pouredQuantity);
/**
* 灌酒動畫
* @param {Element} ele
* @param {Number} start
* @param {Number} end
* @param {Function} resolve
*/
function pourAnimation(ele, start, end, resolve) {
let index = start - 1;
(function loop() {
ele.src = pics[index];
index++;
if (index < end) {
setTimeout(loop, 300);
} else {
resolve();
}
})();
}
/**
* 計算兩個數的和 / 差,保留一位小數
* @param {Number} a
* @param {Number} b
*/
function calc(a, b) {
return parseFloat((a + b).toFixed(1));
}
複製程式碼
除了幾個輔助方法外,核心程式碼都在遞迴的方法裡,主要思路是先對啤酒的容量分級(這裡是10等分),在一個啤酒的動畫完成之後,呼叫 Promise 的 resolve 方法然後再去執行第下一個啤酒的動畫,動畫的實現過程就是用 setTimeout 去替換啤酒的圖片而已。
總結
其實呢,任何問題想通之後也就那麼回事,大多數問題都是在特定場景下尋求解決方案,那麼我們需要做的就是先儘量思考問題的本質,想想痛點難點在哪,再去抽象化問題的層次,建模,那麼慢慢,你會發現大部分問題你都曾遇到並解決過,問題也就不再是問題啦~(BTW,文中演算法沒怎麼精簡過,如果有更好的方法也可提出探討)