原生JS實現彈幕效果


純屬無聊寫的,可能有很多問題,歡迎批評指教。

 

效果圖:圖一是預設的一些彈幕,圖二是自己發射的彈幕,效果是一樣的。demo地址

 

 

首先是彈幕的位置,是要從最右滑到最左,為了防止隨機高度彈幕會覆蓋的問題,設置了通道。

每一個通道是從左到右的一條,高度固定,這樣不同通道的彈幕不會相互覆蓋。

 

彈幕滑動就是簡單設置CSS屬性  transition 實現。開始使用 left 改變彈幕的位置,后來改為 transform ,性能確實提高很多。

 

設置10條彈幕通道,每個通道有一個DOM池,每一次發射彈幕就從DOM池中拿出一個DOM從右滑到左邊直到消失,然后再放回DOM池,當DOM池為空時就不能再通過該通道發射彈幕了,通過這種方式來限制最大同屏彈幕數。

 

因為通過 transition 設置了彈幕滑動的時間,而這個時間固定的,距離彈幕最左露頭到最右消失,也就是“屏幕寬度+彈幕長度”,所以: 彈幕越長,速度越快 。這樣的話,后面特別長的彈幕就有可能超過前面比較短的彈幕,本來根據彈幕長度設置了滑動時間,但是跑去看了下B站彈幕也有這個屬性,所以就又改回去了>_<

 

最后設置一個彈幕池,設置一個定時器不停的去彈幕池拿彈幕,當DOM空閑且有未發射彈幕時就發射彈幕。

 

點擊發送按鈕就是把彈幕放到彈幕池就好了。

 

其實我寫的挺簡單的,又說了好多廢話,代碼里我都加了注釋。這我是第一次寫JS全部加了分號哦!表揚下自己。

 

完整代碼:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>原生JS實現彈幕效果</title>
  <style>
    #wrapper {
      height: 400px;
      width: 700px;
      position: relative;
      overflow: hidden;
      background: url(http://www.drama-asia.se/wp-content/uploads/2016/06/14375197_1349947520504_800x600.jpg);
      
      color: #ffffff82;
      font-size: 14px;
      text-shadow: 1px 1px #000;
    }
    .right {
      position: absolute;
      visibility: hidden;
      white-space: nowrap;
      /*left: 700px;*/
      transform: translateX(700px);
    }
    .left {
      position: absolute;
      white-space: nowrap;
      user-select: none;
      transition: transform 7s linear; /* 時間相同 越長的彈幕滑動距離越長 所以越快~ */
    }
    input {
      position: absolute;
      bottom: 10px;
      left: 150px;
      width: 300px;
      height: 26px;
    }

    button {
      position: absolute;
      bottom: 8px;
      left: 476px;
      width: 100px;
      height: 38px;
      border-radius: 10px;
      font-size: 16px;
    }
  </style>
</head>
<body>
<div id="wrapper">
  <input type="text">
  <button>&nbsp;&nbsp;</button>
</div>
<script>
/**
 * 設置 彈幕DOM池 每一個通道最多六條彈幕
**/

const MAX_DM_COUNT = 6;
const CHANNEL_COUNT = 10;

let domPool = [];
let danmuPool = [
  '前方大量彈幕來襲,請做好准備!', '2333333', '2333333', '2333333', '2333333', '2333333', 
  '潯陽江頭夜送客, 楓葉荻花秋瑟瑟',  '2333333', '2333333', '2333333', '2333333', '2333333', '2333333',
  '主人下馬客在船, 舉酒欲飲無管弦。', '醉不成歡慘將別, 別時茫茫江浸月', '忽聞水上琵琶聲, 主人忘歸客不發。', 
  '尋聲暗問彈者誰? 琵琶聲停欲語遲。', '移船相近邀相見, 添酒回燈重開宴。', '千呼萬喚始出來, 猶抱琵琶半遮面。',
  '轉軸撥弦三兩聲, 未成曲調先有情。', '弦弦掩抑聲聲思, 似訴平生不得志。', '低眉信手續續彈, 說盡心中無限事。', 
  '輕攏慢捻抹復挑, 初為霓裳后六幺。', '大弦嘈嘈如急雨, 小弦切切如私語。', '嘈嘈切切錯雜彈, 大珠小珠落玉盤。', 
  '間關鶯語花底滑, 幽咽泉流冰下難。', '冰泉冷澀弦凝絕, 凝絕不通聲暫歇。', '別有幽愁暗恨生, 此時無聲勝有聲。', 
  '銀瓶乍破水漿迸, 鐵騎突出刀槍鳴。', '曲終收撥當心畫, 四弦一聲如裂帛。', '東船西舫悄無言, 唯見江心秋月白。', 
  '沉吟放撥插弦中, 整頓衣裳起斂容。', '自言本是京城女, 家在蝦蟆陵下住。', '十三學得琵琶成, 名屬教坊第一部。', 
  '曲罷曾教善才服, 妝成每被秋娘妒。', '五陵年少爭纏頭, 一曲紅綃不知數。', '鈿頭銀篦擊節碎, 血色羅裙翻酒污。', 
  '今年歡笑復明年, 秋月春風等閑度。', '弟走從軍阿姨死, 暮去朝來顏色故。', '門前冷落鞍馬稀, 老大嫁作商人婦。', 
  '商人重利輕別離, 前月浮梁買茶去。', '去來江口守空船, 繞船月明江水寒。', '夜深忽夢少年事, 夢啼妝淚紅闌干。',
  '我聞琵琶已嘆息, 又聞此語重唧唧。', '同是天涯淪落人, 相逢何必曾相識!', '我從去年辭帝京, 謫居卧病潯陽城。',
  '潯陽地僻無音樂, 終歲不聞絲竹聲。', '住近湓江地低濕, 黃蘆苦竹繞宅生。', '其間旦暮聞何物? 杜鵑啼血猿哀鳴。',
  '春江花朝秋月夜, 往往取酒還獨傾。', '豈無山歌與村笛? 嘔啞嘲哳難為聽。', '今夜聞君琵琶語, 如聽仙樂耳暫明。',
  '莫辭更坐彈一曲, 為君翻作《琵琶行》。', '感我此言良久立, 卻坐促弦弦轉急。', '凄凄不似向前聲, 滿座重聞皆掩泣。',
  '座中泣下誰最多? 江州司馬青衫濕。'
];
let hasPosition = [];

