Created
August 8, 2017 06:31
-
-
Save 303182519/5a28b2be0b238fce8ae0987082dae7f8 to your computer and use it in GitHub Desktop.
danmaku
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | |
typeof define === 'function' && define.amd ? define(factory) : | |
(global.Danmaku = factory()); | |
}(this, (function () { 'use strict'; | |
function collidableRange() { | |
var max = 9007199254740991; | |
return [{ | |
range: 0, | |
time: -max, | |
width: max, | |
height: 0 | |
}, { | |
range: max, | |
time: max, | |
width: 0, | |
height: 0 | |
}]; | |
} | |
var space = {}; | |
function resetSpace() { | |
space.ltr = collidableRange(); | |
space.rtl = collidableRange(); | |
space.top = collidableRange(); | |
space.bottom = collidableRange(); | |
} | |
resetSpace(); | |
/* eslint no-invalid-this: 0 */ | |
var allocate = function(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; | |
} | |
//这里的逻辑主要是防止后面的追上来 | |
var crLeftTime = that.duration + cr.time - ct; //前一个弹幕剩余的时间 ( 总时长 + 弹幕发生时刻 - 当前时间 ) | |
var cmtArrivalTime = that.duration * that.width / (that.width + cmt.width); //当前弹幕到达最左边要花费多少时间(还没有出边界) | |
return crLeftTime > cmtArrivalTime; | |
} | |
var crs = space[cmt.mode]; | |
var last = 0; | |
var curr = 0; | |
//i是从1开始算的 | |
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); | |
}; | |
var createCommentNode = function(cmt) { | |
var node = document.createElement('div'); | |
if (cmt.html === true) { | |
node.innerHTML = cmt.text; | |
} else { | |
node.textContent = cmt.text; | |
} | |
node.style.cssText = 'position:absolute;'; | |
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 */ | |
var domEngine = function() { | |
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; | |
} | |
cmt._utc = Date.now() / 1000; | |
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] = 'translate(' + cmt.x + 'px,' + cmt.y + '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] = 'translate(' + cmt.x + 'px,' + cmt.y + 'px)'; | |
} | |
}; | |
//弹幕的字体大小 | |
var containerFontSize = 16; | |
var rootFontSize = 16; | |
function computeFontSize(el) { | |
var fs = window | |
.getComputedStyle(el, null) | |
.getPropertyValue('font-size') | |
.match(/(.+)px/)[1] * 1; | |
if (el.tagName === 'HTML') { | |
rootFontSize = fs; | |
} else { | |
containerFontSize = fs; | |
} | |
} | |
//弹幕的高度获取 | |
var canvasHeightCache = Object.create(null); | |
var canvasHeight = function(font) { | |
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 *= containerFontSize / 100; | |
if (fsu === 'em') fs *= containerFontSize; | |
if (fsu === 'rem') fs *= rootFontSize; | |
if (lhu === 'px') height = lh; | |
if (lhu === '%') height = fs * lh / 100; | |
if (lhu === 'em') height = fs * lh; | |
if (lhu === 'rem') height = rootFontSize * lh; | |
if (lhu === undefined) height = fs * lh; | |
} | |
canvasHeightCache[font] = height; | |
return height; | |
}; | |
//为每一条弹幕创建canvas | |
var createCommentCanvas = function(cmt) { | |
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)) + 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 */ | |
var canvasEngine = function() { | |
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; | |
} | |
cmt._utc = Date.now() / 1000; //当前时间的秒数 | |
cmt.canvas = createCommentCanvas(cmt); //创建canvas | |
cmt.y = allocate.call(this, cmt); | |
//x在中间 | |
if (cmt.mode === 'top' || cmt.mode === 'bottom') { | |
cmt.x = (this.width - cmt.width) >> 1; //右移1 相当于/2的1次方,也就是除2 | |
} | |
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); | |
} | |
}; | |
/* istanbul ignore next */ | |
var raf = | |
window.requestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
function(cb) { | |
return setTimeout(cb, 50 / 3); | |
}; | |
/* istanbul ignore next */ | |
var caf = | |
window.cancelAnimationFrame || | |
window.mozCancelAnimationFrame || | |
window.webkitCancelAnimationFrame || | |
clearTimeout; | |
/* eslint no-invalid-this: 0 */ | |
var play = function() { | |
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 */ | |
// | |
var pause = function() { | |
if (!this.visible || this.paused) { | |
return this; | |
} | |
this.paused = true; | |
caf(this._requestID); | |
this._requestID = 0; | |
return this; | |
}; | |
//折半查找 | |
var binsearch = function(arr, prop, key) { | |
var mid = 0; | |
var left = 0; | |
var right = arr.length; | |
while (left < right - 1) { | |
mid = (left + right) >> 1; // 相当于/2 ,>> x 右移2 的 x次放 | |
if (key >= arr[mid][prop]) { | |
left = mid; | |
} else { | |
right = mid; | |
} | |
} | |
if (arr[left] && key < arr[left][prop]) { | |
return left; | |
} | |
return right; | |
}; | |
/* eslint no-invalid-this: 0 */ | |
//拖到播放地址 | |
var seek = function() { | |
if (!this._hasMedia) { | |
return this; | |
} | |
this.clear(); //清理 | |
resetSpace(); | |
var position = binsearch(this.comments, 'time', this.media.currentTime); //获取播放的位置 | |
this.position = Math.max(0, position - 1); | |
return this; | |
}; | |
var playHandler = null; | |
var pauseHandler = null; | |
var seekingHandler = null; | |
/* eslint no-invalid-this: 0 */ | |
function bindEvents() { | |
playHandler = play.bind(this); | |
pauseHandler = pause.bind(this); | |
seekingHandler = seek.bind(this); | |
this.media.addEventListener('play', playHandler); | |
this.media.addEventListener('pause', pauseHandler); | |
this.media.addEventListener('seeking', seekingHandler); //拖到播放地址 | |
} | |
/* eslint no-invalid-this: 0 */ | |
function unbindEvents() { | |
this.media.removeEventListener('play', playHandler); | |
this.media.removeEventListener('pause', pauseHandler); | |
this.media.removeEventListener('seeking', seekingHandler); | |
playHandler = null; | |
pauseHandler = null; | |
seekingHandler = null; | |
} | |
//格式化 | |
var formatMode = function(mode) { | |
if (!/^(ltr|top|bottom)$/i.test(mode)) { | |
return 'rtl'; | |
} | |
return mode.toLowerCase(); | |
}; | |
var initMixin = function(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 = JSON.parse(JSON.stringify(opt.comments || [])); | |
//时间排序,从小到大 | |
this.comments.sort(function(a, b) { | |
return a.time - b.time; | |
}); | |
//弹幕的模式 ltr、rtl、top、bottom。 | |
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; | |
//假如存在视频资源,但没有包裹层的话,就创建一个空的div包裹层,把视频插入到里面去 | |
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) { | |
bindEvents.call(this); | |
} | |
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); | |
computeFontSize(document.getElementsByTagName('html')[0]); | |
computeFontSize(this.container); | |
//假如没有媒体资源 或者媒体资源没有暂停的话 | |
if (!this._hasMedia || !this.media.paused) { | |
seek.call(this); | |
play.call(this); | |
} | |
this._isInited = true; | |
return this; | |
}; | |
}; | |
/** | |
发送弹幕 | |
var comment = { | |
text: 'bla bla', | |
style: { | |
fontSize: '20px', | |
color: '#ffffff' | |
}, | |
}; | |
danmaku.emit(comment) | |
*/ | |
var emitMixin = function(Danmaku) { | |
Danmaku.prototype.emit = function(obj) { | |
if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') { | |
return this; | |
} | |
var cmt = JSON.parse(JSON.stringify(obj)); | |
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); //折半查找 | |
} | |
this.comments.splice(position, 0, cmt); //在固定的位置插入。固定的时间点 | |
} else { | |
this.comments.push(cmt); | |
} | |
return this; | |
}; | |
}; | |
//清理弹幕 | |
var clearMixin = function(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; | |
}; | |
}; | |
//销毁: | |
//暂停、清理、解绑、重置位置 | |
var destroyMixin = function(Danmaku) { | |
Danmaku.prototype.destroy = function() { | |
if (!this._isInited) { | |
return this; | |
} | |
pause.call(this); | |
this.clear(); | |
if (this._hasMedia) { | |
unbindEvents.call(this); | |
} | |
resetSpace(); | |
if (this._hasVideo && !this._hasInitContainer) { | |
var isPlay = !this.media.paused; | |
this.media.style.position = this.container.style.position; | |
this.container.parentNode.appendChild(this.media); | |
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; | |
}; | |
}; | |
//显示,播放,seek? | |
// | |
var showMixin = function(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; | |
}; | |
}; | |
//隐藏的话,要暂停和清理 | |
var hideMixin = function(Danmaku) { | |
Danmaku.prototype.hide = function() { | |
if (!this.visible) { | |
return this; | |
} | |
pause.call(this); | |
this.clear(); | |
this.visible = false; | |
return this; | |
}; | |
}; | |
// 缩放宽度问题 | |
var resizeMixin = function(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; | |
}; | |
}; | |
//速度的设置 | |
//var danmaku = new Danmaku(); | |
//danmaku.speed | |
var speedMixin = function(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; | |
}))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment