1.代碼如下
<template>
??<div?class="home">
????<h2?style="?text-align:?center;">Vue-barrage?基于vue的彈幕組件</h2>
????<div
??????style="height:400px;width:800px;?position:?relative;margin:0px?auto;background:#000;"
????>
??????<!--?<video?controls
?????????????src="@/assets/1.mp4"
?????????????autoplay
??????style="width:100%;height:100%;z-index:2;"?/>-->
??????<!--?確保父元素是相對定位,彈幕容器是絕對定位?-->
??????<v-barrage?:arr="arr"?:isPause="isPause"?:percent="100"></v-barrage>
????</div>
????<div?class="barrage-control">
??????<input
????????type="text"
????????v-model="sendContent"
????????placeholder="回車發(fā)送"
????????id="sendContent"
????????@keyup.enter="sendBarrage"
??????/>
??????方向:
??????<select?style="margin:0px?12px;"?v-model="direction">
????????<option?value="default">默認</option>
????????<option?value="top">頂部</option>
??????</select>
??????<input?type="checkbox"?v-model="isJs"?/>?js彈幕(直接寫代碼)
??????<button
????????id="sendBarrageBtn"
????????style="margin-left:25px;"
????????@click="sendBarrage"
??????>
????????發(fā)送
??????</button>
??????<button?id="pauseBtn"?@click="isPause?=?true">暫停</button>
??????<button?id="startBtn"?@click="isPause?=?false">開始</button>
????</div>
??</div>
</template>
<script>
//?@?is?an?alias?to?/src
import?VBarrage?from?"../components/index";
export?default?{
??name:?"home",
??components:?{
????VBarrage,
??},
??data()?{
????return?{
??????arr:?[],
??????isPause:?false,
??????sendContent:?null,
??????isJs:?false,
??????direction:?"default",
????};
??},
??mounted()?{
????this.initTestData();
??},
??methods:?{
????//?初始化模擬彈幕數(shù)據
????initTestData()?{
??????let?arr?=?["這是一條有彈幕",?"今天去打LOL",?"可以嗎辆琅?",?"一起嗨8囱Nò馈疤苹!"];
??????for?(let?i?=?0;?i?<?6;?i++)?{
????????for?(let?index?=?0;?index?<?1000;?index++)?{
??????????if?(index?%?2?==?0)?{
????????????this.arr.push({
??????????????direction:?"top",
??????????????content:?arr[parseInt(Math.random()?*?arr.length)],
????????????});
??????????}?else?{
????????????this.arr.push({
??????????????direction:?"default",
??????????????content:?arr[parseInt(Math.random()?*?arr.length)],
????????????});
??????????}
????????}
??????}
????},
????//?發(fā)送彈幕
????sendBarrage()?{
??????if?(
????????this.arr.length?>?1?&&
????????this.sendContent?!=?""?&&
????????this.sendContent?!=?null
??????)?{
????????this.arr.unshift({
??????????content:?this.sendContent,
??????????direction:?this.direction,
??????????isSelf:?true,
??????????style:?{
????????????color:?"red",
????????????fontSize:?"25px",
??????????},
??????????isJs:?this.isJs,
????????});
??????}?else?{
????????this.arr.push({
??????????content:?this.sendContent,
??????????direction:?this.direction,
??????????isSelf:?true,
??????????style:?{
????????????color:?"red",
??????????},
??????????isJs:?this.isJs,
????????});
??????}
??????this.sendContent?=?null;
????},
??},
};
</script>
<style?lang="scss"?scoped>
.barrage-control?{
??text-align:?center;
??margin:?10px?0px;
}
</style>
2.在component 里面創(chuàng)建 index.vue 代碼如下
<template>
??<section?class="barrage-wrapper"
???????????ref="barrageWrapper"
???????????:style="{height:percent+'%'}">
????<div?class="barrage-main">
??????<div?class="barrage-main-dm"
???????????:class="{'ani-pause':isPause,'ani-running':!isPause}"
???????????ref="barrageMainDm"></div>
????</div>
??</section>
</template>
<script>
export?default?{
??props:?{
????//?彈幕源數(shù)組
????arr:?{
??????type:?Array,
??????default:?function?()?{
????????return?[]
??????}
????},
????//?彈幕是否暫停狀態(tài)
????isPause:?{
??????type:?Boolean
????},
????//?彈幕占比
????percent:?{
??????type:?Number,
??????default:?80
????}
??},
??data?()?{
????return?{
??????//?每行彈幕數(shù)最大值
??????MAX_DM_COUNT:?5,
??????//?行數(shù)
??????CHANNEL_COUNT:?8,
??????//?彈幕數(shù)組
??????barrages:?[],
??????//?dom池
??????domPool:?[],
??????//?intervalDM
??????intervalDM:?null,
??????//?取彈幕時間間隔
??????interValTime:?500,
??????//?滾動彈幕的通道
??????hasPosition:?[],
??????//?頂部彈幕的通道
??????hasTopPosition:?[],
??????//?彈幕容器
??????barrageMainDm:?null,
??????//?彈幕容器寬度
??????barMainWidth:?500,
??????//?自定義彈幕樣式屬性列表
??????dmStyles:?[
????????'color',
????????'fontSize'
??????]
????};
??},
??computed:?{
??},
??created?()?{
??},
??mounted?()?{
????this.barrageMainDm?=?this.$refs.barrageMainDm;
????//?緩存容器寬度
????this.barMainWidth?=?this.barrageMainDm.clientWidth;
????//?初始化彈幕dom組
????this.init();
????//?開始播放彈幕
????this.playDm();
????//?注冊頁面監(jiān)聽器
????document.addEventListener("visibilitychange",?this.visibilitychangeFn);
??},
??watch:?{
????arr?(list)?{
??????this.barrages?=?list;
????},
????isPause?(val)?{
??????if?(val)?{
????????this.pauseDm();
??????}?else?{
????????this.playDm();
??????}
????},
????percent?(val)?{
??????this.CHANNEL_COUNT?=?val?/?10;
????}
??},
??methods:?{
????visibilitychangeFn?()?{
??????if?(!document.hidden)?{
????????//處于當前頁面
????????this.playDm();
????????console.log('進入頁面');
??????}?else?{
????????console.log('離開頁面');
????????clearInterval(this.intervalDM);
????????this.intervalDM?=?null;
??????}
????},
????init?()?{
??????let?wrapper?=?this.$refs.barrageMainDm;
??????//?先new一些div?重復利用這些DOM
??????for?(let?j?=?0;?j?<?this.CHANNEL_COUNT;?j++)?{
????????let?doms?=?[];
????????for?(let?i?=?0;?i?<?this.MAX_DM_COUNT;?i++)?{
??????????//?初始化dom
??????????let?dom?=?document.createElement('div');
??????????//?初始化dom的位置?通過設置className
??????????dom.className?=?'barrage-item';
??????????dom.style.transform?=?`translate3d(${this.barMainWidth}px,0,0)`
??????????//?DOM的通道是固定的?所以設置好top就不需要再改變了
??????????dom.style.top?=?j?*?(this.barrageMainDm.clientHeight?/?this.CHANNEL_COUNT)?+?'px';
??????????console.log(dom.style.top)
??????????//?每次到animationend結束的時候?就是彈幕劃出屏幕了?將DOM位置重置?再放回DOM池
??????????dom.addEventListener('animationend',?(e)?=>?{
????????????//?初始化dom樣式
????????????dom.className?=?'barrage-item';
????????????dom.style.transform?=?`translate3d(${this.barMainWidth}px,0,0)`
????????????dom.style.animation?=?null;
????????????//?清空自定義樣式
????????????this.dmStyles.forEach(key?=>?{
??????????????dom.style[key]?=?null;
????????????})
????????????//?this.domPool[j].push({?el:?dom?});
????????????//?動畫結束后設置當前dom為空閑狀態(tài)
????????????//?console.log(i,?j)
????????????if?(this.domPool[i][j])?{
??????????????this.domPool[i][j].isFree?=?false;
????????????}
????????????//?console.log(dom)
??????????});
??????????//?放入該通道的DOM池
??????????doms.push({
????????????row:?j,
????????????col:?i,
????????????el:?dom,
????????????isUse:?false,
????????????isFree:?false
??????????});
????????}
????????this.domPool.push(doms);
??????}
??????//?hasPosition?標記每個通道目前是否有位置
??????for?(let?i?=?0;?i?<?this.CHANNEL_COUNT;?i++)?{
????????this.hasPosition[i]?=?true;
????????this.hasTopPosition[i]?=?true;
??????}
????},
????/**
?????*?獲取一個可以發(fā)射彈幕的通道?沒有則返回-1
?????*/
????getChannel?()?{
??????for?(let?i?=?0;?i?<?this.CHANNEL_COUNT;?i++)?{
????????if?(this.hasPosition[i]?&&?this.domPool[i].length)?return?i;
??????}
??????return?-1;
????},
????/**
????*?獲取一個可以發(fā)射頂部彈幕的通道?沒有則返回-1
????*/
????getTopChannel?()?{
??????for?(let?i?=?0;?i?<?this.CHANNEL_COUNT;?i++)?{
????????if?(this.hasTopPosition[i])?return?i;
??????}
??????return?-1;
????},
????/**
????*?根據DOM和彈幕信息?發(fā)射彈幕
????*/
????shootDanmu?(domItem,?dmItem,?channel)?{
??????//?設置當前通道為false
??????this.hasPosition[channel]?=?false;
??????//獲取dom
??????let?dom?=?domItem.el;
??????//?是否已經使用的dom
??????if?(!domItem.isUse?&&?this.barrageMainDm)?{
????????domItem.isUse?=?true;
????????this.barrageMainDm.appendChild(domItem.el);
??????}
??????//?判斷是否有自定義樣式
??????if?(dmItem.style)?{
????????let?keys?=?Object.keys(dmItem.style);
????????let?self?=?this;
????????keys.forEach(key?=>?{
??????????//?檢查樣式屬性是否合法
??????????let?isStyle?=?self.dmStyles.includes(key)
??????????if?(isStyle)?{
????????????dom.style[key]?=?dmItem.style[key];
??????????}
????????});
??????}
??????domItem.isFree?=?true;
??????//?console.log('biu~?['?+?dmItem.content?+?']');
??????//?dom.innerText?=?dmItem.content;
??????//?判斷是否是js彈幕
??????if?(dmItem.isJs)?{
????????dom.innerHTML?=?dmItem.content;
??????}?else?{
????????dom.innerText?=?dmItem.content;
??????}
??????//?設置彈幕的位置信息?性能優(yōu)化?left?->?transform
??????//?dom.style.transform?=?`translateX(${-dom.clientWidth}px)`;
??????//?dom.style.transform?=?`translate3d(${-dom.clientWidth}px,0,0)`;?//?css硬件加速
??????dom.className?=?dmItem.isSelf???'barrage-item?self-dm'?:?'barrage-item';
??????dom.style.animation?=?'barrage-run?5s?linear';
??????//?彈幕全部顯示之后?才能開始下一條彈幕
??????//?大概?dom.clientWidth?*?10?的時間?該條彈幕就從右邊全部劃出到可見區(qū)域?再加1秒保證彈幕之間距離
??????setTimeout(()?=>?{
????????this.hasPosition[channel]?=?true;
??????},?dom.clientWidth?*?10?+?1000);
????},
????shootTopDanmu?(dmItem,?channel)?{
??????//?設置當前通道為false
??????this.hasTopPosition[channel]?=?false;
??????//獲取dom
??????let?dom?=?document.createElement('div');
??????dom.className?=?dmItem.isSelf???'barrage-item?top-item?self-dm'?:?'barrage-item?top-item';
??????//?判斷是否有自定義樣式
??????if?(dmItem.style)?{
????????let?keys?=?Object.keys(dmItem.style);
????????let?self?=?this;
????????keys.forEach(key?=>?{
??????????//?檢查樣式屬性是否合法
??????????let?isStyle?=?self.dmStyles.includes(key)
??????????if?(isStyle)?{
????????????dom.style[key]?=?dmItem.style[key];
??????????}
????????});
??????}
??????//?判斷是否是js彈幕
??????if?(dmItem.isJs)?{
????????dom.innerHTML?=?dmItem.content;
??????}?else?{
????????dom.innerText?=?dmItem.content;
??????}
??????dom.addEventListener('animationend',?()?=>?{
????????this.barrageMainDm.removeChild(dom)
????????this.hasTopPosition[channel]?=?true;
??????})
??????this.barrageMainDm.appendChild(dom);?//?一定要在獲取寬度和執(zhí)行動畫之前渲染dom
??????dom.style.transform?=?`translate3d(${this.barMainWidth?/?2?-?(dom.clientWidth?/?2)}px,${channel?*?dom.clientHeight}px,0)`
??????dom.style.animation?=?'barrage-fade?3s';
????},
????//?獲取空閑通道中空閑的dom
????getFreeChannelDom?(channel)?{
??????let?item;
??????item?=?this.domPool[channel].find(it?=>?!it.isFree)
??????return?item
????},
????//?暫停彈幕
????pauseDm?()?{
??????if?(this.intervalDM)?{
????????clearInterval(this.intervalDM)
????????this.intervalDM?=?null;
??????}
????},
????//?播放彈幕
????playDm?()?{
??????//?每隔1ms從彈幕池里獲取彈幕(如果有的話)并發(fā)射
??????let?self?=?this;?//?這里取一個self?this?為了方便調試的時候看到this具體內容
??????self.intervalDM?=?setInterval(()?=>?{
????????//?let?channel;
????????//?if?(self.barrages.length?&&?(channel?=?self.getChannel())?!=?-1)?{
????????//???let?domItem?=?self.getFreeChannelDom(channel);
????????//???//?let?domItem?=?self.domPool[channel].shift();
????????//???if?(domItem)?{
????????//?????let?danmu?=?self.barrages.shift();
????????//?????self.shootDanmu(domItem,?danmu,?channel);
????????//???}
????????//?}
????????//?更新邏輯
????????if?(self.barrages.length?>?0)?{
??????????let?channel;
??????????let?topChannel;
??????????let?barrage?=?self.barrages.shift();
??????????if?(barrage.direction?==?'top'?&&?(topChannel?=?self.getTopChannel())?!=?-1)?{
????????????//?top....
????????????self.shootTopDanmu(barrage,?topChannel);
??????????}
??????????if?(barrage.direction?==?'default'?&&?(channel?=?self.getChannel())?!=?-1)?{
????????????//?default....
????????????let?domItem?=?self.getFreeChannelDom(channel);
????????????if?(domItem)?{
??????????????//?let?danmu?=?self.barrages.shift();
??????????????self.shootDanmu(domItem,?barrage,?channel);
????????????}
??????????}
????????}
??????},?self.interValTime);
????}
??},
??components:?{
??},
};
</script>
<style??lang="scss">
.barrage-wrapper?{
??*?{
????margin:?0px;
????padding:?0px;
??}
??z-index:?1;
??position:?absolute;
??left:?0px;
??top:?0px;
??width:?100%;
??height:?100%;
??.barrage-item?{
????z-index:?99;
????position:?absolute;
????//?left:?0px;
????//?top:?0px;
????//?transform:?translateX(-100%);
????//?padding:?5px?0px;
????user-select:?none;?//?禁用選擇文字
????position:?absolute;
????white-space:?pre;
????cursor:?pointer;
????//?pointer-events:?none;
????//?perspective:?500px;
????display:?inline-block;
????will-change:?transform;
????font-size:?25px;
????color:?rgb(255,?255,?255);
????font-family:?SimHei,?"Microsoft?JhengHei",?Arial,?Helvetica,?sans-serif;
????font-weight:?bold;
????line-height:?1.125;
????opacity:?1;
????text-shadow:?rgb(0,?0,?0)?1px?0px?1px,?rgb(0,?0,?0)?0px?1px?1px,
??????rgb(0,?0,?0)?0px?-1px?1px,?rgb(0,?0,?0)?-1px?0px?1px;
????&:hover?{
??????color:?red;
??????animation-play-state:?paused?!important;
??????z-index:?150;
????}
??}
??.top-item?{
????z-index:?100;
??}
??.barrage-main?{
????/*?border:?2px?solid?blue;?*/
????width:?100%;
????height:?100%;
????position:?relative;
????overflow:?hidden;
????//?background:?#000;
??}
??.barrage-main-dm?{
????position:?absolute;
????width:?100%;
????height:?100%;
????left:?0;
????top:?0;
??}
}
.self-dm?{
??border:?2px?solid?#87ceeb;
??box-sizing:?border-box;
}
@keyframes?barrage-run?{
??0%?{
????//?transform:?translate3d(500px,?0,?0);
??}
??100%?{
????transform:?translate3d(-100%,?0,?0);
??}
}
@keyframes?barrage-fade?{
??0%?{
????visibility:?visible;
????//?opacity:?1;
??}
??100%?{
????visibility:?hidden;
????//?opacity:?0;
??}
}
.ani-pause?{
??&?div?{
????animation-play-state:?paused?!important;
??}
}
.ani-running?{
??&?div?{
????animation-play-state:?running?!important;
??}
}
</style>