摘要:創(chuàng)建彈幕功能的類(lèi)及基本參數(shù)處理布局時(shí)需要注意的默認(rèn)寬為,高為,我們要保證完全覆蓋整個(gè)視頻,需要讓與寬高相等。因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以畫(huà)布的寬高并沒(méi)有通過(guò)來(lái)設(shè)置,而是通過(guò)在類(lèi)創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。
首先,我們需要實(shí)現(xiàn)頁(yè)面布局,在根目錄創(chuàng)建 index.html 布局中我們需要有一個(gè) video 多媒體標(biāo)簽引入我們的本地視頻,添加輸入彈幕的輸入框、確認(rèn)發(fā)送的按鈕、顏色選擇器、字體大小滑動(dòng)條,創(chuàng)建一個(gè) style.css 來(lái)調(diào)整頁(yè)面布局的樣式,這里我們順便創(chuàng)建一個(gè) index.js 文件用于后續(xù)實(shí)現(xiàn)我們的核心邏輯,先引入到頁(yè)面當(dāng)中。
HTML 布局代碼如下:
視頻彈幕 Canvas + WebSocket + Redis 實(shí)現(xiàn)視頻彈幕
CSS 樣式代碼如下:
/* 文件:style.css */ #cantainer { text-align: center; } #content { width: 640px; margin: 0 auto; position: relative; } #canvas { position: absolute; } video { width: 640px; height: 360px; } input { vertical-align: middle; }
布局效果如下圖:
我們彈幕中的彈幕數(shù)據(jù)正常情況下應(yīng)該是通過(guò)與后臺(tái)數(shù)據(jù)交互請(qǐng)求回來(lái),所以我們需要先定義數(shù)據(jù)接口,并構(gòu)造假數(shù)據(jù)來(lái)實(shí)現(xiàn)前端邏輯。
數(shù)據(jù)字段定義:
value:表示彈幕的內(nèi)容(必填)
time:表示彈幕出現(xiàn)的時(shí)間(必填)
speed:表示彈幕移動(dòng)的速度(選填)
color:表示彈幕文字的顏色(選填)
fontSize:表示彈幕的字體大?。ㄟx填)
opacity:表示彈幕文字的透明度(選填)
上面的 value 和 time 是必填參數(shù),其他的選填參數(shù)可以在前端設(shè)置默認(rèn)值。
前端定義的假數(shù)據(jù)如下:
// 文件:index.js let data = [ { value: "這是第一條彈幕", speed: 2, time: 0, color: "red", fontSize: 20 }, { value: "這是第二條彈幕", time: 1 } ];
我們希望是把彈幕封裝成一個(gè)功能,只要有需要的地方就可以使用,從而實(shí)現(xiàn)復(fù)用,那么不同的地方使用這個(gè)功能通常的方式是 new 一個(gè)實(shí)例,傳入當(dāng)前使用該功能對(duì)應(yīng)的參數(shù),我們也使用這種方式來(lái)實(shí)現(xiàn),所以我們需要封裝一個(gè)統(tǒng)一的構(gòu)造函數(shù)或者類(lèi),參數(shù)為當(dāng)前的 canvas 元素、video 元素和一個(gè) options 對(duì)象,options 里面的 data 屬性為我們的彈幕數(shù)據(jù),之所以不直接傳入 data 是為了后續(xù)參數(shù)的擴(kuò)展,嚴(yán)格遵循開(kāi)放封閉原則,這里我們就統(tǒng)一使用 ES6 的 class 類(lèi)來(lái)實(shí)現(xiàn)。
1、創(chuàng)建彈幕功能的類(lèi)及基本參數(shù)處理布局時(shí)需要注意 Canvas 的默認(rèn)寬為 300px,高為 150px,我們要保證 Canvas 完全覆蓋整個(gè)視頻,需要讓 Canvas 與 video 寬高相等。
因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以 Canvas 畫(huà)布的寬高并沒(méi)有通過(guò) CSS 來(lái)設(shè)置,而是通過(guò) JS 在類(lèi)創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。
// 文件:index.js class CanvasBarrage { constructor(canvas, video, options = {}) { // 如果沒(méi)有傳入 canvas 或者 video 直接跳出 if (!canvas || !video) return; this.canvas = canvas; // 當(dāng)前的 canvas 元素 this.video = video; // 當(dāng)前的 video 元素 // 設(shè)置 canvas 與 video 等高 this.canvas.width = video.clientWidth; this.canvas.height = video.clientHeight; // 默認(rèn)暫停播放,表示不渲染彈幕 this.isPaused = true; // 沒(méi)傳參數(shù)的默認(rèn)值 let defaultOptions = { fontSize: 20, color: "gold", speed: 2, opacity: 0.3, data: [] }; // 對(duì)象的合并,將默認(rèn)參數(shù)對(duì)象的屬性和傳入對(duì)象的屬性統(tǒng)一放到當(dāng)前實(shí)例上 Object.assign(this, defaultOptions, options); } }
應(yīng)該掛在實(shí)例上的屬性除了有當(dāng)前的 canvas 元素、video 元素、彈幕數(shù)據(jù)的默認(rèn)選項(xiàng)以及彈幕數(shù)據(jù)之外,還應(yīng)該有一個(gè)代表當(dāng)前是否渲染彈幕的參數(shù),因?yàn)橐曨l暫停的時(shí)候,彈幕也是暫停的,所以沒(méi)有重新渲染,因?yàn)槭欠駮和Ec彈幕是否渲染的狀態(tài)是一致的,所以我們這里就用 isPaused 參數(shù)來(lái)代表當(dāng)前是否暫停或重新渲染彈幕,值類(lèi)型為布爾值。
2、創(chuàng)建構(gòu)造每一條彈幕的類(lèi)我們知道,后臺(tái)返回給我們的彈幕數(shù)據(jù)是一個(gè)數(shù)組,這個(gè)數(shù)組里的每一個(gè)彈幕都是一個(gè)對(duì)象,而對(duì)象上有著這條彈幕的信息,如果我們需要在每一個(gè)彈幕對(duì)象上再加一些新的信息或者在每一個(gè)彈幕對(duì)象的處理時(shí)用到了當(dāng)前彈幕功能類(lèi) CanvasBarrage 實(shí)例的一些屬性值,取值顯然是不太方便的,這樣為了后續(xù)方便擴(kuò)展,遵循開(kāi)放封閉原則,我們把每一個(gè)彈幕的對(duì)象轉(zhuǎn)變成同一個(gè)類(lèi)的實(shí)例,所以我們創(chuàng)建一個(gè)名為 Barrage 的類(lèi),讓我們每一條彈幕的對(duì)象進(jìn)入這個(gè)類(lèi)里面走一遭,掛上一些擴(kuò)展的屬性。
// 文件:index.js class Barrage { constructor(item, ctx) { this.value = item.value; // 彈幕的內(nèi)容 this.time = item.time; // 彈幕出現(xiàn)的時(shí)間 this.item = item; // 每一個(gè)彈幕的數(shù)據(jù)對(duì)象 this.ctx = ctx; // 彈幕功能類(lèi)的執(zhí)行上下文 } }
在我們的 CanvasBarrage 類(lèi)上有一個(gè)存儲(chǔ)彈幕數(shù)據(jù)的數(shù)組 data,此時(shí)我們需要給 CanvasBarrage 增加一個(gè)屬性用來(lái)存放 “加工” 后的每條彈幕對(duì)應(yīng)的實(shí)例。
// 文件:index.js class CanvasBarrage { constructor(canvas, video, options = {}) { // 如果沒(méi)有傳入 canvas 或者 video 直接跳出 if (!canvas || !video) return; this.canvas = canvas; // 當(dāng)前的 canvas 元素 this.video = video; // 當(dāng)前的 video 元素 // 設(shè)置 canvas 與 video 等高 this.canvas.width = video.clientWidth; this.canvas.height = video.clientHeight; // 默認(rèn)暫停播放,表示不渲染彈幕 this.isPaused = true; // 沒(méi)傳參數(shù)的默認(rèn)值 let defaultOptions = { fontSize: 20, color: "gold", speed: 2, opacity: 0.3, data: [] }; // 對(duì)象的合并,將默認(rèn)參數(shù)對(duì)象的屬性和傳入對(duì)象的屬性統(tǒng)一放到當(dāng)前實(shí)例上 Object.assign(this, defaultOptions, options); // ********** 以下為新增代碼 ********** // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類(lèi) this.barrages = this.data.map(item => new Barrage(item, this)); // ********** 以上為新增代碼 ********** } }
其實(shí)通過(guò)上面操作以后,我們相當(dāng)于把 data 里面的每一條彈幕對(duì)象轉(zhuǎn)換成了一個(gè) Barrage 類(lèi)的一個(gè)實(shí)例,把當(dāng)前的上下文 this 傳入后可以隨時(shí)在每一個(gè)彈幕實(shí)例上獲取 CanvasBarrage 類(lèi)實(shí)例的屬性,也方便我們后續(xù)擴(kuò)展方法,遵循這種開(kāi)放封閉原則的方式開(kāi)發(fā),意義是不言而喻的。
3、在 CanvasBarrage 類(lèi)實(shí)現(xiàn)渲染所有彈幕的 render 方法CanvasBarrage 的 render 方法是在創(chuàng)建彈幕功能實(shí)例的時(shí)候應(yīng)該渲染 Canvas 所以應(yīng)該在 CanvasBarrage 中調(diào)用,在 render 內(nèi)部,每一次渲染之前都應(yīng)該先將 Canvas 畫(huà)布清空,所以需要給當(dāng)前的 CanvasBarrage 類(lèi)新增一個(gè)屬性用于存儲(chǔ) Canvas 畫(huà)布的內(nèi)容。
// 文件:index.js class CanvasBarrage { constructor(canvas, video, options = {}) { // 如果沒(méi)有傳入 canvas 或者 video 直接跳出 if (!canvas || !video) return; this.canvas = canvas; // 當(dāng)前的 canvas 元素 this.video = video; // 當(dāng)前的 video 元素 // 設(shè)置 canvas 與 video 等高 this.canvas.width = video.clientWidth; this.canvas.height = video.clientHeight; // 默認(rèn)暫停播放,表示不渲染彈幕 this.isPaused = true; // 沒(méi)傳參數(shù)的默認(rèn)值 let defaultOptions = { fontSize: 20, color: "gold", speed: 2, opacity: 0.3, data: [] }; // 對(duì)象的合并,將默認(rèn)參數(shù)對(duì)象的屬性和傳入對(duì)象的屬性統(tǒng)一放到當(dāng)前實(shí)例上 Object.assign(this, defaultOptions, options); // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類(lèi) this.barrages = this.data.map(item => new Barrage(item, this)); // ********** 以下為新增代碼 ********** // Canvas 畫(huà)布的內(nèi)容 this.context = canvas.getContext("2d"); // 渲染所有的彈幕 this.render(); // ********** 以上為新增代碼 ********** } // ********** 以下為新增代碼 ********** render() { // 渲染整個(gè)彈幕 // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒(méi)有暫停,繼續(xù)渲染 this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); // 渲染彈幕 this.renderBarrage(); if (this.isPaused == false) { // 遞歸渲染 requestAnimationFrame(this.render.bind(this)); } } // ********** 以上為新增代碼 ********** }
在上面的 CanvasBarrage 的 render 函數(shù)中,清空時(shí)由于 Canvas 性能比較好,所以將整個(gè)畫(huà)布清空,所以從坐標(biāo) (0, 0) 點(diǎn),清空的寬高為整個(gè) Canvas 畫(huà)布的寬高。
只要視頻是在播放狀態(tài)應(yīng)該不斷的調(diào)用 render 方法實(shí)現(xiàn)清空畫(huà)布、渲染彈幕、判斷是否暫停,如果非暫停狀態(tài)繼續(xù)渲染,所以我們用到了遞歸調(diào)用 render 去不斷的實(shí)現(xiàn)渲染,但是遞歸時(shí)如果直接調(diào)用 render,性能特別差,程序甚至?xí)斓?,以往這種情況我們會(huì)在遞歸外層加一個(gè) setTimeout 來(lái)定義一個(gè)短暫的遞歸時(shí)間,但是這個(gè)過(guò)程類(lèi)似于動(dòng)畫(huà)效果,如果使用 setTimeout 其實(shí)是將同步代碼轉(zhuǎn)成了異步執(zhí)行,會(huì)增加不確定性導(dǎo)致畫(huà)面出現(xiàn)卡頓的現(xiàn)象。
這里我們使用 H5 的新 API requestAnimationFrame,可以在平均 1/60 S 內(nèi)幫我執(zhí)行一次該方法傳入的回調(diào),我們直接把 render 函數(shù)作為回調(diào)函數(shù)傳入 requestAnimationFrame,該方法是按照幀的方式執(zhí)行,動(dòng)畫(huà)流暢,需要注意的是,render 函數(shù)內(nèi)使用了 this,所以應(yīng)該處理一下 this 指向問(wèn)題。
由于我們使用面向?qū)ο蟮姆绞?,所以渲染彈幕的具體細(xì)節(jié),我們抽離出一個(gè)多帶帶的方法 renderBarrage,接下來(lái)看一下 renderBarrage 的實(shí)現(xiàn)。
4、CanvasBarrage 類(lèi) render 內(nèi)部 renderBarrage 的實(shí)現(xiàn)// 文件:index.js class CanvasBarrage { constructor(canvas, video, options = {}) { // 如果沒(méi)有傳入 canvas 或者 video 直接跳出 if (!canvas || !video) return; this.canvas = canvas; // 當(dāng)前的 canvas 元素 this.video = video; // 當(dāng)前的 video 元素 // 設(shè)置 canvas 與 video 等高 this.canvas.width = video.clientWidth; this.canvas.height = video.clientHeight; // 默認(rèn)暫停播放,表示不渲染彈幕 this.isPaused = true; // 沒(méi)傳參數(shù)的默認(rèn)值 let defaultOptions = { fontSize: 20, color: "gold", speed: 2, opacity: 0.3, data: [] }; // 對(duì)象的合并,將默認(rèn)參數(shù)對(duì)象的屬性和傳入對(duì)象的屬性統(tǒng)一放到當(dāng)前實(shí)例上 Object.assign(this, defaultOptions, options); // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類(lèi) this.barrages = this.data.map(item => new Barrage(item, this)); // Canvas 畫(huà)布的內(nèi)容 this.context = canvas.getContext("2d"); // 渲染所有的彈幕 this.render(); } render() { // 渲染整個(gè)彈幕 // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒(méi)有暫停,繼續(xù)渲染 this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); // 渲染彈幕 this.renderBarrage(); if (this.isPaused == false) { // 遞歸渲染 requestAnimationFrame(this.render.bind(this)); } } // ********** 以下為新增代碼 ********** renderBarrage() { // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕 let time = this.video.currentTime; this.barrages.forEach(barrage => { // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開(kāi)始渲染(時(shí)間都是以秒為單位) if (time >= barrage.time) { // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制 // 如果沒(méi)有初始化,先去初始化一下 if (!barrage.isInited) { // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識(shí) isInited barrage.init(); barrage.isInited = true; } } }); } // ********** 以上為新增代碼 ********** }
此處的 renderBarrage 方法內(nèi)部主要對(duì)每一條彈幕實(shí)例所設(shè)置的出現(xiàn)時(shí)間和視頻的播放時(shí)間做對(duì)比,如果視頻的播放時(shí)間大于等于了彈幕出現(xiàn)的時(shí)間,說(shuō)明彈幕需要繪制在 Canvas 畫(huà)布內(nèi)。
之前我們的每一條彈幕實(shí)例的屬性可能不全,彈幕的其他未傳參數(shù)并沒(méi)有初始化,所以為了最大限度的節(jié)省性能,我們?cè)趶椖辉摰谝淮卫L制的時(shí)候去初始化參數(shù),等到視頻播放的時(shí)間變化再去重新繪制時(shí),不再初始化參數(shù),所以初始化參數(shù)的方法放在了判斷彈幕出現(xiàn)時(shí)間的條件里面執(zhí)行,又設(shè)置了代表彈幕實(shí)例是不是初始化了的參數(shù) isInited,初始化函數(shù) init 執(zhí)行過(guò)一次后,馬上修改 isInited 的值,保證只初始化參數(shù)一次。
在 renderBarrage 方法中我們可以看出來(lái),其實(shí)我們是循環(huán)了專(zhuān)門(mén)存放每一條彈幕實(shí)例(Barrage 類(lèi)的實(shí)例)的數(shù)組,我們?cè)趦?nèi)部用實(shí)例去調(diào)用的方法 init 應(yīng)該是在 Barrage 類(lèi)的原型上,下面我們?nèi)?Barrage 類(lèi)上實(shí)現(xiàn) init 的邏輯。
5、Barrage 類(lèi) init 的實(shí)現(xiàn)// 文件:index.js class Barrage { constructor(item, ctx) { this.value = item.value; // 彈幕的內(nèi)容 this.time = item.time; // 彈幕出現(xiàn)的時(shí)間 this.item = item; // 每一個(gè)彈幕的數(shù)據(jù)對(duì)象 this.ctx = ctx; // 彈幕功能類(lèi)的執(zhí)行上下文 } // ********** 以下為新增代碼 ********** init() { this.opacity = this.item.opacity || this.ctx.opacity; this.color = this.item.color || this.ctx.color; this.fontSize = this.item.fontSize || this.ctx.fontSize; this.speed = this.item.speed || this.ctx.speed; // 求自己的寬度,目的是用來(lái)校驗(yàn)當(dāng)前是否還要繼續(xù)繪制(邊界判斷) let span = document.createElement("span"); // 能決定寬度的只有彈幕的內(nèi)容和文字的大小,和字體,字體默認(rèn)為微軟雅黑,我們就不做設(shè)置了 span.innerText = this.value; span.style.font = this.fontSize + "px "Microsoft YaHei"; // span 為行內(nèi)元素,取不到寬度,所以我們通過(guò)定位給轉(zhuǎn)換成塊級(jí)元素 span.style.position = "absolute"; document.body.appendChild(span); // 放入頁(yè)面 this.width = span.clientWidth; // 記錄彈幕的寬度 document.body.removeChild(span); // 從頁(yè)面移除 // 存儲(chǔ)彈幕出現(xiàn)的橫縱坐標(biāo) this.x = this.ctx.canvas.width; this.y = this.ctx.canvas.height; // 處理彈幕縱向溢出的邊界處理 if (this.y < this.fontSize) { this.y = this.fontSize; } if (this.y > this.ctx.canvas.height - this.fontSize) { this.y = this.ctx.canvas.height - this.fontSize; } } // ********** 以上為新增代碼 ********** }
在上面代碼的 init 方法中我們其實(shí)可以看出,每條彈幕實(shí)例初始化的時(shí)候初始的信息除了之前說(shuō)的彈幕的基本參數(shù)外,還獲取了每條彈幕的寬度(用于后續(xù)做彈幕是否已經(jīng)完全移出屏幕的邊界判斷)和每一條彈幕的 x 和 y 軸方向的坐標(biāo)并為了防止彈幕在 y 軸顯示不全做了邊界處理。
6、實(shí)現(xiàn)每條彈幕的渲染和彈幕移除屏幕的處理我們當(dāng)時(shí)在 CanvasBarrage 類(lèi)的 render 方法中的渲染每個(gè)彈幕的方法 renderBarrage中(原諒這么啰嗦,因?yàn)榈浆F(xiàn)在內(nèi)容已經(jīng)比較多,說(shuō)的具體一點(diǎn)方便知道是哪個(gè)步驟,哈哈)只做了對(duì)每一條彈幕實(shí)例的初始化操作,并沒(méi)有渲染在 Canvas 畫(huà)布中,這時(shí)我們主要做兩部操作,實(shí)現(xiàn)每條彈幕渲染在畫(huà)布中和左側(cè)移出屏幕不再渲染的邊界處理。
// 文件:index.js class CanvasBarrage { constructor(canvas, video, options = {}) { // 如果沒(méi)有傳入 canvas 或者 video 直接跳出 if (!canvas || !video) return; this.canvas = canvas; // 當(dāng)前的 canvas 元素 this.video = video; // 當(dāng)前的 video 元素 // 設(shè)置 canvas 與 video 等高 this.canvas.width = video.clientWidth; this.canvas.height = video.clientHeight; // 默認(rèn)暫停播放,表示不渲染彈幕 this.isPaused = true; // 沒(méi)傳參數(shù)的默認(rèn)值 let defaultOptions = { fontSize: 20, color: "gold", speed: 2, opacity: 0.3, data: [] }; // 對(duì)象的合并,將默認(rèn)參數(shù)對(duì)象的屬性和傳入對(duì)象的屬性統(tǒng)一放到當(dāng)前實(shí)例上 Object.assign(this, defaultOptions, options); // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類(lèi) this.barrages = this.data.map(item => new Barrage(item, this)); // Canvas 畫(huà)布的內(nèi)容 this.context = canvas.getContext("2d"); // 渲染所有的彈幕 this.render(); } render() { // 渲染整個(gè)彈幕 // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒(méi)有暫停,繼續(xù)渲染 this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); // 渲染彈幕 this.renderBarrage(); if (this.isPaused == false) { // 遞歸渲染 requestAnimationFrame(this.render.bind(this)); } } renderBarrage() { // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕 let time = this.video.currentTime; this.barrages.forEach(barrage => { // ********** 以下為改動(dòng)的代碼 ********** // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開(kāi)始渲染(時(shí)間都是以秒為單位) if (!barrage.flag && time >= barrage.time) { // ********** 以上為改動(dòng)的代碼 ********** // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制 // 如果沒(méi)有初始化,先去初始化一下 if (!barrage.isInited) { // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識(shí) isInited barrage.init(); barrage.isInited = true; } // ********** 以下為新增代碼 ********** barrage.x -= barrage.speed; barrage.render(); // 渲染該條彈幕 if (barrage.x < barrage.width * -1) { barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作 } // ********** 以上為新增代碼 ********** } }); } }
每個(gè)彈幕實(shí)例都有一個(gè) speed 屬性,該屬性代表著彈幕移動(dòng)的速度,換個(gè)說(shuō)法其實(shí)就是每次減少的 x 軸的差值,所以我們其實(shí)是通過(guò)改變 x 軸的值再重新渲染而實(shí)現(xiàn)彈幕的左移,我們創(chuàng)建了一個(gè)標(biāo)識(shí) flag 掛在每個(gè)彈幕實(shí)例下,代表是否已經(jīng)離開(kāi)屏幕,如果離開(kāi)則更改 flag 的值,使外層的 CanvasBarrage 類(lèi)的 render 函數(shù)再次遞歸時(shí)不進(jìn)入渲染程序。
每一條彈幕具體是怎么渲染的,通過(guò)代碼可以看出每個(gè)彈幕實(shí)例在 x 坐標(biāo)改變后都調(diào)用了實(shí)例方法 render 函數(shù),注意此 render 非彼 render,該 render 函數(shù)屬于 Barrage 類(lèi),目的是為了渲染每一條彈幕,而 CanvasBarrage 類(lèi)下的 render,是為了在視頻時(shí)間變化時(shí)清空并重新渲染整個(gè) Canvas 畫(huà)布。
7、Barrage 類(lèi)下的 render 方法的實(shí)現(xiàn)// 文件:index.js class Barrage { constructor(item, ctx) { this.value = item.value; // 彈幕的內(nèi)容 this.time = item.time; // 彈幕出現(xiàn)的時(shí)間 this.item = item; // 每一個(gè)彈幕的數(shù)據(jù)對(duì)象 this.ctx = ctx; // 彈幕功能類(lèi)的執(zhí)行上下文 } init() { this.opacity = this.item.opacity || this.ctx.opacity; this.color = this.item.color || this.ctx.color; this.fontSize = this.item.fontSize || this.ctx.fontSize; this.speed = this.item.speed || this.ctx.speed; // 求自己的寬度,目的是用來(lái)校驗(yàn)當(dāng)前是否還要繼續(xù)繪制(邊界判斷) let span = document.createElement("span"); // 能決定寬度的只有彈幕的內(nèi)容和文字的大小,和字體,字體默認(rèn)為微軟雅黑,我們就不做設(shè)置了 span.innerText = this.value; span.style.font = this.fontSize + "px "Microsoft YaHei"; // span 為行內(nèi)元素,取不到寬度,所以我們通過(guò)定位給轉(zhuǎn)換成塊級(jí)元素 span.style.position = "absolute"; document.body.appendChild(span); // 放入頁(yè)面 this.width = span.clientWidth; // 記錄彈幕的寬度 document.body.removeChild(span); // 從頁(yè)面移除 // 存儲(chǔ)彈幕出現(xiàn)的橫縱坐標(biāo) this.x = this.ctx.canvas.width; this.y = this.ctx.canvas.height; // 處理彈幕縱向溢出的邊界處理 if (this.y < this.fontSize) { this.y = this.fontSize; } if (this.y > this.ctx.canvas.height - this.fontSize) { this.y = this.ctx.canvas.height - this.fontSize; } } // ********** 以下為新增代碼 ********** render() { this.ctx.context.font = this.fontSize + "px "Microsoft YaHei""; this.ctx.context.fillStyle = this.color; this.ctx.context.fillText(this.value, this.x, this.y); } // ********** 以上為新增代碼 ********** }
從上面新增代碼我們可以看出,其實(shí) Barrage 類(lèi)的 render 方法只是將每一條彈幕的字號(hào)、顏色、內(nèi)容、坐標(biāo)等屬性通過(guò) Canvas 的 API 添加到了畫(huà)布上。
8、實(shí)現(xiàn)播放、暫停事件還記得我們的 CanvasBarrage 類(lèi)里面有一個(gè)屬性 isPaused,屬性值控制了我們是否遞歸渲染,這個(gè)屬性與視頻暫停的狀態(tài)是一致的,我們?cè)诓シ诺臅r(shí)候,彈幕不斷的清空并重新繪制,當(dāng)暫停的時(shí)候彈幕也應(yīng)該跟著暫停,說(shuō)白了就是不在調(diào)用 CanvasBarrage 類(lèi)的 render 方法,其實(shí)就是在暫停、播放的過(guò)程中不斷的改變 isPaused 的值即可。
還記得我們之前構(gòu)造的兩條假數(shù)據(jù) data 吧,接下來(lái)我們添加播放、暫停事件,來(lái)嘗試使用一下我們的彈幕功能。
// 文件:index.js // 實(shí)現(xiàn)一個(gè)簡(jiǎn)易選擇器,方便獲取元素,后面獲取元素直接調(diào)用 $ const $ = document.querySelector.bind(document); // 獲取 Canvas 元素和 Video 元素 let canvas = $("#canvas"); let video = $("#video"); let canvasBarrage = new CanvasBarrage(canvas, video, { data }); // 添加播放事件 video.addEventListener("play", function() { canvasBarrage.isPaused = false; canvasBarrage.render(); }); // 添加暫停事件 video.addEventListener("pause", function() { canvasBarrage.isPaused = true; });9、實(shí)現(xiàn)發(fā)送彈幕事件
// 文件:index.js $("#add").addEventListener("click", function() { let time = video.currentTime; // 發(fā)送彈幕的時(shí)間 let value = $("#text").value; // 發(fā)送彈幕的文字 let color = $("#color").value; // 發(fā)送彈幕文字的顏色 let fontSize = $("#range").value; // 發(fā)送彈幕的字體大小 let sendObj = { time, value, color, fontSize }; //發(fā)送彈幕的參數(shù)集合 canvasBarrage.add(sendObj); // 發(fā)送彈幕的方法 });
其實(shí)我們發(fā)送彈幕時(shí),就是向 CanvasBarrage 類(lèi)的 barrages 數(shù)組里添加了一條彈幕的實(shí)例,我們多帶帶封裝了一個(gè) add 的實(shí)例方法。
// 文件:index.js class CanvasBarrage { constructor(canvas, video, options = {}) { // 如果沒(méi)有傳入 canvas 或者 video 直接跳出 if (!canvas || !video) return; this.canvas = canvas; // 當(dāng)前的 canvas 元素 this.video = video; // 當(dāng)前的 video 元素 // 設(shè)置 canvas 與 video 等高 this.canvas.width = video.clientWidth; this.canvas.height = video.clientHeight; // 默認(rèn)暫停播放,表示不渲染彈幕 this.isPaused = true; // 沒(méi)傳參數(shù)的默認(rèn)值 let defaultOptions = { fontSize: 20, color: "gold", speed: 2, opacity: 0.3, data: [] }; // 對(duì)象的合并,將默認(rèn)參數(shù)對(duì)象的屬性和傳入對(duì)象的屬性統(tǒng)一放到當(dāng)前實(shí)例上 Object.assign(this, defaultOptions, options); // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類(lèi) this.barrages = this.data.map(item => new Barrage(item, this)); // Canvas 畫(huà)布的內(nèi)容 this.context = canvas.getContext("2d"); // 渲染所有的彈幕 this.render(); } render() { // 渲染整個(gè)彈幕 // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒(méi)有暫停,繼續(xù)渲染 this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); // 渲染彈幕 this.renderBarrage(); if (this.isPaused == false) { // 遞歸渲染 requestAnimationFrame(this.render.bind(this)); } } renderBarrage() { // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕 let time = this.video.currentTime; this.barrages.forEach(barrage => { // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開(kāi)始渲染(時(shí)間都是以秒為單位) if (!barrage.flag && time >= barrage.time) { // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制 // 如果沒(méi)有初始化,先去初始化一下 if (!barrage.isInited) { // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識(shí) isInited barrage.init(); barrage.isInited = true; } barrage.x -= barrage.speed; barrage.render(); // 渲染該條彈幕 if (barrage.x < barrage.width * -1) { barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作 } } }); } // ********** 以下為新增代碼 ********** add(item) { this.barrages.push(new Barrage(item, this)); } // ********** 以上為新增代碼 ********** }10、拖動(dòng)進(jìn)度條實(shí)現(xiàn)彈幕的前進(jìn)和后退
其實(shí)我們發(fā)現(xiàn),彈幕雖然實(shí)現(xiàn)了正常的播放、暫停以及發(fā)送,但是當(dāng)我們拖動(dòng)進(jìn)度條的時(shí)候彈幕應(yīng)該是跟著視頻時(shí)間同步播放的,現(xiàn)在的彈幕一旦播放過(guò)無(wú)論怎樣拉動(dòng)進(jìn)度條彈幕都不會(huì)再出現(xiàn),我們現(xiàn)在就來(lái)解決這個(gè)問(wèn)題。
// 文件:index.js // 拖動(dòng)進(jìn)度條事件 video.addEventListener("seeked", function() { canvasBarrage.reset(); });
我們?cè)谑录?nèi)部其實(shí)只是調(diào)用了一下 CanvasBarrage 類(lèi)的 reset 方法,這個(gè)方法就是在拖動(dòng)進(jìn)度條的時(shí)候來(lái)幫我們初始化彈幕的狀態(tài)。
// 文件:index.js class CanvasBarrage { constructor(canvas, video, options = {}) { // 如果沒(méi)有傳入 canvas 或者 video 直接跳出 if (!canvas || !video) return; this.canvas = canvas; // 當(dāng)前的 canvas 元素 this.video = video; // 當(dāng)前的 video 元素 // 設(shè)置 canvas 與 video 等高 this.canvas.width = video.clientWidth; this.canvas.height = video.clientHeight; // 默認(rèn)暫停播放,表示不渲染彈幕 this.isPaused = true; // 沒(méi)傳參數(shù)的默認(rèn)值 let defaultOptions = { fontSize: 20, color: "gold", speed: 2, opacity: 0.3, data: [] }; // 對(duì)象的合并,將默認(rèn)參數(shù)對(duì)象的屬性和傳入對(duì)象的屬性統(tǒng)一放到當(dāng)前實(shí)例上 Object.assign(this, defaultOptions, options); // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類(lèi) this.barrages = this.data.map(item => new Barrage(item, this)); // Canvas 畫(huà)布的內(nèi)容 this.context = canvas.getContext("2d"); // 渲染所有的彈幕 this.render(); } render() { // 渲染整個(gè)彈幕 // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒(méi)有暫停,繼續(xù)渲染 this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); // 渲染彈幕 this.renderBarrage(); if (this.isPaused == false) { // 遞歸渲染 requestAnimationFrame(this.render.bind(this)); } } renderBarrage() { // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕 let time = this.video.currentTime; this.barrages.forEach(barrage => { // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開(kāi)始渲染(時(shí)間都是以秒為單位) if (!barrage.flag && time >= barrage.time) { // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制 // 如果沒(méi)有初始化,先去初始化一下 if (!barrage.isInited) { // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識(shí) isInited barrage.init(); barrage.isInited = true; } barrage.x -= barrage.speed; barrage.render(); // 渲染該條彈幕 if (barrage.x < barrage.width * -1) { barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作 } } }); } add(item) { this.barrages.push(new Barrage(item, this)); } // ********** 以下為新增代碼 ********** reset() { // 先清空 Canvas 畫(huà)布 this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); let time = this.video.currentTime; // 循環(huán)每一條彈幕實(shí)例 this.barrages.forEach(barrage => { // 更改已經(jīng)移出屏幕的彈幕狀態(tài) barrage.flag = false; // 當(dāng)拖動(dòng)到的時(shí)間小于等于當(dāng)前彈幕時(shí)間是,重新初始化彈幕的數(shù)據(jù),實(shí)現(xiàn)渲染 if (time <= barrage.time) { barrage.isInited = false; } else { barrage.flag = true; // 否則將彈幕的狀態(tài)設(shè)置為以移出屏幕 } }); } // ********** 以上為新增代碼 ********** }
其實(shí) reset 方法中值做了幾件事:
清空 Canvas 畫(huà)布;
獲取當(dāng)前進(jìn)度條拖動(dòng)位置的時(shí)間;
循環(huán)存儲(chǔ)彈幕實(shí)例的數(shù)組;
將所有彈幕更改為未移出屏幕;
判斷拖動(dòng)時(shí)間和每條彈幕的時(shí)間;
在當(dāng)前時(shí)間以后的彈幕重新初始化數(shù)據(jù);
以前的彈幕更改為已移出屏幕。
從而實(shí)現(xiàn)了拖動(dòng)進(jìn)度條彈幕的 “前進(jìn)” 和 “后退” 功能。
要使用 WebSocket 和 Redis 首先需要去安裝 ws、redis 依賴,在項(xiàng)目根目錄執(zhí)行下面命令:
npm install ws redis
我們創(chuàng)建一個(gè) server.js 文件,用來(lái)寫(xiě)服務(wù)端的代碼:
// 文件:index.js const WebSocket = require("ws"); // 引入 WebSocket const redis = require("redis"); // 引入 redis // 初始化 WebSocket 服務(wù)器,端口號(hào)為 3000 let wss = new WebSocket.Server({ port: 3000 }); // 創(chuàng)建 redis 客戶端 let client = redis.createClient(); // key value // 原生的 websocket 就兩個(gè)常用的方法 on("message")、on("send") wss.on("connection", function(ws) { // 監(jiān)聽(tīng)連接 // 連接上需要立即把 redis 數(shù)據(jù)庫(kù)的數(shù)據(jù)取出返回給前端 client.lrange("barrages", 0, -1, function(err, applies) { // 由于 redis 的數(shù)據(jù)都是字符串,所以需要把數(shù)組中每一項(xiàng)轉(zhuǎn)成對(duì)象 applies = applies.map(item => JSON.parse(item)); // 使用 websocket 服務(wù)器將 redis 數(shù)據(jù)庫(kù)的數(shù)據(jù)發(fā)送給前端 // 構(gòu)建一個(gè)對(duì)象,加入 type 屬性告訴前端當(dāng)前返回?cái)?shù)據(jù)的行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串 ws.send( JSON.stringify({ type: "INIT", data: applies }) ); }); // 當(dāng)服務(wù)器收到消息時(shí),將數(shù)據(jù)存入 redis 數(shù)據(jù)庫(kù) ws.on("message", function(data) { // 向數(shù)據(jù)庫(kù)存儲(chǔ)時(shí)存的是字符串,存入并打印數(shù)據(jù),用來(lái)判斷是否成功存入數(shù)據(jù)庫(kù) client.rpush("barrages", data, redis.print); // 再將當(dāng)前這條數(shù)據(jù)返回給前端,同樣添加 type 字段告訴前端當(dāng)前行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串 ws.send( JSON.stringify({ type: "ADD", data: JSON.parse(data) }) ); }); });
服務(wù)器的邏輯很清晰,在 WebSocket 連接上時(shí),立即獲取 Redis 數(shù)據(jù)庫(kù)的所有彈幕數(shù)據(jù)返回給前端,當(dāng)前端點(diǎn)擊發(fā)送彈幕按鈕發(fā)送數(shù)據(jù)時(shí),接收數(shù)據(jù)存入 Redis 數(shù)據(jù)庫(kù)中并打印驗(yàn)證數(shù)據(jù)是否成功存入,再通過(guò) WebSocket 服務(wù)把當(dāng)前這一條數(shù)返回給前端,需要注意一下幾點(diǎn):
從 Redis 數(shù)據(jù)庫(kù)中取出全部彈幕數(shù)據(jù)的數(shù)組內(nèi)部都存儲(chǔ)的是字符串,需要使用 JSON.parse 方法進(jìn)行解析;
將數(shù)據(jù)發(fā)送前端時(shí),最外層要使用 JSON.stringify 重新轉(zhuǎn)換成字符串發(fā)送;
在初始化階段 WebSocket 發(fā)送所有數(shù)據(jù)和前端添加新彈幕 WebSocket 將彈幕的單條數(shù)據(jù)重新返回時(shí),需要添加對(duì)應(yīng)的 type 值告訴前端,當(dāng)前的操作行為。
2、前端代碼的修改在沒(méi)有實(shí)現(xiàn)后端代碼之前,前端使用的是 data 的假數(shù)據(jù),是在添加彈幕事件中,將獲取的新增彈幕信息通過(guò) CanvasBarrage 類(lèi)的 add 方法直接創(chuàng)建 Barrage 類(lèi)的實(shí)例,并加入到存放彈幕實(shí)例的 barrages 數(shù)組中。
現(xiàn)在我們需要更正一下交互邏輯,在發(fā)送彈幕事件觸發(fā)時(shí),我們應(yīng)該先將獲取的單條彈幕數(shù)據(jù)通過(guò) WebSocket 發(fā)送給后端服務(wù)器,在服務(wù)器重新將消息返還給我們的時(shí)候,去將這條數(shù)據(jù)通過(guò) CanvasBarrage 類(lèi)的 add 方法加入到存放彈幕實(shí)例的 barrages 數(shù)組中。
還有在頁(yè)面初始化時(shí),我們之前在創(chuàng)建 CanvasBarrage 類(lèi)實(shí)例的時(shí)候直接傳入了 data 假數(shù)據(jù),現(xiàn)在需要通過(guò) WebSocket 的連接事件,在監(jiān)聽(tīng)到連接 WebSocket 服務(wù)時(shí),去創(chuàng)建 CanvasBarrage 類(lèi)的實(shí)例,并直接把服務(wù)端返回 Redis 數(shù)據(jù)庫(kù)真實(shí)的數(shù)據(jù)作為參數(shù)傳入,前端代碼修改如下:
// 文件:index.js // ********** 下面代碼被刪掉了 ********** // let canvasBarrage = new CanvasBarrage(canvas, video, { // data // }); // ********** 上面代碼被刪掉了 ********** // ********** 以下為新增代碼 ********** let canvasBarrage; // 創(chuàng)建 WebSocket 連接 let socket = new WebSocket("ws://localhost:3000"); // 監(jiān)聽(tīng)連接事件 socket.onopen = function() { // 監(jiān)聽(tīng)消息 socket.onmessage = function(e) { // 將收到的消息從字符串轉(zhuǎn)換成對(duì)象 let message = JSON.parse(e.data); // 根據(jù)不同情況判斷是初始化還是發(fā)送彈幕 if (message.type === "INIT") { // 創(chuàng)建 CanvasBarrage 的實(shí)例添加彈幕功能,傳入真實(shí)的數(shù)據(jù) canvasBarrage = new CanvasBarrage(canvas, video, { data: message.data }); } else if (message.type === "ADD") { // 如果是添加彈幕直接將 WebSocket 返回的單條彈幕存入 barrages 中 canvasBarrage.add(message.data); } }; }; // ********** 以上為新增代碼 ********** $("#add").addEventListener("click", function() { let time = video.currentTime; // 發(fā)送彈幕的時(shí)間 let value = $("#text").value; // 發(fā)送彈幕的文字 let color = $("#color").value; // 發(fā)送彈幕文字的顏色 let fontSize = $("#range").value; // 發(fā)送彈幕的字體大小 let sendObj = { time, value, color, fontSize }; //發(fā)送彈幕的參數(shù)集合 // ********** 以下為新增代碼 ********** socket.send(JSON.stringify(sendObj)); // ********** 以上為新增代碼 ********** // ********** 下面代碼被刪掉了 ********** // canvasBarrage.add(sendObj); // 發(fā)送彈幕的方法 // ********** 上面代碼被刪掉了 ********** });
現(xiàn)在我們可以打開(kāi) index.html 文件并啟動(dòng) server.js 服務(wù)器,就可以實(shí)現(xiàn)真實(shí)的視頻彈幕操作了,但是我們還是差了最后一步,當(dāng)前的服務(wù)只能同時(shí)服務(wù)一個(gè)人,但真實(shí)的場(chǎng)景是同時(shí)看視頻的有很多人,而且發(fā)送的彈幕是共享的。
3、實(shí)現(xiàn)多端通信、彈幕共享我們需要處理兩件事情:
第一件事情是實(shí)現(xiàn)多端通信共享數(shù)據(jù)庫(kù)信息;
第二件事情是當(dāng)有人離開(kāi)的時(shí)候清除關(guān)閉的 WebSocket 對(duì)象。
// 文件:server.js const WebSocket = require("ws"); // 引入 WebSocket const redis = require("redis"); // 引入 redis // 初始化 WebSocket 服務(wù)器,端口號(hào)為 3000 let wss = new WebSocket.Server({ port: 3000 }); // 創(chuàng)建 redis 客戶端 let client = redis.createClient(); // key value // ********** 以下為新增代碼 ********** // 存儲(chǔ)所有 WebSocket 用戶 let clientsArr = []; // ********** 以上為新增代碼 ********** // 原生的 websocket 就兩個(gè)常用的方法 on("message")、on("send") wss.on("connection", function(ws) { // ********** 以下為新增代碼 ********** // 將所有通過(guò) WebSocket 連接的用戶存入數(shù)組中 clientsArr.push(ws); // ********** 以上為新增代碼 ********** // 監(jiān)聽(tīng)連接 // 連接上需要立即把 redis 數(shù)據(jù)庫(kù)的數(shù)據(jù)取出返回給前端 client.lrange("barrages", 0, -1, function(err, applies) { // 由于 redis 的數(shù)據(jù)都是字符串,所以需要把數(shù)組中每一項(xiàng)轉(zhuǎn)成對(duì)象 applies = applies.map(item => JSON.parse(item)); // 使用 websocket 服務(wù)器將 redis 數(shù)據(jù)庫(kù)的數(shù)據(jù)發(fā)送給前端 // 構(gòu)建一個(gè)對(duì)象,加入 type 屬性告訴前端當(dāng)前返回?cái)?shù)據(jù)的行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串 ws.send( JSON.stringify({ type: "INIT", data: applies }) ); }); // 當(dāng)服務(wù)器收到消息時(shí),將數(shù)據(jù)存入 redis 數(shù)據(jù)庫(kù) ws.on("message", function(data) { // 向數(shù)據(jù)庫(kù)存儲(chǔ)時(shí)存的是字符串,存入并打印數(shù)據(jù),用來(lái)判斷是否成功存入數(shù)據(jù)庫(kù) client.rpush("barrages", data, redis.print); // ********** 以下為修改后的代碼 ********** // 循環(huán)數(shù)組,將某一個(gè)人新發(fā)送的彈幕在存儲(chǔ)到 Redis 之后返回給所有用戶 clientsArr.forEach(w => { // 再將當(dāng)前這條數(shù)據(jù)返回給前端,同樣添加 type 字段告訴前端當(dāng)前行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串 w.send( JSON.stringify({ type: "ADD", data: JSON.parse(data) }) ); }); // ********** 以上為修改后的代碼 ********** }); // ********** 以下為新增代碼 ********** // 監(jiān)聽(tīng)關(guān)閉連接事件 ws.on("close", function() { // 當(dāng)某一個(gè)人關(guān)閉連接離開(kāi)時(shí),將這個(gè)人從當(dāng)前存儲(chǔ)用戶的數(shù)組中移除 clientsArr = clientsArr.filter(client => client != ws); }); // ********** 以上為新增代碼 ********** });
上面就是 Canvas + WebSocket + Redis 視頻彈幕的實(shí)現(xiàn),實(shí)現(xiàn)過(guò)程可能有些復(fù)雜,但整個(gè)過(guò)程寫(xiě)的還是比較詳細(xì),可能需要一定的耐心慢慢的讀完,并最好一步一步跟著寫(xiě)一寫(xiě),希望這篇文章可以讓讀到的人解決視頻彈幕類(lèi)似的需求,真正理解整個(gè)過(guò)程和開(kāi)放封閉原則,認(rèn)識(shí)到前端面向?qū)ο缶幊趟枷氲拿馈?/p>
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/113984.html
摘要:創(chuàng)建彈幕功能的類(lèi)及基本參數(shù)處理布局時(shí)需要注意的默認(rèn)寬為,高為,我們要保證完全覆蓋整個(gè)視頻,需要讓與寬高相等。因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以畫(huà)布的寬高并沒(méi)有通過(guò)來(lái)設(shè)置,而是通過(guò)在類(lèi)創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。 showImg(https://segmentfault.com/img/remote/1460000018998386); 閱讀原文 頁(yè)面布...
摘要:創(chuàng)建彈幕功能的類(lèi)及基本參數(shù)處理布局時(shí)需要注意的默認(rèn)寬為,高為,我們要保證完全覆蓋整個(gè)視頻,需要讓與寬高相等。因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以畫(huà)布的寬高并沒(méi)有通過(guò)來(lái)設(shè)置,而是通過(guò)在類(lèi)創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。 showImg(https://segmentfault.com/img/remote/1460000018998386); 閱讀原文 頁(yè)面布...
showImg(https://segmentfault.com/img/bVbk1Nl?w=1080&h=602); 說(shuō)起彈幕看過(guò)視頻的都不會(huì)陌生,那滿屏充滿著飄逸評(píng)論的效果,讓人如癡如醉,無(wú)法自拔 最近也是因?yàn)樵趯W(xué)習(xí)關(guān)于 canvas 的知識(shí),所以今天就想和大家分享一個(gè)關(guān)于彈幕的故事 那么究竟彈幕是怎樣煉成的呢? 我們且往下看(look) 看什么?看效果 showImg(https://s...
摘要:經(jīng)過(guò)琢磨,其實(shí)是要考慮安全性的。具體在以下幾個(gè)方面跨域連接協(xié)議升級(jí)前握手?jǐn)r截器消息信道攔截器對(duì)于跨域問(wèn)題,我們可以通過(guò)方法來(lái)設(shè)置可連接的域名,防止跨站連接。 前言 大學(xué)的學(xué)習(xí)時(shí)光臨近尾聲,感嘆時(shí)光匆匆,三年一晃而過(guò)。同學(xué)們都忙著找工作,我也在這里拋一份簡(jiǎn)歷吧,歡迎各位老板和獵手誠(chéng)邀。我們進(jìn)入正題。直播行業(yè)是當(dāng)前火熱的行業(yè),誰(shuí)都想從中分得一杯羹,直播養(yǎng)活了一大批人,一個(gè)平臺(tái)主播粗略估計(jì)就...
閱讀 1318·2023-04-25 18:57
閱讀 2229·2023-04-25 16:28
閱讀 4050·2021-11-24 09:39
閱讀 3707·2021-11-16 11:45
閱讀 1940·2021-10-13 09:40
閱讀 1312·2019-08-30 15:52
閱讀 1785·2019-08-30 10:57
閱讀 718·2019-08-29 16:55