341 lines
10 KiB
JavaScript
341 lines
10 KiB
JavaScript
class Danmaku {
|
|
constructor (options) {
|
|
this.options = options;
|
|
this.container = this.options.container;
|
|
this.danTunnel = {
|
|
right: {},
|
|
top: {},
|
|
bottom: {}
|
|
};
|
|
this.danIndex = 0;
|
|
this.dan = [];
|
|
this.showing = true;
|
|
this._opacity = this.options.opacity;
|
|
this.events = this.options.events;
|
|
this.unlimited = this.options.unlimited;
|
|
this._measure('');
|
|
|
|
this.load();
|
|
}
|
|
|
|
load () {
|
|
let apiurl;
|
|
if (this.options.api.maximum) {
|
|
apiurl = `${this.options.api.address}?id=${this.options.api.id}&max=${this.options.api.maximum}`;
|
|
}
|
|
else {
|
|
apiurl = `${this.options.api.address}?id=${this.options.api.id}`;
|
|
}
|
|
const endpoints = (this.options.api.addition || []).slice(0);
|
|
endpoints.push(apiurl);
|
|
this.events && this.events.trigger('danmaku_load_start', endpoints);
|
|
|
|
this._readAllEndpoints(endpoints, (results) => {
|
|
this.dan = [].concat.apply([], results).sort((a, b) => a.time - b.time);
|
|
window.requestAnimationFrame(() => {
|
|
this.frame();
|
|
});
|
|
|
|
this.options.callback();
|
|
|
|
this.events && this.events.trigger('danmaku_load_end');
|
|
});
|
|
}
|
|
|
|
reload (newAPI) {
|
|
this.options.api = newAPI;
|
|
this.dan = [];
|
|
this.clear();
|
|
this.load();
|
|
}
|
|
|
|
/**
|
|
* Asynchronously read danmaku from all API endpoints
|
|
*/
|
|
_readAllEndpoints (endpoints, callback) {
|
|
const results = [];
|
|
let readCount = 0;
|
|
const cbk = (i) => (err, data) => {
|
|
++readCount;
|
|
if (err) {
|
|
if (err.response) {
|
|
this.options.error(err.response.msg);
|
|
}
|
|
else {
|
|
this.options.error('Request was unsuccessful: ' + err.status);
|
|
}
|
|
results[i] = [];
|
|
}
|
|
else {
|
|
results[i] = data;
|
|
}
|
|
if (readCount === endpoints.length) {
|
|
return callback(results);
|
|
}
|
|
};
|
|
|
|
for (let i = 0; i < endpoints.length; ++i) {
|
|
this.options.apiBackend.read(endpoints[i], cbk(i));
|
|
}
|
|
}
|
|
|
|
send (dan, callback) {
|
|
const danmakuData = {
|
|
token: this.options.api.token,
|
|
player: this.options.api.id,
|
|
author: this.options.api.user,
|
|
time: this.options.time(),
|
|
text: dan.text,
|
|
color: dan.color,
|
|
type: dan.type
|
|
};
|
|
this.options.apiBackend.send(this.options.api.address, danmakuData, callback);
|
|
|
|
this.dan.splice(this.danIndex, 0, danmakuData);
|
|
this.danIndex++;
|
|
const danmaku = {
|
|
text: this.htmlEncode(danmakuData.text),
|
|
color: danmakuData.color,
|
|
type: danmakuData.type,
|
|
border: `2px solid ${this.options.borderColor}`
|
|
};
|
|
this.draw(danmaku);
|
|
|
|
this.events && this.events.trigger('danmaku_send', danmakuData);
|
|
}
|
|
|
|
frame () {
|
|
if (this.dan.length && !this.paused && this.showing) {
|
|
let item = this.dan[this.danIndex];
|
|
const dan = [];
|
|
while (item && this.options.time() > parseFloat(item.time)) {
|
|
dan.push(item);
|
|
item = this.dan[++this.danIndex];
|
|
}
|
|
this.draw(dan);
|
|
}
|
|
window.requestAnimationFrame(() => {
|
|
this.frame();
|
|
});
|
|
}
|
|
|
|
opacity (percentage) {
|
|
if (percentage !== undefined) {
|
|
const items = this.container.getElementsByClassName('dplayer-danmaku-item');
|
|
for (let i = 0; i < items.length; i++) {
|
|
items[i].style.opacity = percentage;
|
|
}
|
|
this._opacity = percentage;
|
|
|
|
this.events && this.events.trigger('danmaku_opacity', this._opacity);
|
|
}
|
|
return this._opacity;
|
|
}
|
|
|
|
/**
|
|
* Push a danmaku into DPlayer
|
|
*
|
|
* @param {Object Array} dan - {text, color, type}
|
|
* text - danmaku content
|
|
* color - danmaku color, default: `#fff`
|
|
* type - danmaku type, `right` `top` `bottom`, default: `right`
|
|
*/
|
|
draw (dan) {
|
|
const itemHeight = this.options.height;
|
|
const danWidth = this.container.offsetWidth;
|
|
const danHeight = this.container.offsetHeight;
|
|
const itemY = parseInt(danHeight / itemHeight);
|
|
|
|
const danItemRight = (ele) => {
|
|
const eleWidth = ele.offsetWidth || parseInt(ele.style.width);
|
|
const eleRight = ele.getBoundingClientRect().right || this.container.getBoundingClientRect().right + eleWidth;
|
|
return this.container.getBoundingClientRect().right - eleRight;
|
|
};
|
|
|
|
const danSpeed = (width) => (danWidth + width) / 5;
|
|
|
|
const getTunnel = (ele, type, width) => {
|
|
const tmp = danWidth / danSpeed(width);
|
|
|
|
for (let i = 0; this.unlimited || i < itemY; i++) {
|
|
const item = this.danTunnel[type][i + ''];
|
|
if (item && item.length) {
|
|
if (type !== 'right') {
|
|
continue;
|
|
}
|
|
for (let j = 0; j < item.length; j++) {
|
|
const danRight = danItemRight(item[j]) - 10;
|
|
if (danRight <= danWidth - tmp * danSpeed(parseInt(item[j].style.width)) || danRight <= 0) {
|
|
break;
|
|
}
|
|
if (j === item.length - 1) {
|
|
this.danTunnel[type][i + ''].push(ele);
|
|
ele.addEventListener('animationend', () => {
|
|
this.danTunnel[type][i + ''].splice(0, 1);
|
|
});
|
|
return i % itemY;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
this.danTunnel[type][i + ''] = [ele];
|
|
ele.addEventListener('animationend', () => {
|
|
this.danTunnel[type][i + ''].splice(0, 1);
|
|
});
|
|
return i % itemY;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
if (Object.prototype.toString.call(dan) !== '[object Array]') {
|
|
dan = [dan];
|
|
}
|
|
|
|
const docFragment = document.createDocumentFragment();
|
|
|
|
for (let i = 0; i < dan.length; i++) {
|
|
if (!dan[i].type) {
|
|
dan[i].type = 'right';
|
|
}
|
|
if (!dan[i].color) {
|
|
dan[i].color = '#fff';
|
|
}
|
|
const item = document.createElement(`div`);
|
|
item.classList.add(`dplayer-danmaku-item`);
|
|
item.classList.add(`dplayer-danmaku-${dan[i].type}`);
|
|
if (dan[i].border) {
|
|
item.innerHTML = `<span style="border:${dan[i].border}">${dan[i].text}</span>`;
|
|
}
|
|
else {
|
|
item.innerHTML = dan[i].text;
|
|
}
|
|
item.style.opacity = this._opacity;
|
|
item.style.color = dan[i].color;
|
|
item.addEventListener('animationend', () => {
|
|
this.container.removeChild(item);
|
|
});
|
|
|
|
const itemWidth = this._measure(dan[i].text);
|
|
let tunnel;
|
|
|
|
// adjust
|
|
switch (dan[i].type) {
|
|
case 'right':
|
|
tunnel = getTunnel(item, dan[i].type, itemWidth);
|
|
if (tunnel >= 0) {
|
|
item.style.width = itemWidth + 1 + 'px';
|
|
item.style.top = itemHeight * tunnel + 'px';
|
|
item.style.transform = `translateX(-${danWidth}px)`;
|
|
}
|
|
break;
|
|
case 'top':
|
|
tunnel = getTunnel(item, dan[i].type);
|
|
if (tunnel >= 0) {
|
|
item.style.top = itemHeight * tunnel + 'px';
|
|
}
|
|
break;
|
|
case 'bottom':
|
|
tunnel = getTunnel(item, dan[i].type);
|
|
if (tunnel >= 0) {
|
|
item.style.bottom = itemHeight * tunnel + 'px';
|
|
}
|
|
break;
|
|
default:
|
|
console.error(`Can't handled danmaku type: ${dan[i].type}`);
|
|
}
|
|
|
|
if (tunnel >= 0) {
|
|
// move
|
|
item.classList.add(`dplayer-danmaku-move`);
|
|
|
|
// insert
|
|
docFragment.appendChild(item);
|
|
}
|
|
}
|
|
|
|
this.container.appendChild(docFragment);
|
|
|
|
return docFragment;
|
|
}
|
|
|
|
play () {
|
|
this.paused = false;
|
|
}
|
|
|
|
pause () {
|
|
this.paused = true;
|
|
}
|
|
|
|
_measure (text) {
|
|
if (!this.context) {
|
|
const measureStyle = getComputedStyle(this.container.getElementsByClassName('dplayer-danmaku-item')[0], null);
|
|
this.context = document.createElement('canvas').getContext('2d');
|
|
this.context.font = measureStyle.getPropertyValue('font');
|
|
}
|
|
return this.context.measureText(text).width;
|
|
}
|
|
|
|
seek () {
|
|
for (let i = 0; i < this.dan.length; i++) {
|
|
if (this.dan[i].time >= this.options.time()) {
|
|
this.danIndex = i;
|
|
break;
|
|
}
|
|
this.danIndex = this.dan.length;
|
|
}
|
|
}
|
|
|
|
clear () {
|
|
this.danTunnel = {
|
|
right: {},
|
|
top: {},
|
|
bottom: {}
|
|
};
|
|
this.danIndex = 0;
|
|
this.options.container.innerHTML = '';
|
|
|
|
this.events && this.events.trigger('danmaku_clear');
|
|
}
|
|
|
|
htmlEncode (str) {
|
|
return str.
|
|
replace(/&/g, "&").
|
|
replace(/</g, "<").
|
|
replace(/>/g, ">").
|
|
replace(/"/g, """).
|
|
replace(/'/g, "'").
|
|
replace(/\//g, "/");
|
|
}
|
|
|
|
resize () {
|
|
const danWidth = this.container.offsetWidth;
|
|
const items = this.container.getElementsByClassName('dplayer-danmaku-item');
|
|
for (let i = 0; i < items.length; i++) {
|
|
items[i].style.transform = `translateX(-${danWidth}px)`;
|
|
}
|
|
}
|
|
|
|
hide () {
|
|
this.showing = false;
|
|
this.pause();
|
|
this.clear();
|
|
|
|
this.events && this.events.trigger('danmaku_hide');
|
|
}
|
|
|
|
show () {
|
|
this.seek();
|
|
this.showing = true;
|
|
this.play();
|
|
|
|
this.events && this.events.trigger('danmaku_show');
|
|
}
|
|
|
|
unlimit (boolean) {
|
|
this.unlimited = boolean;
|
|
}
|
|
}
|
|
|
|
module.exports = Danmaku;
|