HTML5 CANVAS 彈幕插件--DanMuer.js(V3.2.5)

最新版本 V 3.2.5

新增了圖片彈幕類型,修改了demo展現頁面,調整了部分代碼,具體請參看git裏的CHANGELOG.md和README.mdcss

文章裏主要講實現方法和設計思想,因此有部分接口依舊是老版本接口,最新的接口請去git裏面查看linux

前言

說實話,從第二版到如今又過了半年,原本覺得可能不會寫第三版的,頂多將第二版的代碼重構下就能夠了,沒想到仍是花了一個星期左右續寫了第三版。主要是由於第二版中 播放器模塊和彈幕模塊耦合得太嚴重了,遠遠達不到我想要的效果,因此續寫了第三版。此次的代碼將更輕,我去除了播放器模塊,使得插件的適用範圍更加的擴大,並且讓我有點驚喜的是在寫第三版的過程當中又讓彈幕系統的性能進一步獲得了提高,能夠講也是額外的驚喜了。git

因爲第三版我是用ES6語法寫的,因此兼容性不是很好(沒錯,我只是在針對IE),就算用babel轉成ES5,IE依舊毒,目前支持IE10+。因此後面我會抽個時間去寫個ES5全兼容版本的,不考慮IE或者只是對源碼感興趣的能夠盡情使用github

github : github
API接口都在git裏面,文章不會介紹插件使用相關的內容,僅僅解釋部分源碼和設計思想,若是以爲插件還行,請你們git給個星,謝謝
demo : 我是demo算法

注意:

我碰到好像有人對demo不知道怎麼操做,下面我簡單介紹下基本操做:
全部已發佈功能項能夠經過下拉框進行切換,目前包括「添加普通彈幕」,「添加高級彈幕」,「過濾」,「添加全局樣式」,「控制項」canvas

  1. 添加普通彈幕和添加高級彈幕都只是添加數據而已,不會運行插件,你須要跳到「控制項」點擊啓動,而後等彈幕出來便可
  2. 高級彈幕的動畫是屬於排隊動畫,你要先將修改後的數據「保存爲第n步」(n至少爲1)後點擊肯定添加才能夠
  3. 過濾功能的話,最簡單的「type」:「slide」,表示過濾全部滾動型的彈幕,或者「text」:「string」表示過濾包含string的全部彈幕
  4. 你能夠先啓動而後添加彈幕,也是同樣的,操做順序沒過高要求

代碼的那些事

源碼總共由5部分組成:segmentfault

  1. 普通彈幕類
  2. 高級彈幕類
  3. 主程序類
  4. 封裝輸出函數
  5. Tween算法類

第4和第5個部分比較簡單,第5部分就是Tween算法,原汁原味,第4部分則是將全部內部的接口進行過濾,選擇性地暴露一些我想暴露的內部功能接口,而且提供一個對外的接口,增長一點穩定性罷了。源碼以下:babel

let DanMuer = function(wrapper,opts){
    let proxyDMer = new Proxy( new DMer(wrapper,opts), {
        get : function(target,key){
            if(typeof target[key] == "function")
            return target[key].bind(target);
            return target[key];
        }
    }); //保證this指向原對象

    let DM = proxyDMer;

    //選擇性的暴露某些接口
    return {
        pause : DM.pause, //暫停
        run : DM.run, //繼續
        start : DM.start, //運行
        stop : DM.stop,    //中止
        changeStyle : DM.changeStyle, //修改普通彈幕全局樣式
        addGradient : DM.addGradient, //普通彈幕漸變
        setSize : DM.setSize, //修改寬高
        inputData : DM.inputData, //向普通彈幕插入數據
        inputEffect : DM.inputEffect, //向高級彈幕插入數據
        clear : DM.clear, //清除全部彈幕
        reset : DM.reset, //從新從某個彈幕開始
        addFilter : DM.addFilter, //添加過濾
        removeFilter : DM.removeFilter, //刪除過濾
        disableEffect : DM.disableEffect, //不啓用高級彈幕
        enableEffect : DM.enableEffect, //啓用高級彈幕
        getSize : DM.getSize, //獲取寬高,
        getFPS : DM.getFPS //獲取fps
    };
};