/**
 * 做一下初始化工作
 */
function init() {
  let wrapper = document.getElementById('wrapper')
  // 先new一些span 重復利用這些DOM
  for (let j = 0; j < CHANNEL_COUNT; j++) {
    let doms = [];
    for (let i = 0; i < MAX_DM_COUNT; i++) {
      // 要全部放進wrapper
      let dom = document.createElement('span');
      wrapper.appendChild(dom);
      // 初始化dom的位置 通過設置className
      dom.className = 'right';
      // DOM的通道是固定的 所以設置好top就不需要再改變了
      dom.style.top = j * 20 + 'px';
      // 放入改通道的DOM池
      doms.push(dom);
      // 每次到transition結束的時候 就是彈幕划出屏幕了 將DOM位置重置 再放回DOM池
      dom.addEventListener('transitionend', () => {
        dom.className = 'right';
        // dom.style.transition = null;
        // dom.style.left = null;
        dom.style.transform = null;

        domPool[j].push(dom);
      });
    }
    domPool.push(doms);
  }
  // hasPosition 標記每個通道目前是否有位置
  for (let i = 0; i < CHANNEL_COUNT; i++) {
    hasPosition[i] = true;
  }
}

/**
 * 獲取一個可以發射彈幕的通道 沒有則返回-1
 */
function getChannel() {
  for (let i = 0; i < CHANNEL_COUNT; i++) {
    if (hasPosition[i] && domPool[i].length) return i;
  }
  return -1;
}

/**
 * 根據DOM和彈幕信息 發射彈幕
 */
function shootDanmu(dom, text, channel) {
  console.log('biu~ [' + text + ']');
  dom.innerText = text;
  // 如果為每個彈幕設置 transition 可以保證每個彈幕的速度相同 這里沒有保證速度相同
  // dom.style.transition = `transform ${7 + dom.clientWidth / 100}s linear`;

  // dom.style.left = '-' + dom.clientWidth + 'px';
  // 設置彈幕的位置信息 性能優化 left -> transform
  dom.style.transform = `translateX(${-dom.clientWidth}px)`;
  dom.className = 'left';
  
  hasPosition[channel] = false;
  // 彈幕全部顯示之后 才能開始下一條彈幕
  // 大概 dom.clientWidth * 10 的時間 該條彈幕就從右邊全部划出到可見區域 再加1秒保證彈幕之間距離
  setTimeout(() => {
    hasPosition[channel] = true;
  }, dom.clientWidth * 10 + 1000);
}

window.onload = function() {

  init();

  // 為input和button添加事件監聽
  let btn = document.getElementsByTagName('button')[0];
  let input = document.getElementsByTagName('input')[0];
  btn.addEventListener('click', () => {
    input.value = input.value.trim();
    if (input.value) danmuPool.push(input.value);
  })
  input.addEventListener('keyup', (e) => {
    if (e.key === 'Enter' && (input.value = input.value.trim())) {
      danmuPool.push(input.value);
    }
  })
  // 每隔1ms從彈幕池里獲取彈幕(如果有的話)並發射
  setInterval(() => {
    let channel;
    if (danmuPool.length && (channel = getChannel()) != -1) {
      let dom = domPool[channel].shift();
      let danmu = danmuPool.shift();
      shootDanmu(dom, danmu, channel);
    }
  }, 1);

}
 
</script>
</body>
</html>

 

最后加一個 transform 和 left 的性能圖對比:

 

transform

left

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM