摘要:主要是因為第二版中播放器模塊和彈幕模塊耦合得太嚴重了,遠遠達不到我想要的效果,所以續寫了第三版。這里的通道是指彈幕從右往左運行時所在的那一行位置,這些通道是在尺寸變化時生成的,不同類型的彈幕都有其通道集合。
最新版本 V 3.2.5
新增了圖片彈幕類型,修改了demo展示頁面,調整了部分代碼,具體請參看git里的CHANGELOG.md和README.md
文章里主要講實現方法和設計思想,所以有部分接口依舊是老版本接口,最新的接口請去git里面查看
前言說實話,從第二版到現在又過了半年,本來以為可能不會寫第三版的,頂多將第二版的代碼重構下就可以了,沒想到還是花了一個星期左右續寫了第三版。主要是因為第二版中 播放器模塊和彈幕模塊耦合得太嚴重了,遠遠達不到我想要的效果,所以續寫了第三版。這次的代碼將更輕,我去除了播放器模塊,使得插件的適用范圍更加的擴大,而且讓我有點驚喜的是在寫第三版的過程中又讓彈幕系統的性能進一步得到了提升,可以講也是額外的驚喜了。
由于第三版我是用ES6語法寫的,所以兼容性不是很好(沒錯,我只是在針對IE),就算用babel轉成ES5,IE依舊毒,目前支持IE10+。所以后面我會抽個時間去寫個ES5全兼容版本的,不考慮IE或者只是對源碼感興趣的可以盡情使用。
github : github
API接口都在git里面,文章不會介紹插件使用相關的內容,僅僅解釋部分源碼和設計思想,如果覺得插件還行,請大家git給個星,謝謝
demo : 我是demo
我碰到好像有人對demo不知道怎么操作,下面我簡單介紹下基本操作:
所有已發布功能項可以通過下拉框進行切換,目前包括“添加普通彈幕”,“添加高級彈幕”,“過濾”,“添加全局樣式”,“控制項”
添加普通彈幕和添加高級彈幕都只是添加數據而已,不會運行插件,你需要跳到“控制項”點擊啟動,然后等彈幕出來即可
高級彈幕的動畫是屬于排隊動畫,你要先將修改后的數據“保存為第n步”(n至少為1)后點擊確定添加才可以
過濾功能的話,最簡單的“type”:“slide”,表示過濾所有滾動型的彈幕,或者“text”:“string”表示過濾包含string的所有彈幕
你可以先啟動然后添加彈幕,也是一樣的,操作順序沒太高要求
代碼的那些事源碼總共由5部分組成:
普通彈幕類
高級彈幕類
主程序類
封裝輸出函數
Tween算法類
第4和第5個部分比較簡單,第5部分就是Tween算法,原汁原味,第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 }; }
高級彈幕類與普通彈幕類有點微妙的差別,但總體是一樣,唯一需要在意的是與計算相關的代碼,因為不難所以這里也不做繼續說明了,請參看源碼里的注釋。
結語就第二版來說,第三版性能更好,而且實現了播放器模塊和彈幕模塊的解耦,也就是說相比第二版,第三版 可以適用但不限于播放器,可用性更高,而且實現了高級彈幕的發送,未來將慢慢補齊更多的功能和代碼重構,希望大家遇到什么BUG或者是有某些的需求,請私信或是將反饋提交到本郵箱:454236029@qq.com || z454236029@gmail.com,如果覺得本插件對你有用歡迎給個星,謝謝。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83909.html
摘要:主要是因為第二版中播放器模塊和彈幕模塊耦合得太嚴重了,遠遠達不到我想要的效果,所以續寫了第三版。這里的通道是指彈幕從右往左運行時所在的那一行位置,這些通道是在尺寸變化時生成的,不同類型的彈幕都有其通道集合。 最新版本 V 3.2.5 新增了圖片彈幕類型,修改了demo展示頁面,調整了部分代碼,具體請參看git里的CHANGELOG.md和README.md 文章里主要講實現方法和設計思...
摘要:考慮到只適合在單位時間內繪制少量彈幕,這對于我們播放器來說明顯是不合需求的。比如說直播播放器模式和視頻播放器模式的優化細節又有點不一樣,還有高級彈幕的處理圖片,圖形等。。 前言 大家年前好,馬上就要元旦了,在很久沒有寫文章之后,想到這篇文章將會成為本人今年的絕響也是有點蛋疼。不過也好,畢竟本人算不得什么勤快的生物,而且比起那些大神來說也差遠了,就作為自己工作半年后的一次沉淀算了。 文中...
閱讀 1049·2021-11-24 09:39
閱讀 3578·2021-11-22 13:54
閱讀 2542·2021-10-11 10:59
閱讀 773·2021-09-02 15:40
閱讀 1025·2019-08-30 15:55
閱讀 1042·2019-08-30 13:57
閱讀 2305·2019-08-30 13:17
閱讀 3025·2019-08-29 18:32