用canvas實現一個vue彈幕元件

ideagay發表於2018-11-29

看B站時,對彈幕的實現產生了興趣,一開始想到用css3動畫去實現,後來感覺這樣效能不是很好,查了下資料,發現可以用canvas實現,於是就摸索著寫了一個簡單的彈幕。

彈幕功能

  1. 支援動態新增彈幕
  2. 彈幕不重疊
  3. 自定義彈幕顏色

效果圖

demo 

原始碼地址

用canvas實現一個vue彈幕元件

前端框架選了比較熟悉的vuejs

彈幕滾動的基本思路就是通過定時器不斷地改變彈幕的位置,時時重繪畫布。

實現步驟

先加入一個canvas標籤,這裡有個注意點,關於裝置畫素比對canvas的影響,會出現繪圖模糊。

canvas 繪圖模糊問題

<canvas width="600" height="600"></canvas> // 如果單純這樣寫,canvas會出現模糊

<canvas width="600" height="600" style="width: 300px;height: 300px"></canvas>

//為了不出現模糊,需要設定canvas的css寬高為上下文寬高的1/devicePixelRatio,
本文是對於devicePixelRatio:2的裝置設定的,該值可從window.devicePixelRatio取得。
<canvas ref="hiddenCanvas" width="0" height="0" style="display: none"></canvas> 
// 後面會用到複製程式碼

我們先定義一個陣列來存放彈幕資料,一條彈幕資訊,包括文字內容,x,y座標位置,顏色,速度(可以是隨機或者固定,為了計算簡單,我們這裡採用了固定的速度)

var dmArr = [];
var gap = 80; // 彈幕的上下間距
var hiddenCanvas = this.$refs.hiddenCanvas;

// 增加彈幕的方法
function pushDm(text, color) {
    let y = getY(); // 先確定跑道
    let x = 600; // 初始x座標為canvas的右邊界
    let delayWidth = 0; // 同跑道
    for (let i = 0, len = dmArr.length; i < len; i++) {
        let dm = dmArr[i];
        if (y === dm.y) { // 如果是同跑道,則往後排,設定一定的間隔,保證彈幕不會重疊;
            delayWidth += Math.floor(hiddenCanvas.getContext('2d').measureText(dm.text).width * 4 + 50);
        }   }
   dmArr.push({
       text: text,
       x: x + delayWidth,
       y: y,
       speed: 8,
       color: color || getColor()
   });
}
// 隨機獲得y座標
function getY() {
    let range = Math.floor(600 / gap); // 跑道數量
    return Math.floor(Math.random() * range + 1) * gap;
}
// 隨機獲得顏色
function getColor() {
    return `${Math.floor(Math.random() * 16777215).toString(16)}`;
}

// 寫一個for迴圈,初始化30條彈幕
for (let i = 0; i < 30; i++) {
    pushDm(`It's barrage ${i}`);
}

複製程式碼

接下來設定一個20ms的定時器,實現彈幕滾動效果

var timer = null;
var ctx = this.$refs.canvas.getContext('2d');
function start(){
  timer = setInterval(() => {
    ctx.clearRect(0, 0, 600, 600); // 每次需要清空畫布
    ctx.save();
    ctx.font = '30px Microsoft YaHei'; // 這裡需要把字型大小設為需要顯示的css大小的2倍(devicePixelRatio為2時)
    if (!dmArr.length) stop(); // 如果沒有新彈幕了,就停止計時器
    for (let i = 0, len = this.dmArr.length; i < len; i++) {
        let dm = dmArr[i];
        let overRange = -ctx.measureText(dm.text).width * 2;
        dm.x -= dm.speed;
        if (dm.x < overRange) {
            dmArr.splice(i, 1); // 彈幕在畫布中不可見時,從陣列中移除該項
            continue;
        }
        ctx.fillStyle = `#${dm.color}`;
        ctx.fillText(dm.text, dm.x, dm.y);
    }
    ctx.restore();
  }, 20);
}
function stop() {
    clearInterval(timer);
    ctx.clearRect(0, 0, 600, 600);
}複製程式碼

我們還需要一個輸入框,來實現手動新增彈幕功能

<input type="text" @keyup.enter="sent" v-model="dmInput" maxlength="20">
<button type="button" @click="sent">發表</button>

var dmInput = '';
var color = ''; // 可自定義彈幕的顏色
function sent() {
    if (!dmInput) return;
    stop();
    pushDm(dmInput, color);
    start();
    dmInput = '';
}複製程式碼

有待改進的地方和疑問?

  1. 速度不恆定時,怎麼保持彈幕不重疊
  2. 視訊彈幕是根據彈幕傳送時間點來定位到視訊的每一幀?如何實現?


相關文章