UNPKG

19.1 kBJavaScriptView Raw
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 typeof define === 'function' && define.amd ? define(factory) :
4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Danmaku = factory());
5}(this, (function () { 'use strict';
6
7 var transform = (function() {
8 /* istanbul ignore next */
9 if (typeof document === 'undefined') return 'transform';
10 var properties = [
11 'oTransform', // Opera 11.5
12 'msTransform', // IE 9
13 'mozTransform',
14 'webkitTransform',
15 'transform'
16 ];
17 var style = document.createElement('div').style;
18 for (var i = 0; i < properties.length; i++) {
19 /* istanbul ignore else */
20 if (properties[i] in style) {
21 return properties[i];
22 }
23 }
24 /* istanbul ignore next */
25 return 'transform';
26 }());
27
28 function createCommentNode(cmt) {
29 var node = document.createElement('div');
30 node.style.cssText = 'position:absolute;';
31 if (typeof cmt.render === 'function') {
32 var $el = cmt.render();
33 if ($el instanceof HTMLElement) {
34 node.appendChild($el);
35 return node;
36 }
37 }
38 node.textContent = cmt.text;
39 if (cmt.style) {
40 for (var key in cmt.style) {
41 node.style[key] = cmt.style[key];
42 }
43 }
44 return node;
45 }
46
47 function init() {
48 var stage = document.createElement('div');
49 stage.style.cssText = 'overflow:hidden;white-space:nowrap;transform:translateZ(0);';
50 return stage;
51 }
52
53 function clear(stage) {
54 var lc = stage.lastChild;
55 while (lc) {
56 stage.removeChild(lc);
57 lc = stage.lastChild;
58 }
59 }
60
61 function resize(stage, width, height) {
62 stage.style.width = width + 'px';
63 stage.style.height = height + 'px';
64 }
65
66 function framing() {
67 //
68 }
69
70 function setup(stage, comments) {
71 var df = document.createDocumentFragment();
72 var i = 0;
73 var cmt = null;
74 for (i = 0; i < comments.length; i++) {
75 cmt = comments[i];
76 cmt.node = cmt.node || createCommentNode(cmt);
77 df.appendChild(cmt.node);
78 }
79 if (comments.length) {
80 stage.appendChild(df);
81 }
82 for (i = 0; i < comments.length; i++) {
83 cmt = comments[i];
84 cmt.width = cmt.width || cmt.node.offsetWidth;
85 cmt.height = cmt.height || cmt.node.offsetHeight;
86 }
87 }
88
89 function render(stage, cmt) {
90 cmt.node.style[transform] = 'translate(' + cmt.x + 'px,' + cmt.y + 'px)';
91 }
92
93 /* eslint no-invalid-this: 0 */
94 function remove(stage, cmt) {
95 stage.removeChild(cmt.node);
96 /* istanbul ignore else */
97 if (!this.media) {
98 cmt.node = null;
99 }
100 }
101
102 var domEngine = {
103 name: 'dom',
104 init: init,
105 clear: clear,
106 resize: resize,
107 framing: framing,
108 setup: setup,
109 render: render,
110 remove: remove,
111 };
112
113 var dpr = typeof window !== 'undefined' && window.devicePixelRatio || 1;
114
115 var canvasHeightCache = Object.create(null);
116
117 function canvasHeight(font, fontSize) {
118 if (canvasHeightCache[font]) {
119 return canvasHeightCache[font];
120 }
121 var height = 12;
122 var regex = /(\d+(?:\.\d+)?)(px|%|em|rem)(?:\s*\/\s*(\d+(?:\.\d+)?)(px|%|em|rem)?)?/;
123 var p = font.match(regex);
124 if (p) {
125 var fs = p[1] * 1 || 10;
126 var fsu = p[2];
127 var lh = p[3] * 1 || 1.2;
128 var lhu = p[4];
129 if (fsu === '%') fs *= fontSize.container / 100;
130 if (fsu === 'em') fs *= fontSize.container;
131 if (fsu === 'rem') fs *= fontSize.root;
132 if (lhu === 'px') height = lh;
133 if (lhu === '%') height = fs * lh / 100;
134 if (lhu === 'em') height = fs * lh;
135 if (lhu === 'rem') height = fontSize.root * lh;
136 if (lhu === undefined) height = fs * lh;
137 }
138 canvasHeightCache[font] = height;
139 return height;
140 }
141
142 function createCommentCanvas(cmt, fontSize) {
143 if (typeof cmt.render === 'function') {
144 var cvs = cmt.render();
145 if (cvs instanceof HTMLCanvasElement) {
146 cmt.width = cvs.width;
147 cmt.height = cvs.height;
148 return cvs;
149 }
150 }
151 var canvas = document.createElement('canvas');
152 var ctx = canvas.getContext('2d');
153 var style = cmt.style || {};
154 style.font = style.font || '10px sans-serif';
155 style.textBaseline = style.textBaseline || 'bottom';
156 var strokeWidth = style.lineWidth * 1;
157 strokeWidth = (strokeWidth > 0 && strokeWidth !== Infinity)
158 ? Math.ceil(strokeWidth)
159 : !!style.strokeStyle * 1;
160 ctx.font = style.font;
161 cmt.width = cmt.width ||
162 Math.max(1, Math.ceil(ctx.measureText(cmt.text).width) + strokeWidth * 2);
163 cmt.height = cmt.height ||
164 Math.ceil(canvasHeight(style.font, fontSize)) + strokeWidth * 2;
165 canvas.width = cmt.width * dpr;
166 canvas.height = cmt.height * dpr;
167 ctx.scale(dpr, dpr);
168 for (var key in style) {
169 ctx[key] = style[key];
170 }
171 var baseline = 0;
172 switch (style.textBaseline) {
173 case 'top':
174 case 'hanging':
175 baseline = strokeWidth;
176 break;
177 case 'middle':
178 baseline = cmt.height >> 1;
179 break;
180 default:
181 baseline = cmt.height - strokeWidth;
182 }
183 if (style.strokeStyle) {
184 ctx.strokeText(cmt.text, strokeWidth, baseline);
185 }
186 ctx.fillText(cmt.text, strokeWidth, baseline);
187 return canvas;
188 }
189
190 function computeFontSize(el) {
191 return window
192 .getComputedStyle(el, null)
193 .getPropertyValue('font-size')
194 .match(/(.+)px/)[1] * 1;
195 }
196
197 function init$1(container) {
198 var stage = document.createElement('canvas');
199 stage.context = stage.getContext('2d');
200 stage._fontSize = {
201 root: computeFontSize(document.getElementsByTagName('html')[0]),
202 container: computeFontSize(container)
203 };
204 return stage;
205 }
206
207 function clear$1(stage, comments) {
208 stage.context.clearRect(0, 0, stage.width, stage.height);
209 // avoid caching canvas to reduce memory usage
210 for (var i = 0; i < comments.length; i++) {
211 comments[i].canvas = null;
212 }
213 }
214
215 function resize$1(stage, width, height) {
216 stage.width = width * dpr;
217 stage.height = height * dpr;
218 stage.style.width = width + 'px';
219 stage.style.height = height + 'px';
220 }
221
222 function framing$1(stage) {
223 stage.context.clearRect(0, 0, stage.width, stage.height);
224 }
225
226 function setup$1(stage, comments) {
227 for (var i = 0; i < comments.length; i++) {
228 var cmt = comments[i];
229 cmt.canvas = createCommentCanvas(cmt, stage._fontSize);
230 }
231 }
232
233 function render$1(stage, cmt) {
234 stage.context.drawImage(cmt.canvas, cmt.x * dpr, cmt.y * dpr);
235 }
236
237 function remove$1(stage, cmt) {
238 // avoid caching canvas to reduce memory usage
239 cmt.canvas = null;
240 }
241
242 var canvasEngine = {
243 name: 'canvas',
244 init: init$1,
245 clear: clear$1,
246 resize: resize$1,
247 framing: framing$1,
248 setup: setup$1,
249 render: render$1,
250 remove: remove$1,
251 };
252
253 /* eslint no-invalid-this: 0 */
254 function allocate(cmt) {
255 var that = this;
256 var ct = this.media ? this.media.currentTime : Date.now() / 1000;
257 var pbr = this.media ? this.media.playbackRate : 1;
258 function willCollide(cr, cmt) {
259 if (cmt.mode === 'top' || cmt.mode === 'bottom') {
260 return ct - cr.time < that._.duration;
261 }
262 var crTotalWidth = that._.width + cr.width;
263 var crElapsed = crTotalWidth * (ct - cr.time) * pbr / that._.duration;
264 if (cr.width > crElapsed) {
265 return true;
266 }
267 // (rtl mode) the right end of `cr` move out of left side of stage
268 var crLeftTime = that._.duration + cr.time - ct;
269 var cmtTotalWidth = that._.width + cmt.width;
270 var cmtTime = that.media ? cmt.time : cmt._utc;
271 var cmtElapsed = cmtTotalWidth * (ct - cmtTime) * pbr / that._.duration;
272 var cmtArrival = that._.width - cmtElapsed;
273 // (rtl mode) the left end of `cmt` reach the left side of stage
274 var cmtArrivalTime = that._.duration * cmtArrival / (that._.width + cmt.width);
275 return crLeftTime > cmtArrivalTime;
276 }
277 var crs = this._.space[cmt.mode];
278 var last = 0;
279 var curr = 0;
280 for (var i = 1; i < crs.length; i++) {
281 var cr = crs[i];
282 var requiredRange = cmt.height;
283 if (cmt.mode === 'top' || cmt.mode === 'bottom') {
284 requiredRange += cr.height;
285 }
286 if (cr.range - cr.height - crs[last].range >= requiredRange) {
287 curr = i;
288 break;
289 }
290 if (willCollide(cr, cmt)) {
291 last = i;
292 }
293 }
294 var channel = crs[last].range;
295 var crObj = {
296 range: channel + cmt.height,
297 time: this.media ? cmt.time : cmt._utc,
298 width: cmt.width,
299 height: cmt.height
300 };
301 crs.splice(last + 1, curr - last - 1, crObj);
302
303 if (cmt.mode === 'bottom') {
304 return this._.height - cmt.height - channel % this._.height;
305 }
306 return channel % (this._.height - cmt.height);
307 }
308
309 /* eslint no-invalid-this: 0 */
310 function createEngine(framing, setup, render, remove) {
311 return function() {
312 framing(this._.stage);
313 var dn = Date.now() / 1000;
314 var ct = this.media ? this.media.currentTime : dn;
315 var pbr = this.media ? this.media.playbackRate : 1;
316 var cmt = null;
317 var cmtt = 0;
318 var i = 0;
319 for (i = this._.runningList.length - 1; i >= 0; i--) {
320 cmt = this._.runningList[i];
321 cmtt = this.media ? cmt.time : cmt._utc;
322 if (ct - cmtt > this._.duration) {
323 remove(this._.stage, cmt);
324 this._.runningList.splice(i, 1);
325 }
326 }
327 var pendingList = [];
328 while (this._.position < this.comments.length) {
329 cmt = this.comments[this._.position];
330 cmtt = this.media ? cmt.time : cmt._utc;
331 if (cmtt >= ct) {
332 break;
333 }
334 // when clicking controls to seek, media.currentTime may changed before
335 // `pause` event is fired, so here skips comments out of duration,
336 // see https://github.com/weizhenye/Danmaku/pull/30 for details.
337 if (ct - cmtt > this._.duration) {
338 ++this._.position;
339 continue;
340 }
341 if (this.media) {
342 cmt._utc = dn - (this.media.currentTime - cmt.time);
343 }
344 pendingList.push(cmt);
345 ++this._.position;
346 }
347 setup(this._.stage, pendingList);
348 for (i = 0; i < pendingList.length; i++) {
349 cmt = pendingList[i];
350 cmt.y = allocate.call(this, cmt);
351 this._.runningList.push(cmt);
352 }
353 for (i = 0; i < this._.runningList.length; i++) {
354 cmt = this._.runningList[i];
355 var totalWidth = this._.width + cmt.width;
356 var elapsed = totalWidth * (dn - cmt._utc) * pbr / this._.duration;
357 if (cmt.mode === 'ltr') cmt.x = (elapsed - cmt.width + .5) | 0;
358 if (cmt.mode === 'rtl') cmt.x = (this._.width - elapsed + .5) | 0;
359 if (cmt.mode === 'top' || cmt.mode === 'bottom') {
360 cmt.x = (this._.width - cmt.width) >> 1;
361 }
362 render(this._.stage, cmt);
363 }
364 };
365 }
366
367 var raf =
368 (
369 typeof window !== 'undefined' &&
370 (
371 window.requestAnimationFrame ||
372 window.mozRequestAnimationFrame ||
373 window.webkitRequestAnimationFrame
374 )
375 ) ||
376 function(cb) {
377 return setTimeout(cb, 50 / 3);
378 };
379
380 var caf =
381 (
382 typeof window !== 'undefined' &&
383 (
384 window.cancelAnimationFrame ||
385 window.mozCancelAnimationFrame ||
386 window.webkitCancelAnimationFrame
387 )
388 ) ||
389 clearTimeout;
390
391 function binsearch(arr, prop, key) {
392 var mid = 0;
393 var left = 0;
394 var right = arr.length;
395 while (left < right - 1) {
396 mid = (left + right) >> 1;
397 if (key >= arr[mid][prop]) {
398 left = mid;
399 } else {
400 right = mid;
401 }
402 }
403 if (arr[left] && key < arr[left][prop]) {
404 return left;
405 }
406 return right;
407 }
408
409
410 function formatMode(mode) {
411 if (!/^(ltr|top|bottom)$/i.test(mode)) {
412 return 'rtl';
413 }
414 return mode.toLowerCase();
415 }
416
417 function collidableRange() {
418 var max = 9007199254740991;
419 return [
420 { range: 0, time: -max, width: max, height: 0 },
421 { range: max, time: max, width: 0, height: 0 }
422 ];
423 }
424
425 function resetSpace(space) {
426 space.ltr = collidableRange();
427 space.rtl = collidableRange();
428 space.top = collidableRange();
429 space.bottom = collidableRange();
430 }
431
432 /* eslint no-invalid-this: 0 */
433 function play() {
434 if (!this._.visible || !this._.paused) {
435 return this;
436 }
437 this._.paused = false;
438 if (this.media) {
439 for (var i = 0; i < this._.runningList.length; i++) {
440 var cmt = this._.runningList[i];
441 cmt._utc = Date.now() / 1000 - (this.media.currentTime - cmt.time);
442 }
443 }
444 var that = this;
445 var engine = createEngine(
446 this._.engine.framing.bind(this),
447 this._.engine.setup.bind(this),
448 this._.engine.render.bind(this),
449 this._.engine.remove.bind(this)
450 );
451 function frame() {
452 engine.call(that);
453 that._.requestID = raf(frame);
454 }
455 this._.requestID = raf(frame);
456 return this;
457 }
458
459 /* eslint no-invalid-this: 0 */
460 function pause() {
461 if (!this._.visible || this._.paused) {
462 return this;
463 }
464 this._.paused = true;
465 caf(this._.requestID);
466 this._.requestID = 0;
467 return this;
468 }
469
470 /* eslint no-invalid-this: 0 */
471 function seek() {
472 if (!this.media) {
473 return this;
474 }
475 this.clear();
476 resetSpace(this._.space);
477 var position = binsearch(this.comments, 'time', this.media.currentTime);
478 this._.position = Math.max(0, position - 1);
479 return this;
480 }
481
482 /* eslint no-invalid-this: 0 */
483 function bindEvents(_) {
484 _.play = play.bind(this);
485 _.pause = pause.bind(this);
486 _.seeking = seek.bind(this);
487 this.media.addEventListener('play', _.play);
488 this.media.addEventListener('pause', _.pause);
489 this.media.addEventListener('playing', _.play);
490 this.media.addEventListener('waiting', _.pause);
491 this.media.addEventListener('seeking', _.seeking);
492 }
493
494 /* eslint no-invalid-this: 0 */
495 function unbindEvents(_) {
496 this.media.removeEventListener('play', _.play);
497 this.media.removeEventListener('pause', _.pause);
498 this.media.removeEventListener('playing', _.play);
499 this.media.removeEventListener('waiting', _.pause);
500 this.media.removeEventListener('seeking', _.seeking);
501 _.play = null;
502 _.pause = null;
503 _.seeking = null;
504 }
505
506 /* eslint-disable no-invalid-this */
507 function init$2(opt) {
508 this._ = {};
509 this.container = opt.container || document.createElement('div');
510 this.media = opt.media;
511 this._.visible = true;
512 /* istanbul ignore else */
513 {
514 this.engine = (opt.engine || 'DOM').toLowerCase();
515 this._.engine = this.engine === 'canvas' ? canvasEngine : domEngine;
516 }
517 /* eslint-enable no-undef */
518 this._.requestID = 0;
519
520 this._.speed = Math.max(0, opt.speed) || 144;
521 this._.duration = 4;
522
523 this.comments = opt.comments || [];
524 this.comments.sort(function(a, b) {
525 return a.time - b.time;
526 });
527 for (var i = 0; i < this.comments.length; i++) {
528 this.comments[i].mode = formatMode(this.comments[i].mode);
529 }
530 this._.runningList = [];
531 this._.position = 0;
532
533 this._.paused = true;
534 if (this.media) {
535 this._.listener = {};
536 bindEvents.call(this, this._.listener);
537 }
538
539 this._.stage = this._.engine.init(this.container);
540 this._.stage.style.cssText += 'position:relative;pointer-events:none;';
541
542 this.resize();
543 this.container.appendChild(this._.stage);
544
545 this._.space = {};
546 resetSpace(this._.space);
547
548 if (!this.media || !this.media.paused) {
549 seek.call(this);
550 play.call(this);
551 }
552 return this;
553 }
554
555 /* eslint-disable no-invalid-this */
556 function destroy() {
557 if (!this.container) {
558 return this;
559 }
560
561 pause.call(this);
562 this.clear();
563 this.container.removeChild(this._.stage);
564 if (this.media) {
565 unbindEvents.call(this, this._.listener);
566 }
567 for (var key in this) {
568 /* istanbul ignore else */
569 if (Object.prototype.hasOwnProperty.call(this, key)) {
570 this[key] = null;
571 }
572 }
573 return this;
574 }
575
576 var properties = ['mode', 'time', 'text', 'render', 'style'];
577
578 /* eslint-disable no-invalid-this */
579 function emit(obj) {
580 if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') {
581 return this;
582 }
583 var cmt = {};
584 for (var i = 0; i < properties.length; i++) {
585 if (obj[properties[i]] !== undefined) {
586 cmt[properties[i]] = obj[properties[i]];
587 }
588 }
589 cmt.text = (cmt.text || '').toString();
590 cmt.mode = formatMode(cmt.mode);
591 cmt._utc = Date.now() / 1000;
592 if (this.media) {
593 var position = 0;
594 if (cmt.time === undefined) {
595 cmt.time = this.media.currentTime;
596 position = this._.position;
597 } else {
598 position = binsearch(this.comments, 'time', cmt.time);
599 if (position < this._.position) {
600 this._.position += 1;
601 }
602 }
603 this.comments.splice(position, 0, cmt);
604 } else {
605 this.comments.push(cmt);
606 }
607 return this;
608 }
609
610 /* eslint-disable no-invalid-this */
611 function show() {
612 if (this._.visible) {
613 return this;
614 }
615 this._.visible = true;
616 if (this.media && this.media.paused) {
617 return this;
618 }
619 seek.call(this);
620 play.call(this);
621 return this;
622 }
623
624 /* eslint-disable no-invalid-this */
625 function hide() {
626 if (!this._.visible) {
627 return this;
628 }
629 pause.call(this);
630 this.clear();
631 this._.visible = false;
632 return this;
633 }
634
635 /* eslint-disable no-invalid-this */
636 function clear$2() {
637 this._.engine.clear(this._.stage, this._.runningList);
638 this._.runningList = [];
639 return this;
640 }
641
642 /* eslint-disable no-invalid-this */
643 function resize$2() {
644 this._.width = this.container.offsetWidth;
645 this._.height = this.container.offsetHeight;
646 this._.engine.resize(this._.stage, this._.width, this._.height);
647 this._.duration = this._.width / this._.speed;
648 return this;
649 }
650
651 var speed = {
652 get: function() {
653 return this._.speed;
654 },
655 set: function(s) {
656 if (typeof s !== 'number' ||
657 isNaN(s) ||
658 !isFinite(s) ||
659 s <= 0) {
660 return this._.speed;
661 }
662 this._.speed = s;
663 if (this._.width) {
664 this._.duration = this._.width / s;
665 }
666 return s;
667 }
668 };
669
670 function Danmaku(opt) {
671 opt && init$2.call(this, opt);
672 }
673 Danmaku.prototype.destroy = function() {
674 return destroy.call(this);
675 };
676 Danmaku.prototype.emit = function(cmt) {
677 return emit.call(this, cmt);
678 };
679 Danmaku.prototype.show = function() {
680 return show.call(this);
681 };
682 Danmaku.prototype.hide = function() {
683 return hide.call(this);
684 };
685 Danmaku.prototype.clear = function() {
686 return clear$2.call(this);
687 };
688 Danmaku.prototype.resize = function() {
689 return resize$2.call(this);
690 };
691 Object.defineProperty(Danmaku.prototype, 'speed', speed);
692
693 return Danmaku;
694
695})));