弹幕,你知道是怎样练成的?
天下視頻唯彈幕不破
說起彈幕看過視頻的都不會陌生,那滿屏充滿著飄逸評論的效果,讓人如癡如醉,無法自拔
最近也是因為在學習關于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));}+++++++++++++++++++++++++++ }完成,漂亮,看看效果吧
寫到這里我們已經完成了視頻網站上的彈幕功能了,可喜可賀
下面我們再來完善一下視頻播放時對彈幕的播放處理吧
暫停和拖動
-
暫停就停止渲染彈幕
-
回放時需要重新渲染該時刻的彈幕
讓我們再次回到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設為falsebarrage.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學習參考
總結
以上是生活随笔為你收集整理的弹幕,你知道是怎样练成的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 5~11各个版本新特性史上最全
- 下一篇: 面试常考!缓存三大问题及解决方案