原生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>发 送</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