看 B 站时, 对弹幕的实现产生了兴趣, 一开始想到用 CSS3 动画去实现, 后来感觉这样性能不是很好, 查了下资料, 发现可以用 canvas 实现, 于是就摸索着写了一个简单的弹幕.
弹幕功能
支持动态添加弹幕
弹幕不重叠
自定义弹幕颜色
效果图
demo
源码地址 https://github.com/ideagay/ideagay.github.io
前端框架选了比较熟悉的 vuejs
弹幕滚动的基本思路就是通过定时器不断地改变弹幕的位置, 时时重绘画布.
实现步骤
先加入一个 canvas 标签, 这里有个注意点, 关于设备像素比对 canvas 的影响, 会出现绘图模糊.
canvas 绘图模糊问题 https://www.jianshu.com/p/f443685953ea
- <canvas width="600" height="600">
- </canvas>
- // 如果单纯这样写, canvas 会出现模糊
- <canvas width="600" height="600" style="width: 300px;height: 300px">
- </canvas>
- // 为了不出现模糊, 需要设置 canvas 的 CSS 宽高为上下文宽高的 1/devicePixelRatio,
本文是对于 devicePixelRatio:2 的设备设置的, 该值可从 Windows.devicePixelRatio 取得.
- <canvas ref="hiddenCanvas" width="0" height="0" style="display: none">
- </canvas>
- // 后面会用到
我们先定义一个数组来存放弹幕数据, 一条弹幕信息, 包括文本内容, x,y 坐标位置, 颜色, 速度 (可以是随机或者固定, 为了计算简单, 我们这里采用了固定的速度)
- var dmArr = [];
- var gap = 80; // 弹幕的上下间距
- var hiddenCanvas = this.$refs.hiddenCanvas;
- // 增加弹幕的方法
- function pushDm(text, color) {
- let y = getY(); // 先确定跑道
- let x = 600; // 初始 x 坐标为 canvas 的右边界
- let delayWidth = 0; // 同跑道
- for (let i = 0, len = dmArr.length; i <len; i++) {
- let dm = dmArr[i];
- if (y === dm.y) { // 如果是同跑道, 则往后排, 设置一定的间隔, 保证弹幕不会重叠;
- delayWidth += Math.floor(hiddenCanvas.getContext('2d').measureText(dm.text).width * 4 + 50);
- } }
- dmArr.push({
- text: text,
- x: x + delayWidth,
- y: y,
- speed: 8,
- color: color || getColor()
- });
- }
- // 随机获得 y 坐标
- function getY() {
- let range = Math.floor(600 / gap); // 跑道数量
- return Math.floor(Math.random() * range + 1) * gap;
- }
- // 随机获得颜色
- function getColor() {
- return `${Math.floor(Math.random() * 16777215).toString(16)}`;
- }
- // 写一个 for 循环, 初始化 30 条弹幕
- for (let i = 0; i <30; i++) {
- pushDm(`It's barrage ${i}`);
- }
接下来设置一个 20ms 的定时器, 实现弹幕滚动效果
- var timer = null;
- var ctx = this.$refs.canvas.getContext('2d');
- function start(){
- timer = setInterval(() => {
- ctx.clearRect(0, 0, 600, 600); // 每次需要清空画布
- ctx.save();
- ctx.font = '30px Microsoft YaHei'; // 这里需要把字体大小设为需要显示的 CSS 大小的 2 倍 (devicePixelRatio 为 2 时)
- if (!dmArr.length) stop(); // 如果没有新弹幕了, 就停止计时器
- for (let i = 0, len = this.dmArr.length; i <len; i++) {
- let dm = dmArr[i];
- let overRange = -ctx.measureText(dm.text).width * 2;
- dm.x -= dm.speed;
- if (dm.x <overRange) {
- dmArr.splice(i, 1); // 弹幕在画布中不可见时, 从数组中移除该项
- continue;
- }
- ctx.fillStyle = `#${dm.color}`;
- ctx.fillText(dm.text, dm.x, dm.y);
- }
- ctx.restore();
- }, 20);
- }
- function stop() {
- clearInterval(timer);
- ctx.clearRect(0, 0, 600, 600);
- }
我们还需要一个输入框, 来实现手动添加弹幕功能
- <input type="text" @keyup.enter="sent" v-model="dmInput" maxlength="20">
- <button type="button" @click="sent">
- 发表
- </button>
- var dmInput = '';
- var color = ''; // 可自定义弹幕的颜色
- function sent() {
- if (!dmInput) return;
- stop();
- pushDm(dmInput, color);
- start();
- dmInput = '';
- }
有待改进的地方和疑问?
速度不恒定时, 怎么保持弹幕不重叠
视频弹幕是根据弹幕发送时间点来定位到视频的每一帧? 如何实现?
来源: https://juejin.im/post/5bfe3f7c6fb9a04a0c2e250b