前言
以前玩卷軸射擊游戲的時候,大量的BOSS子彈讓我們無路可逃的時候,讓我見識到了真正彈幕的威力,可自從A站B站火了之后,大量評論留言參與到了視頻的播放中,也讓我見識到了“彈幕”的威力,壓根視頻就沒法看了……全看評論去了,就是那么好玩。
現在沒有彈幕功能都不好意思說是做視頻or直播網站的。而我們也不能落后吶,產品提需求了,活動H5里面弄個彈幕留言,看起來就高大上有木有啊,以前的靜態留言形勢都太古板啦,彈幕才能用戶high起來啊!好吧,本來說這玩意已經比較成熟了,找輪子,結果發現貌似沒有html&js版本的,索性就自己寫一個吧,暫時滿足簡單的需求就行。
思路
如下圖所示
主要有3個步驟
1、生成彈幕
根據相應的參數,設置彈幕的字體大小,字體顏色,甚至是彈幕背景(VIP功能,類似QQ聊天氣泡),總之可以根據需求設計各種不同的彈幕樣式。
2、展示彈幕
生成好彈幕我們就要對其進行展示了,目前主流的展示方式有2種,一種是從屏幕的右往左漂移展示,一種是在屏幕的上、中、下進行展示,這里需要考慮到盡量避免彈幕重疊,有效率用屏幕空間,當實在是沒有空間了,才考慮重疊。
3、清除彈幕
在設定時間內顯示完彈幕,為了節約資源,所以要對其進行移除清理,或者對其進行復用。
實現
話不多說,上代碼看注釋
/**
* by Leestar54
* 簡易彈幕插件 V1.0
*/
;
(function($) {
$.fn.danmu = function(options) {
//默認參數
var defaults = {
fontSize: 16,
color: 'black',
showTime: 10000
}
//使用jQuery.extend 覆蓋插件默認參數
var options = $.extend(defaults, options);
var hideTime = defaults.showTime + 500;
//彈幕的行數
var dm_lines = Math.floor($(this).height() / 16);
//當前彈幕行是否有空間進行顯示
var dm_line_empty = Array(dm_lines);
for (var i = 0; i < dm_line_empty.length; i++) {
dm_line_empty[i] = true;
}
//中文和英文的寬度要分別算
function getDivWidth(txt, fontSize) {
var len = 0;
var charRatio = fontSize * 5 / 8;
for (var i = 0; i < txt.length; i++) {
if (txt[i].match(/[^\x00-\xff]/ig) != null) //全角中文
len += fontSize;
else
len += charRatio; //非中文經過測試得出比例
}
return len;
}
//公開函數
this.add = function(txt, id, line, color, fontSize) {
if (txt === undefined || txt === '') {
//拋出異常
throw 'txt should not be null';
}
if (line === undefined || line === '') {
for (var i = 0; i < dm_line_empty.length; i++) {
if (dm_line_empty[i]) {
dm_line_empty[i] = false;
line = i;
break;
}
}
//如果都滿了,就隨機覆蓋了
if (line === undefined || line === '') {
line = Math.floor(Math.random() * dm_lines);
}
}
if (color === undefined || color === '') {
color = defaults.color;
}
if (fontSize === undefined || fontSize === '') {
fontSize = defaults.fontSize;
}
if (id === undefined || id === '') {
id = Math.floor(Math.random() * 100000000);
}
//使用dom構造單個彈幕div
var div = document.createElement("div");
div.id = 'txt_dm' + id;
div.innerHTML = txt;
div.style.webkitAnimation = 'run 1s 8s linear forwards';
div.style.position = 'absolute';
div.style.top = line * fontSize + 'px';
div.style.left = $(this).width() + 'px';
//設置寬度,否則字體會自適應換行,影響顯示
div.style.width = getDivWidth(txt, fontSize) + 'px';
div.style.fontSize = fontSize + 'px';
div.style.color = color;
$(this).append(div);
$(div).velocity({
translateX: '-' + ($(this).width() + $(div).width()) + 'px'
},
defaults.showTime,
'linear');
//屏幕完全顯示完彈幕,就可以添加下一條了, +1為了用一定緩沖距離
var fullShowTime = Math.floor($(div).width() / $(this).width() * defaults.showTime);
setTimeout(function() {
dm_line_empty[line] = true;
}, fullShowTime);
//刪除顯示完的彈幕
setTimeout(function() {
$('#txt_dm' + id).remove();
}, hideTime);
}
return this;
}
})(jQuery);
需要注意的是
- 中文與英文混合的情況下,寬度計算需要注意,經過測試,得出5/8的比例剛剛好,否則不是太長,就是不夠導致換行。
- 使用
var dm_line_empty = Array(dm_lines);
來對每行的彈幕能否顯示做判斷,添加彈幕的時候,哪行空閑就忘哪行添加數據,提高屏幕的利用率。 - 彈幕移動使用的是velocity.js,否則效率太低,在移動設備上卡的飛起。
- 並沒有實現屏幕上中下的彈幕模式……
使用起來非常的簡單,樣式自行設置即可
var dm = $('.container').danmu();
dm.add('123');
dm.add('雙擊66666666666666666');
dm.add('老鐵沒毛病!')
模式
插件是有了,但是應用到業務上還需要我們自己做空置,一般來說有2個模式,供不同場景使用
這里設計彈幕的基礎數據庫格式為,至於擴展和業務字段自行添加。
id:自增
txt:用戶輸入的彈幕
time:用戶提交時的總秒數,進入頁面就開始從0每秒自增,提交時就是當時的總數,如果是視頻,就是當時的播放時間。
type:彈幕模式,可以包括從左往右,從右往左,屏幕上方,中間,下方等等。由於只實現了一種,這個字段我們這里去掉。
可以考慮time,type作為復合索引。
無限循環模式
這應該是大多情況使用的模式,因為H5活動不像看視頻,不可能按照發布時間來展示彈幕,因為也許用戶發布一條評論的時間有20秒,那么豈不是前20秒整個彈幕都空空的?所以我們首要目的就是呈現那種熱鬧勁,一次性的讀取全部彈幕,如果數量過多,可以做一些取舍,循環播放,每隔一定時間,重復顯示彈幕。
//--------------------------------重復展示彈幕----------------------------------------
//重復展示時間間隔,如果彈幕少,就設置小一些,這樣重復的多。
var recycle_time = 10;
var load_recycle_interval;
var recycle_load_timeout;
//重復展示彈幕
function dmRecycleStart(data) {
for (var i = 0; i < data.length; i++) {
var id = data[i]['id'];
var txt = data[i]['txt'];
//閉包&立即執行傳遞參數,返回新的方法,否則add方法會被立即執行
recycle_load_timeout[i] = setTimeout(function(txt, id) {
return function() {
dm.add(txt, id);
};
}(txt, id), 1000 * Math.floor(Math.random() * recycle_time) + 1); //隨機時間點顯示文字
}
}
recycle_load_timeout = Array(data.length);
//先顯示一次
dmRecycleStart(data);
//然后每隔一段時間都展示一次
load_recycle_interval = setInterval(function() {
dmRecycleStart(data);
}, recycle_time * 1000);
時間線模式
這種模式就是和視頻彈幕一樣的模式了,按照用戶發送彈幕的時間進行播放, 我們需要設置個定時器,每一秒顯示一次該時間點的彈幕,每隔一段時間讀取時間段內的所有彈幕。
我這里設計時,為了方便直接調用,服務器做了些格式轉化,預先把消息按照時間點進行分組了,php參考代碼如下:
public function getdm()
{
$start_time = I('get.start_time');
$end_time = I('get.end_time');
$mode = I('get.type');
$db_result = M('dm')->query("select id,txt,ptime from dm where and ptime between %d and %d" and type=%d, $start_time, $end_time, $type);
$ret = array();
//根據時間進行分組
foreach ($db_result as $key => $value) {
$ret[$value['ptime']][] = array($value['id'], $value['txt']);
}
$this->ajaxReturn($ret);
}
JS代碼參考如下:
//---------------------------------時間線彈幕-----------------------------------------
var dm_get_count = 0;
//用戶提交彈幕的時候,時間參數可以以此為基准。
var dm_time = 0;
var load_interval;
var load_timeout;
//加載彈幕
function dmLoadTimeLine() {
//每秒鍾執行一次,更新時間,這里設置為每30秒為間隔顯示一次彈幕(16秒開始預加載),根據時間點投放
load_interval = setInterval(function() {
if (dm_time % 16 == 0) {
var start_time = dm_get_count * 30;
var end_time = (dm_get_count + 1) * 30;
//中間預加載
// $.get('./getdm', {
// start_time: start_time,
// end_time: end_time,
// type: typeid,
// }, function(data) {
dm_pre = line_data;
console.log(start_time + '-' + end_time + '預加載完成');
//第一次加載完成直接顯示
if (dm_get_count == 0) {
dmTimeLineStart();
}
dm_get_count++;
// });
} else if (dm_time % 30 == 0) {
dmTimeLineStart();
}
dm_time++;
}, 1000);
//每秒1個彈幕,30秒則有30個
load_timeout = Array(30);
}
//時間線彈幕
function dmTimeLineStart() {
var start_time = dm_get_count * 30;
var end_time = (dm_get_count + 1) * 30;
console.log(start_time + '-' + end_time + '顯示彈幕');
//深度拷貝數組,如果是從接口獲取數據,最好拷貝一次。
var dm_current = $.extend(true, {}, dm_pre);
//時間點內顯示彈幕
for (var i = start_time; i < end_time; i++) {
if (dm_current[i] != null) {
for (var j = 0; j < dm_current[i].length; j++) {
var id = dm_current[i][j]['id'];
var txt = dm_current[i][j]['txt'];
//內部函數立即執行,閉包傳遞參數,
load_timeout[i] = setTimeout(function(txt, id) {
return function() {
dm.add(txt, id)
};
}(txt, id), 1000 * i + 1); //指定時間點顯示文字
}
}
}
}
停止顯示彈幕
當用戶關閉彈幕時,可以用下列代碼,也說明了上面代碼中一些參數的用處
function dmStop() {
clearInterval(load_interval);
clearInterval(load_recycle_interval);
dm_time = 0;
dm_get_count = 0;
dm_pre = Array();
if (load_timeout != undefined) {
for (var i = 0; i < load_timeout.length; i++) {
clearTimeout(load_timeout[i]);
}
}
if (recycle_load_timeout != undefined) {
for (var i = 0; i < recycle_load_timeout.length; i++) {
clearTimeout(recycle_load_timeout[i]);
}
}
}
最終demo效果如下:
demo地址:
https://github.com/leestar54/h5-demo/blob/master/danmu.html