女王控的博客

使用canvas实现和HTML5 video交互的弹幕效果

canvas 实现弹幕性能较好

从技术实现成本角度讲,要实现弹幕效果,最简单的方法就是 DOM+CSS3 控制,如果我们的弹幕效果比较简单,使用 CSS3 动画实现也不失为一个好的方法。

但是如果我们的弹幕数据量比较大,就像下面这种:

2019 12 21 16 34 45

使用 DOM 来实现很容易卡成了 80 年代的拖拉机——一顿一顿的。

很显然,面对这种多元素的复杂动画,使用 canvas 实现是更加合适的,动画会流畅很多。

本文就将展示两个案例,使用 canvas 实现弹幕效果。其中一个效果是静态的弹幕数据固定的无限循环的效果,适合在个人博客或者运营页面,这种非视频场景使用;另外一个效果是动态的弹幕数据可变的和真实 HTML5 <video> 交互的弹幕效果,也就是真视频弹幕效果。

两个高保真 demo 的源代码都是可以免费使用的,使用 MIT 许可,也就是需要保留源代码中的版权声明。

好,下面我们一个人来看一下。

canvas 实现的静态循环滚动播放弹幕

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    html,
    body {
      background-color: white;
      margin: 0;
    }

    .video-x {
      position: relative;
      width: 640px;
      margin: auto;
    }

    .canvas-barrage {
      position: absolute;
      width: 640px;
      height: 360px;
    }

    .video-placeholder {
      height: 360px;
      background-color: #000;
      animation: bgColor 10s infinite alternate;
    }

    @keyframes bgColor {
      25% {
        background-color: darkred;
      }
      50% {
        background-color: darkgreen;
      }
      75% {
        background-color: darkblue;
      }
      100% {
        background-color: sliver;
      }
    }
  </head>
  </style>
<body>
  <div class="demo">
    <div class="video-x">
      <canvas id="canvasBarrage" class="canvas-barrage"></canvas>
      <div class="video-placeholder"></div>
    </div>
  </div>
  <script>
    // 弹幕数据
    var dataBarrage = [{
      value: '使用的是静态死数据',
      color: 'blue',
      range: [0, 0.5]
    }, {
      value: '随机循环播放',
      color: 'blue',
      range: [0, 0.6]
    }, {
      value: '可以控制区域和垂直分布范围',
      color: 'blue',
      range: [0, 0.5]
    }, {
      value: '字体大小和速度在方法内设置',
      color: 'black',
      range: [0.1, 1]
    }, {
      value: '适合用在一些静态页面上',
      color: 'black',
      range: [0.2, 1]
    }, {
      value: '基于canvas实现',
      color: 'black',
      range: [0.2, 0.9]
    }, {
      value: '因此IE9+浏览器才支持',
      color: 'black',
      range: [0.2, 1]
    }, {
      value: '可以设置边框颜色',
      color: 'black',
      range: [0.2, 1]
    }, {
      value: '文字颜色默认都是白色',
      color: 'black',
      range: [0.2, 0.9]
    }, {
      value: '若文字颜色不想白色',
      color: 'black',
      range: [0.2, 1]
    }, {
      value: '需要自己调整下JS',
      color: 'black',
      range: [0.6, 0.7]
    }, {
      value: '如果需要的是真实和视频交互的弹幕',
      color: 'black',
      range: [0.2, 1]
    }, {
      value: '可以回到原文',
      color: 'black',
      range: [0, 0.9]
    }, {
      value: '查看另外一个demo',
      color: 'black',
      range: [0.7, 1]
    }, {
      value: '下面就是占位弹幕了',
      color: 'black',
      range: [0.7, 0.95]
    }, {
      value: '前方高能预警!!!',
      color: 'orange',
      range: [0.5, 0.8]
    }, {
      value: '前方高能预警!!!',
      color: 'orange',
      range: [0.5, 0.9]
    }, {
      value: '前方高能预警!!!',
      color: 'orange',
      range: [0, 1]
    }, {
      value: '前方高能预警!!!',
      color: 'orange',
      range: [0, 1]
    }];

    // 弹幕方法
    var canvasBarrage = function (canvas, data) {
      if (!canvas || !data || !data.length) {
        return;
      }
      if (typeof canvas == 'string') {
        canvas = document.querySelector(canvas);
        canvasBarrage(canvas, data);
        return;
      }
      var context = canvas.getContext('2d');
      canvas.width = canvas.clientWidth;
      canvas.height = canvas.clientHeight;

      // 存储实例
      var store = {};

      // 字号大小
      var fontSize = 28;

      // 实例方法
      var Barrage = function (obj, index) {
        // 随机x坐标也就是横坐标,对于y纵坐标,以及变化量moveX
        this.x = (1 + index * 0.1 / Math.random()) * canvas.width;
        this.y = obj.range[0] * canvas.height + (obj.range[1] - obj.range[0]) * canvas.height * Math.random() + 36;
        if (this.y < fontSize) {
          this.y = fontSize;
        } else if (this.y > canvas.height - fontSize) {
          this.y = canvas.height - fontSize;
        }
        this.moveX = 1 + Math.random() * 3;

        this.opacity = 0.8 + 0.2 * Math.random();
        this.params = obj;

        this.draw = function () {
          var params = this.params;
          // 根据此时x位置绘制文本
          context.strokeStyle = params.color;
          context.font = 'bold ' + fontSize + 'px "microsoft yahei", sans-serif';
          context.fillStyle = 'rgba(255,255,255,'+ this.opacity +')';
          context.fillText(params.value, this.x, this.y);
          context.strokeText(params.value, this.x, this.y);
        };
      };

      data.forEach(function (obj, index) {
        store[index] = new Barrage(obj, index);
      });

      // 绘制弹幕文本
      var draw = function () {
        for (var index in store) {
          var barrage = store[index];
          // 位置变化
          barrage.x -= barrage.moveX;
          if (barrage.x < -1 * canvas.width * 1.5) {
            // 移动到画布外部时候从左侧开始继续位移
            barrage.x = (1 + index * 0.1 / Math.random()) * canvas.width;
            barrage.y = (barrage.params.range[0] + (barrage.params.range[1] - barrage.params.range[0]) * Math.random()) * canvas.height;
            if (barrage.y < fontSize) {
              barrage.y = fontSize;
            } else if (barrage.y > canvas.height - fontSize) {
              barrage.y = canvas.height - fontSize;
            }
            barrage.moveX = 1 + Math.random() * 3;
          }
          // 根据新位置绘制圆圈圈
          store[index].draw();
        }
      };

      // 画布渲染
      var render = function () {
        // 清除画布
        context.clearRect(0, 0, canvas.width, canvas.height);

        // 绘制画布上所有的圆圈圈
        draw();

        // 继续渲染
        requestAnimationFrame(render);
      };

      render();
    };

    canvasBarrage('#canvasBarrage', dataBarrage);
  </script>
</body>
</html>

使用方法和 API

语法如下:

js 复制代码
canvasBarrage(canvas, data);

其中:

  • canvas
    canvas 表示我们的<canvas>画布元素,可以直接是 DOM 元素,也可以是<canvas>画布元素的选择器。

  • data
    data 表示弹幕数据,是一个数组。例如下面:

    js 复制代码
    [
    {
      value: '弹幕1',
      color: 'blue',
      range: [0, 0.5]
    },
    {
      value: '弹幕2',
      color: 'red',
      range: [0.5, 1]
    }
    ];

可以看到数组中的每一个值表示一个弹幕的信息对象。其中 value 表示弹幕的文字内容;color 表示弹幕描边的颜色(弹幕文字本身默认是白色);range 表示弹幕在画布中的区域范围,例如[0, 0.5]表示弹幕在画布中的上半区域显示,[0.5, 1]表示弹幕在画布中的下半区域显示。

然后就可以看到无限滚动的弹幕效果了。

补充说明

此弹幕效果默认文字大小是 28px,并且文字加粗,如果这个效果不符合您的需求,需要在 canvasBarrage()方法中修改源代码。因为本来就是个简单静态效果,因此没有专门设计成 API。

此弹幕效果默认是白色文字加可变颜色描边,同样的,如果这个效果不符合您的需求,需要在 canvasBarrage()方法中修改源代码。跟真实的弹幕效果有所不同,这里的弹幕出现的速度和时机不是基于特定时间,而是随机产生。所以看到有些文字好像开飞机,而有些文字好像坐着拖拉机。因为是死数据,这样设计会看上去更真实写。

canvas 实现的 video 真实交互的弹幕

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    html,
    body {
      background-color: white;
      margin: 0;
    }

    .video-x {
      position: relative;
      width: 640px;
      margin: auto;
    }

    .video-x video {
      background-color: black;
      outline: 1px solid #eee;
    }

    .canvas-barrage {
      position: absolute;
      width: 640px;
      height: 360px;
      pointer-events: none;
      z-index: 1;
    }

    input[type="range"] {
      vertical-align: middle;
      margin-right: 50px;
    }

    .ui-radio + label {
      margin-left: 5px;
      margin-right: 20px;
    }

    input[type="submit"] {
      margin-left: 10px;
      margin-right: 50px;
    }

    [disabled] {
      pointer-events: none;
      opacity: .4;
    }

    .last {
      border-top: 1px solid #eee;
      margin-top: 1.5em;
      padding-top: 2em;
    }
  </style>
