开发者社区> 问答> 正文

H5 直播的疯狂点赞动画是如何实现的?(附完整源码)

直播有一个很重要的互动:点赞。

为了烘托直播间的氛围,直播相对于普通视频或者文本内容,点赞通常有两个特殊需求:

点赞动作无限次,引导用户疯狂点赞 直播间的所有疯狂点赞,都需要在所有用户界面都动画展现出来 我们先来看效果图:

1.jpg

展开
收起
请回答1024 2020-04-15 19:03:48 37067 0
1 条回答
写回答
取消 提交回答
  • 从效果图上我们还看到有几点重要信息:

    • 点赞动画图片大小不一,运动轨迹也是随机的
    • 点赞动画图片都是先放大再匀速运动。
    • 快到顶部的时候,是渐渐消失。
    • 收到大量的点赞请求的时候,点赞动画不扎堆,井然有序持续出现。

    那么如何实现这些要求呢?下面介绍两种实现方式来实现(底部附完整 demo):

    CSS3 实现

    用 CSS3 实现动画,显然,我们想到的是用 animation 。 首先看下 animation 合并写法,具体含义就不解释了,如果需要可以自行了解。

    animation: name duration timing-function delay iteration-count direction fill-mode play-state;
    
    

    我们开始来一步一步实现。

    Step 1: 固定区域,设置基本样式

    首先,我们先准备 1 张点赞动画图片:

    2.jpg

    看一下 HTML 结构。外层一个结构固定整个显示动画区域的位置。这里在一个宽 100px ,高 200px 的 div 区域。

    3.jpg

    4.jpg

    4.PNG

    5.jpg

    6.jpg

    7.jpg

    8.jpg

    9.jpg

    Step 5: 设置偏移

    我们先定义帧动画:bubble_1 来执行偏移。图片开始放大阶段,这里没有设置偏移,保持中间原点不变。 在运行到 25% * 4 = 1s,即 1s之后,是向左偏移 -8px, 2s 的时候,向右偏移 8px,3s 的时候,向做偏移 15px ,最终向右偏移 15px。 大家可以想到了,这是定义的一个经典的左右摆动轨迹,“向左向右向左向右” 曲线摆动效果。

    @keyframes bubble_1 {
        0% {
        }
        25% {
            margin-left:-8px;
        }
        50% {
            margin-left:8px
        }
        75% {
            margin-left:-15px
        }
        100% {
            margin-left:15px
        }
    }
    
    

    效果如下:

    10.jpg

    Step 6: 补齐动画样式

    这里预设了一种运行曲线轨迹,左右摆动的样式,我们在再预设更多种曲线,达到随机轨迹的目的。 比如 bubble_1 的左右偏移动画轨迹,我们可以修改偏移值,来达到不同的曲线轨迹。

    Step 7: JS 操作随机增加节点样式

    提供增加点赞的方法,随机将点赞的样式组合,然后渲染到节点上。

    let praiseBubble = document.getElementById("praise_bubble"); let last = 0; function addPraise() { const b =Math.floor(Math.random() * 6) + 1; const bl =Math.floor(Math.random() * 11) + 1; // bl1~bl11 let d = document.createElement("div"); d.className = bubble b${b} bl${bl}; d.dataset.t = String(Date.now()); praiseBubble.appendChild(d); } setInterval(() => { addPraise(); },300)

    在使用 CSS 来实现点赞的时候,通常还需要注意设置 bubble 的随机延时,比如:

    .bl2{ animation:bubble_2 $bubble_time linear .4s 1 forwards,bubble_big_2 $bubble_scale linear .4s 1 forwards,bubble_y $bubble_time linear .4s 1 forwards;
    }

    这里如果是随机到 bl2,那么延时 0.4s 再运行,bl3 延时 0.6s …… 如果是批量更新到节点上,不设置延时的话,那就会扎堆出现。随机“ bl ”样式,就随机了延时,然后批量出现,都会自动错峰显示。当然,我们还需要增加当前用户手动点赞的动画,这个不需要延时。 另外,有可能同时别人下发了点赞 40 个,业务需求通常是希望这 40 个点赞气泡都能依次出现,制造持续的点赞氛围,否则下发量大又会扎堆显示了。 那么我们还需要分批打散点赞数量,比如一次点赞的时间($bubble_time)是 4s, 那么 4s 内,希望同时出现多少个点赞呢?比如是 10个,那么 40 个点赞,需要分批 4 次渲染。

    window.requestAnimationFrame(() => {
         // 继续循环处理批次
         render();
     });
    
    

    另外还需要手动清除节点。以防节点过多带来的性能问题。如下是完整的效果图。

    11.jpg

    12.jpg

    class ThumbsUpAni{
        constructor(){
            const canvas = document.getElementById('thumsCanvas');
            this.context = canvas.getContext('2d')!;
            this.width = canvas.width;
            this.height = canvas.height;
        }
    }
    
    

    13.jpg

    Step 2:创建渲染对象

    实时渲染图片,使其变成一个连贯的动画,很重要的是:生成曲线轨迹。这个曲线轨迹需要是平滑的均匀曲线。

    假如生成的曲线轨迹不平滑的话,那看到的效果就会太突兀,比如上一个是 10 px,下一个就是 -10px,那显然,动画就是忽左忽右左右闪烁了。 理想的轨迹是上一个位置是 10px,接下来是 9px,然后一直平滑到 -10px,这样的坐标点就是连贯的,看起来动画就是平滑运行。

    随机平滑 X 轴偏移

    如果要做到平滑曲线,其实可以使用我们再熟悉不过的正弦( Math.sin )函数来实现均匀曲线。 看下图的正弦曲线:

    14.jpg

    这是 Math.sin(0) 到 Math.sin(9) 的曲线图走势图,它是一个平滑的从正数到负数,然后再从负数到正数的曲线图,完全符合我们的需求,于是我们再需要生成一个随机比率值,让摆动幅度随机起来。

    const angle = getRandom(2, 10);
    let ratio = getRandom(10,30)*((getRandom(0, 1) ? 1 : -1));
    const getTranslateX = (diffTime) => {
        if (diffTime < this.scaleTime) {// 放大期间,不进行摇摆偏移
            return basicX;
        } else {
            return basicX + ratio*Math.sin(angle*(diffTime - this.scaleTime));
        }
    };
    
    

    复制代码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 个波峰(如果时间短,出现三个波峰,就会运行过快,有闪烁现象)。如下图:

    15.jpg

    Y 轴偏移

    这个容易理解,开始 diffTime 为 0 ,所以运行偏移从 this.height --> image.height / 2。即从最底部,运行到顶部留下,实际上我们在顶部会淡化隐藏。

    const getTranslateY = (diffTime) => {
        return image.height / 2 + (this.height - image.height / 2) * (1-diffTime);
    };
    
    

    复制代码放大缩小

    当运行时间 diffTime 小于设置的 scaleTime 的时候,按比例随着时间增大,scale 变大。超过设置的时间阈值,则返回最终大小。

    const basicScale = [0.6, 0.9, 1.2][getRandom(0, 2)];
    const getScale = (diffTime) => {
        if (diffTime < this.scaleTime) {
            return +((diffTime/ this.scaleTime).toFixed(2)) * basicScale;
        } else {
            return basicScale;
        }
    };
    
    

    复制代码淡出

    同放大逻辑一致,只不过淡出是在运行快到最后的位置开始生效。

    **const fadeOutStage = getRandom(14, 18) / 100; const getAlpha = (diffTime) => { let left = 1 - +diffTime; if (left > fadeOutStage) { return 1; } else { return 1 - +((fadeOutStage - left) / fadeOutStage).toFixed(2); } }; **

    实时绘制

    创建完绘制对象之后,就可以实时绘制了,根据上述获取到的“偏移值”,“放大”和“淡出”值,然后实时绘制点赞图片的位置即可。

    每个执行周期,都需要重新绘制 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 = [0.6, 0.9, 1.2][getRandom(0, 2)];
    

    实时绘制扫描器

    开启实时绘制扫描器,将创建的渲染对象放入 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 < length) { 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++; } } }

    这里就是根据执行的时间来对比,判断动画执行到的位置了:

    diffTime = (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;
    }
    
    

    1111.jpg

    这里开启定时器,记录定时器里面处理的 thumbsStart 的值,如果有新增点赞,且定时器还在运行,直接更新最后的 praiseLast 值,定时器会依次将点赞请求全部处理完。 定时器的延时时间 time 根据开启定时器的时候,需要渲染多少点赞动画来决定的,比如需要渲染 100 个点赞动画,我们将 100 个点赞动画分布在 5s 内渲染完。

    • 对于热门直播,会同时渲染的动画很多,不会扎堆显示,且动画完全能衔接上,不停的冒泡点赞动画。
    • 对于冷门直播,有多余一个的点赞请求,我们能打散到 5s 内显示,也不会扎堆显示。

    End

    两种方式渲染点赞动画都已经完成,完整源码,源码戳这里

    源码运行效果图:

    16.jpg

    再比较

    这两种实现方式,都可以满足要求,那么到底哪种更优呢?

    我们来看下两者的数据对比。以下为未开启硬件加速的对比,采用不间断疯狂渲染点赞动画的数据对比:

    11111.jpg

    整体来说,差异如下:

    • CSS3 实现简单
    • Canvas 更灵活,操作更细腻
    • CSS3 内存消耗比 Canvas 大,如果开启硬件加速,内存消耗更大一些。
    2020-04-15 19:17:18
    赞同 1 展开评论 打赏
问答分类:
问答地址:
问答排行榜
最热
最新

相关电子书

更多
《优酷响应式布局技术全解析》 立即下载
如何做微信小程序性能优化 立即下载
ReactNative实战优化之路 立即下载