HTML5 CANVAS 弹幕插件
概述
详细
修改了普通弹幕运动的算法,新增了部分功能,具体请参看附件里的CHANGELOG.md和README.md
一、概述
说实话,从第二版到现在又过了半年,本来以为可能不会写第三版的,顶多将第二版的代码重构下就可以了,没想到还是花了一个星期左右续写了第三版。主要是因为第二版中 播放器模块和弹幕模块耦合得太严重了,远远达不到我想要的效果,所以续写了第三版。这次的代码将更轻,我去除了播放器模块,使得插件的适用范围更加的扩大,而且让我有点惊喜的是在写第三版的过程中又让弹幕系统的性能进一步得到了提升,可以讲也是额外的惊喜了。
由于第三版我是用ES6语法写的,所以兼容性不是很好(没错,我只是在针对IE10以下),就算用babel转成ES5,IE依旧毒,所以后面我会抽个时间去写个ES5全兼容版本的,不考虑IE或者只是对源码感兴趣的可以尽情使用。
二、程序实现
源码总共由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 弹幕插件的更多相关文章
- HTML5 canvas生成图片马赛克特效插件
HTML5 canvas生成图片马赛克特效插件 简要教程 这是一款使用html5 canvas来将图片制作成马赛克效果的js插件.该插件的灵感来自于美国肖像画家Chuck Close.已经有人使用这个 ...
- 程序猿必备的10款超炫酷HTML5 Canvas插件
1.超炫酷HTML5 Canvas 3D旋转地球动画 这是一款基于HTML5 Canvas的3D地球模拟动画,动画以太空作为背景,地球在太空中旋转,同时我们也可以拖拽鼠标来从不同的角度观察地球.另外我 ...
- 基于html5 canvas 的客户端异步上传图片的插件,支持客户端压缩图片尺寸
/** * Created by xx on 15-05-28. * 基于html5 canvas 的客户端异步上传画片的插件 * 在实际应用中,常常要用于上传图片的功能.在现在越来越多的手机weba ...
- HTML5 程序设计 - 使用HTML5 Canvas API
请你跟着本篇示例代码实现每个示例,30分钟后,你会高喊:“HTML5 Canvas?!在哥面前,那都不是事儿!” 呵呵.不要被滚动条吓到,很多都是代码和图片.我没有分开写,不过上面给大家提供了目录,方 ...
- HTML5 Canvas绘制转盘抽奖
新项目:完整的Canvas转盘抽奖代码 https://github.com/givebest/GB-canvas-turntable 演示 http://blog.givebest.cn/GB-ca ...
- html5 canvas简易版捕鱼达人游戏源码
插件描述:html5利用canvas写的一个js版本的捕鱼,有积分统计,鱼可以全方位移动,炮会跟着鼠标移动,第一次打开需要鼠标移出背景图,再移入的时候就可以控制炮的转动,因为是用的mouseover触 ...
- 利用html5 canvas实现纯前端上传图片的裁剪
今天跟大家分享一个前端裁剪图片的方法.许多网站都有设置用户头像的功能,用户可以选择一张本地的图片,然后用网站的裁剪工具进行裁剪,然后设置大小,位置合适的头像.当然,网上也有一些用js写的诸如此类裁剪的 ...
- HTML5 Canvas 实现的9个 Loading 效果
Sonic.js 是一个很小的 JavaScript 类,用于创建基于 HTML5 画布的加载图像.更强大的是 Sonic.js 还提供了基于现成的例子的创建工具,可以帮助你实现更多自定义的(Load ...
- 使用 HTML5 Canvas 绘制出惊艳的水滴效果
HTML5 在不久前正式成为推荐标准,标志着全新的 Web 时代已经来临.在众多 HTML5 特性中,Canvas 元素用于在网页上绘制图形,该元素标签强大之处在于可以直接在 HTML 上进行图形操作 ...
随机推荐
- Mac 在启动eclipse时 Failed to load JavaHL Library解决方法
在Mac 10.9.1系统里, 在Eclipse中安装svn的插件,出现如下提示 方法一: 1.根据提示进入链接 http://subclipse.tigris.org/wiki/JavaHL 2. ...
- Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
执行Maven Install打包的时候,提示以下警告信息: [WARNING] Using platform encoding (GBK actually) to copy filtered res ...
- NLP入门(八)使用CRF++实现命名实体识别(NER)
CRF与NER简介 CRF,英文全称为conditional random field, 中文名为条件随机场,是给定一组输入随机变量条件下另一组输出随机变量的条件概率分布模型,其特点是假设输出随机 ...
- 阿里云96页报告详解《云上转型》(10个案例、10大趋势/完整版PPT)
阿里云96页报告详解<云上转型>(10个案例.10大趋势/完整版PPT) 2017-12-29 14:20阿里云/云计算/技术 ﹃产业前沿超级干货﹄ ﹃数据观○重磅速递﹄ 阿里云研究中心云 ...
- SpringMVC处理方法的数据绑定
一.SpringMVC数据绑定流程 Spring MVC通过反射机制对目标处理方法的签名进行解析,将请求消息中的信息以一定的方式转换并绑定到处理方法的入参中.数据绑定的核心部件是DataBinder, ...
- 第七章 JVM性能监控与故障处理工具(1)
1.定位系统问题 依据 GC日志 堆转储快照(heapdump/hprof文件) 线程快照(threaddump/javacore文件) 运行日志 异常堆栈 分析依据的工具 jps:显示指定系统内的所 ...
- [leetcode]Convert Sorted Array to Binary Search Tree @ Python
原题地址:http://oj.leetcode.com/problems/convert-sorted-array-to-binary-search-tree/ 题意:将一个排序好的数组转换为一颗二叉 ...
- const char * 转换为char*
可以用const_cast const char* aa = "this is a const string."; char* bb = const_cast< ...
- PasswordlessAPI
passwordlessapiYOURLS允许API调用的老式的方法,使用用户名和密码参数(如果你的设置是私人的,很明显).如果担心将证书发送到野外,还可以使用秘密签名令牌进行API调用.签名的令牌你 ...
- UML建模学习1:UML统一建模语言简单介绍
一什么是UML? Unified Modeling Language(UML又称为统一建模语言或标准建模语言)是国际对象管理组织OMG制定的一个通 用的.可视化建模语言标准.能够用来描写叙述(spec ...