当前位置: 首页 > Web前端 > CSS

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

时间:2023-03-30 17:19:03 CSS

最新版本V3.2.5增加了图片弹幕类型,修改了demo展示页面,调整了部分代码。具体可以参考git中的CHANGELOG.md和README.md文章,主要讲实现方法和设计思路,所以部分接口还是老版本接口。最新的接口请到git查看前言。说实话,第二版到现在已经半年了。我想我可能写不出第三版了。顶多重构一下第二版的代码。出乎意料的是,用了大约一个星期的时间继续写第三版。主要是第二版的播放器模块和弹幕模块的耦合太严重了,和我想要的效果相去甚远,所以继续写第三版。这次代码会更轻。我去掉了播放器模块,扩大了插件的适用范围,让我有点意外的是弹幕系统的性能在写第三版的过程中得到了进一步的提升,可以说是额外的惊喜。由于我用ES6语法写的第三版,兼容性不是很好(是的,我只针对IE),即使我用babel转ES5,IE还是有毒,目前支持IE10+。所以后面会花点时间写一个ES5完全兼容的版本,不考虑IE或者只是对源码感兴趣的小伙伴可以尽情使用。github:githubAPI接口都在git里面。文章不会介绍插件使用相关的内容,仅对部分源码和设计思路进行说明。如果觉得插件还行,请给git一个star,谢谢demo:我是demo注:我demo好像有些人不知道怎么操作。简单介绍一下基本操作:所有发布的功能项都可以通过下拉框进行切换,目前包括“添加普通弹幕”、“添加高级弹幕”、“筛选”、“添加全局样式”、“ControlItems”,添加普通弹幕和添加高级弹幕只是添加数据,不会运行插件,需要跳转到“ControlItems”点击启动,然后等待弹幕的到来outtoadvanced弹幕的动画属于排队动画。需要“将修改后的数据保存为第n步”(n至少为1),然后点击确定,添加过滤功能。最简单的"type":"slide",表示过滤所有滚动弹幕,或者"text":"string"表示过滤所有包含字符串的弹幕。可以先开局再加弹幕。同样的道理。那些不需要太多代码的东西的源码都是按照操作顺序来的。总共由5部分组成:普通弹幕类、高级弹幕类、主程序类、封装输出函数、Tween算法类,第四和第五部分比较简单,第五部分是原始Tween算法,以及第四部分是对所有内部接口进行过滤,选择性地暴露一些我想暴露的内部功能接口,并提供一个外部接口,增加一点稳定性。源码如下:letDanMuer=function(wrapper,opts){letproxyDMer=newProxy(newDMer(wrapper,opts),{get:function(target,key){if(typeoftarget[key]==“函数”)返回目标[键].bind(目标);返回目标[键];}});//确保this指向原始对象letDM=proxyDMer;//选择性暴露一些接口return{pause:DM.pause,//暂停run:DM.run,//继续启动:DM.start,//运行stop:DM.stop,//停止changeStyle:DM.changeStyle,//修改普通弹幕全局样式addGradient:DM.addGradient,//普通弹幕渐变setSize:DM.setSize,//修改宽高inputData:DM.inputData,//给普通弹幕插入数据inputEffect:DM.inputEffect,//向高级弹幕插入清除数据:DM.clear,//清除所有弹幕重置:DM.reset,//从某个弹幕重新开始addFilter:DM.addFilter,//添加过滤器removeFilter:DM.removeFilter,//删除过滤器disableEffect:DM.disableEffect,//不开启高级弹幕enableEffect:DM.enableEffect,//开启高级弹幕getSize:DM.getSize,//获取宽高,getFPS:DM.getFPS//获取fps};};//提供外部参考接口if(typeofmodule!='undefined'&&module.exports){module.exports=DanMuer;}elseif(typeofdefine=="function"&&define.amd){define(function(){returnDanMuer;});}else{window.DanMuer=DanMuer;}第三部分属于入门类,其实每一个时间调用插件会先实例化第三部分。这里主要保存了一些暴露的API接口,以及插件初始化函数、事件函数和主循环函数,用于对插件进行整体控制。部分源码如下://Initializeconstructor(wrap,opts={}){if(!wrap){thrownewError("Thecorrectwrapperisnotset");}//数据this.wrapper=wrap;this.width=wrap.clientWidth;这个.height=wrap.clientHeight;this.canvas=document.createElement("canvas");this.canvas2=document.createElement("canvas");this.normal=newnormalDM(this.canvas,opts);//这里是普通弹幕对象this.effect=neweffectDM(this.canvas2,opts);//这里是高级弹幕对象this.name=opts.name||"";//没用this.fps=0;//状态这个.drawing=opts.auto||错误的;this.startTime=newDate().getTime();//fn这个[初始化]();这个[循环]();如果(opts.enableEvent)this.initEvent(opts);}[init](){//生成对应的canvasthis.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;";这个.setSize();this.wrapper.appendChild(this.canvas);this.wrapper.appendChild(this.canvas2);}//loop[loop](normal=this.normal,effect=this.effect,prev=this.startTime){letnow=newDate().getTime();如果(!this.drawing){normal.clearRect();effect.clearRect();返回假;}else{让[w,h,time]=[this.width,this.height,now-prev];this.fps=1000/次>>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||事件;如果(搜索)返回假;搜索=真;if(e.button==2){让[pos,result]=[e.target.getBoundingClientRect(),""];让[x,y,i,items,item]=[e.clientX-pos.left,e.clientY-pos.top,0,normal.save];for(;item=items[i++];){让[ix,iy,w,h]=[item.x,item.y,item.width+10,item.height];如果(xix+w||yiy+h/2||item.hide||item.recovery)继续;关于sult=项目;休息;}让回调=opts.callback||功能(){};回调(结果);搜索=假;}};el.oncontextmenu=function(e){e=e||事件;e.preventDefault();};}源码的主要部分是第一部分和第二部分。在git->src中可以看到这两个类对应的文件。我在源码里打了很多注释,每个函数的篇幅都不长,很容易看懂。这里就不详细介绍每个功能了。下面主要说说几个比较重要的功能和设计思路:/*loop,这里是暴露给主程序的主程序。该接口用于普通弹幕内部的循环工作。其实工作流主要有几个步骤:**1.判断全局样式是否发生变化,保持全局样式的准确性**2.判断当前弹幕机的状态(比如暂停,运行,等)并进行相关操作**3.更新for循环的初始下标(startIndex),主要是为了性能优化**4.计算每条弹幕的状态**5.绘制弹幕**6.评估各个弹幕的状态,如果显示为完成则回收**其他功能基本上都是围绕这几个步骤进行扩展和完善的。了解了工作原理之后,其他的功能也就很好理解了,就是完成这些工作流程,基本上源码里面都有注释,这里就不赘述了*/update(w,h,时间){让[items,cxt]=[this.save,this.cxt];this.globalChanged&&this.initStyle(cxt);//初始化全局样式!this.looped&&this.countWidth(items);//计算文字宽度并初始化位置(只执行一次)如果(this.paused)返回false;//暂停this.refresh(items);//更新初始下标startIndexlet[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);有点难理解的是“通道”的获取。这里的“通道”指的是弹幕从右向左运行的那条线。这些通道是在画布大小发生变化时生成的。不同类型的弹幕都有其频道合集。当新的弹幕需要在画布上显示时,需要获取其分配的位置,即频道。当频道被占用时,这条线不会重新定位新的弹幕。当频道被分配后,会随机生成一个临时频道,临时频道的位置随机出现,临时频道在释放时不会被收回到频道集合中,而普通频道会被收回到下一次弹幕调用的收藏。代码如下://生成通道行countRows(){//保存临时变量letunitHeight=parseInt(this.globalSize)+this.space;让[rowNum,rows]=[((this.height-20)/unitHeight)>>0,this.rows];//重置通道for(letkeyofObject.keys(rows)){rows[key]=[];}//重新生成通道for(leti=0;i=rowNum/2?行。底部。推(对象):行。top.push(obj);}//更新实例属性this.unitHeight=unitHeight;this.rowNum=rowNum;}//获取频道getRow(item){//如果弹幕正在显示,则返回其现有频道if(item.row)returnitem.row;//获取一个新的频道const[rows,type]=[this.rows,item.type];constrow=(type!="bottom"?rows[type].shift():rows[type].pop());//生成临时ChannelconsttempRow=this["getRow_"+type]();if(row&&item.type=="slide"){item.x+=(row.idx*8);item.speed+=(row.idx/3);}//返回分配的通道返回行||临时行;}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,如果您觉得这个插件对您有用,请给个star,谢谢。