• 前端、vue、Vue3弹幕实现;前端CSS实现弹幕


    前端基于CSS3实现弹幕

    基于CSS3动画
    1. 根据 Google Developer,渲染线程分为 主线程 (main thread) 和 合成线程 (compositor thread)。如果 CSS 动画只是改变 transforms 和 opacity,这时整个 CSS 动画得以在 合成线程 完成(而JS动画则会在 主线程 执行,然后触发合成线程进行下一步操作),在 JS 执行一些昂贵的任务时,主线程繁忙,CSS 动画由于使用了合成线程可以保持流畅
    2. 在许多情况下,也可以由合成线程来处理 transforms 和 opacity 属性值的更改
    3. 对于帧速表现不好的低版本浏览器,CSS3可以做到自然降级,而JS则需要撰写额外代码
    4. CSS动画有天然事件支持(TransitionEnd、AnimationEnd,但是它们都需要针对浏览器加前缀),JS则需要自己写事件
    5. 如果有任何动画触发绘画,布局或两者,则需要 “主线程” 才能完成工作。 这对于基于 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>  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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; 
                }
              }
            }
          }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124

    调用(可手动调用/监听回调)

       // 发送弹幕 (弹幕目前传参格式如下代码所示、也可根据自己需求修改格式简化/添加新字段)
       sendBarrage([
        {
             content:{
                "content": "这是要发的消息内容",
                "extra": "",
                "mentionedInfo": {
                    "userIdList": [],
                    "type": 1,
                    "mentionedContent": ""
                }
            }
        },
        {
             content:{
                "content": "这也是消息内容哦",
                "extra": "",
                "mentionedInfo": {
                    "userIdList": [],
                    "type": 1,
                    "mentionedContent": ""
                }
            }
        },
    ])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
  • 相关阅读:
    1python模块和库
    ACM MM 2023 | 基于去中心化表征的人体姿态估计方法
    PG数据库内核源码分析——执行计划EXPLAIN
    LeetCode每日一题——522. 最长特殊序列 II
    k8s configMap挂载(项目配置文件放到configMap中,不同环境不同配置)
    【python】算法与数据结构作业记录分析
    微服务和注册中心
    JS逆向之常见编码和混淆加密方式(二)
    产品思维训练 | 如何让更多人用支付宝点外卖?
    Aeraki Mesh 正式成为CNCF沙箱项目,腾讯云携手合作伙伴加速服务网格成熟商用
  • 原文地址:https://blog.csdn.net/lys20000913/article/details/127976370