(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Danmaku = factory()); }(this, (function () { 'use strict'; /* eslint no-invalid-this: 0 */ function allocate(cmt) { var that = this; var ct = this._hasMedia ? this.media.currentTime : Date.now() / 1000; var pbr = this._hasMedia ? this.media.playbackRate : 1; function willCollide(cr, cmt) { if (cmt.mode === 'top' || cmt.mode === 'bottom') { return ct - cr.time < that.duration; } var crTotalWidth = that.width + cr.width; var crElapsed = crTotalWidth * (ct - cr.time) * pbr / that.duration; if (cr.width > crElapsed) { return true; } // (rtl mode) the right end of `cr` move out of left side of stage var crLeftTime = that.duration + cr.time - ct; var cmtTotalWidth = that.width + cmt.width; var cmtTime = that._hasMedia ? cmt.time : cmt._utc; var cmtElapsed = cmtTotalWidth * (ct - cmtTime) * pbr / that.duration; var cmtArrival = that.width - cmtElapsed; // (rtl mode) the left end of `cmt` reach the left side of stage var cmtArrivalTime = that.duration * cmtArrival / (that.width + cmt.width); return crLeftTime > cmtArrivalTime; } var crs = this._space[cmt.mode]; var last = 0; var curr = 0; for (var i = 1; i < crs.length; i++) { var cr = crs[i]; var requiredRange = cmt.height; if (cmt.mode === 'top' || cmt.mode === 'bottom') { requiredRange += cr.height; } if (cr.range - cr.height - crs[last].range >= requiredRange) { curr = i; break; } if (willCollide(cr, cmt)) { last = i; } } var channel = crs[last].range; var crObj = { range: channel + cmt.height, time: this._hasMedia ? cmt.time : cmt._utc, width: cmt.width, height: cmt.height }; crs.splice(last + 1, curr - last - 1, crObj); if (cmt.mode === 'bottom') { return this.height - cmt.height - channel % this.height; } return channel % (this.height - cmt.height); } function createCommentNode(cmt) { var node = document.createElement('div'); node.style.cssText = 'position:absolute;'; if (typeof cmt.render === 'function') { var $el = cmt.render(); if ($el instanceof HTMLElement) { node.appendChild($el); return node; } } if (cmt.html === true) { node.innerHTML = cmt.text; } else { node.textContent = cmt.text; } if (cmt.style) { for (var key in cmt.style) { node.style[key] = cmt.style[key]; } } return node; } var transform = (function() { var properties = [ 'oTransform', // Opera 11.5 'msTransform', // IE 9 'mozTransform', 'webkitTransform', 'transform' ]; var style = document.createElement('div').style; for (var i = 0; i < properties.length; i++) { /* istanbul ignore else */ if (properties[i] in style) { return properties[i]; } } /* istanbul ignore next */ return 'transform'; }()); /* eslint no-invalid-this: 0 */ function domEngine() { this.duration = 9; var dn = Date.now() / 1000; var ct = this._hasMedia ? this.media.currentTime : dn; var pbr = this._hasMedia ? this.media.playbackRate : 1; var cmt = null; var cmtt = 0; var i = 0; for (i = this.runningList.length - 1; i >= 0; i--) { cmt = this.runningList[i]; cmtt = this._hasMedia ? cmt.time : cmt._utc; if (ct - cmtt > this.duration) { this.stage.removeChild(cmt.node); /* istanbul ignore else */ if (!this._hasMedia) { cmt.node = null; } this.runningList.splice(i, 1); } } var pendingList = []; var df = document.createDocumentFragment(); while (this.position < this.comments.length) { cmt = this.comments[this.position]; cmtt = this._hasMedia ? cmt.time : cmt._utc; if (cmtt >= ct) { break; } if (ct - cmtt > this.duration) { ++this.position; continue; } if (this._hasMedia) { cmt._utc = dn - (this.media.currentTime - cmt.time); } cmt.node = cmt.node || createCommentNode(cmt); this.runningList.push(cmt); pendingList.push(cmt); df.appendChild(cmt.node); ++this.position; } if (pendingList.length) { this.stage.appendChild(df); } for (i = 0; i < pendingList.length; i++) { cmt = pendingList[i]; cmt.width = cmt.width || cmt.node.offsetWidth; cmt.height = cmt.height || cmt.node.offsetHeight; } for (i = 0; i < pendingList.length; i++) { cmt = pendingList[i]; cmt.y = allocate.call(this, cmt); if (cmt.mode === 'top' || cmt.mode === 'bottom') { cmt.x = (this.width - cmt.width) >> 1; cmt.node.style[transform] = 'translateX(' + cmt.x + 'px)'; } } for (i = 0; i < this.runningList.length; i++) { cmt = this.runningList[i]; if (cmt.mode === 'top' || cmt.mode === 'bottom') { continue; } var totalWidth = this.width + cmt.width; var elapsed = totalWidth * (dn - cmt._utc) * pbr / this.duration; elapsed |= 0; if (cmt.mode === 'ltr') cmt.x = elapsed - cmt.width; if (cmt.mode === 'rtl') cmt.x = this.width - elapsed; cmt.node.style[transform] = 'translateX(' + cmt.x + 'px)'; } } var canvasHeightCache = Object.create(null); function canvasHeight(font, fontSize) { if (canvasHeightCache[font]) { return canvasHeightCache[font]; } var height = 12; // eslint-disable-next-line max-len var regex = /(\d+(?:\.\d+)?)(px|%|em|rem)(?:\s*\/\s*(\d+(?:\.\d+)?)(px|%|em|rem)?)?/; var p = font.match(regex); if (p) { var fs = p[1] * 1 || 10; var fsu = p[2]; var lh = p[3] * 1 || 1.2; var lhu = p[4]; if (fsu === '%') fs *= fontSize.container / 100; if (fsu === 'em') fs *= fontSize.container; if (fsu === 'rem') fs *= fontSize.root; if (lhu === 'px') height = lh; if (lhu === '%') height = fs * lh / 100; if (lhu === 'em') height = fs * lh; if (lhu === 'rem') height = fontSize.root * lh; if (lhu === undefined) height = fs * lh; } canvasHeightCache[font] = height; return height; } function createCommentCanvas(cmt, fontSize) { if (typeof cmt.render === 'function') { var cvs = cmt.render(); if (cvs instanceof HTMLCanvasElement) { cmt.width = cvs.width; cmt.height = cvs.height; return cvs; } } var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var style = cmt.canvasStyle || {}; style.font = style.font || '10px sans-serif'; style.textBaseline = style.textBaseline || 'bottom'; var strokeWidth = style.lineWidth * 1; strokeWidth = (strokeWidth > 0 && strokeWidth !== Infinity) ? Math.ceil(strokeWidth) : !!style.strokeStyle * 1; ctx.font = style.font; cmt.width = cmt.width || Math.max(1, Math.ceil(ctx.measureText(cmt.text).width) + strokeWidth * 2); cmt.height = cmt.height || Math.ceil(canvasHeight(style.font, fontSize)) + strokeWidth * 2; canvas.width = cmt.width; canvas.height = cmt.height; for (var key in style) { ctx[key] = style[key]; } var baseline = 0; switch (style.textBaseline) { case 'top': case 'hanging': baseline = strokeWidth; break; case 'middle': baseline = cmt.height >> 1; break; default: baseline = cmt.height - strokeWidth; } if (style.strokeStyle) { ctx.strokeText(cmt.text, strokeWidth, baseline); } ctx.fillText(cmt.text, strokeWidth, baseline); return canvas; } /* eslint no-invalid-this: 0 */ function canvasEngine() { this.stage.context.clearRect(0, 0, this.width, this.height); var dn = Date.now() / 1000; var ct = this._hasMedia ? this.media.currentTime : dn; var pbr = this._hasMedia ? this.media.playbackRate : 1; var cmt = null; var cmtt = 0; var i = 0; for (i = this.runningList.length - 1; i >= 0; i--) { cmt = this.runningList[i]; cmtt = this._hasMedia ? cmt.time : cmt._utc; if (ct - cmtt > this.duration) { // avoid caching canvas to reduce memory usage cmt.canvas = null; this.runningList.splice(i, 1); } } while (this.position < this.comments.length) { cmt = this.comments[this.position]; cmtt = this._hasMedia ? cmt.time : cmt._utc; if (cmtt >= ct) { break; } if (ct - cmtt > this.duration) { ++this.position; continue; } if (this._hasMedia) { cmt._utc = dn - (this.media.currentTime - cmt.time); } cmt.canvas = createCommentCanvas(cmt, this._fontSize); cmt.y = allocate.call(this, cmt); if (cmt.mode === 'top' || cmt.mode === 'bottom') { cmt.x = (this.width - cmt.width) >> 1; } this.runningList.push(cmt); ++this.position; } for (i = 0; i < this.runningList.length; i++) { cmt = this.runningList[i]; var totalWidth = this.width + cmt.width; var elapsed = totalWidth * (dn - cmt._utc) * pbr / this.duration; if (cmt.mode === 'ltr') cmt.x = (elapsed - cmt.width + .5) | 0; if (cmt.mode === 'rtl') cmt.x = (this.width - elapsed + .5) | 0; this.stage.context.drawImage(cmt.canvas, cmt.x, cmt.y); } } var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || function(cb) { return setTimeout(cb, 50 / 3); }; var caf = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame || clearTimeout; /* eslint no-invalid-this: 0 */ function play() { if (!this.visible || !this.paused) { return this; } this.paused = false; if (this._hasMedia) { for (var i = 0; i < this.runningList.length; i++) { var cmt = this.runningList[i]; cmt._utc = Date.now() / 1000 - (this.media.currentTime - cmt.time); } } var that = this; var engine = this._useCanvas ? canvasEngine : domEngine; function frame() { engine.call(that); that._requestID = raf(frame); } this._requestID = raf(frame); return this; } /* eslint no-invalid-this: 0 */ function pause() { if (!this.visible || this.paused) { return this; } this.paused = true; caf(this._requestID); this._requestID = 0; return this; } function binsearch(arr, prop, key) { var mid = 0; var left = 0; var right = arr.length; while (left < right - 1) { mid = (left + right) >> 1; if (key >= arr[mid][prop]) { left = mid; } else { right = mid; } } if (arr[left] && key < arr[left][prop]) { return left; } return right; } function collidableRange() { var max = 9007199254740991; return [{ range: 0, time: -max, width: max, height: 0 }, { range: max, time: max, width: 0, height: 0 }]; } function resetSpace(space) { space.ltr = collidableRange(); space.rtl = collidableRange(); space.top = collidableRange(); space.bottom = collidableRange(); } /* eslint no-invalid-this: 0 */ function seek() { if (!this._hasMedia) { return this; } this.clear(); resetSpace(this._space); var position = binsearch(this.comments, 'time', this.media.currentTime); this.position = Math.max(0, position - 1); return this; } /* eslint no-invalid-this: 0 */ function bindEvents(_) { _.play = play.bind(this); _.pause = pause.bind(this); _.seeking = seek.bind(this); this.media.addEventListener('play', _.play); this.media.addEventListener('pause', _.pause); this.media.addEventListener('seeking', _.seeking); } /* eslint no-invalid-this: 0 */ function unbindEvents(_) { this.media.removeEventListener('play', _.play); this.media.removeEventListener('pause', _.pause); this.media.removeEventListener('seeking', _.seeking); _.play = null; _.pause = null; _.seeking = null; } function computeFontSize(el, fontSize) { var fs = window .getComputedStyle(el, null) .getPropertyValue('font-size') .match(/(.+)px/)[1] * 1; if (el.tagName === 'HTML') { fontSize.root = fs; } else { fontSize.container = fs; } } function formatMode(mode) { if (!/^(ltr|top|bottom)$/i.test(mode)) { return 'rtl'; } return mode.toLowerCase(); } function initMixin(Danmaku) { Danmaku.prototype.init = function(opt) { if (this._isInited) { return this; } if ( !opt || ( !opt.container && (!opt.video || (opt.video && !opt.video.parentNode)) ) ) { throw new Error('Danmaku requires container when initializing.'); } this._hasInitContainer = !!opt.container; this.container = opt.container; this.visible = true; this.engine = (opt.engine || 'DOM').toLowerCase(); this._useCanvas = (this.engine === 'canvas'); this._requestID = 0; this._speed = Math.max(0, opt.speed) || 144; this.duration = 4; this.comments = opt.comments || []; this.comments.sort(function(a, b) { return a.time - b.time; }); for (var i = 0; i < this.comments.length; i++) { this.comments[i].mode = formatMode(this.comments[i].mode); } this.runningList = []; this.position = 0; this.paused = true; this.media = opt.video || opt.audio; this._hasMedia = !!this.media; this._hasVideo = !!opt.video; if (this._hasVideo && !this._hasInitContainer) { var isPlay = !this.media.paused; this.container = document.createElement('div'); this.container.style.position = this.media.style.position; this.media.style.position = 'absolute'; this.media.parentNode.insertBefore(this.container, this.media); this.container.appendChild(this.media); // In Webkit/Blink, making a change to video element will pause the video. if (isPlay && this.media.paused) { this.media.play(); } } if (this._hasMedia) { this._listener = {}; bindEvents.call(this, this._listener); } if (this._useCanvas) { this.stage = document.createElement('canvas'); this.stage.context = this.stage.getContext('2d'); } else { this.stage = document.createElement('div'); this.stage.style.cssText = 'overflow:hidden;white-space:nowrap;transform:translateZ(0);'; } this.stage.style.cssText += 'position:relative;pointer-events:none;'; this.resize(); this.container.appendChild(this.stage); this._space = {}; resetSpace(this._space); this._fontSize = { root: 16, container: 16 }; computeFontSize(document.getElementsByTagName('html')[0], this._fontSize); computeFontSize(this.container, this._fontSize); if (!this._hasMedia || !this.media.paused) { seek.call(this); play.call(this); } this._isInited = true; return this; }; } var properties = [ // meta 'mode', 'time', // both engine 'text', 'render', // DOM engine 'html', 'style', // canvas engine 'canvasStyle' ]; function emitMixin(Danmaku) { Danmaku.prototype.emit = function(obj) { if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') { return this; } var cmt = {}; for (var i = 0; i < properties.length; i++) { if (obj[properties[i]] !== undefined) { cmt[properties[i]] = obj[properties[i]]; } } cmt.text = (cmt.text || '').toString(); cmt.mode = formatMode(cmt.mode); cmt._utc = Date.now() / 1000; if (this._hasMedia) { var position = 0; if (cmt.time === undefined) { cmt.time = this.media.currentTime; position = this.position; } else { position = binsearch(this.comments, 'time', cmt.time); if (position < this.position) { this.position += 1; } } this.comments.splice(position, 0, cmt); } else { this.comments.push(cmt); } return this; }; } function clearMixin(Danmaku) { Danmaku.prototype.clear = function() { if (this._useCanvas) { this.stage.context.clearRect(0, 0, this.width, this.height); // avoid caching canvas to reduce memory usage for (var i = 0; i < this.runningList.length; i++) { this.runningList[i].canvas = null; } } else { var lc = this.stage.lastChild; while (lc) { this.stage.removeChild(lc); lc = this.stage.lastChild; } } this.runningList = []; return this; }; } function destroyMixin(Danmaku) { Danmaku.prototype.destroy = function() { if (!this._isInited) { return this; } pause.call(this); this.clear(); if (this._hasMedia) { unbindEvents.call(this, this._listener); } if (this._hasVideo && !this._hasInitContainer) { var isPlay = !this.media.paused; this.media.style.position = this.container.style.position; this.container.parentNode.insertBefore(this.media, this.container); this.container.parentNode.removeChild(this.container); /* istanbul ignore next */ if (isPlay && this.media.paused) { this.media.play(); } } for (var key in this) { /* istanbul ignore else */ if (Object.prototype.hasOwnProperty.call(this, key)) { this[key] = null; } } return this; }; } function showMixin(Danmaku) { Danmaku.prototype.show = function() { if (this.visible) { return this; } this.visible = true; if (this._hasMedia && this.media.paused) { return this; } seek.call(this); play.call(this); return this; }; } function hideMixin(Danmaku) { Danmaku.prototype.hide = function() { if (!this.visible) { return this; } pause.call(this); this.clear(); this.visible = false; return this; }; } function resizeMixin(Danmaku) { Danmaku.prototype.resize = function() { if (this._hasInitContainer) { this.width = this.container.offsetWidth; this.height = this.container.offsetHeight; } if (this._hasVideo && (!this._hasInitContainer || !this.width || !this.height)) { this.width = this.media.clientWidth; this.height = this.media.clientHeight; } if (this._useCanvas) { this.stage.width = this.width; this.stage.height = this.height; } else { this.stage.style.width = this.width + 'px'; this.stage.style.height = this.height + 'px'; } this.duration = this.width / this._speed; return this; }; } function speedMixin(Danmaku) { Object.defineProperty(Danmaku.prototype, 'speed', { get: function() { return this._speed; }, set: function(s) { if (typeof s !== 'number' || isNaN(s) || !isFinite(s) || s <= 0) { return this._speed; } this._speed = s; if (this.width) { this.duration = this.width / s; } return s; } }); } function Danmaku(opt) { this._isInited = false; opt && this.init(opt); } initMixin(Danmaku); emitMixin(Danmaku); clearMixin(Danmaku); destroyMixin(Danmaku); showMixin(Danmaku); hideMixin(Danmaku); resizeMixin(Danmaku); speedMixin(Danmaku); return Danmaku; })));