• 原生Canvas循环滚动弹幕(现金红包活动带头像弹幕)


    效果

    gif有些糊,可以 在线预览


    实现关键点

    • requestAnimationFrame 循环帧;
    • 绘制单条弹幕,画框子 -> 画头像 -> 写黑色的字 -> 写红色的字, measureText获取文字宽度;
    • 防止弹幕重叠,分行且记录当前行是否可插入,弹幕随机行插入;
    • 弹幕滚出屏幕外时,移除此条弹幕;
    • 循环发射弹幕的实现。

    代码

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>弹幕(头像,文字)</title>
    </head>
    
    <body>
      <canvas id="canvas" style="background: #333;"></canvas>
    </body>
    <script>
      // 圆角矩形
      CanvasRenderingContext2D.prototype.roundRect = function (left, top, width, height, r) {
        const pi = Math.PI;
        this.beginPath();
        this.arc(left + r, top + r, r, -pi, -pi / 2);
        this.arc(left + width - r, top + r, r, -pi / 2, 0);
        this.arc(left + width - r, top + height - r, r, 0, pi / 2);
        this.arc(left + r, top + height - r, r, pi / 2, pi);
        this.closePath();
      }
    
      class Barrage {
        constructor(id) {
          this.scale = 2;  // 缩放倍数,1会糊
          this.canvas = document.getElementById(id);
          this.canvas.width = this.w = document.body.offsetWidth * this.scale;
          this.canvas.height = this.h = 220 * this.scale;
          this.canvas.style.width = this.w / this.scale + 'px';
    
          this.ctx = this.canvas.getContext('2d');
    
          this.style = { // 弹幕样式
            height: 27 * this.scale,  // 弹幕高度
            fontSize: 14 * this.scale,  // 字体大小
            marginBottom: 4 * this.scale,  // 弹幕 margin-bottom
            paddingX: 8 * this.scale,  // 弹幕 padding x
            avatarWidth: 18 * this.scale,  // 头像宽度
          }
          this.ctx.font = this.style.fontSize + 'px PingFangSC-Regular';
    
          this.barrageList = [];  // 弹幕列表
          this.rowStatusList = [];  // 记录每行是否可插入,防止重叠。 行号为可插入 false为不可插入
    
          let rowLength = Math.floor(this.h / (this.style.height + this.style.marginBottom));
          for (var i = 0; i < rowLength; i++) {
            this.rowStatusList.push(i)
          }
        }
    
        shoot(value) {
          const { height, avatarWidth, fontSize, marginBottom, paddingX } = this.style;
          const { img, t1, t2 } = value;
          let row = this.getRow();
          let color = this.getColor();
          let offset = this.getOffset();
          let w_0 = paddingX;  // 头像开始位置
          let w_1 = w_0 + avatarWidth + 8;  // t1文字开始位置
          let w_2 = w_1 + Math.ceil(this.ctx.measureText(t1).width) + 8;  // t2文字开始位置
          let w_3 = w_2 + Math.ceil(this.ctx.measureText(t2).width) + paddingX;  // 弹幕总长度
    
          let barrage = {
            value,
            color,
            row,
            top: row * (height + marginBottom),
            left: this.w,
            offset,
             [w_0, w_1, w_2, w_3],
          }
    
          this.barrageList.push(barrage);
        }
    
        draw() {
          if (!!this.barrageList.length) {
            this.ctx.clearRect(0, 0, this.w, this.h);
            for (let i = 0, barrage; barrage = this.barrageList[i]; i++) {
              // 弹幕滚出屏幕,从数组中移除
              if (barrage.left + barrage.width[3] <= 0) {
                this.barrageList.splice(i, 1);
                i--;
                continue;
              }
    
              // 弹幕完全滚入屏幕,当前行可插入
              if (!barrage.rowFlag) {
                if ((barrage.left + barrage.width[3]) < this.w) {  // 
                  this.rowStatusList[barrage.row] = barrage.row;
                  barrage.rowFlag = true;
                }
              }
    
              barrage.left -= barrage.offset;
              this.drawBarrage(barrage);
            }
          }
          requestAnimationFrame(this.draw.bind(this));
        }
    
        drawBarrage(barrage) {
          const { height, avatarWidth, fontSize, marginBottom, paddingX } = this.style;
          const {
            value: { img, t1, t2 },
            color,
            row,
            left,
            top,
            offset,
            width,
          } = barrage;
    
          // 画框子
          this.ctx.roundRect(left, top, width[3], height, height / 2)
          this.ctx.fillStyle = 'rgba(255,255,255,0.50)';
          this.ctx.fill();
          // 画头像
          this.ctx.drawImage(img, 0, 0, img.width, img.height, left + width[0], top + (height - avatarWidth) / 2, avatarWidth, avatarWidth);
          // 画黑色的字
          this.ctx.fillStyle = color;
          this.ctx.fillText(t1, left + width[1], top + fontSize + 8);
          // 画红色的字
          this.ctx.fillStyle = '#F24949';
          this.ctx.fillText(t2, left + width[2], top + fontSize + 8);
        }
    
        getRow() {
          let emptyRowList = this.rowStatusList.filter(d => /d/.test(d));  // 找出可插入行
          let row = emptyRowList[Math.floor(Math.random() * emptyRowList.length)];  // 随机选一行
          this.rowStatusList[row] = false;
          return row;
        }
    
        haveEmptyRow() {
          let emptyRowList = this.rowStatusList.filter(d => /d/.test(d));  // 找出可插入行
          return !!emptyRowList.length;
        }
    
        getColor() {
          return '#000000';
        }
    
        getOffset() {
          return 1 * this.scale;
        }
      }
    
      var list = [
        {
          avatar: 'https://image.duliday.com/living-cost/20200303/2a94df636b91ad15bbbb4408e2f285e4164115?roundPic/radius/66',
          t1: '张**三 给 李**四',
          t2: '红包',
        },
        {
          avatar: 'https://image.duliday.com/living-cost/20200317/4cd9f827d439f7a1227501f9b09cd1e8622417?roundPic/radius/66',
          t1: '王**五 给 赵**六',
          t2: '红包',
        }
      ]
    
      // 循环插入发射弹幕
      var index = 0;
      var shootBarrage = function (list) {
        setTimeout(function () {
          if (barrage.haveEmptyRow()) {
            var data = list[index++] || list[(index = 0) || index++];
            var img = new Image();
            img.setAttribute("crossOrigin", 'anonymous');
            img.onload = function () {
              barrage.shoot({
                img,
                t1: data.t1,
                t2: data.t2,
              });
            }
            img.src = data.avatar;
          }
          shootBarrage(list);
        }, 1000)
      }
    
      var barrage = new Barrage('canvas');
      barrage.draw();
      shootBarrage(list)
    
    </script>
    
    </html>
    
  • 相关阅读:
    [Go] 写文件和判断文件是否存在
    [日常] 解决github速度特别慢
    [Go] imap收信非并发
    [Linux] 使用secureCRT实现SSH隧道服务器端口转发到本机内网穿透
    [Linux] 解决nginx: [emerg] directive "rewrite" is not terminated by ";"
    [MySQL] 解决Error 1698: Access denied for user 'root'@'localhost'
    [Go] gocron源码阅读-判断是否使用root用户执行
    [日常] 前端资源测试机上忽略版本号的的nginx配置
    [Go] 使用go mod安装beego
    [Go] tcp服务下的数据传递
  • 原文地址:https://www.cnblogs.com/whosmeya/p/12516287.html
Copyright © 2020-2023  润新知