彈幕,你知道是怎樣練成的?
天下視訊唯彈幕不破
說起彈幕看過視訊的都不會陌生,那滿屏充滿著飄逸評論的效果,讓人如痴如醉,無法自拔
最近也是因為在學習關於canvas的知識,所以今天就想和大家分享一個關於彈幕的故事
那麼究竟彈幕是怎樣煉成的呢? 我們且往下看(look)
看什麼?看效果
效果圖已經呈現給各位了,那麼是不是有點小激動呢?是的,感慨萬分,思緒寧亂,無語凝噎
無論以後我們的工作中是否會遇到這樣的需求,也請給自己一個增加技能的機會吧!!!
本次彈幕的效果,專案結構如下圖所示
專案整體已經給出,那麼我們就擼起袖子加油幹吧。
讓彈幕飛
上面我們提到了canvas的事情,所以呢,這就是製作彈幕的殺手鐗了。我們利用canvas繪圖來實現彈幕的功能
首先,我們先給出html的結構
// index.html檔案 <div class="wrap"> <h1>聽媽媽的話 - 周杰倫</h1> <div class="main"> <canvas id="canvas"></canvas> <video src="../source/mv.mp4" id="video" controls width="720" height="480"></video> </div> <div class="content"> <input type="text" id="text"> <input type="button" value="發彈幕" id="btn"> <input type="color" id="color"> <input type="range" id="range" max="40" min="20"> </div> </div> // 引入index.js檔案用來實現彈幕功能 <script src="./index.js"></script>
如需要視訊資源的,就點這裡吧(提取碼:tsei)
結構相對來說沒什麼高階的內容,主要就是寫上了canvas標籤還有video標籤,他們才是視訊網站中彈幕的絕佳拍檔
那麼不再賣關子了,趕緊進行主要活動吧
模擬資料
// index.js檔案 let data = [ {value: '周杰倫的聽媽媽的話,讓我反覆迴圈再迴圈', time: 5, color: 'red', speed: 1, fontSize: 22}, {value: '想快快長大,才能保護她', time: 10, color: '#00a1f5', speed: 1, fontSize: 30}, {value: '聽媽媽的話吧,晚點再戀愛吧!愛呢?', time: 15}, ];
資料裡代表了什麼:
-
value:代表彈幕的內容 (必填)
-
time:代表彈幕展現的時間 (必填)
-
color:代表彈幕文字的顏色
-
speed:代表彈幕飄過的速度
-
fontSize:代表彈幕文字的大小
-
opacity:代表彈幕文字的透明度
除了彈幕的內容和展現的時間外,其他都是可選的,模擬的資料裡沒有這些引數也沒關係的
獲取dom元素
// index.js檔案
// 模擬資料
...省略
// 獲取到所有需要的dom元素
let doc = document;
let canvas = doc.getElementById('canvas');
let video = doc.getElementById('video');
let $txt = doc.getElementById('text');
let $btn = doc.getElementById('btn');
let $color = doc.getElementById('color');
let $range = doc.getElementById('range');
Canvas渲染彈幕
下面我們將用面向物件的方式來實現canvas繪製彈幕的功能,之所以選擇用這種方式主要是方便複用和後續新增方法
下面我們先來建立一個CanvasBarrage類,主要用做canvas來渲染整個彈幕
在實現之前,我們先來呼叫一下,看看是如何建立例項的
// index.js檔案
// 模擬資料
...省略
// 獲取到所有需要的dom元素
...省略
// 建立CanvasBarrage類
class CanvasBarrage {
// todo
}
// 建立CanvasBarrage例項
let canvasBarrage = new CanvasBarrage(canvas, video, { data });
建立例項很簡單,沒有物件,只需要new一個就有了,哈哈。接下來,說回正事,我們趕緊完成上面程式碼中todo的部分,來完善CanvasBarrage類吧
實現CanvasBarrage
// index.js檔案
class CanvasBarrage {
constructor(canvas, video, opts = {}) {
// opts = {}表示如果opts沒傳就設為{},防止報錯,ES6語法
// 如果canvas和video都沒傳,那就直接return掉
if (!canvas || !video) return;
// 直接掛載到this上
this.video = video;
this.canvas = canvas;
// 設定canvas的寬高和video一致
this.canvas.width = video.width;
this.canvas.height = video.height;
// 獲取畫布,操作畫布
this.ctx = canvas.getContext('2d');
// 設定預設引數,如果沒有傳就給帶上
let defOpts = {
color: '#e91e63',
speed: 1.5,
opacity: 0.5,
fontSize: 20,
data: []
};
// 合併物件並全都掛到this例項上
Object.assign(this, defOpts, opts);
// 添加個屬性,用來判斷播放狀態,預設是true暫停
this.isPaused = true;
// 得到所有的彈幕訊息
this.barrages = this.data.map(item => new Barrage(item, this));
// 渲染
this.render();
console.log(this);
}
// 渲染canvas繪製的彈幕
render() {
// todo
}
}
我們在“得到所有的彈幕訊息”那裡,通過陣列的map方法返回的還是個陣列,不過返回的內容是一個Barrage類,這是為什麼呢?
還記得之前說過麼,用類的好處就是方便擴充套件,後續再新增方法的話可以直接在該類中新增即可。
所以我們也不推崇直接map方法裡直接返回一個{}這種形式
// 不推薦
this.barrages = this.data.map(item => { item });
說到這裡我們還要先寫一下Barrage這個類,不然接下來的console.log(this)會因為找不到Barrage類而報錯
// index.js檔案
++++++++++++++++++++++
// 建立Barrage類,用來例項化每一個彈幕元素
class Barrage {
constructor(obj, ctx) {
// todo
}
}
++++++++++++++++++++++
class CanvasBarrage {
...省略
}
Now,通過上面程式碼中的console.log(this),我們可以看到,所有掛載到this例項上的屬性和原型上的方法都呈現眼前了
render一下
接著上面的CanvasBarrage類裡render方法繼續寫,我們來把todo完成
// index.js檔案
class CanvasBarrage {
constructor(canvas, video, opts = {}) {
...省略
// 渲染
this.render();
}
render() {
// 渲染的第一步是清除原來的畫布,方便複用寫成clear方法來呼叫
this.clear();
// 渲染彈幕
this.renderBarrage();
// 如果沒有暫停的話就繼續渲染
if (this.isPaused === false) {
// 通過raf渲染動畫,遞迴進行渲染
requestAnimationFrame(this.render.bind(this));
}
}
clear() {
// 清除整個畫布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
todo都做了什麼?
1、清除之前畫布所有的繪製,防止繪製重疊的影響
-
this.clear()
2、渲染真正的彈幕資料 (還未實現)
-
this.renderBarrage()
3、判斷是否繼續渲染彈幕
-
this.isPaused為false時表示為播放狀態
4、遞迴呼叫render
-
通過requestAnimationFrame來遞迴呼叫render
-
要比setInterval這樣的方式好很多
渲染整個彈幕render方法就完成了,那麼要繼續寫了,應該是剛才未實現的renderBarrage方法了
But,在此之前,我們要先寫個別的,它就是Barrage類
因為還需要它來大顯身手一下呢,每一個彈幕的例項都由它來製造
建立Barrage類
彈幕製造者來了,下面我們就來實現一下這個Barrage類,看它都具備哪些屬性和方法,繼續todo吧
// index.js檔案
class Barrage {
constructor(obj, ctx) {
this.value = obj.value; // 彈幕的內容
this.time = obj.time; // 彈幕出現時間
// 把obj和ctx都掛載到this上方便獲取
this.obj = obj;
this.context = ctx;
}
// 初始化彈幕
init() {
// 如果資料裡沒有涉及到下面4種引數,就直接取預設引數
this.color = this.obj.color || this.context.color;
this.speed = this.obj.speed || this.context.speed;
this.opacity = this.obj.opacity || this.context.opacity;
this.fontSize = this.obj.fontSize || this.context.fontSize;
// 為了計算每個彈幕的寬度,我們必須建立一個元素p,然後計算文字的寬度
let p = document.createElement('p');
p.style.fontSize = this.fontSize + 'px';
p.innerHTML = this.value;
document.body.appendChild(p);
// 把p元素新增到body裡了,這樣就可以拿到寬度了
// 設定彈幕的寬度
this.width = p.clientWidth;
// 得到了彈幕的寬度後,就把p元素從body中刪掉吧
document.body.removeChild(p);
// 設定彈幕出現的位置
this.x = this.context.canvas.width;
this.y = this.context.canvas.height * Math.random();
// 做下超出範圍處理
if (this.y < this.fontSize) {
this.y = this.fontSize;
} else if (this.y > this.context.canvas.height - this.fontSize) {
this.y = this.context.canvas.height - this.fontSize;
}
}
// 渲染每個彈幕
render() {
// 設定畫布文字的字號和字型
this.context.ctx.font = `${this.fontSize}px Arial`;
// 設定畫布文字顏色
this.context.ctx.fillStyle = this.color;
// 繪製文字
this.context.ctx.fillText(this.value, thix.x, this.y);
}
}
todo都做了什麼?
1、從傳入的obj中取到必要的value和time
this.value = obj.value; // 內容
this.time = obj.time; // 時間
2、初始化彈幕
-
對每個彈幕所需的引數進行設定,如果obj上沒有,就取預設引數
-
計算每個彈幕的寬度
-
由於不能直接操縱canvas畫布裡的元素,所以先建立一個p標籤
-
p標籤的寬度即為彈幕的寬 -> this.width = p.clientWidth
-
-
設定每個彈幕的x和y座標 (起始位置)
-
橫向x座標起始位置都是從右邊進入,即:畫布的寬度
-
this.x = this.context.canvas.width
-
縱向y座標起始位置是不固定的,選在畫布之內的任意位置出現
-
this.y = this.context.canvas.height * Math.random()
-
-
處理彈幕超出畫布區域
-
canvas是按照字號基線來展示字型的,如果
小於
這個
字號
大小
-
this.y = this.fontSize
-
如果
大於
了
畫布高度
-
字號
大小
-
this.y = this.context.canvas.height - this.fontSize
-
3、渲染每個彈幕
-
繪製文字需要設定文字的字型字號、顏色和文字的內容與座標
-
字型字號api
-
this.context.ctx.font = `${this.value}px Arial`
-
-
顏色api
-
this.context.ctx.fillStyle = this.color
-
-
內容與座標api
-
this.context.ctx.fillText(this.value, this.x, this.y)
-
以上三步就是整個Barrage類所做的事情了。Barrage這個類都已經敲完了,那麼接下來開始真正的渲染步驟吧
renderBarrage才是主角
// index.js檔案
class CanvasBarrage {
...省略
renderBarrage() {
// 首先拿到當前視訊播放的時間
// 要根據該時間來和彈幕要展示的時間做比較,來判斷是否展示彈幕
let time = this.video.currentTime;
// 遍歷所有的彈幕,每個barrage都是Barrage的例項
this.barrages.forEach(barrage => {
// 用一個flag來處理是否渲染,預設是false
// 並且只有在視訊播放時間大於等於當前彈幕的展現時間時才做處理
if (!barrage.flag && time >= barrage.time) {
// 判斷當前彈幕是否有過初始化了
// 如果isInit還是false,那就需要先對當前彈幕進行初始化操作
if (!barrage.isInit) {
barrage.init();
barrage.isInit = true;
}
// 彈幕要從右向左渲染,所以x座標減去當前彈幕的speed即可
barrage.x -= barrage.speed;
barrage.render(); // 渲染當前彈幕
// 如果當前彈幕的x座標比自身的寬度還小了,就表示結束渲染了
if (barrage.x < -barrage.width) {
barrage.flag = true; // 把flag設為true下次就不再渲染
}
}
});
}
}
此時我們再新增一個觸發彈幕的事件,讓彈幕飛起來
// index.js檔案
class CanvasBarrage {
...省略
}
// 建立CanvasBarrage例項
let canvasBarrage = new CanvasBarrage(canvas, video, { data });
++++++++++++++++++++++++++++++++++++++
// 設定video的play事件來呼叫CanvasBarrage例項的render方法
video.addEventListener('play', () => {
canvasBarrage.isPaused = false;
canvasBarrage.render(); // 觸發彈幕
});
++++++++++++++++++++++++++++++++++++++
大家一起寫到了這裡,也是時候展示一下成果了,往下看
別急,讓彈幕再飛一會兒
渲染彈幕的功能,我們已經完成了,接下來讓我們馬不停蹄的寫下如何發彈幕吧。別猶豫,開擼!!!
發彈幕
// index.js檔案
class CanvasBarrage {
...省略
}
video.addEventListener('play', ...省略);
+++++++++++++++++++++++++++++++++++++++
// 傳送彈幕的方法
function send() {
let value = $txt.value; // 輸入的內容
let time = video.currentTime; // 當前視訊時間
let color = $color.value; // 選取的顏色值
let fontSize = $range.value; // 選取的字號大小
let obj = { value, time, color, fontSize };
// 新增彈幕資料
canvasBarrage.add(obj);
$txt.value = ''; // 清空輸入框
}
// 點選按鈕傳送彈幕
$btn.addEventListener('click', send);
// 回車傳送彈幕
$txt.addEventListener('keyup', e => {
let key = e.keyCode;
key === 13 && send();
});
+++++++++++++++++++++++++++++++++++++++
發彈幕相對來說還是很簡單的,獲取到value, time, color, fontSize之後把他們當作物件傳給CanvasBarrage的add方法進行新增就好了
下面我們再寫一下add方法,回到CanvasBarrage類裡繼續寫
// index.js檔案
class CanvasBarrage {
constructor() { ...省略}
render() { ...省略 }
renderBarrage() { ...省略 }
clear() { ...省略 }
+++++++++++++++++++++++++++
add(obj) {
// 實際上就是往barrages數組裡再新增一項Barrage的例項而已
this.barrages.push(new Barrage(obj, this));
}
+++++++++++++++++++++++++++
}
完成,漂亮,看看效果吧
寫到這裡我們已經完成了視訊網站上的彈幕功能了,可喜可賀
下面我們再來完善一下視訊播放時對彈幕的播放處理吧
暫停和拖動
-
暫停就停止渲染彈幕
// index.js檔案
...省略
// 播放
video.addEventListener('play', () => {
canvasBarrage.isPaused = false;
canvasBarrage.render();
});
+++++++++++++++++++++++++++++++++++++++
// 暫停
video.addEventListener('pause', () => {
// isPaused設為true表示暫停播放
canvasBarrage.isPaused = true;
});
+++++++++++++++++++++++++++++++++++++++
-
回放時需要重新渲染該時刻的彈幕
// index.js檔案
// 暫停
video.addEventListener('pause', () => {
canvasBarrage.isPaused = true;
});
+++++++++++++++++++++++++++++++++++++++
// 拖動進度條時觸發seeked事件
video.addEventListener('seeked', () => {
// 呼叫CanvasBarrage類的replay方法進行回放,重新渲染彈幕
canvasBarrage.replay();
});
+++++++++++++++++++++++++++++++++++++++
讓我們再次回到CanvasBarrage這個類上
// index.js檔案
class CanvasBarrage {
constructor() { ...省略}
render() { ...省略 }
renderBarrage() { ...省略 }
clear() { ...省略 }
add(obj) { ...省略 }
+++++++++++++++++++++++++++
replay() {
this.clear(); //先清除畫布
// 獲取當前視訊播放時間
let time = this.video.currentTime;
// 遍歷barrages彈幕陣列
this.barrages.forEach(barrage => {
// 當前彈幕的flag設為false
barrage.flag = false;
// 並且,當前視訊時間小於等於當前彈幕所展現的時間
if (time <= barrage.time) {
// 就把isInit重設為false,這樣才會重新初始化渲染
barrage.isInit = false;
} else { // 其他時間對比不匹配的,flag還是true不用重新渲染
barrage.flag = true;
}
});
}
+++++++++++++++++++++++++++
}
盡善盡美一下
OK,寫到這裡,所有關於彈幕功能的程式碼就全部結束了!!!如果工作中讓你開發彈幕功能,你也可以在多敲幾遍以上程式碼之後,得心應手的保證完成任務了
不過做事總是要做全套比較好,我們接下來再利用WebSocket和redis來進行一下較為實戰的功能吧
大家之前看到過目錄結構,還有一個app.js檔案其實是沒有寫任何東西的,那麼接下來我們就開始寫寫看吧
WebSocket通訊和redis儲存
久違的app.js檔案,開始動手 首先我們需要安裝兩個包,一個是處理服務端WebSocket通訊的ws模組,另一個就是用來儲存redis資料的redis模組
npm i ws redis -S
安裝完成後可以繼續寫東西了
// app.js檔案
const WebSocket = require('ws');
const redis = require('redis');
const clientRedis = redis.createClient(); // 建立redis客戶端
const ws = new WebSocket.Server({ port: 9999 }); // 建立ws服務
// 用來儲存不同的socket例項,區分不同使用者
let clients = [];
// 監聽連線
ws.on('connection', socket => {
clients.push(socket); // 把socket例項新增到陣列
// 通過redis客戶端的lrange方法來獲取資料庫中key為barrages的資料
clientRedis.lrange('barrages', 0, -1, (err, data) => {
// 由於redis儲存的是key value型別,因此需要JSON.parse轉成物件
data = data.map(item => JSON.parse(item));
// 傳送給客戶端,send方法傳遞的是字串需要JSON.stringify
// type為init是用來初始化彈幕資料的
socket.send(JSON.stringify({
type: 'init',
data
}));
});
// 監聽客戶端發來的訊息
socket.on('message', data => {
// redis客戶端通過rpush的方法把每個訊息都新增到barrages表的最後面
clientRedis.rpush('barrages', data);
// 每個socket例項(使用者)之間都可以發彈幕,並顯示在對方的畫布上
// type為add表示此次操作為新增處理
// 你可以開啟兩個index.html,分別發彈幕試試吧
clients.forEach(sk => {
sk.send(JSON.stringify({
type: 'add',
data: JSON.parse(data)
}));
});
});
// 當有socket例項斷開與ws服務端的連線時
// 重新更新一下clients陣列,去掉斷開的使用者
socket.on('close', () => {
clients = clients.filter(client => client !== socket);
});
});
服務端的內容已經全部完事了,接下來我們再稍微改下客戶端的程式碼,回到熟悉的index.js中
// index.js檔案
class CanvasBarrage {
...省略
}
+++++++++++++++++++++++++++++++
// 建立CanvasBarrage例項
// let canvasBarrage = new CanvasBarrage(canvas, video, { data });
let canvasBarrage;
let ws = new WebSocket('ws://localhost:9999');
// 監聽與ws服務端的連線
ws.onopen = function () {
// 監聽ws服務端發來的訊息
ws.onmessage = function (e) {
let msg = JSON.parse(e.data); //e.data裡是真正的資料
// 判斷如果type為init就初始化彈幕的資料
if (msg.type === 'init') {
canvasBarrage = new CanvasBarrage(canvas, video, { data: msg.data });
} else if (msg.type === 'add') { // 新增彈幕資料
canvasBarrage.add(msg.data);
}
}
};
+++++++++++++++++++++++++++++++
// 傳送彈幕的方法
function send() {
let value = $txt.value;
let time = video.currentTime;
let color = $color.value;
let fontSize = $range.value;
let obj = { value, time, color, fontSize };
// 新增彈幕資料
// canvasBarrage.add(obj);
+++++++++++++++++++++++++++++++
// 把新增的彈幕資料發給ws服務端
// 由ws服務端拿到後新增到redis資料庫中
ws.send(JSON.stringify(obj));
+++++++++++++++++++++++++++++++
$txt.value = '';
}
前後端都搞定了,那麼我們接下來只需要連線一下redis資料庫就可以了
連線redis資料庫的正確方式
首先無論是windows還是mac都需要先安裝一下
windows系統
-
windows:下載redis (提取碼:svua)
windows連線redis資料庫
進入下載解壓好的redis目錄,在命令列工具中輸入以下指令建立連線
redis-server.exe redis.windows.conf
出現如下圖顯示的樣子就表示已經成功建立了連線
windows下的redis視覺化工具(Redis Desktop Manager)
mac系統
-
mac:
brew install redis
-
連線:
brew services start redis
redis資料庫如果成功的連線了,那麼就可以直接啟動app.js的服務了,開啟index.html檔案,會發現可以拿到資料庫裡儲存的彈幕資料了
好了,這下大家滿足了吧,很厲害,我們每個人都可以敲出自己的彈幕了。
不斷的學習會讓我們一點一滴的進步下去,前端的路還很長,我們都在慢慢前行
對了,忘記重要的事情了,如果大家有什麼疑問可以看下原始碼地址進行參考
結束了
之後一段時間打算好好的研究一下canvas繪圖的知識點了,也希望在研究後可以很好的梳理一下分享給大家一起來學習
作為大前端來說,我們要學的東西實在太多了,一專多精才是王道,不負好時光,一起努力吧!謝謝大家的觀看了
參考
-
HTML5 Video元素介紹
-
Canvas學習教程
-
珠峰架構培訓公開課 實現彈幕系統
-
ES6語法學習
-
WebSocket學習參考