腾讯课堂 H5 直播间点赞动效实现
Posted 趣谈前端
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了腾讯课堂 H5 直播间点赞动效实现相关的知识,希望对你有一定的参考价值。
bubble_swing
中间
最左
最右
中间
2.2 轨迹设计根据上面的分析,我们可以设计一段相同的上升轨迹,以及几段不同的左右摇曳轨迹。
上升轨迹很简单,bubble_y
其中,--cntHeight 指的是容器的高度。也就是说,我们通过让 margin-bottom 不断增大,来控制点赞图标从容器底部上升到容器顶部。
而对于横向运动的轨迹,为了增加运动轨迹的多样性,我们bubble_swing_1
// 中间
// 最左
// 最右
这里同样使用 margin 来控制图标的左右移动。类似的,我们还可以设计几段别的轨迹:
bubble_swing_2
0%
33%
100%
bubble_swing_3
0%
25%
75%
100%
接下来我们把 x 轴 和 y 轴 的轨迹(@keyframes)结合起来,并设置一个随机的动画时间,比如说:
from through
from through
+ * forwards,
bubble_swing_#+ * forwards;
这里生成了 3 * 2 = 6 种不同的轨迹。针对这类重复的选择器,用 SCSS 中的循环语法,可以少写很多代码。
2.3 随机选择图片(雪碧图)我们每次点赞会出现不同的图标,于是这里设计了一系列选择器给不同的图标,让它们呈现不同的图片。首先我们要准备一张雪碧图,保持所有图标的大小一致,然后同样使用 SCSS 的循环语法:
from through
像上面生成了 8 个选择器,我们在程序执行时就可以随机给图标赋予一个选择器。
2.4 生成一个点赞图标CSS 的部分差不多了,我们现在来看 JS 是怎么执行的。我们需要有一个容器 div,让它来装载要生成的点赞图标。以及一个按钮来绑定点击事件:
const cacheRef = useRef<LikeCache>(
bubbleCnt: likeIcon: bubbleIndex: timer: =>
cacheRef.current.bubbleCnt = cacheRef.current.likeIcon = * 添加 bubble
*/
addBubble =
bubbleCnt = cacheRef.current;
cacheRef.current.bubbleIndex %= maxBubble;
d = swing = speed = d.className = bl_ bubbleCnt?.appendChild(d);
cacheRef.current.bubbleIndex++;
bubbleCnt?.removeChild(d);
, * 点击“喜欢”
*/
onClick =
timer, likeIcon = cacheRef.current;
(!likeIcon)
(timer)
cacheRef.current.timer =
likeIcon.classList.remove(
likeIcon.classList.add( , cacheRef.current.timer =
likeIcon.classList.remove( cacheRef.current.timer = , addBubble();
;
2.5 最终效果最后来看看效果吧!
3. Canvas 实现点赞动效
我们都知道 Canvas 的绘制更流畅一些,能够带来更好的体验。但苦于编码比较复杂,也有一定的学习成本,实现起来要比 CSS 复杂不少。
接下来我们看看基于 的点赞动效实现。
3.1 画布创建首先我们读取一个 元素的 id,并通过 getContext 获取它的上下文。除此之外,还传入了一个 canvasScale,指的是画布放大的比例,这个在之后会用到:
canvas = htmlCanvasElement;
likeSprites * 预加载图片
*/
loadImages =
p =
img = Image();
img.onerror = resolve(img);
img.onload = resolve(img);
img.src = likeSprites;
);
p.then(
(img && img.width >
);
;
3.3 轨迹拆解同样的,我们需要从 Canvas 的视角来拆解点赞图标的运动轨迹。
y 轴 的运动和 CSS 一样,我们知道起始位置和终止位置就可以得出。
x 轴 的运动可以好好推敲。由于 Canvas 是逐帧绘制的,我们可以模拟出一个比较逼真的简谐运动。这里要来讲一讲大家耳熟能详的初中数学了,下面是我们要使用的一条正弦函数的公式:
y = A sin(Bx + C) + D
参数说明:
IMAGE_WIDTH = SOURCE_IMAGE_WIDTH = IMG_NUM = ENLARGE_STAGE = FADE_OUT_STAGE = basicX = frequency = random(amplitude = random(: -* 获取横向位移(x轴)
*/
getTranslateX =
(progress < ENLARGE_STAGE)
basicX;
basicX + amplitude * * 获取竖向位移(y轴)
*/
getTranslateY =
IMAGE_WIDTH / + (- progress);
;3.5 大小和透明度计算要绘制的图标大小怎么控制呢?在 Canvas 中,其实就是计算一个 scale,表示放缩的比例。
我们根据放大/收缩阶段的过程常量和 progress 变量来调节它的大小。起始阶段先线性放大至 1,最后阶段再线性缩小至 0。
透明度同理,在消失之前都是返回 1,其余时刻线性缩小。
* 获取放缩比例
*/
getScale =
r = (progress < ENLARGE_STAGE)
r = progress / ENLARGE_STAGE;
(progress > FADE_OUT_STAGE)
r = (- progress) / (- FADE_OUT_STAGE);
r;
;
* 获取透明度
*/
getAlpha =
(progress < FADE_OUT_STAGE)
- (progress - FADE_OUT_STAGE) / (- FADE_OUT_STAGE);
;
3.6 Canvas 绘制绘制时,我们先挑选一张图片。如下:
curImgIndex = newWidth = IMAGE_WIDTH *
(progress >= context.save();
scale = getScale(progress);
translateX = getTranslateX(progress);
translateY = getTranslateY(progress);
context.translate(translateX, translateY);
context.scale(scale, scale);
context.globalAlpha = getAlpha(progress);
context.drawImage(
SOURCE_IMAGE_WIDTH * curImgIndex,
SOURCE_IMAGE_WIDTH,
SOURCE_IMAGE_WIDTH,
-newWidth / -newWidth / newWidth,
newWidth,
);
context.restore();
render = duration = random( (!render)
render,
duration,
);
(! requestAnimationFrame(
index = length = (length > requestAnimationFrame(
(index < length)
child = (!child || !child.render || child.render.call( length--;
index++;
;
3.7 调用接下来我们只需要在点击的时候,调用一下 start
方法即可。
* 点击“喜欢”
*/
onClick =
cacheRef.current.LikeAni?.start?.();
;
(
/>
/>
</div>
);
在直播场景下,还有很多不同的触发方式。除了自己点击,我们还可以接受来自其他用户的反馈(网络请求)来触发 start
方法。或者根据在线人数,多次调用 start
方法来生成一定数量的点赞图标。
3.8 最终效果4. 性能比较
以下内容是在 MacBook Pro 16 的屏幕上测试的。
4.1 Frame Rendering Stats在 chrome devtools 中,有两个小功能可以来观察我们绘制的性能情况:
Paint flashing:可以高亮当前发生重绘的区域。
Frame Rendering Stats,可以观察动画的 fps 和 GPU 使用情况。我们分别来看看 CSS 和 Canvas 两种实现方式的性能情况。
这两个功能,可以在 chrome devtools 中使用快捷键 Command + Shift + P,呼起命令搜索的 Panel 来搜索到。
CSS 性能
我们可以看到高亮区域在频繁闪动,以及 GPU 内存的使用比率较高,这是因为 CSS 的实现方式是不断生成新的元素(并在随后销毁),会消耗更多的内存。
Canvas 性能
相反,Canvas 是集中在画布上绘制并输出的,不会反复创建和销毁元素。会比 CSS 的实现更加流畅,性能更好一点。
除了流畅以外,Canvas 还能够放大画布和画布元素,这也是一个非常重要的优势。这意味着 Canvas 能够绘制出更清晰的内容,生成出来的点赞图标更加细腻。
4.2 Performance
在 chrome devtools 中切换到 Performance 面板,还可以观察动画绘制过程中,页面的一些性能指标。
CSS 性能
CSS 的实现之所以看起比较卡顿,主要是因为绘制任务太频繁。
具体到每一帧,我们可以观察到 LayoutShift 的警告。
每次可视元素在两次渲染帧中的起始位置不同时,就说是发生了 LS(Layout Shift)。改变了起始位置的元素被认为是不稳定元素。
Canvas 性能
Canvas 实现的性能情况看起来就比较正常,即使绘制清晰一些的图片也不在话下。
5. 相关
实现参考:https://github.com/antiter/praise-animation
更多推荐从零开发一套基于React的加载动画库 从零开发一款轻量级滑动验证码插件 如何用不到200行代码写一款属于自己的js框架
从零设计可视化大屏搭建引擎 从零使用electron搭建桌面端可视化编辑器Dooring
点个在看 你最好看
点赞动画还可以做得那么飘逸!
关注公众号 前端开发博客,领27本电子书
回复加群,自助秒进前端群
1. 前言
以前在看微信视频号直播的时候,经常点击右下角的点赞按钮。看着它的数字慢慢从一位数变成五位数,还是挺有氛围感的。特别是长按的时候,有个手机震动的反馈,很带感。
虽然之前很好奇这些飘动的点赞动效是怎么实现的,但没有特别去钻研。直到前阵子投入腾讯课堂 H5 直播间的需求,需要自己去实现一个这样的效果时,才开始摸索。
先看看最后的效果:
相比视频号的点赞动效,轨迹复杂了很多。可以看到课堂直播间的这一段点赞动效,大概分为这么三个阶段:
从无到有,在上升过程中放大成正常大小
上升过程中左右摇曳,且摇曳的幅度随机
左右摇曳上升的过程中,渐隐并缩小
在动手之前,我先想到了使用 CSS animation 去实现这种运动轨迹。在完成之后,又用 Canvas 重构了一版,优化了性能。
接下来我们分别来看看这两种实现方式。
2. CSS 实现点赞动效
2.1 轨迹分析
由于点赞动画是在一个二维平面上的,我们可以将它的运动轨迹拆分为 x 轴 和 y 轴 上的两段。
在 y 轴 上非常简单,我们的点赞图标会做一段垂直上升的匀速运动,从容器底部上升到容器顶部。
而 x 轴 上是左右摇曳的,用数学的角度说,是一段简谐运动。
但用 css 实现的时候,其实不用这么精细。为了简化计算,我们可以用几个关键帧来串联这段运动轨迹,例如:
@keyframes bubble_swing
0%
中间
25%
最左
75%
最右
100%
中间
2.2 轨迹设计
根据上面的分析,我们可以设计一段相同的上升轨迹,以及几段不同的左右摇曳轨迹。
上升轨迹很简单,同时我们还可以加上透明度(opacity)、大小(transform)的变化,如下:
@keyframes bubble_y
0%
transform: scale(1);
margin-bottom: 0;
opacity: 0;
5%
transform: scale(1.5);
opacity: 1;
80%
transform: scale(1);
opacity: 1;
100%
margin-bottom: var(--cntHeight);
transform: scale(0.8);
opacity: 0;
其中,--cntHeight 指的是容器的高度。也就是说,我们通过让 margin-bottom 不断增大,来控制点赞图标从容器底部上升到容器顶部。
而对于横向运动的轨迹,为了增加运动轨迹的多样性,我们可以设计多段左右摇曳的轨迹,比如说一段 “中间 -> 最左 -> 中间 -> 最右” 的轨迹:
@keyframes bubble_swing_1
0%
// 中间
margin-left: 0;
25%
// 最左
margin-left: -12px;
75%
// 最右
margin-left: 12px;
100%
margin-left: 0;
这里同样使用 margin 来控制图标的左右移动。类似的,我们还可以设计几段别的轨迹:
// 任意轨迹
@keyframes bubble_swing_2
0%
// 中间
margin-left: 0;
33%
// 最左
margin-left: -12px;
100%
// 随机位置
margin-left: 6px;
// 简谐反向
@keyframes bubble_swing_3
0%
// 中间
margin-left: 0;
25%
// 最右
margin-left: 12px;
75%
// 最左
margin-left: -12px;
100%
margin-left: 0;
接下来我们把 x 轴 和 y 轴 的轨迹(@keyframes)结合起来,并设置一个随机的动画时间,比如说:
@for$i from 1 through 3
@for$j from 1 through 2
.bl_#$i_#$j
animation: bubble_y calc(1.5s + $j * 0.5s) linear 1 forwards,
bubble_swing_#$i calc(1.5s + $j * 0.5s) linear 1 forwards;
这里生成了 3 * 2 = 6 种不同的轨迹。针对这类重复的选择器,用 SCSS 中的循环语法,可以少写很多代码。
2.3 随机选择图片(雪碧图)
我们每次点赞会出现不同的图标,于是这里设计了一系列选择器给不同的图标,让它们呈现不同的图片。首先我们要准备一张雪碧图,保持所有图标的大小一致,然后同样使用 SCSS 的循环语法:
@for$i from 0 through 7
.b#$i
background: url('../../images/like_sprites.png') calc(#$i * -24px) 0;
像上面生成了 8 个选择器,我们在程序执行时就可以随机给图标赋予一个选择器。
2.4 生成一个点赞图标
CSS 的部分差不多了,我们现在来看 JS 是怎么执行的。我们需要有一个容器 div,让它来装载要生成的点赞图标。以及一个按钮来绑定点击事件:
const cacheRef = useRef<LikeCache>(
bubbleCnt: null,
likeIcon: null,
bubbleIndex: 0,
timer: null,
);
useEffect(() =>
cacheRef.current.bubbleCnt = document.getElementById('like-bubble-cnt');
cacheRef.current.likeIcon = document.getElementById('like-icon');
, []);
在点击事件中,生成一个新的 div 元素,并为它设置 className。接着将它 append 到容器下,最后在一段时间后销毁这个点赞图标元素。如下:
/**
* 添加 bubble
*/
const addBubble = () =>
const bubbleCnt = cacheRef.current;
cacheRef.current.bubbleIndex %= maxBubble;
const d = document.createElement('div');
// 图片类 b0 - b7
// 随机动画类 bl_1_1 - bl_3_2
const swing = Math.floor(Math.random() * 3) + 1;
const speed = Math.floor(Math.random() * 2) + 1;
d.className = `like-bubble b$cacheRef.current.bubbleIndex bl_$swing_$speed`;
bubbleCnt?.appendChild(d);
cacheRef.current.bubbleIndex++;
// 动画结束后销毁元素
setTimeout(() =>
bubbleCnt?.removeChild(d);
, 2600);
;
到这里,我们就实现得差不多了。不过,我们还可以给点击的图标加点动画,让它有一个被按压后弹起的效果:
/**
* 点击“喜欢”
*/
const onClick = () =>
const timer, likeIcon = cacheRef.current;
if (!likeIcon)
return;
if (timer)
clearTimeout(timer);
cacheRef.current.timer = null;
likeIcon.classList.remove('bounce-click');
// 删除并重新添加类,需要延迟添加
setTimeout(() =>
likeIcon.classList.add('bounce-click');
, 0);
cacheRef.current.timer = window.setTimeout(() =>
likeIcon.classList.remove('bounce-click');
clearTimeout(timer!);
cacheRef.current.timer = null;
, 300);
addBubble();
;
2.5 最终效果
最后来看看效果吧!
3. Canvas 实现点赞动效
我们都知道 Canvas 的绘制更流畅一些,能够带来更好的体验。但苦于编码比较复杂,也有一定的学习成本,实现起来要比 CSS 复杂不少。
接下来我们看看基于 Canvas 的点赞动效实现。
3.1 画布创建
首先我们读取一个 Canvas 元素的 id,并通过 getContext 获取它的上下文。除此之外,还传入了一个 canvasScale,指的是画布放大的比例,这个在之后会用到:
constructor(canvasId: string, canvasScale: number)
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
this.context = canvas.getContext('2d')!;
this.width = canvas.width;
this.height = canvas.height;
this.canvasScale = canvasScale;
this.img = null;
this.loadImages();
3.2 预加载图片(雪碧图)
在 constructor 这里,我们还通过 loadImages 这个函数,预加载了雪碧图:
import likeSprites from'../../images/like_sprites.png';
/**
* 预加载图片
*/
loadImages = () =>
const p = newPromise((resolve: (image: HTMLImageElement) => void) =>
const img = new Image();
img.onerror = () => resolve(img);
img.onload = () => resolve(img);
img.src = likeSprites;
);
p.then((img) =>
if (img && img.width > 0)
this.img = img;
else
// error('[live-connect]预加载喜欢动效图片失败');
);
;
3.3 轨迹拆解
同样的,我们需要从 Canvas 的视角来拆解点赞图标的运动轨迹。
y 轴 的运动和 CSS 一样,我们知道起始位置和终止位置就可以得出。
x 轴 的运动可以好好推敲。由于 Canvas 是逐帧绘制的,我们可以模拟出一个比较逼真的简谐运动。这里要来讲一讲大家耳熟能详的初中数学了,下面是我们要使用的一条正弦函数的公式:
y = A sin(Bx + C) + D
参数说明:
振幅是 A
周期是 2π/B
相移是 −C/B
垂直移位是 D
套入点赞动效:
赋予图标元素随机的振幅 A。
赋予图标元素随机的周期,即 B 是随机的。
取 C = 0,即相移为 0。
取 D = 0,即不需要垂直移位。
y = A sinBx。
3.4 横竖位移计算
确定位移轨迹之后,我们先定义一些常量,如下:
/** 图片显示宽高 */
const IMAGE_WIDTH = 30;
/** 图片原始宽高 */
const SOURCE_IMAGE_WIDTH = 144;
/** 图片数量 */
const IMG_NUM = 8;
/** 放大阶段(百分比)*/
const ENLARGE_STAGE = 0.1;
/** 收缩渐隐阶段(百分比)*/
const FADE_OUT_STAGE = 0.8;
首先我们可以设计 x 轴 和 y 轴 两个方向上的位移计算函数,函数参数 progress 是 0 到 1 之间的数值,表示一个过程量(0 -> 1)。
// 起始位置
const basicX = this.width / 2;
// 正弦频率
const frequency = random(2, 10);
// 正弦振幅
const amplitude = random(5, 20) * (random(0, 1) ? 1 : -1) * this.canvasScale;
/**
* 获取横向位移(x轴)
*/
const getTranslateX = (progress: number) =>
if (progress < ENLARGE_STAGE)
// 放大期间,不进行摇摆位移
return basicX;
return basicX + amplitude * Math.sin(frequency * (progress - ENLARGE_STAGE));
;
/**
* 获取竖向位移(y轴)
*/
const getTranslateY = (progress: number) =>
return IMAGE_WIDTH / 2 + (this.height - IMAGE_WIDTH / 2) * (1 - progress);
;
3.5 大小和透明度计算
要绘制的图标大小怎么控制呢?在 Canvas 中,其实就是计算一个 scale,表示放缩的比例。
我们根据放大/收缩阶段的过程常量和 progress 变量来调节它的大小。起始阶段先线性放大至 1,最后阶段再线性缩小至 0。
透明度同理,在消失之前都是返回 1,其余时刻线性缩小。
/**
* 获取放缩比例
*/
const getScale = (progress: number) =>
let r = 1;
if (progress < ENLARGE_STAGE)
// 放大
r = progress / ENLARGE_STAGE;
elseif (progress > FADE_OUT_STAGE)
// 缩小
r = (1 - progress) / (1 - FADE_OUT_STAGE);
return r;
;
/**
* 获取透明度
*/
const getAlpha = (progress: number) =>
if (progress < FADE_OUT_STAGE)
return 1;
return 1 - (progress - FADE_OUT_STAGE) / (1 - FADE_OUT_STAGE);
;
3.6 Canvas 绘制
绘制时,我们先挑选一张图片。如下:
// 按顺序读取图片
const curImgIndex = this;
// 更新顺序
this.curImgIndex = ++this.curImgIndex % IMG_NUM;
3.6.1 画布元素清晰度
接下来需要用到我们之前提到的 canvasScale 了:
const newWidth = IMAGE_WIDTH * this.canvasScale;
为什么这里要乘以一个 canvasScale 呢?因为 Canvas 是位图模式的,它会根据设备的 dpi 来渲染图片。
首先先介绍一下高分屏的概念:
高分屏:在同样大小的屏幕面积上显示更多的像素点,也就是更多的可视信息。常见的就是 SXGA(1400 * 1050),UXGA(1600 * 1200)。1024 * 768 分辨率的屏幕叫普通屏,也就是 XGA 的屏幕,这个分辨率以上的屏幕叫高分屏。
在高分屏上,每平方英寸会有更多的像素。原来在普通屏上绘制的 1 个像素,为了适应高分屏,被迫放大,变成了 4 个像素或者更多。
可以想象成,一张清晰度正常的普通图片为了布满整个背景被强行放大 n 倍,所以看起来模糊了。
为了解决这个问题,就需要我们将绘制的图片放大。同时还要控制 Canvas 画布在 CSS 中的宽高。做到绘制内容变大的同时,画布依然呈现原来的大小。这样一来,图片就会因为绘制了更多的内容,而在高分屏上变得清晰且细腻。
3.6.2 绘制元素
绘制我们用到了 drawImage。在调用它之前,我们需要根据计算出的 translateX 和 translateY,调整绘制的起点。并且调整放缩比例和透明度,即 context.scale()
和 context.globalAlpha
。如下:
return(progress: number) =>
// 动画过程 0 -> 1
if (progress >= 1) return true;
context.save();
const scale = getScale(progress);
const translateX = getTranslateX(progress);
const translateY = getTranslateY(progress);
context.translate(translateX, translateY);
context.scale(scale, scale);
context.globalAlpha = getAlpha(progress);
context.drawImage(
this.img!,
SOURCE_IMAGE_WIDTH * curImgIndex,
0,
SOURCE_IMAGE_WIDTH,
SOURCE_IMAGE_WIDTH,
-newWidth / 2,
-newWidth / 2,
newWidth,
newWidth,
);
context.restore();
return false;
;
3.6.3 创建绘制实例
我们用一个 start 函数来生成点赞动画,每当调用它时,都会创建一个 render 方法,并塞入一个 renderList。renderList 中存放的就是当前所有点赞图标的绘制任务。如下:
start = () =>
const render = this.createRender();
const duration = random(2100, 2600);
if (!render)
return;
this.renderList.push(
render,
duration,
timestamp: Date.now(),
);
if (!this.scanning)
this.scanning = true;
requestAnimationFrame(this.scan);
return this;
;
3.6.4 实时绘制
知道了需要绘制哪些对象之后,就需要通过下面的 scan 方法,让 Canvas 在每一帧都去绘制内容。
每次绘制分为这么几个过程:
清空画布为透明。
从绘制列表中取出一个点赞图标的 render 方法,并调用它。
假如它返回了 true,代表点赞图标已经完整经历了整个动效的过程,需要将它从绘制列表中剔除出去。
重复 2、3 过程,直至列表中没有任务需要执行。
通过 requestAnimationFrame
调用 scan 方法自身,等待下一帧重新调用 scan 绘制内容。
scan = () =>
this.context.clearRect(0, 0, this.width, this.height);
let index = 0;
let length = this.renderList;
if (length > 0)
requestAnimationFrame(this.scan);
this.scanning = true;
else
this.scanning = false;
while (index < length)
const child = this.renderList[index];
if (!child || !child.render || child.render.call(null, (Date.now() - child.timestamp) / child.duration))
// 结束了,删除该动画
this.renderList.splice(index, 1);
length--;
else
index++;
;
3.7 调用
接下来我们只需要在点击的时候,调用一下 start
方法即可。
/**
* 点击“喜欢”
*/
const onClick = () =>
cacheRef.current.LikeAni?.start?.();
;
return (
<div className=cn('like-wrap', className)>
<canvas id=CANVAS_ID width=CANVAS_WIDTH height=CANVAS_HEIGHT className="like-bubble-cnt" />
<div className=cn('like-icon-cnt', className) onClick=onClick>
<i id="like-icon" className="like-icon" />
</div>
</div>
);
在直播场景下,还有很多不同的触发方式。除了自己点击,我们还可以接受来自其他用户的反馈(网络请求)来触发 start
方法。或者根据在线人数,多次调用 start
方法来生成一定数量的点赞图标。
3.8 最终效果
4. 性能比较
以下内容是在 MacBook Pro 16 的屏幕上测试的。
4.1 Frame Rendering Stats
在 chrome devtools 中,有两个小功能可以来观察我们绘制的性能情况:
Paint flashing:可以高亮当前发生重绘的区域。
Frame Rendering Stats,可以观察动画的 fps 和 GPU 使用情况。我们分别来看看 CSS 和 Canvas 两种实现方式的性能情况。
这两个功能,可以在 chrome devtools 中使用快捷键 Command + Shift + P,呼起命令搜索的 Panel 来搜索到。
CSS 性能
我们可以看到高亮区域在频繁闪动,以及 GPU 内存的使用比率较高,这是因为 CSS 的实现方式是不断生成新的元素(并在随后销毁),会消耗更多的内存。
Canvas 性能
相反,Canvas 是集中在画布上绘制并输出的,不会反复创建和销毁元素。会比 CSS 的实现更加流畅,性能更好一点。
除了流畅以外,Canvas 还能够放大画布和画布元素,这也是一个非常重要的优势。这意味着 Canvas 能够绘制出更清晰的内容,生成出来的点赞图标更加细腻。
4.2 Performance
在 chrome devtools 中切换到 Performance 面板,还可以观察动画绘制过程中,页面的一些性能指标。
CSS 性能
CSS 的实现之所以看起比较卡顿,主要是因为绘制任务太频繁。
具体到每一帧,我们可以观察到 LayoutShift 的警告。
每次可视元素在两次渲染帧中的起始位置不同时,就说是发生了 LS(Layout Shift)。改变了起始位置的元素被认为是不稳定元素。
Canvas 性能
Canvas 实现的性能情况看起来就比较正常,即使绘制清晰一些的图片也不在话下。
5. 相关
实现参考:https://github.com/antiter/praise-animation
往期推荐:
另外欢迎大家围观我的朋友圈,搞搞技术,吹吹牛逼。关注我,秒添加,回复加群,可以进入 500人前端群。
以上是关于腾讯课堂 H5 直播间点赞动效实现的主要内容,如果未能解决你的问题,请参考以下文章
其中,--cntHeight 指的是容器的高度。也就是说,我们通过让 margin-bottom 不断增大,来控制点赞图标从容器底部上升到容器顶部。
而对于横向运动的轨迹,为了增加运动轨迹的多样性,我们bubble_swing_1 这里同样使用 margin 来控制图标的左右移动。类似的,我们还可以设计几段别的轨迹: 接下来我们把 x 轴 和 y 轴 的轨迹(@keyframes)结合起来,并设置一个随机的动画时间,比如说: 这里生成了 3 * 2 = 6 种不同的轨迹。针对这类重复的选择器,用 SCSS 中的循环语法,可以少写很多代码。 我们每次点赞会出现不同的图标,于是这里设计了一系列选择器给不同的图标,让它们呈现不同的图片。首先我们要准备一张雪碧图,保持所有图标的大小一致,然后同样使用 SCSS 的循环语法: 像上面生成了 8 个选择器,我们在程序执行时就可以随机给图标赋予一个选择器。 CSS 的部分差不多了,我们现在来看 JS 是怎么执行的。我们需要有一个容器 div,让它来装载要生成的点赞图标。以及一个按钮来绑定点击事件: 最后来看看效果吧! 我们都知道 Canvas 的绘制更流畅一些,能够带来更好的体验。但苦于编码比较复杂,也有一定的学习成本,实现起来要比 CSS 复杂不少。 接下来我们看看基于 的点赞动效实现。 首先我们读取一个 元素的 id,并通过 getContext 获取它的上下文。除此之外,还传入了一个 canvasScale,指的是画布放大的比例,这个在之后会用到: 同样的,我们需要从 Canvas 的视角来拆解点赞图标的运动轨迹。 y 轴 的运动和 CSS 一样,我们知道起始位置和终止位置就可以得出。 x 轴 的运动可以好好推敲。由于 Canvas 是逐帧绘制的,我们可以模拟出一个比较逼真的简谐运动。这里要来讲一讲大家耳熟能详的初中数学了,下面是我们要使用的一条正弦函数的公式: 参数说明: IMAGE_WIDTH = SOURCE_IMAGE_WIDTH = IMG_NUM = ENLARGE_STAGE = FADE_OUT_STAGE = basicX = frequency = random(amplitude = random(: -* 获取横向位移(x轴) 要绘制的图标大小怎么控制呢?在 Canvas 中,其实就是计算一个 scale,表示放缩的比例。 我们根据放大/收缩阶段的过程常量和 progress 变量来调节它的大小。起始阶段先线性放大至 1,最后阶段再线性缩小至 0。 透明度同理,在消失之前都是返回 1,其余时刻线性缩小。 绘制时,我们先挑选一张图片。如下: 接下来我们只需要在点击的时候,调用一下 在直播场景下,还有很多不同的触发方式。除了自己点击,我们还可以接受来自其他用户的反馈(网络请求)来触发 以下内容是在 MacBook Pro 16 的屏幕上测试的。 在 chrome devtools 中,有两个小功能可以来观察我们绘制的性能情况: Paint flashing:可以高亮当前发生重绘的区域。 Frame Rendering Stats,可以观察动画的 fps 和 GPU 使用情况。我们分别来看看 CSS 和 Canvas 两种实现方式的性能情况。 这两个功能,可以在 chrome devtools 中使用快捷键 Command + Shift + P,呼起命令搜索的 Panel 来搜索到。 我们可以看到高亮区域在频繁闪动,以及 GPU 内存的使用比率较高,这是因为 CSS 的实现方式是不断生成新的元素(并在随后销毁),会消耗更多的内存。 相反,Canvas 是集中在画布上绘制并输出的,不会反复创建和销毁元素。会比 CSS 的实现更加流畅,性能更好一点。 除了流畅以外,Canvas 还能够放大画布和画布元素,这也是一个非常重要的优势。这意味着 Canvas 能够绘制出更清晰的内容,生成出来的点赞图标更加细腻。 在 chrome devtools 中切换到 Performance 面板,还可以观察动画绘制过程中,页面的一些性能指标。 CSS 的实现之所以看起比较卡顿,主要是因为绘制任务太频繁。 具体到每一帧,我们可以观察到 LayoutShift 的警告。 每次可视元素在两次渲染帧中的起始位置不同时,就说是发生了 LS(Layout Shift)。改变了起始位置的元素被认为是不稳定元素。 Canvas 实现的性能情况看起来就比较正常,即使绘制清晰一些的图片也不在话下。 实现参考:https://github.com/antiter/praise-animation 点个在看 你最好看 关注公众号 前端开发博客,领27本电子书 回复加群,自助秒进前端群 以前在看微信视频号直播的时候,经常点击右下角的点赞按钮。看着它的数字慢慢从一位数变成五位数,还是挺有氛围感的。特别是长按的时候,有个手机震动的反馈,很带感。 虽然之前很好奇这些飘动的点赞动效是怎么实现的,但没有特别去钻研。直到前阵子投入腾讯课堂 H5 直播间的需求,需要自己去实现一个这样的效果时,才开始摸索。 先看看最后的效果: 相比视频号的点赞动效,轨迹复杂了很多。可以看到课堂直播间的这一段点赞动效,大概分为这么三个阶段: 从无到有,在上升过程中放大成正常大小 上升过程中左右摇曳,且摇曳的幅度随机 左右摇曳上升的过程中,渐隐并缩小 在动手之前,我先想到了使用 CSS animation 去实现这种运动轨迹。在完成之后,又用 Canvas 重构了一版,优化了性能。 接下来我们分别来看看这两种实现方式。 由于点赞动画是在一个二维平面上的,我们可以将它的运动轨迹拆分为 x 轴 和 y 轴 上的两段。 在 y 轴 上非常简单,我们的点赞图标会做一段垂直上升的匀速运动,从容器底部上升到容器顶部。 而 x 轴 上是左右摇曳的,用数学的角度说,是一段简谐运动。 但用 css 实现的时候,其实不用这么精细。为了简化计算,我们可以用几个关键帧来串联这段运动轨迹,例如: 根据上面的分析,我们可以设计一段相同的上升轨迹,以及几段不同的左右摇曳轨迹。 上升轨迹很简单,同时我们还可以加上透明度(opacity)、大小(transform)的变化,如下: 其中,--cntHeight 指的是容器的高度。也就是说,我们通过让 margin-bottom 不断增大,来控制点赞图标从容器底部上升到容器顶部。 而对于横向运动的轨迹,为了增加运动轨迹的多样性,我们可以设计多段左右摇曳的轨迹,比如说一段 “中间 -> 最左 -> 中间 -> 最右” 的轨迹: 这里同样使用 margin 来控制图标的左右移动。类似的,我们还可以设计几段别的轨迹: 接下来我们把 x 轴 和 y 轴 的轨迹(@keyframes)结合起来,并设置一个随机的动画时间,比如说: 这里生成了 3 * 2 = 6 种不同的轨迹。针对这类重复的选择器,用 SCSS 中的循环语法,可以少写很多代码。 我们每次点赞会出现不同的图标,于是这里设计了一系列选择器给不同的图标,让它们呈现不同的图片。首先我们要准备一张雪碧图,保持所有图标的大小一致,然后同样使用 SCSS 的循环语法: 像上面生成了 8 个选择器,我们在程序执行时就可以随机给图标赋予一个选择器。 CSS 的部分差不多了,我们现在来看 JS 是怎么执行的。我们需要有一个容器 div,让它来装载要生成的点赞图标。以及一个按钮来绑定点击事件: 在点击事件中,生成一个新的 div 元素,并为它设置 className。接着将它 append 到容器下,最后在一段时间后销毁这个点赞图标元素。如下: 到这里,我们就实现得差不多了。不过,我们还可以给点击的图标加点动画,让它有一个被按压后弹起的效果: 最后来看看效果吧! 我们都知道 Canvas 的绘制更流畅一些,能够带来更好的体验。但苦于编码比较复杂,也有一定的学习成本,实现起来要比 CSS 复杂不少。 接下来我们看看基于 Canvas 的点赞动效实现。 首先我们读取一个 Canvas 元素的 id,并通过 getContext 获取它的上下文。除此之外,还传入了一个 canvasScale,指的是画布放大的比例,这个在之后会用到: 在 constructor 这里,我们还通过 loadImages 这个函数,预加载了雪碧图: 同样的,我们需要从 Canvas 的视角来拆解点赞图标的运动轨迹。 y 轴 的运动和 CSS 一样,我们知道起始位置和终止位置就可以得出。 x 轴 的运动可以好好推敲。由于 Canvas 是逐帧绘制的,我们可以模拟出一个比较逼真的简谐运动。这里要来讲一讲大家耳熟能详的初中数学了,下面是我们要使用的一条正弦函数的公式: 参数说明: 振幅是 A 周期是 2π/B 相移是 −C/B 垂直移位是 D 套入点赞动效: 赋予图标元素随机的振幅 A。 赋予图标元素随机的周期,即 B 是随机的。 取 C = 0,即相移为 0。 取 D = 0,即不需要垂直移位。 确定位移轨迹之后,我们先定义一些常量,如下: 首先我们可以设计 x 轴 和 y 轴 两个方向上的位移计算函数,函数参数 progress 是 0 到 1 之间的数值,表示一个过程量(0 -> 1)。 要绘制的图标大小怎么控制呢?在 Canvas 中,其实就是计算一个 scale,表示放缩的比例。 我们根据放大/收缩阶段的过程常量和 progress 变量来调节它的大小。起始阶段先线性放大至 1,最后阶段再线性缩小至 0。 透明度同理,在消失之前都是返回 1,其余时刻线性缩小。 绘制时,我们先挑选一张图片。如下: 接下来需要用到我们之前提到的 canvasScale 了: 为什么这里要乘以一个 canvasScale 呢?因为 Canvas 是位图模式的,它会根据设备的 dpi 来渲染图片。 首先先介绍一下高分屏的概念: 高分屏:在同样大小的屏幕面积上显示更多的像素点,也就是更多的可视信息。常见的就是 SXGA(1400 * 1050),UXGA(1600 * 1200)。1024 * 768 分辨率的屏幕叫普通屏,也就是 XGA 的屏幕,这个分辨率以上的屏幕叫高分屏。 在高分屏上,每平方英寸会有更多的像素。原来在普通屏上绘制的 1 个像素,为了适应高分屏,被迫放大,变成了 4 个像素或者更多。 可以想象成,一张清晰度正常的普通图片为了布满整个背景被强行放大 n 倍,所以看起来模糊了。 为了解决这个问题,就需要我们将绘制的图片放大。同时还要控制 Canvas 画布在 CSS 中的宽高。做到绘制内容变大的同时,画布依然呈现原来的大小。这样一来,图片就会因为绘制了更多的内容,而在高分屏上变得清晰且细腻。 绘制我们用到了 drawImage。在调用它之前,我们需要根据计算出的 translateX 和 translateY,调整绘制的起点。并且调整放缩比例和透明度,即 我们用一个 start 函数来生成点赞动画,每当调用它时,都会创建一个 render 方法,并塞入一个 renderList。renderList 中存放的就是当前所有点赞图标的绘制任务。如下: 知道了需要绘制哪些对象之后,就需要通过下面的 scan 方法,让 Canvas 在每一帧都去绘制内容。 每次绘制分为这么几个过程: 清空画布为透明。 从绘制列表中取出一个点赞图标的 render 方法,并调用它。 假如它返回了 true,代表点赞图标已经完整经历了整个动效的过程,需要将它从绘制列表中剔除出去。 重复 2、3 过程,直至列表中没有任务需要执行。 通过 接下来我们只需要在点击的时候,调用一下 在直播场景下,还有很多不同的触发方式。除了自己点击,我们还可以接受来自其他用户的反馈(网络请求)来触发 以下内容是在 MacBook Pro 16 的屏幕上测试的。 在 chrome devtools 中,有两个小功能可以来观察我们绘制的性能情况: Paint flashing:可以高亮当前发生重绘的区域。 Frame Rendering Stats,可以观察动画的 fps 和 GPU 使用情况。我们分别来看看 CSS 和 Canvas 两种实现方式的性能情况。 这两个功能,可以在 chrome devtools 中使用快捷键 Command + Shift + P,呼起命令搜索的 Panel 来搜索到。 我们可以看到高亮区域在频繁闪动,以及 GPU 内存的使用比率较高,这是因为 CSS 的实现方式是不断生成新的元素(并在随后销毁),会消耗更多的内存。 相反,Canvas 是集中在画布上绘制并输出的,不会反复创建和销毁元素。会比 CSS 的实现更加流畅,性能更好一点。 除了流畅以外,Canvas 还能够放大画布和画布元素,这也是一个非常重要的优势。这意味着 Canvas 能够绘制出更清晰的内容,生成出来的点赞图标更加细腻。 在 chrome devtools 中切换到 Performance 面板,还可以观察动画绘制过程中,页面的一些性能指标。 CSS 的实现之所以看起比较卡顿,主要是因为绘制任务太频繁。 具体到每一帧,我们可以观察到 LayoutShift 的警告。 每次可视元素在两次渲染帧中的起始位置不同时,就说是发生了 LS(Layout Shift)。改变了起始位置的元素被认为是不稳定元素。 Canvas 实现的性能情况看起来就比较正常,即使绘制清晰一些的图片也不在话下。 实现参考:https://github.com/antiter/praise-animation 往期推荐: 另外欢迎大家围观我的朋友圈,搞搞技术,吹吹牛逼。关注我,秒添加,回复加群,可以进入 500人前端群。 以上是关于腾讯课堂 H5 直播间点赞动效实现的主要内容,如果未能解决你的问题,请参考以下文章
// 中间
// 最左
// 最右
bubble_swing_2
0%
33%
100%
bubble_swing_3
0%
25%
75%
100%
from through
from through
+ * forwards,
bubble_swing_#+ * forwards;
from through
2.5 最终效果const cacheRef = useRef<LikeCache>(
bubbleCnt: likeIcon: bubbleIndex: timer: =>
cacheRef.current.bubbleCnt = cacheRef.current.likeIcon = * 添加 bubble
*/
addBubble =
bubbleCnt = cacheRef.current;
cacheRef.current.bubbleIndex %= maxBubble;
d = swing = speed = d.className = bl_ bubbleCnt?.appendChild(d);
cacheRef.current.bubbleIndex++;
bubbleCnt?.removeChild(d);
, * 点击“喜欢”
*/
onClick =
timer, likeIcon = cacheRef.current;
(!likeIcon)
(timer)
cacheRef.current.timer =
likeIcon.classList.remove(
likeIcon.classList.add( , cacheRef.current.timer =
likeIcon.classList.remove( cacheRef.current.timer = , addBubble();
;
3.3 轨迹拆解
canvas = htmlCanvasElement;
likeSprites * 预加载图片
*/
loadImages =
p =
img = Image();
img.onerror = resolve(img);
img.onload = resolve(img);
img.src = likeSprites;
);
p.then(
(img && img.width >
);
;y = A sin(Bx + C) + D
*/
getTranslateX =
(progress < ENLARGE_STAGE)
basicX;
basicX + amplitude * * 获取竖向位移(y轴)
*/
getTranslateY =
IMAGE_WIDTH / + (- progress);
;3.5 大小和透明度计算
3.6 Canvas 绘制* 获取放缩比例
*/
getScale =
r = (progress < ENLARGE_STAGE)
r = progress / ENLARGE_STAGE;
(progress > FADE_OUT_STAGE)
r = (- progress) / (- FADE_OUT_STAGE);
r;
;
* 获取透明度
*/
getAlpha =
(progress < FADE_OUT_STAGE)
- (progress - FADE_OUT_STAGE) / (- FADE_OUT_STAGE);
;
3.7 调用 curImgIndex = newWidth = IMAGE_WIDTH *
(progress >= context.save();
scale = getScale(progress);
translateX = getTranslateX(progress);
translateY = getTranslateY(progress);
context.translate(translateX, translateY);
context.scale(scale, scale);
context.globalAlpha = getAlpha(progress);
context.drawImage(
SOURCE_IMAGE_WIDTH * curImgIndex,
SOURCE_IMAGE_WIDTH,
SOURCE_IMAGE_WIDTH,
-newWidth / -newWidth / newWidth,
newWidth,
);
context.restore();
render = duration = random( (!render)
render,
duration,
);
(! requestAnimationFrame(
index = length = (length > requestAnimationFrame(
(index < length)
child = (!child || !child.render || child.render.call( length--;
index++;
;start
方法即可。* 点击“喜欢”
*/
onClick =
cacheRef.current.LikeAni?.start?.();
;
(
/>
/>
</div>
);start
方法。或者根据在线人数,多次调用 start
方法来生成一定数量的点赞图标。CSS 性能
Canvas 性能
CSS 性能
Canvas 性能
点赞动画还可以做得那么飘逸!
1. 前言
2. CSS 实现点赞动效
2.1 轨迹分析
@keyframes bubble_swing
0%
中间
25%
最左
75%
最右
100%
中间
2.2 轨迹设计
@keyframes bubble_y
0%
transform: scale(1);
margin-bottom: 0;
opacity: 0;
5%
transform: scale(1.5);
opacity: 1;
80%
transform: scale(1);
opacity: 1;
100%
margin-bottom: var(--cntHeight);
transform: scale(0.8);
opacity: 0;
@keyframes bubble_swing_1
0%
// 中间
margin-left: 0;
25%
// 最左
margin-left: -12px;
75%
// 最右
margin-left: 12px;
100%
margin-left: 0;
// 任意轨迹
@keyframes bubble_swing_2
0%
// 中间
margin-left: 0;
33%
// 最左
margin-left: -12px;
100%
// 随机位置
margin-left: 6px;
// 简谐反向
@keyframes bubble_swing_3
0%
// 中间
margin-left: 0;
25%
// 最右
margin-left: 12px;
75%
// 最左
margin-left: -12px;
100%
margin-left: 0;
@for$i from 1 through 3
@for$j from 1 through 2
.bl_#$i_#$j
animation: bubble_y calc(1.5s + $j * 0.5s) linear 1 forwards,
bubble_swing_#$i calc(1.5s + $j * 0.5s) linear 1 forwards;
2.3 随机选择图片(雪碧图)
@for$i from 0 through 7
.b#$i
background: url('../../images/like_sprites.png') calc(#$i * -24px) 0;
2.4 生成一个点赞图标
const cacheRef = useRef<LikeCache>(
bubbleCnt: null,
likeIcon: null,
bubbleIndex: 0,
timer: null,
);
useEffect(() =>
cacheRef.current.bubbleCnt = document.getElementById('like-bubble-cnt');
cacheRef.current.likeIcon = document.getElementById('like-icon');
, []);
/**
* 添加 bubble
*/
const addBubble = () =>
const bubbleCnt = cacheRef.current;
cacheRef.current.bubbleIndex %= maxBubble;
const d = document.createElement('div');
// 图片类 b0 - b7
// 随机动画类 bl_1_1 - bl_3_2
const swing = Math.floor(Math.random() * 3) + 1;
const speed = Math.floor(Math.random() * 2) + 1;
d.className = `like-bubble b$cacheRef.current.bubbleIndex bl_$swing_$speed`;
bubbleCnt?.appendChild(d);
cacheRef.current.bubbleIndex++;
// 动画结束后销毁元素
setTimeout(() =>
bubbleCnt?.removeChild(d);
, 2600);
;
/**
* 点击“喜欢”
*/
const onClick = () =>
const timer, likeIcon = cacheRef.current;
if (!likeIcon)
return;
if (timer)
clearTimeout(timer);
cacheRef.current.timer = null;
likeIcon.classList.remove('bounce-click');
// 删除并重新添加类,需要延迟添加
setTimeout(() =>
likeIcon.classList.add('bounce-click');
, 0);
cacheRef.current.timer = window.setTimeout(() =>
likeIcon.classList.remove('bounce-click');
clearTimeout(timer!);
cacheRef.current.timer = null;
, 300);
addBubble();
;
2.5 最终效果
3. Canvas 实现点赞动效
3.1 画布创建
constructor(canvasId: string, canvasScale: number)
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
this.context = canvas.getContext('2d')!;
this.width = canvas.width;
this.height = canvas.height;
this.canvasScale = canvasScale;
this.img = null;
this.loadImages();
3.2 预加载图片(雪碧图)
import likeSprites from'../../images/like_sprites.png';
/**
* 预加载图片
*/
loadImages = () =>
const p = newPromise((resolve: (image: HTMLImageElement) => void) =>
const img = new Image();
img.onerror = () => resolve(img);
img.onload = () => resolve(img);
img.src = likeSprites;
);
p.then((img) =>
if (img && img.width > 0)
this.img = img;
else
// error('[live-connect]预加载喜欢动效图片失败');
);
;
3.3 轨迹拆解
y = A sin(Bx + C) + D
y = A sinBx。
3.4 横竖位移计算
/** 图片显示宽高 */
const IMAGE_WIDTH = 30;
/** 图片原始宽高 */
const SOURCE_IMAGE_WIDTH = 144;
/** 图片数量 */
const IMG_NUM = 8;
/** 放大阶段(百分比)*/
const ENLARGE_STAGE = 0.1;
/** 收缩渐隐阶段(百分比)*/
const FADE_OUT_STAGE = 0.8;
// 起始位置
const basicX = this.width / 2;
// 正弦频率
const frequency = random(2, 10);
// 正弦振幅
const amplitude = random(5, 20) * (random(0, 1) ? 1 : -1) * this.canvasScale;
/**
* 获取横向位移(x轴)
*/
const getTranslateX = (progress: number) =>
if (progress < ENLARGE_STAGE)
// 放大期间,不进行摇摆位移
return basicX;
return basicX + amplitude * Math.sin(frequency * (progress - ENLARGE_STAGE));
;
/**
* 获取竖向位移(y轴)
*/
const getTranslateY = (progress: number) =>
return IMAGE_WIDTH / 2 + (this.height - IMAGE_WIDTH / 2) * (1 - progress);
;
3.5 大小和透明度计算
/**
* 获取放缩比例
*/
const getScale = (progress: number) =>
let r = 1;
if (progress < ENLARGE_STAGE)
// 放大
r = progress / ENLARGE_STAGE;
elseif (progress > FADE_OUT_STAGE)
// 缩小
r = (1 - progress) / (1 - FADE_OUT_STAGE);
return r;
;
/**
* 获取透明度
*/
const getAlpha = (progress: number) =>
if (progress < FADE_OUT_STAGE)
return 1;
return 1 - (progress - FADE_OUT_STAGE) / (1 - FADE_OUT_STAGE);
;
3.6 Canvas 绘制
// 按顺序读取图片
const curImgIndex = this;
// 更新顺序
this.curImgIndex = ++this.curImgIndex % IMG_NUM;
3.6.1 画布元素清晰度
const newWidth = IMAGE_WIDTH * this.canvasScale;
3.6.2 绘制元素
context.scale()
和 context.globalAlpha
。如下:return(progress: number) =>
// 动画过程 0 -> 1
if (progress >= 1) return true;
context.save();
const scale = getScale(progress);
const translateX = getTranslateX(progress);
const translateY = getTranslateY(progress);
context.translate(translateX, translateY);
context.scale(scale, scale);
context.globalAlpha = getAlpha(progress);
context.drawImage(
this.img!,
SOURCE_IMAGE_WIDTH * curImgIndex,
0,
SOURCE_IMAGE_WIDTH,
SOURCE_IMAGE_WIDTH,
-newWidth / 2,
-newWidth / 2,
newWidth,
newWidth,
);
context.restore();
return false;
;
3.6.3 创建绘制实例
start = () =>
const render = this.createRender();
const duration = random(2100, 2600);
if (!render)
return;
this.renderList.push(
render,
duration,
timestamp: Date.now(),
);
if (!this.scanning)
this.scanning = true;
requestAnimationFrame(this.scan);
return this;
;
3.6.4 实时绘制
requestAnimationFrame
调用 scan 方法自身,等待下一帧重新调用 scan 绘制内容。scan = () =>
this.context.clearRect(0, 0, this.width, this.height);
let index = 0;
let length = this.renderList;
if (length > 0)
requestAnimationFrame(this.scan);
this.scanning = true;
else
this.scanning = false;
while (index < length)
const child = this.renderList[index];
if (!child || !child.render || child.render.call(null, (Date.now() - child.timestamp) / child.duration))
// 结束了,删除该动画
this.renderList.splice(index, 1);
length--;
else
index++;
;
3.7 调用
start
方法即可。/**
* 点击“喜欢”
*/
const onClick = () =>
cacheRef.current.LikeAni?.start?.();
;
return (
<div className=cn('like-wrap', className)>
<canvas id=CANVAS_ID width=CANVAS_WIDTH height=CANVAS_HEIGHT className="like-bubble-cnt" />
<div className=cn('like-icon-cnt', className) onClick=onClick>
<i id="like-icon" className="like-icon" />
</div>
</div>
);
start
方法。或者根据在线人数,多次调用 start
方法来生成一定数量的点赞图标。3.8 最终效果
4. 性能比较
4.1 Frame Rendering Stats
CSS 性能
Canvas 性能
4.2 Performance
CSS 性能
Canvas 性能
5. 相关