用 Promise + 遞迴實現灌酒動畫

xinkule發表於2018-07-23

國際慣例先來看一下最後的實現效果~

pour

專案背景(扯淡)

世界盃期間公司拉了一批啤酒的贊助,有一個邀請(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,文中演算法沒怎麼精簡過,如果有更好的方法也可提出探討)

相關文章