UNPKG

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