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

最新版本 V 3.2.5

新增了图片弹幕范例,修正了demo展现页面,调整了部份代码,细致请参看git里的CHANGELOG.md和README.md

文章里主要讲完成要领和设想头脑,所以有部份接口照旧是老版本接口,最新的接口请去git内里检察

媒介

说实话,从第二版到现在又过了半年,原本认为能够不会写第三版的,顶多将第二版的代码重构下就能够了,没想到照样花了一个礼拜摆布续写了第三版。主如果由于第二版中 播放器模块和弹幕模块耦合得太严峻了,远远达不到我想要的结果,所以续写了第三版。此次的代码将更轻,我去除了播放器模块,使得插件的实用范围越发的扩展,而且让我有点欣喜的是在写第三版的过程当中又让弹幕体系的机能进一步得到了提拔,能够讲也是分外的欣喜了。

由于第三版我是用ES6语法写的,所以兼容性不是很好(没错,我只是在针对IE),就算用babel转成ES5,IE照旧毒,现在支撑IE10+。所以背面我会抽个时候去写个ES5全兼容版本的,不斟酌IE或许只是对源码感兴趣的能够恣意运用。

github : github
API接口都在git内里,文章不会引见插件运用相干的内容,仅仅诠释部份源码和设想头脑,假如以为插件还行,请人人git给个星,感谢
demo : 我是demo

注重:

我碰到彷佛有人对demo不知道怎样操纵,下面我简朴引见下基础操纵:
一切已宣布功用项能够经由过程下拉框举行切换,现在包括“增添一般弹幕”,“增添高等弹幕”,“过滤”,“增添全局款式”,“掌握项”

  1. 增添一般弹幕和增添高等弹幕都只是增添数据罢了,不会运转插件,你须要跳到“掌握项”点击启动,然后等弹幕出来即可
  2. 高等弹幕的动画是属于列队动画,你要先将修正后的数据“保留为第n步”(n最少为1)后点击肯定增添才能够
  3. 过滤功用的话,最简朴的“type”:“slide”,示意过滤一切转动型的弹幕,或许“text”:“string”示意过滤包括string的一切弹幕
  4. 你能够先启动然后增添弹幕,也是一样的,操纵递次没太高请求

代码的那些事

源码统共由5部份构成:

  1. 一般弹幕类
  2. 高等弹幕类
  3. 主程序类
  4. 封装输出函数
  5. Tween算法类

第4和第5个部份比较简朴,第5部份就是Tween算法,原汁原味,第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或许是有某些的需求,请私信或是将反应提交到本邮箱:454236029@qq.com || z454236029@gmail.com,假如以为本插件对你有效迎接给个星,感谢。

    原文作者:孤月
    原文地址: https://segmentfault.com/a/1190000010095752
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