//提供對外的引用接口
if( typeof module != 'undefined' && module.exports ){
    module.exports = DanMuer;
} else if( typeof define == "function" && define.amd ){
    define(function(){ return DanMuer;});
} else {
    window.DanMuer = DanMuer;
}

第3個部分屬於入口類,事實上每次調用插件都會先對第3部分進行實例化,這裏主要保存一些對外暴露的API接口,還有就是插件的初始化函數,事件函數以及主循環函數,用於對插件整體的控制,部分源碼以下:app

//初始化
    constructor(wrap,opts = {}){

        if(!wrap){
            throw new Error("沒有設置正確的wrapper");
        }

        //datas
        this.wrapper = wrap;
        this.width = wrap.clientWidth;
        this.height = wrap.clientHeight;
        this.canvas = document.createElement("canvas");
        this.canvas2 = document.createElement("canvas");

        this.normal = new normalDM(this.canvas,opts); //這裏是普通彈幕的對象
        this.effect = new effectDM(this.canvas2,opts); //這裏是高級彈幕的對象

        this.name = opts.name || ""; //沒卵用
        this.fps = 0;

        //status
        this.drawing = opts.auto || false;
        this.startTime = new Date().getTime();

        //fn
        this[init]();
        this[loop]();
        if(opts.enableEvent)
        this.initEvent(opts);
    }

    [init](){
        //生成對應的canvas
        this.canvas.style.cssText = "position:absolute;z-index:100;top:0px;left:0px;";
        this.canvas2.style.cssText = "position:absolute;z-index:101;top:0px;left:0px;";
        this.setSize();
        this.wrapper.appendChild(this.canvas);
        this.wrapper.appendChild(this.canvas2);
    }

    //loop
    [loop](normal = this.normal,effect = this.effect,prev = this.startTime){
        
        let now = new Date().getTime();

        if(!this.drawing){
            normal.clearRect();
            effect.clearRect();
            return false;
        } else {
            let [w,h,time] = [this.width,this.height,now - prev];
            this.fps = 1000 / time >> 0;
            //這裏進行內部的循環操做
            normal.update(w,h,time);
            effect.update(w,h,time);
        }

        requestAnimationFrame( () => { this[loop](normal,effect,now); } );
    }
    
    //主要對鼠標右鍵進行綁定
    initEvent(opts){
        let [el,normal,searching] = [this.canvas2,this.normal,false];

        el.onmouseup = function(e){
            e = e || event;

            if( searching ) return false;
            searching = true;

            if( e.button == 2 ){
                let [pos,result] = [e.target.getBoundingClientRect(),""];
                let [x,y,i,items,item] = [ e.clientX - pos.left,
                                             e.clientY - pos.top,
                                             0, normal.save ];
                for( ; item = items[i++]; ){
                    let [ix,iy,w,h] = [item.x, item.y, item.width + 10, item.height];

                    if( x < ix  || x > ix + w || y < iy - h/2 || y > iy + h/2 || item.hide || item.recovery )
                    continue;

                    result = item;
                    break;
                }
            
                let callback = opts.callback || function(){};

                callback(result);

                searching = false;
            }

        };

        el.oncontextmenu = function(e){
            e = e || event;
            e.preventDefault();
        };

    }

源碼最主要的就是第1部分和第2部分,你們在git->src裏面能夠看到兩個類分別對應的文件,源碼裏面個人註釋打了不少,並且每一個函數的長度都不長,很容易看懂,這裏就不對每個功能作具體介紹了,下面主要講講幾個比較重要的函數和設計思想:dom

