直播有一个很重要的互动:点赞。
为了烘托直播间的氛围,直播相对于普通视频或者文本内容,点赞通常有两个特殊需求:
我们先来看效果图:
从效果图上我们还看到有几点重要信息:
那么如何实现这些要求呢?下面介绍两种实现方式来实现(底部附完整 demo):
用 CSS3 实现动画,显然,我们想到的是用 animation 。
首先看下 animation 合并写法,具体含义就不解释了,如果需要可以自行了解。
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
我们开始来一步一步实现。
首先,我们先准备 1 张点赞动画图片:
看一下 HTML 结构。外层一个结构固定整个显示动画区域的位置。这里在一个宽 100px ,高 200px 的 div 区域。
<div class="praise_bubble">
<div class="bubble b1 bl1"></div>
</div>
.praise_bubble{
width:100px;
height:200px;
position:relative;
background-color:#f4f4f4;
}
.bubble{
position: absolute;
left:50%;
bottom:0;
}
使用 animation 的帧动画,定义一个 bubble_y 的帧序列。
.bl1{
animation:bubble_y 4s linear 1 forwards ;
}
@keyframes bubble_y {
0% {
margin-bottom:0;
}
100% {
margin-bottom:200px;
}
}
这里设置运行时间 4s ; 采用线性运动 linear,如果有需求当然也可以使用其他曲线,比如 ease; 每个点赞动画只运行 1 次; 动画是只需要向前 forwards。
渐隐效果,使用 opacity 即可。这里我们固定在最后 1/4 开始渐隐。修改 bubble_y:
@keyframes bubble_y {
0% {
margin-bottom:0;
}
75%{
opacity:1;
}
100% {
margin-bottom:200px;
opacity:0;
}
}
在最开始一小段时间,图片由小变大。
于是我们新增一个动画:bubble_big_1。
这里从 0.3 倍原图放大到 1 倍。这里注意运行时间,比如上面设置,从动画开始到结束总共是 4s,那么这个放大时间就可以按需设置了,比如 0.5s。
.bl1{
animation:bubble_big 0.5s linear 1 forwards;
}
@keyframes bubble_big_1 {
0% {
transform: scale(.3);
}
100% {
transform: scale(1);
}
}
我们先定义帧动画: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
}
}
效果图如下:
这里预设了一种运行曲线轨迹,左右摆动的样式,我们在再预设更多种曲线,达到随机轨迹的目的。
比如 bubble_1 的左右偏移动画轨迹,我们可以修改偏移值,来达到不同的曲线轨迹。
提供增加点赞的方法,随机将点赞的样式组合,然后渲染到节点上。
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();
});
另外还需要手动清除节点。以防节点过多带来的性能问题。如下是完整的效果图。
这个很容易理解,直接在 canvas 上绘制动画就行,如果不了解 canvas 的,可以后续学习下。
页面元素上新建 canvas 标签,初始化 canvas。
canvas 上可以设置 width 和 height 属性,也可以在 style 属性里面设置 width 和 height。
<canvas id="thumsCanvas" width="200" height="400" style="width:100px;height:200px"></canvas>
页面上一个宽 200,高 400 的 canvas 画布,然后整个 canvas 显示在 页面 宽 100,高 200 的区域内。canvas 画布的内容被等比缩小一倍显示在页面。
定义一个点赞类,ThumbsUpAni,构造函数就是读取 canvas,保存宽高值。
class ThumbsUpAni{
constructor(){
const canvas = document.getElementById('thumsCanvas');
this.context = canvas.getContext('2d')!;
this.width = canvas.width;
this.height = canvas.height;
}
}
将需要随机渲染的点赞图片,先预加载,获得图片的宽高,如果有下载失败的,则不显示该随机图片即可。没啥说的,简单易懂。
loadImages(){
const images = [
'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',
'jfs/t1/102954/26/9241/5069/5e0aea05E7dde8bda/720fcec8bc5be9d4.png'
];
const promiseAll = [] as Array<Promise<any>>;
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;
}
})
}
实时渲染图片,使其变成一个连贯的动画,很重要的是:生成曲线轨迹。这个曲线轨迹需要是平滑的均匀曲线。假如生成的曲线轨迹不平滑的话,那看到的效果就会太突兀,比如上一个是 10 px,下一个就是 -10px,那显然,动画就是忽左忽右左右闪烁了。
理想的轨迹是上一个位置是 10px,接下来是 9px,然后一直平滑到 -10px,这样的坐标点就是连贯的,看起来动画就是平滑运行。
如果要做到平滑曲线,其实可以使用我们再熟悉不过的正弦( Math.sin )函数来实现均匀曲线。
看下图的正弦曲线:
这是 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 个波峰(如果时间短,出现三个波峰,就会运行过快,有闪烁现象)。如下图:
这个容易理解,开始 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;
}
当接收到大量的点赞数据,且连续多次点赞(直播间人气很旺的时候)。那么点赞数据的渲染就需要特别注意了,否则页面就是一坨一坨的点赞动画。且衔接不紧密。
thumbsUp(num: number) {
if (num <= this.praiseLast) return;
this.thumbsStart = this.praiseLast;
this.praiseLast = num;
if (this.thumbsStart + 500 < num)
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 内渲染完。
两者运行效果图:
两种方式渲染点赞动画都已经完成,完整源码,源码戳这里 https://github.com/antiter/praise-animation 。
这里还可以体验线上点赞动画,戳这里: https://wqs.jd.com/pglive/index.html
这两种实现方式,都可以满足要求,那么到底哪种更优呢?
我们来看下两者的数据对比。以下为未开启硬件加速的对比,采用不间断疯狂渲染点赞动画的数据对比:
整体来说,差异如下:
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。