直播有一個很重要的互動:點贊。css
爲了烘托直播間的氛圍,直播相對於普通視頻或者文本內容,點贊一般有兩個特殊需求:html
咱們先來看效果圖:git
從效果圖上咱們還看到有幾點重要信息:github
那麼如何實現這些要求呢?下面介紹兩種實現方式來實現(底部附完整 demo):canvas
用 CSS3 實現動畫,顯然,咱們想到的是用 animation 。數組
首先看下 animation 合併寫法,具體含義就不解釋了,若是須要能夠自行了解。promise
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
複製代碼
咱們開始來一步一步實現。瀏覽器
首先,咱們先準備 1 張點贊動畫圖片:
微信
看一下 HTML 結構。外層一個結構固定整個顯示動畫區域的位置。這裏在一個寬 100px ,高 200px 的 div 區域。app
<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 內渲染完。
兩種方式渲染點贊動畫都已經完成,完整源碼,源碼戳這裏 。
源碼運行效果圖:
這裏還能夠體驗線上點贊動畫,戳這裏
這兩種實現方式,均可以知足要求,那麼到底哪一種更優呢?
咱們來看下二者的數據對比。如下爲未開啓硬件加速的對比,採用不間斷瘋狂渲染點贊動畫的數據對比:
總體來講,差別以下:
歡迎關注個人微信公衆號: