概述

修改了普通弹幕运动的算法,新增了部分功能

详细

修改了普通弹幕运动的算法,新增了部分功能,具体请参看附件里的CHANGELOG.md和README.md

一、概述

说实话,从第二版到现在又过了半年,本来以为可能不会写第三版的,顶多将第二版的代码重构下就可以了,没想到还是花了一个星期左右续写了第三版。主要是因为第二版中 播放器模块和弹幕模块耦合得太严重了,远远达不到我想要的效果,所以续写了第三版。这次的代码将更轻,我去除了播放器模块,使得插件的适用范围更加的扩大,而且让我有点惊喜的是在写第三版的过程中又让弹幕系统的性能进一步得到了提升,可以讲也是额外的惊喜了。

由于第三版我是用ES6语法写的,所以兼容性不是很好(没错,我只是在针对IE10以下),就算用babel转成ES5,IE依旧毒,所以后面我会抽个时间去写个ES5全兼容版本的,不考虑IE或者只是对源码感兴趣的可以尽情使用。

二、程序实现

源码总共由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
};
}

3、具体设计到哪些代码

三、html部分代码

html部分代码展示:

		<div class="setting-content">
<div class="setting-list addNormal" data-status="show">
<div class="setting-item">
<label>文本:</label>
<input type="text" id="normal-text" placeholder="你可以输入一段文字" >
</div>
<div class="setting-item">
<label>数量:</label>
<input type="tel" id="normal-num" placeholder="你可以输入一个数字" maxlength="6" >
</div>
<div class="setting-item">
<button id="normal-btn">确定</button>
</div>
</div>
<div class="setting-list addEffect" data-status="hide">
<div class="setting-item">
<label>类型:</label>
<select id="effect-sel">
<option value="text">文本</option>
<option value="rect">方形</option>
<option value="circle">圆形</option>
</select>
</div>
<div class="setting-item">
<div class="effect-list effectText">
<div class="effect-item">
<label>内容:</label>
<input type="text" id="effect-text" value="我是一条弹幕" >
</div>
<div class="effect-item">
<label>字体大小:</label>
<input type="text" id="fsize" value="26px" class="inline-input" >
<label>字体粗细:</label>
<input type="text" id="fweight" value="normal" class="inline-input" >
</div>
</div>
<div class="effect-list effectRect" data-status="hide">
<div class="effect-item">
<label>宽度:</label>
<input type="tel" id="rw" value="100" class="inline-input" >
<label>高度:</label>
<input type="tel" id="rh" value="100" class="inline-input" >
</div>
</div>
<div class="effect-list effectCircle" data-status="hide">
<div class="effect-item">
<label>半径:</label>
<input type="tel" id="radius" value="10" >
</div>
</div>
<div class="effect-content">
<div class="effect-item">
<label>起始点 X:</label>
<input type="tel" id="sx" value="0" class="inline-input" >
<label>Y:</label>
<input type="tel" id="sy" value="0" class="inline-input" >
</div>
<div class="effect-item">
<label>结束点 X:</label>
<input type="tel" id="ex" value="0" class="inline-input" >
<label>Y:</label>
<input type="tel" id="ey" value="0" class="inline-input" >
</div>
<div class="effect-item">
<label>起始缩放值 X:</label>
<input type="tel" id="scaleSX" value="1" class="inline-input" >
<label>Y:</label>
<input type="tel" id="scaleSY" value="1" class="inline-input" >
</div>
<div class="effect-item">
<label>结束缩放值 X:</label>
<input type="tel" id="scaleEX" value="1" class="inline-input" >
<label>Y:</label>
<input type="tel" id="scaleEY" value="1" class="inline-input" >
</div>
<div class="effect-item">
<label>起始斜切角度 X:</label>
<input type="tel" id="skewSX" value="0" class="inline-input" >
<label>Y:</label>
<input type="tel" id="skewSY" value="0" class="inline-input" >
</div>
<div class="effect-item">
<label>结束斜切角度 X:</label>
<input type="tel" id="skewEX" value="0" class="inline-input" >
<label>Y:</label>
<input type="tel" id="skewEY" value="0" class="inline-input" >
</div>
<div class="effect-item">
<label>起始旋转角度:</label>
<input type="tel" id="sr" value="0" class="inline-input" >
<label>结束旋转角度:</label>
<input type="tel" id="er" value="0" class="inline-input" >
</div>
<div class="effect-item">
<label>填充颜色:</label>
<input type="text" id="fcolor" value="#66ccff" class="inline-input" >
<label>描边颜色:</label>
<input type="text" id="scolor" value="#cccccc" class="inline-input" >
</div>
<div class="effect-item">
<label>透明度:</label>
<input type="tel" id="opa" value="1" class="inline-input" >
<label>持续时间:</label>
<input type="tel" id="dur" value="3000" class="inline-input" >
</div>
</div>
</div>
<div class="setting-item">
<button id="save-btn">保存为第<em>1</em>步</button>
<button id="effect-btn">确定</button>
</div>
</div>
<div class="setting-list addFilter" data-status="hide">
<div class="setting-item">
<label>添加 属性:</label>
<input type="text" id="filter-prop" placeholder="" class="inline-input" >
<label>值:</label>
<input type="text" id="filter-val" placeholder="" class="inline-input" >
</div>
<div class="setting-item">
<button id="filter-btn">确定</button>
</div>
<div class="setting-item">
<label>删除 属性:</label>
<input type="text" id="filter-del-prop" placeholder="" class="inline-input" >
<label>值:</label>
<input type="text" id="filter-del-val" placeholder="" class="inline-input" >
</div>
<div class="setting-item">
<button id="filter-del-btn">确定</button>
</div>
</div>
<div class="setting-list addStyle" data-status="hide">
<div class="setting-item">
<label>字体大小:</label>
<input type="text" id="gfsize" value="24px" class="inline-input" >
<label>字体粗细:</label>
<input type="text" id="gfweight" value="normal" class="inline-input" >
</div>
<div class="setting-item">
<label>字体颜色:</label>
<input type="text" id="gfcolor" value="#66ccff" class="inline-input" >
<label>透明度:</label>
<input type="tel" id="gfopa" value="1" class="inline-input" >
</div>
<div class="setting-item">
<button id="changeStyle-btn">确定</button>
</div>
</div>
<div class="setting-list addControl" data-status="hide">
<div class="setting-item">
<button id="start">启动</button>
<button id="stop">停止</button>
<button id="pause">暂停</button>
<button id="run">继续</button>
<button id="clear">清除弹幕</button>
<button id="full">大屏</button>
<button id="small">小屏</button>
<button id="disable">禁用高级弹幕</button>
<button id="enable">起用高级弹幕</button>
<button id="getsize">获取宽高</button>
</div>
<div class="setting-item"> </div>
</div>
</div>
</div>

