前端基于CSS3实现弹幕
基于CSS3动画
- 根据 Google Developer,渲染线程分为 主线程 (main thread) 和 合成线程 (compositor thread)。如果 CSS 动画只是改变 transforms 和 opacity,这时整个 CSS 动画得以在 合成线程 完成(而JS动画则会在 主线程 执行,然后触发合成线程进行下一步操作),在 JS 执行一些昂贵的任务时,主线程繁忙,CSS 动画由于使用了合成线程可以保持流畅
- 在许多情况下,也可以由合成线程来处理 transforms 和 opacity 属性值的更改
- 对于帧速表现不好的低版本浏览器,CSS3可以做到自然降级,而JS则需要撰写额外代码
- CSS动画有天然事件支持(TransitionEnd、AnimationEnd,但是它们都需要针对浏览器加前缀),JS则需要自己写事件
- 如果有任何动画触发绘画,布局或两者,则需要 “主线程” 才能完成工作。 这对于基于 CSS 和 JavaScript 的动画都是如此,布局或绘制的开销可能会使与 CSS 或 JavaScript 执行相关的任何工作相形见绌,这使得问题没有实际意义
选择原因参考自:点点点
弹幕可配置内容大小颜色
基于sass变量实现
弹幕可自定义弹幕内容
弹幕可插入图片
原理同上
弹幕可配置弹道数量、自适应播放器高度
可配置、但不可超过容器高度;默认填充满屏幕
默认取值方式:容器高度/每行弹道高度 (向下取整 ,下方代码可查看 barrageNum 变量相关)
弹幕可配置弹幕速度
实现思想:将弹道分解为栅格,计算格子数量:播放器宽度/字体宽度 = 格子数量;barrageSpeed 变量控制的是一个文本走一个格子需要的时间;
由此得出: ( 此处提出的为实现思想,内容详细的秒/毫秒记得转换哦,单位不同意容易出问题 )
所需动画时间 = (内容实际宽度 + 容器宽度)/ 字体大小 * 走一个格子需要的时间
弹幕执行完时间(用于销毁弹幕DOM)=所需动画时间 + 实际发送弹幕时间
下次可向弹道发送消息时间(避免弹幕堆叠) = 时间戳 +(内容实际宽度 + 内外边距)/ 字体大小 * 走一个格子需要的时间 )
下方代码可查看 barrageSpeed 变量相关,手动的修改一下值看下效果 更直观
弹幕可配置弹道密度、弹道间距
基于sass动态配置变量
弹幕可配置弹幕开关
实现机制很多 最简单的就是直接v-if的你的弹幕容器
弹幕已处理弹幕内容堆叠问题 (计算发送机制)
配置弹幕速度处以说明解决机制
先看效果
弹幕视频
直接上代码!!!!CV走即可
基于 Vue3 SASS
HTML代码处
<!-- 播放器容器 -->
<div class="anchorVideo" id="anchorVideoContent" >
<!-- 视频 -->
<video class="anchorVideo w100" controls style="height:300px" id="anchorVideo"></video>
<!-- 弹幕 -->
<div class="anchor-barrage">
<!-- 直播弹道 -->
<ul class="barrage-trajectory">
<li v-for="(a,index) in barrageNum">
<template v-if="trajectoryData[index]">
<p
v-for="item of trajectoryData[index]"
v-autoDestroy="{item,$Index:index}"
:style="{
'--animationTime':item.animationTime,
'--msgWidth':item.msgWidth,
color:item.testColor
}"
:key="item.customKey"
:customKey="item.customKey"
>
{{item.content.content}}
</p>
</template>
</li>
</ul>
</div>
</div>
CSS代码
.anchorVideo {
width: 100%;
position: relative;
.anchor-barrage{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #dd818178;
overflow: hidden;
@keyframes scrollTo {
0% {
}
100% {
right: 100%;
// transform: translateX(-100%);
}
}
.barrage-trajectory{
max-height: 100%;
width: 100%;
box-sizing: border-box;
padding: 12px 0;
& > li {
width: 100%;
height: v-bind('barrageHeight');
display: flex;
justify-content: start;
position: relative;
& > p{
position: absolute;;
min-width: var(--msgWidth);
font-size: v-bind('barrageFontSize');
display: flex;
align-items: center;;
color:blue;
// transform: translateX(-100%);
white-space: nowrap;
text-shadow: 2px 2px 3px rgb(248, 81, 20); // 文字阴影
margin-right: v-bind('barrageGap');
right: calc(1px - var(--msgWidth) - v-bind('barrageGap'));
animation: scrollTo linear var(--animationTime) 1; //动画
animation-fill-mode: forwards;
animation-timing-function:linear;
}
}
}
}
}
script代码
//#region 弹道变量声明
// 弹道数据
const trajectoryData = reactive({})
// 直播容器宽度
const videoWidth = ref('')
// 直播容器高度
const videoHeight = ref('')
// 弹道数量
const barrageNum = ref(0)
// 弹道高度 ( 一行多高 )
const barrageHeight = ref('32px')
// 弹幕字体大小
const barrageFontSize = ref('24px')
// 弹幕速度
// const barrageSpeed = ref('5s')
const barrageSpeed = ref(0.1)
// 弹幕间距
const barrageGap = ref('32px')
// 弹幕ID
const barrageId = ref(0)
//#endregion
//#region 弹幕相关
function initBarrage(){
// 计算容器高度
videoWidth.value = document.getElementById('anchorVideoContent').clientWidth + 'px'
videoHeight.value = document.getElementById('anchorVideoContent').clientHeight - 32/* 减去padding */ + 'px'
// 求出最大弹道数量
barrageNum.value = parseInt(parseInt(videoHeight.value) / parseInt(barrageHeight.value))
if(Object.keys(trajectoryData).length > 0){
deleteData(1)
function deleteData(i){
if(trajectoryData[i] && barrageNum.value<i){
delete trajectoryData[i]
deleteData(i++)
}
}
}else{
// 初始化弹道数据
for(let i = 1;i <= barrageNum.value;i++){ trajectoryData[i] = []}
}
}
// 弹幕发送
function sendBarrage(msgs){
if(!Array.isArray(msgs))return;
for(let i = 1;i <= barrageNum.value;i++){
if(msgs.length == 0)return;
let msg = msgs[0]
// 计算消息长度
let msgLength = msgs[0].content.content.length
let chinaText = (msgs[0].content.content || '').match(/[\u4e00-\u9fa5]/g) || ''
msgLength = (msgLength-chinaText.length)/2 + chinaText.length
// 本条弹幕的总长度
let currentMsgLength = parseInt(barrageGap.value) + msgLength*parseInt(barrageFontSize.value)
// 计算动画时间
let animationTime = ((currentMsgLength + parseInt(videoWidth.value))/parseInt(barrageFontSize.value) * barrageSpeed.value).toFixed(2);
msg = {
...msg,
animationTime:animationTime+'s',
msgWidth:msgLength * parseInt(barrageFontSize.value) + 'px',// 弹幕宽度
}
let nextSendTime = 0
if(trajectoryData[i].length == 0 || trajectoryData[i].at(-1).nextSendTime < new Date().getTime()){
// 下次可发送弹幕时间
nextSendTime = ((currentMsgLength/parseInt(barrageFontSize.value)) * barrageSpeed.value).toFixed(2)*1000
msg.nextSendTime = new Date().getTime() + nextSendTime
// 可销毁时间
msg.destroyTime = new Date().getTime() + parseInt(animationTime)*1000
msg.customKey = 'customKey' + barrageId.value++
msg.testColor = getRandomColor()
trajectoryData[i].push(msg)
utilMsg()
return sendBarrage(msgs)
}
// 若循环后仍无可用弹道
if(i == barrageNum.value){
return setTimeout(()=>{
sendBarrage(msgs)
},500)
}
//
}
// 删除一条
function utilMsg(){ msgs.splice(0,1) }
}
// 定时清理弹道数据(也可自定义指令实现清理弹道数据方法、后续完善补充上)
function clearTData(){
for(let i = 1;i <= barrageNum.value;i++){
trajectoryData[i] = trajectoryData[i].filter(item=>{
return item.destroyTime > new Date().getTime()
})
}
setTimeout(()=>{
clearTData()
},3000 )
}
/**
* 获取随机颜色 十六进制
*/
function getRandomColor(){
return '#' + Math.floor( Math.random() * 0xffffff ).toString(16)
}
//#endregion
// 初始化弹幕
initBarrage()
//#region 页面事件监听 (有需要就加上该事件监听 无需要就删除)
window.addEventListener("resize",resizeScreen)
// 监听屏幕缩放
function resizeScreen(){
if(window.resizeScreenTimer)clearTimeout(window.resizeScreenTimer);
window.resizeScreenTimer = setTimeout(()=>{
initBarrage()
},200)
}
//#endregion
调用(可手动调用/监听回调)
// 发送弹幕 (弹幕目前传参格式如下代码所示、也可根据自己需求修改格式简化/添加新字段)
sendBarrage([
{
content:{
"content": "这是要发的消息内容",
"extra": "",
"mentionedInfo": {
"userIdList": [],
"type": 1,
"mentionedContent": ""
}
}
},
{
content:{
"content": "这也是消息内容哦",
"extra": "",
"mentionedInfo": {
"userIdList": [],
"type": 1,
"mentionedContent": ""
}
}
},
])