直播有一个非常重要的互动:赞。
为了烘托直播间的氛围,直播相对于普通视频或者文本内容,点赞通常有两个特殊需求:
- 点赞动作无限次,引导用户疯狂点赞
- 直播间所有疯狂点赞都需要在所有用户界面上展示动画(广播用户使用)websocket消息)
先来看效果图:
我们还可以从效果图中看到一些重要信息:
- 喜欢不同大小的动画图片,运动轨迹也是随机的
- 喜欢动画图片是先放大再匀速运动。
- 当它接近顶部时,它会逐渐消失。
- 当收到大量的赞美请求时,赞美动画并不聚集在一起,井然有序地继续出现。
那么如何实现这些要求呢?以下是两种实现方法(底部附着完整) demo):
CSS3 实现
用 CSS3 显然,我们想到的是实现动画 animation 。
首先看下 animation 不解释合并写法的具体含义。如果需要,可以自己理解。
animation:
我们开始一步一步地实现它。
Step 1: 固定区域,设置基本样式
首先,我们先做好准备 1 张点赞动画图片:
看一下 HTML 结构。外层的一个结构固定了整个显示动画区域的位置。一个宽的地方 100px ,高 200px 的 div 区域。
<divclass="praise_bubble">
.praise_bubble{
Step 2: 运动起来
使用 animation 帧动画,定义一个 bubble_y 的帧序列。
.bl1{
运行时间设置在这里 4s ;线性运动 linear,当然,如果需要,也可以使用其他曲线,比如 ease;每一赞动画只运行 1 次;动画只需要前进 forwards。
Step 3: 增加渐隐
渐隐效果,使用 opacity 就在这里,我们把它固定在最后 1/4 开始隐藏 bubble_y:
keyframesbubble_y{
Step 4: 增加动画放大效果
一开始,图片由小变大。
于是我们又加了一个动画:bubble_big_1。
这里从 0.3 将倍原图放大到 1 倍。注意这里的运行时间,比如上面的设置,从动画开始到结束。 4s,然后可以根据需要设置这个放大时间,比如 0.5s。
.bl1{
Step 5: 设置偏移
先定义帧动画:bubble_1 执行偏移。图片开始放大,这里没有偏移,保持中间原点不变。
在运行到 25% * 4 = 1s,即 1s之后,向左偏移 -8px, 2s 向右偏移 8px,3s 向做偏移 15px ,最后向右偏移 15px。
你可以想到,这是左右摆动轨迹的经典定义,左右左右 曲线摆动效果。
keyframesbubble_1{
效果图如下:
Step 6: 补充动画风格
这里预设了运行曲线轨迹,左右摆动,我们预设了更多的曲线,以达到随机轨迹的目的。
比如 bubble_1 对于左右偏移动画轨迹,我们可以修改偏移值,以实现不同的曲线轨迹。
Step 7: JS 随机操作增加节点样式
提供添加拇指的方法,随机组合拇指的风格,然后渲染到节点。
letpraiseBubble=
在使用 CSS 为了实现表扬,通常需要注意设置 bubble 例如:
.bl2{
如果是随机的话 bl2,那么延时 0.4s 再运行,bl3 延时 0.6s ……
如果批量更新到节点,不设置延迟,就会聚在一起。 bl 风格,随机延迟,然后批量出现,将自动显示错误的峰值。我们还需要添加当前用户手动拇指的动画,这不需要延迟。
此外,其他人可能会同时发表赞扬 40 业务需求通常是希望的 40 一个点赞气泡可以依次出现,营造持续的点赞氛围,否则发量大就会聚在一起。
然后我们还需要分批打散点赞的数量,比如一次点赞的时间($bubble_time)是 4s, 那么 4s 内,希望同时出现多少点赞?比如是吗? 10个,那么 40 点赞,需要分批 4 次渲染。
window.requestAnimationFrame(
还需要手动清除节点。防止节点过多造成的性能问题。以下是完整的渲染。
Canvas 绘图实现
这很容易理解,直接 canvas 只需绘制动画即可。如果你不知道 canvas 是的,可以后续学习。
Step 1:初始化
新建页面元素 canvas 标签,初始化 canvas。
canvas 上可以设置 width 和 height 属性,也可以 style 设置属性 width 和 height。
- canvas 上 style 的 width 和 height 是 canvas 在浏览器中渲染的高度和宽度,即页面中的实际宽度。
- canvas 标签的 width 和 height 是画布的实际宽度和高度。
<canvasid="thumsCanvas"width="200"height="400"style="width:100px;height:200px">canvas>
页面上的宽度 200,高 400 的 canvas 然后整个画布 canvas 显示在 页面 宽 100,高 200 的区域内。canvas 页面上显示的画布内容等比缩小了一倍。
定义点赞类,ThumbsUpAni,构造函数是读取 canvas,保存宽高值。
classThumbsUpAni{
Step 2:提前加载图片资源
先预加载需要随机渲染的喜欢图片,获得图片的宽度和高度。如果下载失败,可以不显示随机图片。没什么好说的,简单易懂。
loadImages(){
constimages=['jfs/t1/93992/8/9049/4680/5e0aea04Ec9dd2be8/608efd890fd61486.png','jfs/t1/108305/14/2849/4908/5e0aea04Efb54912c/bfa59f27e654e29c.png','jfs/t1/98805/29/8975/5106/5e0aea05Ed970e2b4/98803f8ad07147b9.png','jfs/t1/94291/26/9105/4344/5e0aea05Ed64b9187/5165fdf5621d5bbf.png','jfs/t1/102753/34/8504/5522/5e0aea05E0b9ef0b4/74a73178e31bd021.png',&bsp; 'jfs/t1/102954/26/9241/5069/5e0aea05E7dde8bda/720fcec8bc5be9d4.png' ]; const promiseAll = [] as Array<Promise>; images.forEach((src) => {
const p = new Promise(function (resolve) {
const img = new Image; img.onerror = img.onload = resolve.bind(null, img); img.src = 'https://img12.360buyimg.com/img/' + src; }); promiseAll.push(p); });Promise.all(promiseAll).then((imgsList) => {
this.imgsList = imgsList.filter((d) => {
if (d && d.width > 0) return true;return false; });if (this.imgsList.length == 0) {
logger.error('imgsList load all error');return; } })}
Step 2:创建渲染对象
实时渲染图片,使其变成一个连贯的动画,很重要的是:生成曲线轨迹。这个曲线轨迹需要是平滑的均匀曲线。假如生成的曲线轨迹不平滑的话,那看到的效果就会太突兀,比如上一个是 10 px,下一个就是 -10px,那显然,动画就是忽左忽右左右闪烁了。
理想的轨迹是上一个位置是 10px,接下来是 9px,然后一直平滑到 -10px,这样的坐标点就是连贯的,看起来动画就是平滑运行。
随机平滑 X 轴偏移
如果要做到平滑曲线,其实可以使用我们再熟悉不过的正弦( Math.sin )函数来实现均匀曲线。
看下图的正弦曲线:
这是 Math.sin(0) 到 Math.sin(9) 的曲线图走势图,它是一个平滑的从正数到负数,然后再从负数到正数的曲线图,完全符合我们的需求,于是我们再需要生成一个随机比率值,让摆动幅度随机起来。
const angle = getRandom(
scaleTime 是从开始放大到最终大小,用多长时间,这里我们设置 0.1,即总共运行时间前面的 10% 的时间,点赞图片逐步放大。
diffTime,是只从开始动画运行到当前时间过了多长时间了,为百分比。实际值是从 0 --》 1 逐步增大。diffTime - scaleTime = 0 ~ 0.9, diffTime 为 0.4 的时候,说明是已经运行了 40% 的时间。
因为 Math.sin(0) 到 Math.sin(0.9) 曲线几乎是一个直线,所以不太符合摆动效果,从 Math.sin(0) 到 Math.sin(1.8) 开始有细微的变化,所以我们这里设置的 angle 最小值为 2。
这里设置角度系数 angle 最大为 10 ,从底部到顶部运行两个波峰。
当然如果运行距离再长一些,我们可以增大 angle 值,比如变成 3 个波峰(如果时间短,出现三个波峰,就会运行过快,有闪烁现象)。如下图:
Y 轴偏移
这个容易理解,开始 diffTime 为 0 ,所以运行偏移从 this.height --> image.height / 2。即从最底部,运行到顶部留下,实际上我们在顶部会淡化隐藏。
const getTranslateY =
放大缩小
当运行时间 diffTime 小于设置的 scaleTime 的时候,按比例随着时间增大,scale 变大。超过设置的时间阈值,则返回最终大小。
const basicScale = [
淡出
同放大逻辑一致,只不过淡出是在运行快到最后的位置开始生效。
const fadeOutStage = getRandom(
实时绘制
创建完绘制对象之后,就可以实时绘制了,根据上述获取到的“偏移值”,“放大”和“淡出”值,然后实时绘制点赞图片的位置即可。
每个执行周期,都需要重新绘制 canvas 上的所有的动画图片位置,最终形成所有的点赞图片都在运动的效果。
createRender(){
return (diffTime) => {
// 差值满了,即结束了 0 ---》 1 if(diffTime>=1) return true; context.save(); const scale = getScale(diffTime); const translateX = getTranslateX(diffTime); const translateY = getTranslateY(diffTime); context.translate(translateX, translateY); context.scale(scale, scale); context.globalAlpha = getAlpha(diffTime); // const rotate = getRotate(); // context.rotate(rotate * Math.PI / 180); context.drawImage( image, -image.width / 2, -image.height / 2, image.width, image.height ); context.restore(); };}
这里绘制的图片是原图的 width 和 height。前面我们设置了 basiceScale,如果图片更大,我们可以把 scale 再变小即可。
const basicScale = [
实时绘制扫描器
开启实时绘制扫描器,将创建的渲染对象放入 renderList 数组,数组不为空,说明 canvas 上还有动画,就需要不停的去执行 scan,直到 canvas 上没有动画结束为止。
scan() {
this.context.clearRect(0, 0, this.width, this.height); this.context.fillStyle = "#f4f4f4"; this.context.fillRect(0,0,200,400); let index = 0; let length = this.renderList.length; if (length > 0) {
requestAnimationFrame(this.scan.bind(this)); } while (index const render = this.renderList[index]; if (!render || !render.render || render.render.call(null, (Date.now() - render.timestamp) / render.duration)) {
// 结束了,删除该动画 this.renderList.splice(index, 1); length--; } else {
// 当前动画未执行完成,continue index++; } }}
这里就是根据执行的时间来对比,判断动画执行到的位置了:
Date.now() - render.timestamp) / render.duration
如果开始的时间戳是 10000,当前是100100,则说明已经运行了 100 毫秒了,如果动画本来需要执行 1000 毫秒,那么 diffTime = 0.1,代表动画已经运行了 10%。
增加动画
每点赞一次或者每接收到别人点赞一次,则调用一次 start 方法来生成渲染实例,放进渲染实例数组。如果当前扫描器未开启,则需要启动扫描器,这里使用了 scanning 变量,防止开启多个扫描器。
start() {
const render = this.createRender(); const duration = getRandom(1500, 3000); this.renderList.push({
render, duration, timestamp: Date.now(), }); if (!this.scanning) {
this.scanning = true; requestFrame(this.scan.bind(this)); } return this;}
保持不扎堆
当接收到大量的点赞数据,且连续多次点赞(直播间人气很旺的时候)。那么点赞数据的渲染就需要特别注意了,否则页面就是一坨一坨的点赞动画。且衔接不紧密。
thumbsUp(num: number) {
if (num <= this.praiseLast) return; this.thumbsStart = this.praiseLast; this.praiseLast = num; if (this.thumbsStart + 500 this.thumbsStart = num - 500; const diff = this.praiseLast - this.thumbsStart; let time = 100; let isFirst = true; if (this.thumbsInter != 0) {
return; } this.thumbsInter = setInterval(() => {
if (this.thumbsStart >= this.praiseLast) {
clearInterval(this.thumbsInter); this.thumbsInter = 0; return; } this.thumbsStart++; this.thumbsUpAni.start(); if (isFirst) {
isFirst = false; time = Math.round(5000 / diff); } }, time); },
这里开启定时器,记录定时器里面处理的 thumbsStart 的值,如果有新增点赞,且定时器还在运行,直接更新最后的 praiseLast 值,定时器会依次将点赞请求全部处理完。
定时器的延时时间 time 根据开启定时器的时候,需要渲染多少点赞动画来决定的,比如需要渲染 100 个点赞动画,我们将 100 个点赞动画分布在 5s 内渲染完。
- 对于热门直播,会同时渲染的动画很多,不会扎堆显示,且动画完全能衔接上,不停的冒泡点赞动画。
- 对于冷门直播,有多余一个的点赞请求,我们能打散到 5s 内显示,也不会扎堆显示。
End
两者运行效果图:
两种方式渲染点赞动画都已经完成,完整源码,源码戳这里 https://github.com/antiter/praise-animation 。
这里还可以体验线上点赞动画,戳这里: https://wqs.jd.com/pglive/index.html
再比较
这两种实现方式,都可以满足要求,那么到底哪种更优呢?
我们来看下两者的数据对比。以下为未开启硬件加速的对比,采用不间断疯狂渲染点赞动画的数据对比:
整体来说,差异如下:
- CSS3 实现简单
- Canvas 更灵活,操作更细腻
- CSS3 内存消耗比 Canvas 大,如果开启硬件加速,内存消耗更大一些。
支持
如果 你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:点个「」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
关注我的官网
uyiy.cn ,让我们成为长期关系关注公众号「
高级前端进阶 」,公众号后台回复「面试题」 送你高级前端面试题,回复「加群」加入面试互助交流群
》》面试官都在用的题库,快来看看《《