UNPKG

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