HTML5 CANVAS 彈幕插件(V3.0.0)">HTML5 CANVAS 彈幕插件(V3.0.0)

分類:IT技術 時間:2017-07-09

前言

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

由於第三版我是用ES6語法寫的,所以兼容性不是很好(沒錯,我只是在針對IE),就算用babel轉成ES5,IE依舊毒,所以後面我會抽個時間去寫個ES5全兼容版本的,不考慮IE或者只是對源碼感興趣的可以盡情使用。

demo : 我是demo

github : github

API接口都在git裏面,文章不會介紹插件使用相關的內容,僅僅解釋部分源碼和設計思想,如果覺得插件還行,請大家給個星,謝謝

代碼的那些事

源碼總共由4部分組成:

  1. 普通彈幕類

  2. 高級彈幕類

  3. 主程序類

  4. 封裝輸出函數

第4個部分比較簡單,就是將所有內部的接口進行過濾,選擇性地暴露一些我想暴露的內部功能接口,並且提供一個對外的接口,增加一點穩定性罷了。源碼如下:

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接口,還有就是插件的初始化函數,事件函數以及主循環函數,用於對插件總體的控制,部分源碼如下:

//初始化
    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裏面可以看到兩個類分別對應的文件,源碼裏面我的註釋打了很多,而且每個函數的長度都不長,很容易看懂,這裏就不對每一個功能做具體介紹了,下面主要講講幾個比較重要的函數和設計思想:

/*循環,這裏是對主程序暴露的主要接口,用於普通彈幕內部的循環工作,其實工作流程主要由幾個步驟組成:
** 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或者是有某些的需求,請私信或是將反饋提交到本郵箱:[email protected] || [email protected],如果覺得本插件對你有用歡迎給個星,謝謝。


Tags: 插件 源碼 接口 模塊 續寫 所以

文章來源:


ads
ads

相關文章
ads

相關文章

ad