首页 > 其他分享 >HTML5 CANVAS 弹幕插件

HTML5 CANVAS 弹幕插件

时间:2023-06-01 10:03:47浏览次数:42  
标签:插件 DM CANVAS item rows let 弹幕 opts


概述


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


详细



修改了普通弹幕运动的算法,新增了部分功能,具体请参看附件里的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、文件截图

HTML5 CANVAS 弹幕插件_DM

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

HTML5 CANVAS 弹幕插件_DM_02

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

HTML5 CANVAS 弹幕插件_HTML5_03

添加字幕如下:

HTML5 CANVAS 弹幕插件_HTML5_04

点击确定,提示如下:

HTML5 CANVAS 弹幕插件_ide_05

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

HTML5 CANVAS 弹幕插件_HTML5_06

点击启动:

HTML5 CANVAS 弹幕插件_HTML5_07

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

五、其他补充

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

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



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




标签:插件,DM,CANVAS,item,rows,let,弹幕,opts
From: https://blog.51cto.com/u_7583030/6392465

相关文章

  • 使用Paste Image插件来方便的给Markdown添加截图的功能
    日常用vscode写markdown时可能会需要添加截图,这时一般的做法有两种,一种是把图片上传到图床,然后把图片链接写到![]()里另一种是,把图片保存到本地某个目录下,使用相对路径添加图片这两种方式操作起来都比较麻烦,因为都需要先把截图保存下来,所以有没有类似qq,wechat那种可以直接使用截......
  • Canvas API初步学习
    1.字体 在canvas中最常见的字体表现形式有填充字体和漏空字体。   漏空字体用方法:strokeText(Text,left,top,[maxlength]);  填充字体用方法:fillText(Text,left,top,[maxlength]);上面的两个方法的最后一个参数是可选的,四个参数的含义分为是:需绘制的字符串,绘制到画布中时......
  • 微信小程序使用ec-canvas真机上tooltip有阴影
    问题微信小程序项目中,使用了ec-canvas绘制图表,在开发者工具中预览正常,但是在真机上点击图表tooltip会出现一层阴影,如下图所示:修改后解决之后探索到解决方案,代码如下:tooltip:{trigger:'axis',textStyle:{align:'left',textShadowBlur:10,//重点......
  • 别再满屏找日志了!推荐一款 IDEA 日志管理插件,看日志轻松多了!
    1.简介GrepConsole是一款方便开发者对idea控制台输出日志进行个性化管理的插件。2.功能特性GrepConsole的主要功能特性:支持自定义规则来过滤日志信息;支持不同级别的日志的输出样式的个性化配置;总结:通过过滤功能、输出日志样式配置功能,可以更方便开发者在大量的日志信......
  • HTML5的Canvas画图模拟太阳系运转
    今天研究的是利用HTML5的Canvas画图来模拟太阳系运转,首先,在这个太阳系里分为画轨道和画星球两个部分,对于每一个星球我们要知道它的颜色和公转周期,如下图。  采用面向对象编程的思想,代码如下 stars.html<!DOCTYPEHTML><html> <head></head> <body> <canvasid="canvas"......
  • 利用谷歌浏览器插件Autofill一键提取QQ裙所有QQ邮箱
     众所周知,QQ号是公开的,QQ号加上后缀@qq.com就是QQ邮箱。因此只要获取到一批QQ号就意味着获取到一批QQ邮箱,进而利用邮件群发技术来批量发送邮件获取客户。QQ群是QQ用户最集中的地方,在这里可以快速获取大量精准用户,下面详细讲解方法。如题,第一步:下载谷歌浏览器,然后安装Autofill插件......
  • LYT-WPF-基础-布局-Canvas面板
    已亲测!ZIndex实例有修改之处!!!本文转自:WPF教程五:布局之Canvas面板-.NET开发菜鸟-博客园(cnblogs.com),感谢~~Canvas:画布面板画布,用于完全控制每个元素的精确位置。他是布局控件中最为简单的一种,直接将元素放到指定位置,主要来布置图面。使用Canvas,必须指定一个子元素的位置(相对......
  • SOLIDWORKS配置修改插件Solidkits.BOMs工具
    使用SOLIDWORKS配置可以实现在同一个文件中表现不同的产品状态,在某些情况下是非常有用的。当我们想要删除多配置时,就需要一个一个打开模型,选中删除的配置删除,比较麻烦。SOLIDWORKS配置修改插件-Solidkits.BOMs工具就可以实现批量删除配置,比如模型中只想保留默认配置,就可以将模型......
  • 辅助测试和研发人员的一款小插件【数据安全】
    一、为什么要做一款这样的小插件数据,一直在思考如何让数据更安全的流转和服务于客户,围绕这样的想法,我们做过许多方面的扩展。我们落地了服务端的数据切片支持场景化的设计,实现了基于JDBC协议对SQL的拦截与切片,实现了在应用层的全链路数据库审计方案和实现,实现了WEB端明暗水印和文......
  • docker rabbitMQ 安装延时队列插件
    1下载插件到容器内在这个网站上找到插件的下载链接容器内wget或使用dockercp复制到容器内dockercp/rabbitmq_delayed_message_exchange-3.8.0.ezrabbit:/plugins2启用插件#进入容器启用插件dockerexec-itrabbit/bin/bashrabbitmq-pluginsenablera......