ybbarrage.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. export default function YBBarrage ({container, barrages = [], config = {}}){
  2. if(!(this instanceof YBBarrage)){ //如果this不是指向MyClass
  3. throw new TypeError("TypeError: Class constructor YBBarrage cannot be invoked without 'new'")
  4. }
  5. this.container = container
  6. this.canvas = null
  7. this.paused = true
  8. this.barrages = barrages
  9. this.config = {}
  10. this._ctx = null
  11. this._barrages = []
  12. this._w = 0
  13. this._h = 0
  14. this._drawTimer = null
  15. this._drawAnima = null
  16. this.currentTime = 0
  17. this._count = 0
  18. this._startTime = 0
  19. this._refreshTimer = null
  20. this.setConfig(config)
  21. this._init()
  22. }
  23. Object.defineProperty(YBBarrage.prototype,'_init',{
  24. value:function(){
  25. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  26. throw new TypeError("TypeError: YBBarrage._init is not a constructor")
  27. }
  28. this.canvas = document.createElement('CANVAS');
  29. this.canvas.setAttribute('width', this.container.offsetWidth);
  30. this.canvas.setAttribute('height', this.container.offsetHeight);
  31. this.container.appendChild(this.canvas);
  32. this._ctx = this.canvas.getContext('2d');
  33. const rect = this.canvas.getBoundingClientRect();
  34. this._w = rect.right - rect.left;
  35. this._h = rect.bottom - rect.top;
  36. this.currentTime = this.config.initialTime;
  37. window.addEventListener('resize', this.refresh.bind(this))
  38. },
  39. enumerable:false
  40. })
  41. Object.defineProperty(YBBarrage.prototype,'_destroy',{
  42. value:function(){
  43. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  44. throw new TypeError("TypeError: YBBarrage._destroy is not a constructor")
  45. }
  46. this.paused = true;
  47. this._clearTimer();
  48. this._cancelAnima();
  49. this._clear();
  50. this.barrages = [];
  51. this._barrages = [];
  52. this.container = null;
  53. this._ctx = null;
  54. if ( this.canvas ) {
  55. this.canvas.remove();
  56. this.canvas = null;
  57. }
  58. window.removeEventListener('resize', this.refresh.bind(this))
  59. },
  60. enumerable:false
  61. })
  62. //开始绘制
  63. Object.defineProperty(YBBarrage.prototype,'_render',{
  64. value:function(){
  65. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  66. throw new TypeError("TypeError: YBBarrage._render is not a constructor")
  67. }
  68. this._cancelAnima();
  69. if ( this.paused ) return;
  70. if (this._barrages.length) {
  71. this._clear();
  72. for (let i = 0; i < this._barrages.length; i++) {
  73. let b = this._barrages[i];
  74. if (b.left + b.width <= 0) {
  75. this._barrages.splice(i, 1);
  76. i--;
  77. continue;
  78. }
  79. b.offset = this._detectionBump(b);
  80. let offset = b.offset * this.config.playbackRate;
  81. b.left -= offset;
  82. this._drawText(b);
  83. }
  84. }
  85. this._drawAnima = window.requestAnimationFrame(this._render.bind(this));
  86. },
  87. enumerable:false
  88. })
  89. Object.defineProperty(YBBarrage.prototype,'_cancelAnima',{
  90. value:function(){
  91. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  92. throw new TypeError("TypeError: YBBarrage._cancelAnima is not a constructor")
  93. }
  94. if ( this._drawAnima ) {
  95. window.cancelAnimationFrame(this._drawAnima)
  96. this._drawAnima = null
  97. }
  98. },
  99. enumerable:false
  100. })
  101. Object.defineProperty(YBBarrage.prototype,'_clear',{
  102. value:function(){
  103. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  104. throw new TypeError("TypeError: YBBarrage._clear is not a constructor")
  105. }
  106. this._ctx && this._ctx.clearRect(0, 0, this._w, this._h);
  107. },
  108. enumerable:false
  109. })
  110. //绘制文字
  111. Object.defineProperty(YBBarrage.prototype,'_drawText',{
  112. value:function(barrage){
  113. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  114. throw new TypeError("TypeError: YBBarrage._drawText is not a constructor")
  115. }
  116. if ( this._ctx ) {
  117. this._ctx.beginPath();
  118. const fontSize = barrage.fontSize || this.config.fontSize
  119. this._ctx.font = `${fontSize}px ${this.config.fontFamily}`;
  120. this._ctx.strokeStyle = this._hex2rgba(this._getStrokeColor(barrage.color || this.config.defaultColor), this.config.opacity);
  121. this._ctx.strokeText(barrage.text, barrage.left, barrage.top);
  122. this._ctx.fillStyle = this._hex2rgba(barrage.color || this.config.defaultColor, this.config.opacity);
  123. this._ctx.fillText(barrage.text, barrage.left, barrage.top);
  124. this._ctx.closePath();
  125. }
  126. },
  127. enumerable:false
  128. })
  129. Object.defineProperty(YBBarrage.prototype,'_startTimer',{
  130. value:function(){
  131. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  132. throw new TypeError("TypeError: YBBarrage._startTimer is not a constructor")
  133. }
  134. this._startTime = new Date().getTime();
  135. this._count = 0;
  136. this._drawTimer = window.setTimeout(this._fixed.bind(this), 1000);
  137. },
  138. enumerable:false
  139. })
  140. Object.defineProperty(YBBarrage.prototype,'_clearTimer',{
  141. value:function(){
  142. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  143. throw new TypeError("TypeError: YBBarrage._clearTimer is not a constructor")
  144. }
  145. if ( this._drawTimer ) {
  146. window.clearTimeout(this._drawTimer)
  147. this._drawTimer = null
  148. }
  149. },
  150. enumerable:false
  151. })
  152. Object.defineProperty(YBBarrage.prototype,'_fixed',{
  153. value:function(){
  154. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  155. throw new TypeError("TypeError: YBBarrage._fixed is not a constructor")
  156. }
  157. this._clearTimer();
  158. if ( this.paused ) return;
  159. if ( this.config.duration > -1 && this.currentTime >= this.config.duration ) {
  160. this.seek(0);
  161. return;
  162. }
  163. this._count++;
  164. this.currentTime += (1 * this.config.playbackRate);
  165. let barrages = this.barrages.filter(barrage => parseInt(barrage.time) == parseInt(this.currentTime));
  166. for ( let i = 0; i < barrages.length; i++ ) {
  167. let bar = this._getBarrage(barrages[i]);
  168. bar && this._barrages.push(bar);
  169. }
  170. let offset = new Date().getTime() - (this._startTime + (this._count * 1000));
  171. let nextTime = 1000 - offset;
  172. if (nextTime < 0) nextTime = 0;
  173. this._drawTimer = window.setTimeout(this._fixed.bind(this), nextTime);
  174. },
  175. enumerable:false
  176. })
  177. //测绘弹幕
  178. Object.defineProperty(YBBarrage.prototype,'_getBarrage',{
  179. value:function(barrage, isVisible = false){
  180. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  181. throw new TypeError("TypeError: YBBarrage._getBarrage is not a constructor")
  182. }
  183. const fontSize = barrage.fontSize || this.config.fontSize
  184. const offset = this._getOffset();
  185. this._ctx.font = `${barrage.fontSize || this.config.fontSize}px ${this.config.fontFamily}`;
  186. const width = Math.ceil(this._ctx.measureText(barrage.text).width);
  187. const top = this._getTop(fontSize, width, offset, isVisible);
  188. if ( top > -1 ) {
  189. return {
  190. text: barrage.text,
  191. time: barrage.time,
  192. fontSize: fontSize,
  193. color: barrage.color || this.config.defaultColor,
  194. top: top,
  195. left: this._w,
  196. offset: offset,
  197. width: width
  198. }
  199. }
  200. return false;
  201. },
  202. enumerable:false
  203. })
  204. Object.defineProperty(YBBarrage.prototype,'_getStrokeColor',{
  205. value:function(color){
  206. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  207. throw new TypeError("TypeError: YBBarrage._getStrokeColor is not a constructor")
  208. }
  209. let hex = color.length == 7 ? color : '#' + color.slice(1, 4) + color.slice(1, 4)
  210. const r = parseInt(hex.slice(1,3),16);
  211. const g = parseInt(hex.slice(3,5),16);
  212. const b = parseInt(hex.slice(5,7),16);
  213. let $grayLevel = (r * 0.299) + (g * 0.587) + (b * 0.144);
  214. if ($grayLevel >= 120) {
  215. return '#000000';
  216. } else {
  217.   return '#ffffff';
  218. }
  219. },
  220. enumerable:false
  221. })
  222. Object.defineProperty(YBBarrage.prototype,'_hex2rgba',{
  223. value:function(hex, opacity){
  224. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  225. throw new TypeError("TypeError: YBBarrage._hex2rgba is not a constructor")
  226. }
  227. hex = hex.length == 7 ? hex : '#' + hex.slice(1, 4) + hex.slice(1, 4)
  228. let str="rgba("
  229. const r = parseInt(hex.slice(1,3),16).toString();
  230. const g = parseInt(hex.slice(3,5),16).toString();
  231. const b = parseInt(hex.slice(5,7),16).toString();
  232. str += r+","+g+","+b+","+opacity+")";
  233. return str;
  234. },
  235. enumerable:false
  236. })
  237. //计算弹幕距离顶部位置
  238. Object.defineProperty(YBBarrage.prototype,'_getTop',{
  239. value:function(size, width, offset, isVisible = false){
  240. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  241. throw new TypeError("TypeError: YBBarrage._getTop is not a constructor")
  242. }
  243. //canvas绘制文字x,y坐标是按文字左下角计算,预留30px
  244. let top = -1;
  245. let len = -1
  246. //根据弹幕高度分割轨道
  247. for ( let i = 0; i < Math.floor(this._h / (size + this.config.lineHeight)); i++ ) {
  248. //当前轨道的顶部位置
  249. let nowTop = ((i + 1) * size) + (i * this.config.lineHeight);
  250. //当前轨道上有多少条弹幕
  251. let barrages = this._barrages.filter(barrage => barrage.top < nowTop + size && barrage.fontSize + barrage.top > nowTop );
  252. if ( barrages.length > 0 ) {
  253. //当前轨道有弹幕运行
  254. let arr = barrages.map(barrage => barrage.left + barrage.width);
  255. if ( !this.config.overlap || isVisible ) {
  256. //开启防重叠会取消多余弹幕显示
  257. if ( arr.length < len || len < 0 ) {
  258. len = arr.length;
  259. top = nowTop;
  260. }
  261. }
  262. //获取当前轨道最右的弹幕
  263. let max = Math.max(...arr);
  264. //如果当前轨道还有空位则将弹幕放入当前轨道
  265. if ( max < this._w - (isVisible ? width : 10) ) {
  266. top = nowTop;
  267. break;
  268. }
  269. } else {
  270. //当前轨道没有弹幕运行
  271. top = nowTop;
  272. break;
  273. }
  274. }
  275. return top;
  276. },
  277. enumerable:false
  278. })
  279. //获取偏移量
  280. Object.defineProperty(YBBarrage.prototype,'_getOffset',{
  281. value:function(){
  282. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  283. throw new TypeError("TypeError: YBBarrage._getOffset is not a constructor")
  284. }
  285. return parseFloat( ((this.config.speed / 70) + Math.random()).toFixed(1) );
  286. },
  287. enumerable:false
  288. })
  289. //碰撞检测 速度计算
  290. Object.defineProperty(YBBarrage.prototype,'_detectionBump',{
  291. value:function(bar){
  292. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  293. throw new TypeError("TypeError: YBBarrage._detectionBump is not a constructor")
  294. }
  295. let nowTop = bar.top;
  296. let offset = bar.offset;
  297. //检测当前弹幕轨道有多少条弹幕
  298. let barrages = this._barrages.filter(barrage => barrage.top < nowTop + bar.fontSize && barrage.fontSize + barrage.top > nowTop && bar.left != barrage.left);
  299. //检测当前弹幕轨道前方有多少条弹幕
  300. let headbarrages = barrages.filter(barrage => bar.left + bar.width >= barrage.left + barrage.width + 10 && barrage.left + barrage.width > 0);
  301. //检测当前弹幕轨道后方有多少条弹幕
  302. let footbarrages = barrages.filter(barrage => bar.left + bar.width + 10 < barrage.left && barrage.left + barrage.width > 0);
  303. if ( headbarrages.length > 0 ) {
  304. let arr = headbarrages.map(barrage => barrage.left + barrage.width);
  305. //取出离当前弹幕最近的那条弹幕
  306. let max = Math.max(...arr);
  307. let i = arr.indexOf(max);
  308. if ( bar.left > max + 20 ) {
  309. //如果离最近弹幕距离大于10,则加速
  310. offset = headbarrages[i].offset + 0.01;
  311. } else {
  312. //否则减速
  313. offset = headbarrages[i].offset - 0.01;
  314. }
  315. }
  316. if ( footbarrages.length > 0 ) {
  317. //前方一条弹幕都没有 且 未弹幕未完全显示 则加速
  318. if ( bar.left + bar.width > this._w && headbarrages.length == 0 ) {
  319. offset = bar.offset + 0.01;
  320. } else {
  321. let arr = footbarrages.map(barrage => barrage.left);
  322. //取出离当前弹幕最近的那条弹幕
  323. let min = Math.min(...arr);
  324. let i = arr.indexOf(min);
  325. //如果后方存在弹幕 且距离小于10则加速
  326. if ( min - (bar.left + bar.width) <= 20 ) {
  327. offset = bar.offset + 0.01;
  328. }
  329. }
  330. }
  331. if ( offset < 0.5 ) offset = 0.5;
  332. if ( offset > 3 ) offset = 3;
  333. return offset;
  334. },
  335. enumerable:false
  336. })
  337. Object.defineProperty(YBBarrage.prototype,'setBarrages',{
  338. value:function(barrages){
  339. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  340. throw new TypeError("TypeError: YBBarrage.setBarrages is not a constructor")
  341. }
  342. this.barrages = barrages
  343. },
  344. enumerable:false
  345. })
  346. Object.defineProperty(YBBarrage.prototype,'add',{
  347. value:function(barrage){
  348. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  349. throw new TypeError("TypeError: YBBarrage.add is not a constructor")
  350. }
  351. const arr = Object.prototype.toString.call(barrage) == '[object Array]' ? barrage : [barrage]
  352. arr.forEach(item => {
  353. item.time = item.time || this.currentTime;
  354. this.barrages.push(item);
  355. if ( parseInt(item.time) == parseInt(this.currentTime) ) {
  356. const bar = this._getBarrage(item, true);
  357. bar.left = this._w - bar.width;
  358. this._barrages.push(bar);
  359. }
  360. })
  361. },
  362. enumerable:false
  363. })
  364. Object.defineProperty(YBBarrage.prototype,'setConfig',{
  365. value:function(config){
  366. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  367. throw new TypeError("TypeError: YBBarrage.setConfig is not a constructor")
  368. }
  369. this.config = Object.assign({}, {
  370. duration: -1, // 弹幕动画的循环周期,-1 表示不循环播放
  371. speed: 150, // 弹幕的运动速度
  372. fontSize: 24, // 文字大小,单位:像素
  373. fontFamily: 'Microsoft Yahei', // 字体,默认值:微软雅黑
  374. opacity: 1, // 透明度,有效值 0-1
  375. defaultColor: '#fff', // 默认颜色,与 CSS 颜色属性一致
  376. lineHeight: 5,//行间距
  377. overlap: true,//开启防重叠 可能会导致部分弹幕不显示
  378. playbackRate: 1.0,//播放倍速
  379. initialTime: 0//初始时间
  380. }, this.config, config)
  381. },
  382. enumerable:false
  383. })
  384. //刷新尺寸(当容器大小变化时调用此接口)
  385. Object.defineProperty(YBBarrage.prototype,'refresh',{
  386. value:function(){
  387. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  388. throw new TypeError("TypeError: YBBarrage.refresh is not a constructor")
  389. }
  390. if ( this._refreshTimer ) {
  391. window.clearTimeout(this._refreshTimer)
  392. this._refreshTimer = null
  393. }
  394. this._refreshTimer = window.setTimeout(() => {
  395. if ( this.canvas && this.container ) {
  396. this.canvas.width = this.container.offsetWidth;
  397. this.canvas.height = this.container.offsetHeight;
  398. this._w = this.container.offsetWidth;
  399. this._h = this.container.offsetHeight;
  400. let paused = this.paused;
  401. if ( !paused ) {
  402. this.play()
  403. }
  404. window.clearTimeout(this._refreshTimer)
  405. this._refreshTimer = null
  406. }
  407. }, 200)
  408. },
  409. enumerable:false
  410. })
  411. Object.defineProperty(YBBarrage.prototype,'play',{
  412. value:function(){
  413. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  414. throw new TypeError("TypeError: YBBarrage.play is not a constructor")
  415. }
  416. if ( this.paused ) {
  417. this.paused = false;
  418. this._startTimer();
  419. this._render();
  420. }
  421. },
  422. enumerable:false
  423. })
  424. Object.defineProperty(YBBarrage.prototype,'pause',{
  425. value:function(){
  426. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  427. throw new TypeError("TypeError: YBBarrage.pause is not a constructor")
  428. }
  429. if ( !this.paused ) {
  430. this.paused = true;
  431. this._clearTimer();
  432. this._cancelAnima();
  433. }
  434. },
  435. enumerable:false
  436. })
  437. Object.defineProperty(YBBarrage.prototype,'stop',{
  438. value:function(){
  439. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  440. throw new TypeError("TypeError: YBBarrage.stop is not a constructor")
  441. }
  442. this._destroy();
  443. },
  444. enumerable:false
  445. })
  446. Object.defineProperty(YBBarrage.prototype,'seek',{
  447. value:function(time){
  448. if(!(this instanceof YBBarrage)){//那么相反 不是正常调用的就是错误的调用
  449. throw new TypeError("TypeError: YBBarrage.seek is not a constructor")
  450. }
  451. this._barrages = [];
  452. this.currentTime = parseInt(time);
  453. this._clear();
  454. let paused = this.paused;
  455. if ( !paused ) {
  456. this.play()
  457. }
  458. },
  459. enumerable:false
  460. })