国际惯例先来看一下最后的实现效果~
项目背景(扯淡)
世界杯期间公司拉了一批啤酒的赞助,有一个邀请(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,文中算法没怎么精简过,如果有更好的方法也可提出探讨)