有觀看過直播的小伙伴們都應(yīng)該知道在直播中我們可以看到在屏幕中有各種點(diǎn)贊的效果,今天我們就借著這個(gè)機(jī)會(huì)和大家說一下“通過前端實(shí)現(xiàn)直播中的點(diǎn)贊動(dòng)畫效果實(shí)現(xiàn)代碼詳解! 附源碼!”這個(gè)方面的實(shí)現(xiàn)方法和內(nèi)容!
直播有一個(gè)很重要的互動(dòng):點(diǎn)贊。
為了烘托直播間的氛圍,直播相對(duì)于普通視頻或者文本內(nèi)容,點(diǎn)贊通常有兩個(gè)特殊需求:
- 點(diǎn)贊動(dòng)作無限次,引導(dǎo)用戶瘋狂點(diǎn)贊
- 直播間的所有瘋狂點(diǎn)贊,都需要在所有用戶界面都動(dòng)畫展現(xiàn)出來
我們先來看效果圖:
從效果圖上我們還看到有幾點(diǎn)重要信息:
點(diǎn)贊動(dòng)畫圖片大小不一,運(yùn)動(dòng)軌跡也是隨機(jī)的 點(diǎn)贊動(dòng)畫圖片都是先放大再勻速運(yùn)動(dòng)。 快到頂部的時(shí)候,是漸漸消失。 收到大量的點(diǎn)贊請(qǐng)求的時(shí)候,點(diǎn)贊動(dòng)畫不扎堆,井然有序持續(xù)出現(xiàn)。
那么如何實(shí)現(xiàn)這些要求呢?下面介紹兩種實(shí)現(xiàn)方式來實(shí)現(xiàn)(底部附完整 demo):
CSS3 實(shí)現(xiàn)
用 CSS3 實(shí)現(xiàn)動(dòng)畫,顯然,我們想到的是用 animation 。
首先看下 animation 合并寫法,具體含義就不解釋了,如果需要可以自行了解。
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
我們開始來一步一步實(shí)現(xiàn)。
Step 1: 固定區(qū)域,設(shè)置基本樣式
首先,我們先準(zhǔn)備 1 張點(diǎn)贊動(dòng)畫圖片:
看一下 HTML 結(jié)構(gòu)。外層一個(gè)結(jié)構(gòu)固定整個(gè)顯示動(dòng)畫區(qū)域的位置。這里在一個(gè)寬 100px ,高 200px 的 div 區(qū)域。
<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;
}
Step 2: 運(yùn)動(dòng)起來
使用 animation 的幀動(dòng)畫,定義一個(gè) bubble_y 的幀序列。
.bl1{
animation:bubble_y 4s linear 1 forwards ;
}
@keyframes bubble_y {
0% {
margin-bottom:0;
}
100% {
margin-bottom:200px;
}
}
這里設(shè)置運(yùn)行時(shí)間 4s ;
采用線性運(yùn)動(dòng) linear,如果有需求當(dāng)然也可以使用其他曲線,比如 ease;
每個(gè)點(diǎn)贊動(dòng)畫只運(yùn)行 1 次;
動(dòng)畫是只需要向前 forwards。
Step 3: 增加漸隱
漸隱效果,使用 opacity 即可。這里我們固定在最后 1/4 開始漸隱。 修改 bubble_y:
@keyframes bubble_y {
0% {
margin-bottom:0;
}
75%{
opacity:1;
}
100% {
margin-bottom:200px;
opacity:0;
}
}
Step 4: 增加動(dòng)畫放大效果
在最開始一小段時(shí)間,圖片由小變大。
于是我們新增一個(gè)動(dòng)畫:bubble_big_1。
這里從 0.3 倍原圖放大到 1 倍。這里注意運(yùn)行時(shí)間,比如上面設(shè)置,從動(dòng)畫開始到結(jié)束總共是 4s,那么這個(gè)放大時(shí)間就可以按需設(shè)置了,比如 0.5s。
.bl1{
animation:bubble_big 0.5s linear 1 forwards;
}
@keyframes bubble_big_1 {
0% {
transform: scale(.3);
}
100% {
transform: scale(1);
}
}
Step 5: 設(shè)置偏移
我們先定義幀動(dòng)畫:bubble_1 來執(zhí)行偏移。圖片開始放大階段,這里沒有設(shè)置偏移,保持中間原點(diǎn)不變。
在運(yùn)行到 25% * 4 = 1s,即 1s之后,是向左偏移 -8px, 2s 的時(shí)候,向右偏移 8px,3s 的時(shí)候,向做偏移 15px ,最終向右偏移 15px。
大家可以想到了,這是定義的一個(gè)經(jīng)典的左右擺動(dòng)軌跡,“向左向右向左向右” 曲線擺動(dòng)效果。
@keyframes bubble_1 {
0% {
}
25% {
margin-left:-8px;
}
50% {
margin-left:8px
}
75% {
margin-left:-15px
}
100% {
margin-left:15px
}
}
效果圖如下:
Step 6: 補(bǔ)齊動(dòng)畫樣式
這里預(yù)設(shè)了一種運(yùn)行曲線軌跡,左右擺動(dòng)的樣式,我們?cè)谠兕A(yù)設(shè)更多種曲線,達(dá)到隨機(jī)軌跡的目的。
比如 bubble_1 的左右偏移動(dòng)畫軌跡,我們可以修改偏移值,來達(dá)到不同的曲線軌跡。
Step 7: JS 操作隨機(jī)增加節(jié)點(diǎn)樣式
提供增加點(diǎn)贊的方法,隨機(jī)將點(diǎn)贊的樣式組合,然后渲染到節(jié)點(diǎn)上。
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$ bl${bl}`;
d.dataset.t = String(Date.now());
praiseBubble.appendChild(d);
}
setInterval(() => {
addPraise();
},300)
在使用 CSS 來實(shí)現(xiàn)點(diǎn)贊的時(shí)候,通常還需要注意設(shè)置 bubble 的隨機(jī)延時(shí),比如:
.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;
}
這里如果是隨機(jī)到 bl2,那么延時(shí) 0.4s 再運(yùn)行,bl3 延時(shí) 0.6s ……
如果是批量更新到節(jié)點(diǎn)上,不設(shè)置延時(shí)的話,那就會(huì)扎堆出現(xiàn)。隨機(jī)“ bl ”樣式,就隨機(jī)了延時(shí),然后批量出現(xiàn),都會(huì)自動(dòng)錯(cuò)峰顯示。當(dāng)然,我們還需要增加當(dāng)前用戶手動(dòng)點(diǎn)贊的動(dòng)畫,這個(gè)不需要延時(shí)。
另外,有可能同時(shí)別人下發(fā)了點(diǎn)贊 40 個(gè),業(yè)務(wù)需求通常是希望這 40 個(gè)點(diǎn)贊氣泡都能依次出現(xiàn),制造持續(xù)的點(diǎn)贊氛圍,否則下發(fā)量大又會(huì)扎堆顯示了。
那么我們還需要分批打散點(diǎn)贊數(shù)量,比如一次點(diǎn)贊的時(shí)間($bubble_time)是 4s, 那么 4s 內(nèi),希望同時(shí)出現(xiàn)多少個(gè)點(diǎn)贊呢?比如是 10個(gè),那么 40 個(gè)點(diǎn)贊,需要分批 4 次渲染。
window.requestAnimationFrame(() => {
// 繼續(xù)循環(huán)處理批次
render();
});
另外還需要手動(dòng)清除節(jié)點(diǎn)。以防節(jié)點(diǎn)過多帶來的性能問題。如下是完整的效果圖。
Canvas 繪圖實(shí)現(xiàn)
這個(gè)很容易理解,直接在 canvas 上繪制動(dòng)畫就行,如果不了解 canvas 的,可以后續(xù)學(xué)習(xí)下。
Step 1:初始化
頁面元素上新建 canvas 標(biāo)簽,初始化 canvas。
canvas 上可以設(shè)置 width 和 height 屬性,也可以在 style 屬性里面設(shè)置 width 和 height。
- canvas 上 style 的 width 和 height 是 canvas 在瀏覽器中被渲染的高度和寬度,即在頁面中的實(shí)際寬高。
- canvas 標(biāo)簽的 width 和 height 是畫布實(shí)際寬度和高度。
<canvas id="thumsCanvas" width="200" height="400" style="width:100px;height:200px">
</canvas>
頁面上一個(gè)寬 200,高 400 的 canvas 畫布,然后整個(gè) canvas 顯示在 頁面 寬 100,高 200 的區(qū)域內(nèi)。canvas 畫布的內(nèi)容被等比縮小一倍顯示在頁面。
定義一個(gè)點(diǎn)贊類,ThumbsUpAni,構(gòu)造函數(shù)就是讀取 canvas,保存寬高值。
class ThumbsUpAni{
constructor(){
const canvas = document.getElementById('thumsCanvas');
this.context = canvas.getContext('2d')!;
this.width = canvas.width;
this.height = canvas.height;
}
}
Step 2:提前加載圖片資源
將需要隨機(jī)渲染的點(diǎn)贊圖片,先預(yù)加載,獲得圖片的寬高,如果有下載失敗的,則不顯示該隨機(jī)圖片即可。沒啥說的,簡單易懂。
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;
}
})
}
Step 2:創(chuàng)建渲染對(duì)象
實(shí)時(shí)渲染圖片,使其變成一個(gè)連貫的動(dòng)畫,很重要的是:生成曲線軌跡。這個(gè)曲線軌跡需要是平滑的均勻曲線。 假如生成的曲線軌跡不平滑的話,那看到的效果就會(huì)太突兀,比如上一個(gè)是 10 px,下一個(gè)就是 -10px,那顯然,動(dòng)畫就是忽左忽右左右閃爍了。
理想的軌跡是上一個(gè)位置是 10px,接下來是 9px,然后一直平滑到 -10px,這樣的坐標(biāo)點(diǎn)就是連貫的,看起來動(dòng)畫就是平滑運(yùn)行。
隨機(jī)平滑 X 軸偏移
如果要做到平滑曲線,其實(shí)可以使用我們?cè)偈煜げ贿^的正弦( Math.sin )函數(shù)來實(shí)現(xiàn)均勻曲線。
看下圖的正弦曲線:
這是 Math.sin(0) 到 Math.sin(9) 的曲線圖走勢(shì)圖,它是一個(gè)平滑的從正數(shù)到負(fù)數(shù),然后再從負(fù)數(shù)到正數(shù)的曲線圖,完全符合我們的需求,于是我們?cè)傩枰梢粋€(gè)隨機(jī)比率值,讓擺動(dòng)幅度隨機(jī)起來。
const angle = getRandom(2, 10);
let ratio = getRandom(10,30)*((getRandom(0, 1) ? 1 : -1));
const getTranslateX = (diffTime) => {
if (diffTime < this.scaleTime) {// 放大期間,不進(jìn)行搖擺偏移
return basicX;
} else {
return basicX + ratio*Math.sin(angle*(diffTime - this.scaleTime));
}
};
scaleTime 是從開始放大到最終大小,用多長時(shí)間,這里我們?cè)O(shè)置 0.1,即總共運(yùn)行時(shí)間前面的 10% 的時(shí)間,點(diǎn)贊圖片逐步放大。
diffTime,是只從開始動(dòng)畫運(yùn)行到當(dāng)前時(shí)間過了多長時(shí)間了,為百分比。實(shí)際值是從 0 --》 1 逐步增大。 diffTime - scaleTime = 0 ~ 0.9, diffTime 為 0.4 的時(shí)候,說明是已經(jīng)運(yùn)行了 40% 的時(shí)間。
因?yàn)?Math.sin(0) 到 Math.sin(0.9) 曲線幾乎是一個(gè)直線,所以不太符合擺動(dòng)效果,從 Math.sin(0) 到 Math.sin(1.8) 開始有細(xì)微的變化,所以我們這里設(shè)置的 angle 最小值為 2。
這里設(shè)置角度系數(shù) angle 最大為 10 ,從底部到頂部運(yùn)行兩個(gè)波峰。
當(dāng)然如果運(yùn)行距離再長一些,我們可以增大 angle 值,比如變成 3 個(gè)波峰(如果時(shí)間短,出現(xiàn)三個(gè)波峰,就會(huì)運(yùn)行過快,有閃爍現(xiàn)象)。如下圖:
Y 軸偏移
這個(gè)容易理解,開始 diffTime 為 0 ,所以運(yùn)行偏移從 this.height --> image.height / 2。即從最底部,運(yùn)行到頂部留下,實(shí)際上我們?cè)陧敳繒?huì)淡化隱藏。
const getTranslateY = (diffTime) => {
return image.height / 2 + (this.height - image.height / 2) * (1-diffTime);
};
放大縮小
當(dāng)運(yùn)行時(shí)間 diffTime 小于設(shè)置的 scaleTime 的時(shí)候,按比例隨著時(shí)間增大,scale 變大。超過設(shè)置的時(shí)間閾值,則返回最終大小。
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;
}
};
淡出
同放大邏輯一致,只不過淡出是在運(yùn)行快到最后的位置開始生效。
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);
}
};
實(shí)時(shí)繪制
創(chuàng)建完繪制對(duì)象之后,就可以實(shí)時(shí)繪制了,根據(jù)上述獲取到的“偏移值”,“放大”和“淡出”值,然后實(shí)時(shí)繪制點(diǎn)贊圖片的位置即可。
每個(gè)執(zhí)行周期,都需要重新繪制 canvas 上的所有的動(dòng)畫圖片位置,最終形成所有的點(diǎn)贊圖片都在運(yùn)動(dòng)的效果。
createRender(){
return (diffTime) => {
// 差值滿了,即結(jié)束了 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。前面我們?cè)O(shè)置了 basiceScale,如果圖片更大,我們可以把 scale 再變小即可。
const basicScale = [0.6, 0.9, 1.2][getRandom(0, 2)];
實(shí)時(shí)繪制掃描器
開啟實(shí)時(shí)繪制掃描器,將創(chuàng)建的渲染對(duì)象放入 renderList 數(shù)組,數(shù)組不為空,說明 canvas 上還有動(dòng)畫,就需要不停的去執(zhí)行 scan,直到 canvas 上沒有動(dòng)畫結(jié)束為止。
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)) {
// 結(jié)束了,刪除該動(dòng)畫
this.renderList.splice(index, 1);
length--;
} else {
// 當(dāng)前動(dòng)畫未執(zhí)行完成,continue
index++;
}
}
}
這里就是根據(jù)執(zhí)行的時(shí)間來對(duì)比,判斷動(dòng)畫執(zhí)行到的位置了:
diffTime = (Date.now() - render.timestamp) / render.duration
如果開始的時(shí)間戳是 10000,當(dāng)前是100100,則說明已經(jīng)運(yùn)行了 100 毫秒了,如果動(dòng)畫本來需要執(zhí)行 1000 毫秒,那么 diffTime = 0.1,代表動(dòng)畫已經(jīng)運(yùn)行了 10%。
增加動(dòng)畫
每點(diǎn)贊一次或者每接收到別人點(diǎn)贊一次,則調(diào)用一次 start 方法來生成渲染實(shí)例,放進(jìn)渲染實(shí)例數(shù)組。如果當(dāng)前掃描器未開啟,則需要啟動(dòng)掃描器,這里使用了 scanning 變量,防止開啟多個(gè)掃描器。
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;
}
保持不扎堆
當(dāng)接收到大量的點(diǎn)贊數(shù)據(jù),且連續(xù)多次點(diǎn)贊(直播間人氣很旺的時(shí)候)。那么點(diǎn)贊數(shù)據(jù)的渲染就需要特別注意了,否則頁面就是一坨一坨的點(diǎn)贊動(dòng)畫。且銜接不緊密。
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);
},
這里開啟定時(shí)器,記錄定時(shí)器里面處理的 thumbsStart 的值,如果有新增點(diǎn)贊,且定時(shí)器還在運(yùn)行,直接更新最后的 praiseLast 值,定時(shí)器會(huì)依次將點(diǎn)贊請(qǐng)求全部處理完。
定時(shí)器的延時(shí)時(shí)間 time 根據(jù)開啟定時(shí)器的時(shí)候,需要渲染多少點(diǎn)贊動(dòng)畫來決定的,比如需要渲染 100 個(gè)點(diǎn)贊動(dòng)畫,我們將 100 個(gè)點(diǎn)贊動(dòng)畫分布在 5s 內(nèi)渲染完。
- 對(duì)于熱門直播,會(huì)同時(shí)渲染的動(dòng)畫很多,不會(huì)扎堆顯示,且動(dòng)畫完全能銜接上,不停的冒泡點(diǎn)贊動(dòng)畫。
- 對(duì)于冷門直播,有多余一個(gè)的點(diǎn)贊請(qǐng)求,我們能打散到 5s 內(nèi)顯示,也不會(huì)扎堆顯示。
End
兩種方式渲染點(diǎn)贊動(dòng)畫都已經(jīng)完成,完整源碼,源碼戳這里 。
源碼運(yùn)行效果圖:
這里還可以體驗(yàn)線上點(diǎn)贊動(dòng)畫, 戳這里
再比較
這兩種實(shí)現(xiàn)方式,都可以滿足要求,那么到底哪種更優(yōu)呢?
我們來看下兩者的數(shù)據(jù)對(duì)比。以下為未開啟硬件加速的對(duì)比,采用不間斷瘋狂渲染點(diǎn)贊動(dòng)畫的數(shù)據(jù)對(duì)比:
整體來說,差異如下:
- SS3 實(shí)現(xiàn)簡單
- Canvas 更靈活,操作更細(xì)膩
- CSS3 內(nèi)存消耗比 Canvas 大,如果開啟硬件加速,內(nèi)存消耗更大一些。
總結(jié)
那么小編的內(nèi)容分享到這里相信很多小伙伴們都知道了對(duì)于“通過前端實(shí)現(xiàn)直播中的點(diǎn)贊動(dòng)畫效果實(shí)現(xiàn)代碼詳解! 附源碼!”這方面的內(nèi)容有哪些實(shí)現(xiàn)的方法了吧!更多有關(guān)的內(nèi)容我們都可以在W3Cschool中獲取自己想要的內(nèi)容!