四、操作、运行效果

1、文件截图

双击demos文件夹可看到运行文件

双击index.html后,操作截图:

添加字幕如下:

点击确定,提示如下:

添加成功后,选择“选项-控制项”,效果如下:

点击启动:

就出现了字幕,效果实现完毕。

五、其他补充

高级弹幕类与普通弹幕类有点微妙的差别,但总体是一样,唯一需要在意的是与计算相关的代码,因为不难所以这里也不做继续说明了,请参看源码里的注释。

就第二版来说,第三版性能更好,而且实现了播放器模块和弹幕模块的解耦,也就是说相比第二版,第三版 可以适用但不限于播放器,可用性更高,而且实现了高级弹幕的发送,未来将慢慢补齐更多的功能和代码重构。

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

HTML5 CANVAS 弹幕插件的更多相关文章

  1. HTML5 canvas生成图片马赛克特效插件

    HTML5 canvas生成图片马赛克特效插件 简要教程 这是一款使用html5 canvas来将图片制作成马赛克效果的js插件.该插件的灵感来自于美国肖像画家Chuck Close.已经有人使用这个 ...

  2. 程序猿必备的10款超炫酷HTML5 Canvas插件

    1.超炫酷HTML5 Canvas 3D旋转地球动画 这是一款基于HTML5 Canvas的3D地球模拟动画,动画以太空作为背景,地球在太空中旋转,同时我们也可以拖拽鼠标来从不同的角度观察地球.另外我 ...

  3. 基于html5 canvas 的客户端异步上传图片的插件,支持客户端压缩图片尺寸

    /** * Created by xx on 15-05-28. * 基于html5 canvas 的客户端异步上传画片的插件 * 在实际应用中,常常要用于上传图片的功能.在现在越来越多的手机weba ...

  4. HTML5 程序设计 - 使用HTML5 Canvas API

    请你跟着本篇示例代码实现每个示例,30分钟后,你会高喊:“HTML5 Canvas?!在哥面前,那都不是事儿!” 呵呵.不要被滚动条吓到,很多都是代码和图片.我没有分开写,不过上面给大家提供了目录,方 ...

  5. HTML5 Canvas绘制转盘抽奖

    新项目:完整的Canvas转盘抽奖代码 https://github.com/givebest/GB-canvas-turntable 演示 http://blog.givebest.cn/GB-ca ...

  6. html5 canvas简易版捕鱼达人游戏源码

    插件描述:html5利用canvas写的一个js版本的捕鱼,有积分统计,鱼可以全方位移动,炮会跟着鼠标移动,第一次打开需要鼠标移出背景图,再移入的时候就可以控制炮的转动,因为是用的mouseover触 ...

  7. 利用html5 canvas实现纯前端上传图片的裁剪

    今天跟大家分享一个前端裁剪图片的方法.许多网站都有设置用户头像的功能,用户可以选择一张本地的图片,然后用网站的裁剪工具进行裁剪,然后设置大小,位置合适的头像.当然,网上也有一些用js写的诸如此类裁剪的 ...

  8. HTML5 Canvas 实现的9个 Loading 效果

    Sonic.js 是一个很小的 JavaScript 类,用于创建基于 HTML5 画布的加载图像.更强大的是 Sonic.js 还提供了基于现成的例子的创建工具,可以帮助你实现更多自定义的(Load ...

  9. 使用 HTML5 Canvas 绘制出惊艳的水滴效果

    HTML5 在不久前正式成为推荐标准,标志着全新的 Web 时代已经来临.在众多 HTML5 特性中,Canvas 元素用于在网页上绘制图形,该元素标签强大之处在于可以直接在 HTML 上进行图形操作 ...

随机推荐

  1. 实现Hadoop的Writable接口Implementing Writable interface of Hadoop

    As we saw in the previous posts, Hadoop makes an heavy use of network transmissions for executing it ...

  2. Java 从基础到进阶学习之路---类编写以及文档凝视.

    Java之前在学习过,基础知识还没有忘光,并且这些高级语言实在是太像,所以那些数据类型,或者循环控制流,以及标准设备等等就直接略过不说了. 只是一些重大概念会穿插在文章的介绍中. So,这些文章适合于 ...

  3. 说说CSS样式中你不知道的“大于号”

    继承在一定程度上让程序在编写的过程中更加方便,但是有时候也会给我们的程序带来一定的困扰,所以认真的学习继承的原理,以及处理的方法很重要.下面是Css中处理继承的一个方法.在一段CSS代码中见到一个大于 ...

  4. 分解大量switch-case分支的两种方法

    项目经过长期多人的维护,所谓人多手杂,出现不少过多过长的switch-case分支,或者多重switch-case嵌套.每每添加功能,我都会紧皱眉头,然后带着罪恶感向已经成百上千行的函数里再添上一个c ...

  5. .NET MVC自定义错误处理页面的方法

    在ASP.NET MVC中,我们可以使用HandleErrorAttribute特性来具体指定如何处理Action抛出的异常.只要某个Action设置了HandleErrorAttribute特性,那 ...

  6. [INS-30131]执行安装程序验证所需的初始设置失败(原因:无法访问临时位置)

    [INS-30131]执行安装程序验证所需的初始设置失败(原因:无法访问临时位置) 学习了:https://blog.csdn.net/killvoon/article/details/5182192 ...

  7. Java Web -- Servlet(5) 开发Servlet的三种方法、配置Servlet具体解释、Servlet的生命周期(2)

    三.Servlet的生命周期 一个Java servlet具有一个生命周期,这个生命周期定义了一个Servlet怎样被加载并被初始化,怎样接收请求并作出对请求的响应,怎样被从服务中清除.Servlet ...

  8. Python构造字符串

    不断报错 coercing to Unicode : 不要用+连接字符串,要用格式化字符串 None的问题:只能用 or “”的方式来解决了 not all arguments converted d ...

  9. Qt5 for Android: incompatible ABI

      I recently installed Qt5 and works like a charm for API 17 and armeabi-v7a.But I added second AVD ...

  10. 如何判断CapsLock键是否按下

        SHORT cap_state = ::GetKeyState(VK_CAPITAL);     char str[10];     sprintf(str, "%d", ...