UNPKG

15 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 /* eslint no-invalid-this: 0 */
114 function allocate(cmt) {
115 var that = this;
116 var ct = this.media ? this.media.currentTime : Date.now() / 1000;
117 var pbr = this.media ? this.media.playbackRate : 1;
118 function willCollide(cr, cmt) {
119 if (cmt.mode === 'top' || cmt.mode === 'bottom') {
120 return ct - cr.time < that._.duration;
121 }
122 var crTotalWidth = that._.width + cr.width;
123 var crElapsed = crTotalWidth * (ct - cr.time) * pbr / that._.duration;
124 if (cr.width > crElapsed) {
125 return true;
126 }
127 // (rtl mode) the right end of `cr` move out of left side of stage
128 var crLeftTime = that._.duration + cr.time - ct;
129 var cmtTotalWidth = that._.width + cmt.width;
130 var cmtTime = that.media ? cmt.time : cmt._utc;
131 var cmtElapsed = cmtTotalWidth * (ct - cmtTime) * pbr / that._.duration;
132 var cmtArrival = that._.width - cmtElapsed;
133 // (rtl mode) the left end of `cmt` reach the left side of stage
134 var cmtArrivalTime = that._.duration * cmtArrival / (that._.width + cmt.width);
135 return crLeftTime > cmtArrivalTime;
136 }
137 var crs = this._.space[cmt.mode];
138 var last = 0;
139 var curr = 0;
140 for (var i = 1; i < crs.length; i++) {
141 var cr = crs[i];
142 var requiredRange = cmt.height;
143 if (cmt.mode === 'top' || cmt.mode === 'bottom') {
144 requiredRange += cr.height;
145 }
146 if (cr.range - cr.height - crs[last].range >= requiredRange) {
147 curr = i;
148 break;
149 }
150 if (willCollide(cr, cmt)) {
151 last = i;
152 }
153 }
154 var channel = crs[last].range;
155 var crObj = {
156 range: channel + cmt.height,
157 time: this.media ? cmt.time : cmt._utc,
158 width: cmt.width,
159 height: cmt.height
160 };
161 crs.splice(last + 1, curr - last - 1, crObj);
162
163 if (cmt.mode === 'bottom') {
164 return this._.height - cmt.height - channel % this._.height;
165 }
166 return channel % (this._.height - cmt.height);
167 }
168
169 /* eslint no-invalid-this: 0 */
170 function createEngine(framing, setup, render, remove) {
171 return function() {
172 framing(this._.stage);
173 var dn = Date.now() / 1000;
174 var ct = this.media ? this.media.currentTime : dn;
175 var pbr = this.media ? this.media.playbackRate : 1;
176 var cmt = null;
177 var cmtt = 0;
178 var i = 0;
179 for (i = this._.runningList.length - 1; i >= 0; i--) {
180 cmt = this._.runningList[i];
181 cmtt = this.media ? cmt.time : cmt._utc;
182 if (ct - cmtt > this._.duration) {
183 remove(this._.stage, cmt);
184 this._.runningList.splice(i, 1);
185 }
186 }
187 var pendingList = [];
188 while (this._.position < this.comments.length) {
189 cmt = this.comments[this._.position];
190 cmtt = this.media ? cmt.time : cmt._utc;
191 if (cmtt >= ct) {
192 break;
193 }
194 // when clicking controls to seek, media.currentTime may changed before
195 // `pause` event is fired, so here skips comments out of duration,
196 // see https://github.com/weizhenye/Danmaku/pull/30 for details.
197 if (ct - cmtt > this._.duration) {
198 ++this._.position;
199 continue;
200 }
201 if (this.media) {
202 cmt._utc = dn - (this.media.currentTime - cmt.time);
203 }
204 pendingList.push(cmt);
205 ++this._.position;
206 }
207 setup(this._.stage, pendingList);
208 for (i = 0; i < pendingList.length; i++) {
209 cmt = pendingList[i];
210 cmt.y = allocate.call(this, cmt);
211 this._.runningList.push(cmt);
212 }
213 for (i = 0; i < this._.runningList.length; i++) {
214 cmt = this._.runningList[i];
215 var totalWidth = this._.width + cmt.width;
216 var elapsed = totalWidth * (dn - cmt._utc) * pbr / this._.duration;
217 if (cmt.mode === 'ltr') cmt.x = (elapsed - cmt.width + .5) | 0;
218 if (cmt.mode === 'rtl') cmt.x = (this._.width - elapsed + .5) | 0;
219 if (cmt.mode === 'top' || cmt.mode === 'bottom') {
220 cmt.x = (this._.width - cmt.width) >> 1;
221 }
222 render(this._.stage, cmt);
223 }
224 };
225 }
226
227 var raf =
228 (
229 typeof window !== 'undefined' &&
230 (
231 window.requestAnimationFrame ||
232 window.mozRequestAnimationFrame ||
233 window.webkitRequestAnimationFrame
234 )
235 ) ||
236 function(cb) {
237 return setTimeout(cb, 50 / 3);
238 };
239
240 var caf =
241 (
242 typeof window !== 'undefined' &&
243 (
244 window.cancelAnimationFrame ||
245 window.mozCancelAnimationFrame ||
246 window.webkitCancelAnimationFrame
247 )
248 ) ||
249 clearTimeout;
250
251 function binsearch(arr, prop, key) {
252 var mid = 0;
253 var left = 0;
254 var right = arr.length;
255 while (left < right - 1) {
256 mid = (left + right) >> 1;
257 if (key >= arr[mid][prop]) {
258 left = mid;
259 } else {
260 right = mid;
261 }
262 }
263 if (arr[left] && key < arr[left][prop]) {
264 return left;
265 }
266 return right;
267 }
268
269
270 function formatMode(mode) {
271 if (!/^(ltr|top|bottom)$/i.test(mode)) {
272 return 'rtl';
273 }
274 return mode.toLowerCase();
275 }
276
277 function collidableRange() {
278 var max = 9007199254740991;
279 return [
280 { range: 0, time: -max, width: max, height: 0 },
281 { range: max, time: max, width: 0, height: 0 }
282 ];
283 }
284
285 function resetSpace(space) {
286 space.ltr = collidableRange();
287 space.rtl = collidableRange();
288 space.top = collidableRange();
289 space.bottom = collidableRange();
290 }
291
292 /* eslint no-invalid-this: 0 */
293 function play() {
294 if (!this._.visible || !this._.paused) {
295 return this;
296 }
297 this._.paused = false;
298 if (this.media) {
299 for (var i = 0; i < this._.runningList.length; i++) {
300 var cmt = this._.runningList[i];
301 cmt._utc = Date.now() / 1000 - (this.media.currentTime - cmt.time);
302 }
303 }
304 var that = this;
305 var engine = createEngine(
306 this._.engine.framing.bind(this),
307 this._.engine.setup.bind(this),
308 this._.engine.render.bind(this),
309 this._.engine.remove.bind(this)
310 );
311 function frame() {
312 engine.call(that);
313 that._.requestID = raf(frame);
314 }
315 this._.requestID = raf(frame);
316 return this;
317 }
318
319 /* eslint no-invalid-this: 0 */
320 function pause() {
321 if (!this._.visible || this._.paused) {
322 return this;
323 }
324 this._.paused = true;
325 caf(this._.requestID);
326 this._.requestID = 0;
327 return this;
328 }
329
330 /* eslint no-invalid-this: 0 */
331 function seek() {
332 if (!this.media) {
333 return this;
334 }
335 this.clear();
336 resetSpace(this._.space);
337 var position = binsearch(this.comments, 'time', this.media.currentTime);
338 this._.position = Math.max(0, position - 1);
339 return this;
340 }
341
342 /* eslint no-invalid-this: 0 */
343 function bindEvents(_) {
344 _.play = play.bind(this);
345 _.pause = pause.bind(this);
346 _.seeking = seek.bind(this);
347 this.media.addEventListener('play', _.play);
348 this.media.addEventListener('pause', _.pause);
349 this.media.addEventListener('playing', _.play);
350 this.media.addEventListener('waiting', _.pause);
351 this.media.addEventListener('seeking', _.seeking);
352 }
353
354 /* eslint no-invalid-this: 0 */
355 function unbindEvents(_) {
356 this.media.removeEventListener('play', _.play);
357 this.media.removeEventListener('pause', _.pause);
358 this.media.removeEventListener('playing', _.play);
359 this.media.removeEventListener('waiting', _.pause);
360 this.media.removeEventListener('seeking', _.seeking);
361 _.play = null;
362 _.pause = null;
363 _.seeking = null;
364 }
365
366 /* eslint-disable no-invalid-this */
367 function init$1(opt) {
368 this._ = {};
369 this.container = opt.container || document.createElement('div');
370 this.media = opt.media;
371 this._.visible = true;
372
373 /* eslint-disable no-undef */
374 /* istanbul ignore next */
375 {
376 this.engine = 'dom';
377 this._.engine = domEngine;
378 }
379 /* eslint-enable no-undef */
380 this._.requestID = 0;
381
382 this._.speed = Math.max(0, opt.speed) || 144;
383 this._.duration = 4;
384
385 this.comments = opt.comments || [];
386 this.comments.sort(function(a, b) {
387 return a.time - b.time;
388 });
389 for (var i = 0; i < this.comments.length; i++) {
390 this.comments[i].mode = formatMode(this.comments[i].mode);
391 }
392 this._.runningList = [];
393 this._.position = 0;
394
395 this._.paused = true;
396 if (this.media) {
397 this._.listener = {};
398 bindEvents.call(this, this._.listener);
399 }
400
401 this._.stage = this._.engine.init(this.container);
402 this._.stage.style.cssText += 'position:relative;pointer-events:none;';
403
404 this.resize();
405 this.container.appendChild(this._.stage);
406
407 this._.space = {};
408 resetSpace(this._.space);
409
410 if (!this.media || !this.media.paused) {
411 seek.call(this);
412 play.call(this);
413 }
414 return this;
415 }
416
417 /* eslint-disable no-invalid-this */
418 function destroy() {
419 if (!this.container) {
420 return this;
421 }
422
423 pause.call(this);
424 this.clear();
425 this.container.removeChild(this._.stage);
426 if (this.media) {
427 unbindEvents.call(this, this._.listener);
428 }
429 for (var key in this) {
430 /* istanbul ignore else */
431 if (Object.prototype.hasOwnProperty.call(this, key)) {
432 this[key] = null;
433 }
434 }
435 return this;
436 }
437
438 var properties = ['mode', 'time', 'text', 'render', 'style'];
439
440 /* eslint-disable no-invalid-this */
441 function emit(obj) {
442 if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') {
443 return this;
444 }
445 var cmt = {};
446 for (var i = 0; i < properties.length; i++) {
447 if (obj[properties[i]] !== undefined) {
448 cmt[properties[i]] = obj[properties[i]];
449 }
450 }
451 cmt.text = (cmt.text || '').toString();
452 cmt.mode = formatMode(cmt.mode);
453 cmt._utc = Date.now() / 1000;
454 if (this.media) {
455 var position = 0;
456 if (cmt.time === undefined) {
457 cmt.time = this.media.currentTime;
458 position = this._.position;
459 } else {
460 position = binsearch(this.comments, 'time', cmt.time);
461 if (position < this._.position) {
462 this._.position += 1;
463 }
464 }
465 this.comments.splice(position, 0, cmt);
466 } else {
467 this.comments.push(cmt);
468 }
469 return this;
470 }
471
472 /* eslint-disable no-invalid-this */
473 function show() {
474 if (this._.visible) {
475 return this;
476 }
477 this._.visible = true;
478 if (this.media && this.media.paused) {
479 return this;
480 }
481 seek.call(this);
482 play.call(this);
483 return this;
484 }
485
486 /* eslint-disable no-invalid-this */
487 function hide() {
488 if (!this._.visible) {
489 return this;
490 }
491 pause.call(this);
492 this.clear();
493 this._.visible = false;
494 return this;
495 }
496
497 /* eslint-disable no-invalid-this */
498 function clear$1() {
499 this._.engine.clear(this._.stage, this._.runningList);
500 this._.runningList = [];
501 return this;
502 }
503
504 /* eslint-disable no-invalid-this */
505 function resize$1() {
506 this._.width = this.container.offsetWidth;
507 this._.height = this.container.offsetHeight;
508 this._.engine.resize(this._.stage, this._.width, this._.height);
509 this._.duration = this._.width / this._.speed;
510 return this;
511 }
512
513 var speed = {
514 get: function() {
515 return this._.speed;
516 },
517 set: function(s) {
518 if (typeof s !== 'number' ||
519 isNaN(s) ||
520 !isFinite(s) ||
521 s <= 0) {
522 return this._.speed;
523 }
524 this._.speed = s;
525 if (this._.width) {
526 this._.duration = this._.width / s;
527 }
528 return s;
529 }
530 };
531
532 function Danmaku(opt) {
533 opt && init$1.call(this, opt);
534 }
535 Danmaku.prototype.destroy = function() {
536 return destroy.call(this);
537 };
538 Danmaku.prototype.emit = function(cmt) {
539 return emit.call(this, cmt);
540 };
541 Danmaku.prototype.show = function() {
542 return show.call(this);
543 };
544 Danmaku.prototype.hide = function() {
545 return hide.call(this);
546 };
547 Danmaku.prototype.clear = function() {
548 return clear$1.call(this);
549 };
550 Danmaku.prototype.resize = function() {
551 return resize$1.call(this);
552 };
553 Object.defineProperty(Danmaku.prototype, 'speed', speed);
554
555 return Danmaku;
556
557})));