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 上进行图形操作 ...
随机推荐
- freetype
FreeType编译及使用心得收藏 FreeType是一款字体服务库,它支持多种字体,并且提供高效,高质量的文字. freetype相关介绍见:http://blog.csdn.net/ganxi ...
- 如何构建Win32汇编的编程环境(ONEPROBLEM个人推荐)
如何构建Win32汇编的编程环境(ONEPROBLEM个人推荐)1.首先要下载我提供的软件包(里面已经包含所有所需软件); 2.把它解压到D盘根目录下(如果需要安装在其它的地方,请注意设好路径); ...
- 读书笔记之:C语言深度剖析
读书笔记之:C语言深度剖析 <C 语言深度解剖>这本书是一本“解开程序员面试笔试的秘密”的好书.作者陈正冲老师提出“以含金量勇敢挑战国内外同类书籍”,确实,这本书中的知识点都是一些在面试中 ...
- [19] 半球形(Hemisphere)图形的生成算法
顶点数据的生成 bool YfBuildHemisphereVertices ( Yreal radius, Yuint slices, Yuint stacks, YeOriginPose orig ...
- 附 5 springboot之配置文件
本文转载自http://www.jianshu.com/p/80621291373b,作者:龙白一梦 我的boss 代码从开发到测试要经过各种环境,开发环境,测试环境,demo环境,线上环境,各种环境 ...
- 剑指offer-序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树 以前提交的内存超出了,可能现在要用非递归实现了 #include<iostream> #include<math.h> #inc ...
- 科幻大片中那些牛X代码真相
在<黑客帝国>中,救世主Neo的队友通过屏幕上"1"和"0"构成的数据流,就能看到鲜活的画面,这应该算是科幻大片中对代码最极致的表现了.其他科幻电影 ...
- GoLang中如何使用多参数属性传参
我们常常因为传入的参数不确定而头疼不已,golang 为我们提供了接入多值参数用于解决这个问题.但是一般我们直接写已知代码即所有的值都知道一个一个塞进去就好了,但是绝大部分我们是得到用户的大量输入想通 ...
- SparkMLlib分类算法之决策树学习
SparkMLlib分类算法之决策树学习 (一) 决策树的基本概念 决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风 ...
- [Node.js]24. Level 5: Express, Express routes
Create an express route that responds to GET requests at the URL /tweets that responds with the file ...