<body>
  <div class="video-x">
    <canvas id="canvasBarrage" class="canvas-barrage"></canvas>
    <video id="videoBarrage" width="640" height="384" src="./video.mp4" controls></video>
    <form id="barrageForm" action="barrage.php" method="post" autocomplete="off">
      <p>透明度(0-100):<input type="range" class="range" name="opacity" value="100" min="0" max="100"> 文字大小(16-32):<input type="range" class="range" name="fontSize" value="24" min="16" max="32"></p>
        <p>弹幕位置:<input type="radio" id="rangeFull" name="range" checked value="0,1"><label class="ui-radio" for="rangeFull"></label><label for="rangeFull">全部位置</label>
          <input type="radio" id="rangeTop" name="range" value="0,0.3"><label class="ui-radio" for="rangeTop"></label><label for="rangeTop">顶部</label>
          <input type="radio" id="rangeBottom" name="range" value="0.7,1"><label class="ui-radio" for="rangeBottom"></label><label for="rangeBottom">底部</label>
        </p>
        <p class="last"><input class="ui-input" id="input" name="value" required><input type="submit" class="ui-button ui-button-primary" value="发送弹幕" disabled>
        颜色:<input type="color" id="color" name="color" value="#ff0000"></p>
    </form>
  </div>
  <script>
    var CanvasBarrage = function (canvas, video, options) {
      if (!canvas || !video) {
        return;
      }
      var defaults = {
        opacity: 100,
        fontSize: 24,
        speed: 2,
        range: [0,1],
        color: 'white',
        data: []
      };

      options = options || {};

      var params = {};
      for (var key in defaults) {
        if (options[key]) {
          params[key] = options[key];
        } else {
          params[key] = defaults[key];
        }

        this[key] = params[key];
      }
      var top = this;
      var data = top.data;

      if (!data || !data.length) {
        return;
      }

      var context = canvas.getContext('2d');
      canvas.width = canvas.clientWidth;
      canvas.height = canvas.clientHeight;

      var store = {};

      var isPause = true;
      var time = video.currentTime;

      var fontSize = 28;

      var Barrage = function (obj) {
        this.value = obj.value;
        this.time = obj.time;
        this.init = function () {
          var speed = top.speed;
          if (obj.hasOwnProperty('speed')) {
            speed = obj.speed;
          }
          if (speed !== 0) {
            speed = speed + obj.value.length / 100;
          }
          var fontSize = obj.fontSize || top.fontSize;

          var color = obj.color || top.color;
          color = (function () {
            var div = document.createElement('div');
            div.style.backgroundColor = color;
            document.body.appendChild(div);
            var c = window.getComputedStyle(div).backgroundColor;
            document.body.removeChild(div);
            return c;
          })();

          var range = obj.range || top.range;
          var opacity = obj.opacity || top.opacity;
          opacity = opacity / 100;

          var span = document.createElement('span');
          span.style.position = 'absolute';
          span.style.whiteSpace = 'nowrap';
          span.style.font = 'bold ' + fontSize + 'px "microsoft yahei", sans-serif';
          span.innerText = obj.value;
          span.textContent = obj.value;
          document.body.appendChild(span);
          this.width = span.clientWidth;
          document.body.removeChild(span);

          this.x = canvas.width;
          if (speed == 0) {
            this.x	= (this.x - this.width) / 2;
          }
          this.actualX = canvas.width;
          this.y = range[0] * canvas.height + (range[1] - range[0]) * canvas.height * Math.random();
          if (this.y < fontSize) {
            this.y = fontSize;
          } else if (this.y > canvas.height - fontSize) {
            this.y = canvas.height - fontSize;
          }

          this.moveX = speed;
          this.opacity = opacity;
          this.color = color;
          this.range = range;
          this.fontSize = fontSize;
        };

        this.draw = function () {
          context.shadowColor = 'rgba(0,0,0,'+ this.opacity +')';
          context.shadowBlur = 2;
          context.font = this.fontSize + 'px "microsoft yahei", sans-serif';
          if (/rgb\(/.test(this.color)) {
            context.fillStyle = 'rgba('+ this.color.split('(')[1].split(')')[0] +','+ this.opacity +')';
          } else {
            context.fillStyle = this.color;
          }
          context.fillText(this.value, this.x, this.y);
        };
      };

      data.forEach(function (obj, index) {
        store[index] = new Barrage(obj);
      });

      var draw = function () {
        for (var index in store) {
          var barrage = store[index];

          if (barrage && !barrage.disabled && time >= barrage.time) {
            if (!barrage.inited) {
              barrage.init();
              barrage.inited = true;
            }
            barrage.x -= barrage.moveX;
            if (barrage.moveX == 0) {
              barrage.actualX -= top.speed;
            } else {
              barrage.actualX = barrage.x;
            }
            if (barrage.actualX < -1 * barrage.width) {
              barrage.x = barrage.actualX;
              barrage.disabled = true;
            }
            barrage.draw();
          }
        }
      };

      var render = function () {
        time = video.currentTime;
        context.clearRect(0, 0, canvas.width, canvas.height);

        draw();

        if (isPause == false) {
          requestAnimationFrame(render);
        }
      };

      video.addEventListener('play', function () {
        isPause = false;
        render();
      });
      video.addEventListener('pause', function () {
        isPause = true;
      });
      video.addEventListener('seeked', function () {
        top.reset();
      });


      this.add = function (obj) {
        store[Object.keys(store).length] = new Barrage(obj);
      };

      this.reset = function () {
        time = video.currentTime;
        context.clearRect(0, 0, canvas.width, canvas.height);

        for (var index in store) {
          var barrage = store[index];
          if (barrage) {
            barrage.disabled = false;
            if (time < barrage.time) {
              barrage.inited = null;
            } else {
              barrage.disabled = true;
            }
          }
        }
      };
    };
  </script>
  <script>
    // 弹幕数据
    var dataBarrage = [{
      value: 'speed设为0为非滚动',
      time: 1, // 单位秒
      speed: 0
    }, {
      value: 'time控制弹幕时间,单位秒',
      color: 'blue',
      time: 2
    }, {
      value: '视频共21秒',
      time: 3.2
    }, {
      value: '视频背景为白色',
      time: 4.5
    }, {
      value: '视频为录制',
      time: 5.0
    }, {
      value: '视频内容简单',
      time: 6.3
    }, {
      value: '是为了让视频尺寸不至于过大',
      time: 7.8
    }, {
      value: '省流量',
      time: 8.5
    }, {
      value: '支持弹幕暂停(视频暂停)',
      time: 9
    }, {
      value: 'add()方法新增弹幕',
      time: 11
    }, {
      value: 'reset()方法重置弹幕',
      time: 11
    }, {
      value: '颜色,字号,透明度可全局设置',
      time: 13
    }, {
      value: '具体交互细节可参考页面源代码',
      time: 14
    }, {
      value: '内容不错哦!',
      time: 18,
      color: 'yellow'
    }];

    // 初始化弹幕方法
    var eleCanvas = document.getElementById('canvasBarrage');
    var eleVideo = document.getElementById('videoBarrage');

    var demoBarrage = new CanvasBarrage(eleCanvas, eleVideo, {
      data: dataBarrage
    });

    // 下面是交互处理,与弹幕方法本身无关,旨在演示如何修改全局设置,新增弹幕等
    // 1. 全局的弹幕大小,位置和透明度处理
    document.addEventListener("DOMContentLoaded", function() {
      $('.range').on('change', function () {
        // 改变弹幕的透明度和字号大小
        demoBarrage[this.name] = this.value * 1;
      });

      $('input[name="range"]').on('click', function () {
        // 改变弹幕在视频显示的区域范围
        demoBarrage['range'] = this.value.split(',');
      });

      // 发送弹幕
      var elForm = $('#barrageForm'), elInput = $('#input');
      elForm.on('submit', function (event) {
        event.preventDefault();
        // 新增弹幕
        demoBarrage.add({
          value: $('#input').val(),
          color: $('#color').val(),
          time: eleVideo.currentTime
        });

        elInput.val('').trigger('input');
      });
      // 提交按钮
      var elSubmit = elForm.find('input[type="submit"]');

      // 输入框和禁用按钮
      elInput.on('input', function () {
        if (this.value.trim()) {
          elSubmit.removeAttr('disabled');
        } else {
          elSubmit.attr('disabled', 'disabled');
        }
      });
    }, false);
  </script>
  <script src="./lulu.js"></script>
  <script>
    $('.range').range();
    $('#color').color();
  </script>
</body>
</html>

这个原型就有点厉害了,市面上估计很难找到这么负责任的原型页面了。实现的动机完全兴趣使然,上面实现了个简单的,就想着要不实现一个真实的,万一以后用得到呢?

使用方法和 API

语法如下:

js 复制代码
new CanvasBarrage(canvas, video, options);

其中:

  • canvas
    canvas 表示我们的<canvas>画布元素,只能是 DOM 元素。
  • video
    video 表示我们播放的视频元素,只能是 DOM 元素。
  • options
    options 为可选参数,包括:

2019 12 23 01 49 36

如何修改全局设置和添加弹幕?

当我们使用 new CanvasBarrage()构造完我们的弹幕方法后,会返回一个弹幕对象,通过调用这个对象暴露的属性和方法,就可以进行全局的设置和添加弹幕等。例如:

js 复制代码
var myBarrage = new CanvasBarrage(canvas, video, options);

如果我们想把弹幕透明度改成 50%透明,可以:

js 复制代码
myBarrage.opacity = 50;

就这么简单,于是新出现的弹幕都会以 50%透明显示,类似的,弹幕文字大小和颜色,弹幕显示的区域范围都可以通过这样设置,例如:

js 复制代码
myBarrage.fontSize = 20;

就是把弹幕文字颜色改成 20px 大小。

如果我们想动态添加弹幕,可以使用暴露的 add()方法,语法如下:

js 复制代码
myBarrage.add(obj);

其中 obj 的参数类型和支持属性和 options-data 数组中对象一模一样。例如:

js 复制代码
myBarrage.add({
  value: 'new CanvasBarrage()',
  color: '#ff0000',
  time: 0
});

myBarrage 还暴露了一个名为 reset()方法,可以清除屏幕上所有的弹幕,并重新根据视频时间开始运动与显示。视频点击跳转时候的弹幕处理就是调用的此方法。

评论

阅读上一篇

flex深入理解
2019-12-23 11:02:19

阅读下一篇

鼠标无限移动 JS API Pointer Lock简介
2019-12-21 15:57:19
0%