/*循環,這裏是對主程序暴露的主要接口,用於普通彈幕內部的循環工做,其實工做流程主要由幾個步驟組成:
** 1.判斷全局樣式是否發生變化,保持全局樣式的準確性
** 2.判斷當前彈幕機的狀態(如暫停、運行等)並進行相關操做
** 3.更新for循環的初始下標(startIndex),主要是用於性能的優化
** 4.計算每一個彈幕的狀態
** 5.繪製彈幕
** 6.對每一個彈幕的狀態進行評估,若是已經顯示完成就進行回收
** 基本上其餘的功能都是圍繞這些步驟開始拓展和完善,明白了工做原理後其餘的函數就很好理
** 解了,都是爲了完成這些工做流程而進行的,並且基本上源碼裏都有註釋,這裏就不詳細說了
*/
    update(w,h,time){

        let [items,cxt] = [this.save,this.cxt];

        this.globalChanged && this.initStyle(cxt); //初始化全局樣式

        !this.looped && this.countWidth(items); //計算文本寬度以及初始化位置(只執行一次)

        if( this.paused ) return false; //暫停

        this.refresh(items); //更新初始下標startIndex

        let [i,item] = [this.startIndex];

        cxt.clearRect(0,0,w,h);

        for(  ; item = items[i++]; ){
            this.step(item,time);
            this.draw(item,cxt);
            this.recovery(item,w);
        }

    }

針對普通彈幕類還有一個有點難理解的是「通道」的獲取。這裏的「通道」是指彈幕從右往左運行時所在的那一行位置,這些通道是在canvas尺寸變化時生成的,不一樣類型的彈幕都有其通道集合。當一條新彈幕須要顯示在canvas上時須要去獲取它被分配的位置,也就是通道,通道被佔用時,該行將不會從新放置新的彈幕, 當通道已經被分配完成後,將會隨機生成一條臨時通道,臨時通道的位置隨機出現,而且臨時經過被釋放時不會被收回通道集合中,而正常通道會被收回到集合中以待被下一個彈幕調用。下面是代碼:

//生成通道行
    countRows(){

        //保存臨時變量
        let unitHeight = parseInt(this.globalSize) + this.space;
        let [rowNum , rows] = [
            ( ( this.height - 20 ) / unitHeight ) >> 0,
            this.rows
        ];

        //重置通道
        for( let key of Object.keys(rows) ){
            rows[key] = [];
        }

        //從新生成通道
        for( let i = 0 ; i < rowNum; i++ ){
            let obj = {
                idx : i,
                y : unitHeight * i + 20
            };
            rows.slide.push(obj);

            i >= rowNum / 2 ? rows.bottom.push(obj) : rows.top.push(obj);
        }

        //更新實例屬性
        this.unitHeight = unitHeight;
        this.rowNum = rowNum;
    }



//獲取通道
    getRow(item){
        
        //若是該彈幕正在顯示中,則返回其現有通道
        if( item.row ) 
        return item.row;

        //獲取新通道
        const [rows,type] = [this.rows,item.type];
        const row = ( type != "bottom" ? rows[type].shift() : rows[type].pop() );
        //生成臨時通道
        const tempRow = this["getRow_"+type]();

        if( row && item.type == "slide" ){
            item.x += ( row.idx * 8 );
            item.speed += ( row.idx / 3 );
        }

        //返回分配的通道
        return row || tempRow;

    }

    getRow_bottom(){
        return {
            y : 20 + this.unitHeight * ( ( Math.random() * this.rowNum / 2 + this.rowNum / 2 ) << 0 ),
            speedChange : false,
            tempItem : true
        };
    }

    getRow_slide(){
        return {
            y : 20 + this.unitHeight * ( ( Math.random() * this.rowNum ) << 0 ),
            speedChange : true,
            tempItem : true
        };
    }

    getRow_top(){
        return {
            y : 20 + this.unitHeight * ( ( Math.random() * this.rowNum / 2 ) << 0 ),
            speedChange : false,
            tempItem : true
        };
    }

高級彈幕類與普通彈幕類有點微妙的差異,但整體是同樣,惟一須要在乎的是與計算相關的代碼,由於不難因此這裏也不作繼續說明了,請參看源碼裏的註釋。

結語

就第二版來講,第三版性能更好,並且實現了播放器模塊和彈幕模塊的解耦,也就是說相比第二版,第三版 能夠適用但不限於播放器,可用性更高,並且實現了高級彈幕的發送,將來將慢慢補齊更多的功能和代碼重構,但願你們遇到什麼BUG或者是有某些的需求,請私信或是將反饋提交到本郵箱:454236029@qq.com || z454236029@gmail.com,若是以爲本插件對你有用歡迎給個星,謝謝。

相關文章
相關標籤/搜索