前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >弹、弹幕,是怎样练成的?

弹、弹幕,是怎样练成的?

作者头像
苏南
发布2020-12-16 09:58:41
8300
发布2020-12-16 09:58:41
举报
文章被收录于专栏:漫画前端漫画前端

点击上方“IT平头哥联盟”,选择“置顶或者星标”

与您一起成长~

天下视频唯弹幕不破

作者:chenhongdong

说起弹幕看过视频的都不会陌生,那满屏充满着飘逸评论的效果,让人如痴如醉,无法自拔。

最近也是因为在学习关于canvas的知识,所以今天就想和大家分享一个关于弹幕的故事。

那么究竟弹幕是怎样炼成的呢? 我们且往下看(look)。

什么?看效果

效果图已经呈现给各位了,那么是不是有点小激动呢?是的,感慨万分,思绪宁乱,无语凝噎。

无论以后我们的工作中是否会遇到这样的需求,也请给自己一个增加技能的机会吧!!!

本次弹幕的效果,项目结构如下图所示:

项目整体已经给出,那么我们就开干吧。

让弹幕飞

上面我们提到了canvas的事情,所以呢,这就是制作弹幕的杀手锏了。我们利用canvas绘图来实现弹幕的功能

首先,我们先给出html的结构

代码语言:javascript
复制
// index.html文件<div class="wrap">    <h1>听妈妈的话 - 周杰伦</h1>    <div class="main">        <canvas id="canvas"></canvas>        <video src="../source/mv.mp4" id="video" controls width="720" height="480"></video>    </div>    <div class="content">        <input type="text" id="text">        <input type="button" value="发弹幕" id="btn">        <input type="color" id="color">        <input type="range" id="range" max="40" min="20">    </div></div>// 引入index.js文件用来实现弹幕功能<script src="./index.js"></script>

如需要视频资源的,就点 https://pan.baidu.com/share/init?surl=p6MyTM4-kEDAw4WI1buxXQ (提取码:tsei)。

结构相对来说没什么高级的内容,主要就是写上了canvas标签还有video标签,他们才是视频网站中弹幕的绝佳拍档。

那么不再卖关子了,赶紧进行主要活动吧!

模拟数据

代码语言:javascript
复制
// index.js文件let data = [    {value: '周杰伦的听妈妈的话,让我反复循环再循环', time: 5, color: 'red', speed: 1, fontSize: 22},    {value: '想快快长大,才能保护她', time: 10, color: '#00a1f5', speed: 1, fontSize: 30},    {value: '听妈妈的话吧,晚点再恋爱吧!爱呢?', time: 15},];

数据里代表了什么:

  • value:代表弹幕的内容 (必填)
  • time:代表弹幕展现的时间 (必填)
  • color:代表弹幕文字的颜色
  • speed:代表弹幕飘过的速度
  • fontSize:代表弹幕文字的大小
  • opacity:代表弹幕文字的透明度

除了弹幕的内容和展现的时间外,其他都是可选的,模拟的数据里没有这些参数也没关系的

获取dom元素

代码语言:javascript
复制
// index.js文件// 模拟数据...省略
// 获取到所有需要的dom元素let doc = document;let canvas = doc.getElementById('canvas');let video = doc.getElementById('video');let $txt = doc.getElementById('text');let $btn = doc.getElementById('btn');let $color = doc.getElementById('color');let $range = doc.getElementById('range');

Canvas渲染弹幕

下面我们将用面向对象的方式来实现canvas绘制弹幕的功能,之所以选择用这种方式主要是方便复用和后续添加方法。

下面我们先来创建一个CanvasBarrage类,主要用做canvas来渲染整个弹幕。

在实现之前,我们先来调用一下,看看是如何创建实例的。

代码语言:javascript
复制
// index.js文件// 模拟数据...省略// 获取到所有需要的dom元素...省略
// 创建CanvasBarrage类class CanvasBarrage {    // todo}// 创建CanvasBarrage实例let canvasBarrage = new CanvasBarrage(canvas, video, { data });

创建实例很简单,没有对象,只需要new一个就有了,哈哈。接下来,说回正事,我们赶紧完成上面代码中todo的部分,来完善CanvasBarrage类吧。

实现CanvasBarrage

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    constructor(canvas, video, opts = {}) {        // opts = {}表示如果opts没传就设为{},防止报错,ES6语法
        // 如果canvas和video都没传,那就直接return掉        if (!canvas || !video) return;
        // 直接挂载到this上        this.video = video;        this.canvas = canvas;        // 设置canvas的宽高和video一致        this.canvas.width = video.width;        this.canvas.height = video.height;        // 获取画布,操作画布        this.ctx = canvas.getContext('2d');
        // 设置默认参数,如果没有传就给带上        let defOpts = {            color: '#e91e63',            speed: 1.5,            opacity: 0.5,            fontSize: 20,            data: []        };        // 合并对象并全都挂到this实例上        Object.assign(this, defOpts, opts);
       // 添加个属性,用来判断播放状态,默认是true暂停       this.isPaused = true;       // 得到所有的弹幕消息       this.barrages = this.data.map(item => new Barrage(item, this));       // 渲染       this.render();       console.log(this);    }    // 渲染canvas绘制的弹幕    render() {        // todo    }}

我们在“得到所有的弹幕消息”那里,通过数组的map方法返回的还是个数组,不过返回的内容是一个Barrage类,这是为什么呢?

还记得之前说过么,用类的好处就是方便扩展,后续再添加方法的话可以直接在该类中添加即可。

所以我们也不推崇直接map方法里直接返回一个{}这种形式

代码语言:javascript
复制
// 不推荐this.barrages = this.data.map(item => { item });

说到这里我们还要先写一下Barrage这个类,不然接下来的console.log(this)会因为找不到Barrage类而报错

代码语言:javascript
复制
// index.js文件
++++++++++++++++++++++// 创建Barrage类,用来实例化每一个弹幕元素class Barrage {    constructor(obj, ctx) {        // todo    }}++++++++++++++++++++++
class CanvasBarrage {    ...省略}

Now,通过上面代码中的console.log(this),我们可以看到,所有挂载到this实例上的属性和原型上的方法都呈现眼前了

render 一下

接着上面的CanvasBarrage类里render方法继续写,我们来把todo完成

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    constructor(canvas, video, opts = {}) {        ...省略        // 渲染        this.render();    }    render() {        // 渲染的第一步是清除原来的画布,方便复用写成clear方法来调用        this.clear();        // 渲染弹幕        this.renderBarrage();        // 如果没有暂停的话就继续渲染        if (this.isPaused === false) {            // 通过raf渲染动画,递归进行渲染            requestAnimationFrame(this.render.bind(this));        }    }    clear() {        // 清除整个画布        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);    }}

todo都做了什么?

  1. 清除之前画布所有的绘制,防止绘制重叠的影响
    • this.clear()
  2. 渲染真正的弹幕数据 (还未实现)
    • this.renderBarrage()
  3. 判断是否继续渲染弹幕
    • this.isPaused为false时表示为播放状态
  4. 递归调用render
    • 通过requestAnimationFrame来递归调用render
    • 要比setInterval这样的方式好很多

渲染整个弹幕render方法就完成了,那么要继续写了,应该是刚才未实现的renderBarrage方法了。

But,在此之前,我们要先写个别的,它就是Barrage类。

因为还需要它来大显身手一下呢,每一个弹幕的实例都由它来制造。

创建Barrage类

弹幕制造者来了,下面我们就来实现一下这个Barrage类,看它都具备哪些属性和方法,继续todo吧。

代码语言:javascript
复制
// index.js文件class Barrage {    constructor(obj, ctx) {        this.value = obj.value; // 弹幕的内容        this.time = obj.time; // 弹幕出现时间        // 把obj和ctx都挂载到this上方便获取        this.obj = obj;        this.context = ctx;    }    // 初始化弹幕    init() {        // 如果数据里没有涉及到下面4种参数,就直接取默认参数        this.color = this.obj.color || this.context.color;        this.speed = this.obj.speed || this.context.speed;        this.opacity = this.obj.opacity || this.context.opacity;        this.fontSize = this.obj.fontSize || this.context.fontSize;
        // 为了计算每个弹幕的宽度,我们必须创建一个元素p,然后计算文字的宽度        let p = document.createElement('p');        p.style.fontSize = this.fontSize + 'px';        p.innerHTML = this.value;        document.body.appendChild(p);
        // 把p元素添加到body里了,这样就可以拿到宽度了        // 设置弹幕的宽度        this.width = p.clientWidth;        // 得到了弹幕的宽度后,就把p元素从body中删掉吧        document.body.removeChild(p);
        // 设置弹幕出现的位置        this.x = this.context.canvas.width;        this.y = this.context.canvas.height * Math.random();        // 做下超出范围处理        if (this.y < this.fontSize) {            this.y = this.fontSize;        } else if (this.y > this.context.canvas.height - this.fontSize) {            this.y = this.context.canvas.height - this.fontSize;        }    }    // 渲染每个弹幕    render() {        // 设置画布文字的字号和字体        this.context.ctx.font = `${this.fontSize}px Arial`;        // 设置画布文字颜色        this.context.ctx.fillStyle = this.color;        // 绘制文字        this.context.ctx.fillText(this.value, thix.x, this.y);    }}

todo都做了什么?

  1. 从传入的obj中取到必要的value和time this.value = obj.value; // 内容this.time = obj.time; // 时间
  2. 初始化弹幕
    • canvas是按照字号基线来展示字体的,如果小于这个字号大小
    • 如果大于画布高度 - 字号大小
    • this.y = this.fontSize
    • this.y = this.context.canvas.height - this.fontSize
    • 横向x坐标起始位置都是从右边进入,即:画布的宽度
    • 纵向y坐标起始位置是不固定的,选在画布之内的任意位置出现
    • this.x = this.context.canvas.width
    • this.y = this.context.canvas.height * Math.random()
    • 由于不能直接操纵canvas画布里的元素,所以先创建一个p标签
    • p标签的宽度即为弹幕的宽 -> this.width = p.clientWidth
    • 对每个弹幕所需的参数进行设置,如果obj上没有,就取默认参数
    • 计算每个弹幕的宽度
    • 设置每个弹幕的x和y坐标 (起始位置)
    • 处理弹幕超出画布区域
  3. 渲染每个弹幕
    • this.context.ctx.fillText(this.value, this.x, this.y)
    • this.context.ctx.fillStyle = this.color
    • this.context.ctx.font = ${this.value}px Arial
    • 绘制文本需要设置文本的字体字号颜色和文本的内容坐标
    • 字体字号api
    • 颜色api
    • 内容与坐标api

以上三步就是整个Barrage类所做的事情了。Barrage这个类都已经敲完了,那么接下来开始真正的渲染步骤吧。

renderBarrage才是主角

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    ...省略    renderBarrage() {        // 首先拿到当前视频播放的时间        // 要根据该时间来和弹幕要展示的时间做比较,来判断是否展示弹幕        let time = this.video.currentTime;
        // 遍历所有的弹幕,每个barrage都是Barrage的实例        this.barrages.forEach(barrage => {            // 用一个flag来处理是否渲染,默认是false            // 并且只有在视频播放时间大于等于当前弹幕的展现时间时才做处理            if (!barrage.flag && time >= barrage.time) {                // 判断当前弹幕是否有过初始化了                // 如果isInit还是false,那就需要先对当前弹幕进行初始化操作                if (!barrage.isInit) {                    barrage.init();                    barrage.isInit = true;                }                // 弹幕要从右向左渲染,所以x坐标减去当前弹幕的speed即可                barrage.x -= barrage.speed;                barrage.render(); // 渲染当前弹幕
                // 如果当前弹幕的x坐标比自身的宽度还小了,就表示结束渲染了                if (barrage.x < -barrage.width) {                    barrage.flag = true; // 把flag设为true下次就不再渲染                }            }        });    }}

此时我们再添加一个触发弹幕的事件,让弹幕飞起来。

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    ...省略}
// 创建CanvasBarrage实例let canvasBarrage = new CanvasBarrage(canvas, video, { data });++++++++++++++++++++++++++++++++++++++// 设置video的play事件来调用CanvasBarrage实例的render方法video.addEventListener('play', () => {    canvasBarrage.isPaused = false;    canvasBarrage.render(); // 触发弹幕});++++++++++++++++++++++++++++++++++++++

大家一起写到了这里,也是时候展示一下成果了,往下看。

别急,让弹幕再飞一会儿

渲染弹幕的功能,我们已经完成了,接下来让我们马不停蹄的写下如何发弹幕吧。别犹豫,开撸!!!

发弹幕

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    ...省略}video.addEventListener('play', ...省略);
+++++++++++++++++++++++++++++++++++++++// 发送弹幕的方法function send() {    let value = $txt.value; // 输入的内容    let time = video.currentTime; // 当前视频时间    let color = $color.value; // 选取的颜色值    let fontSize = $range.value; // 选取的字号大小    let obj = { value, time, color, fontSize };    // 添加弹幕数据    canvasBarrage.add(obj);    $txt.value = ''; // 清空输入框}// 点击按钮发送弹幕$btn.addEventListener('click', send);// 回车发送弹幕$txt.addEventListener('keyup', e => {    let key = e.keyCode;    key === 13 && send();});+++++++++++++++++++++++++++++++++++++++

发弹幕相对来说还是很简单的,获取到value, time, color, fontSize之后把他们当作对象传给CanvasBarrage的add方法进行添加就好了。

下面我们再写一下add方法,回到CanvasBarrage类里继续写。

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    constructor() { ...省略}    render() { ...省略 }    renderBarrage() { ...省略 }    clear() { ...省略 }    +++++++++++++++++++++++++++    add(obj) {        // 实际上就是往barrages数组里再添加一项Barrage的实例而已        this.barrages.push(new Barrage(obj, this));    }    +++++++++++++++++++++++++++}

完成,漂亮,看看效果吧

写到这里我们已经完成了视频网站上的弹幕功能了,可喜可贺。

下面我们再来完善一下视频播放时对弹幕的播放处理吧。

暂停和拖动

  • 暂停就停止渲染弹幕

代码语言:javascript
复制
// index.js文件...省略// 播放video.addEventListener('play', () => {    canvasBarrage.isPaused = false;    canvasBarrage.render();});+++++++++++++++++++++++++++++++++++++++// 暂停video.addEventListener('pause', () => {    // isPaused设为true表示暂停播放    canvasBarrage.isPaused = true;});+++++++++++++++++++++++++++++++++++++++
  • 回放时需要重新渲染该时刻的弹幕

代码语言:javascript
复制
// index.js文件
// 暂停video.addEventListener('pause', () => {    canvasBarrage.isPaused = true;});+++++++++++++++++++++++++++++++++++++++// 拖动进度条时触发seeked事件video.addEventListener('seeked', () => {    // 调用CanvasBarrage类的replay方法进行回放,重新渲染弹幕    canvasBarrage.replay();});+++++++++++++++++++++++++++++++++++++++

让我们再次回到CanvasBarrage这个类上。

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    constructor() { ...省略}    render() { ...省略 }    renderBarrage() { ...省略 }    clear() { ...省略 }    add(obj) { ...省略 }    +++++++++++++++++++++++++++    replay() {        this.clear(); //先清除画布        // 获取当前视频播放时间        let time = this.video.currentTime;        // 遍历barrages弹幕数组        this.barrages.forEach(barrage => {            // 当前弹幕的flag设为false            barrage.flag = false;            // 并且,当前视频时间小于等于当前弹幕所展现的时间            if (time <= barrage.time) {                // 就把isInit重设为false,这样才会重新初始化渲染                barrage.isInit = false;            } else { // 其他时间对比不匹配的,flag还是true不用重新渲染                barrage.flag = true;            }        });    }    +++++++++++++++++++++++++++}

尽善尽美一下

OK,写到这里,所有关于弹幕功能的代码就全部结束了!!!如果工作中让你开发弹幕功能,你也可以在多敲几遍以上代码之后,得心应手的保证完成任务了。

不过做事总是要做全套比较好,我们接下来再利用WebSocketredis来进行一下较为实战的功能吧。

大家之前看到过目录结构,还有一个app.js文件其实是没有写任何东西的,那么接下来我们就开始写写看吧。

WebSocket通信和redis存储

久违的app.js文件,开始动手 首先我们需要安装两个包,一个是处理服务端WebSocket通信的ws模块,另一个就是用来储存redis数据的redis模块。

npm i ws redis -S

安装完成后可以继续写东西了

代码语言:javascript
复制
// app.js文件const WebSocket = require('ws');const redis = require('redis');const clientRedis = redis.createClient(); // 创建redis客户端const ws = new WebSocket.Server({ port: 9999 }); // 创建ws服务// 用来存储不同的socket实例,区分不同用户let clients = [];// 监听连接ws.on('connection', socket => {    clients.push(socket); // 把socket实例添加到数组
    // 通过redis客户端的lrange方法来获取数据库中key为barrages的数据    clientRedis.lrange('barrages', 0, -1, (err, data) => {        // 由于redis存储的是key value类型,因此需要JSON.parse转成对象        data = data.map(item => JSON.parse(item));
        // 发送给客户端,send方法传递的是字符串需要JSON.stringify        // type为init是用来初始化弹幕数据的        socket.send(JSON.stringify({            type: 'init',            data        }));    });    // 监听客户端发来的消息    socket.on('message', data => {        // redis客户端通过rpush的方法把每个消息都添加到barrages表的最后面        clientRedis.rpush('barrages', data);
        // 每个socket实例(用户)之间都可以发弹幕,并显示在对方的画布上        // type为add表示此次操作为添加处理        // 你可以打开两个index.html,分别发弹幕试试吧        clients.forEach(sk => {            sk.send(JSON.stringify({                type: 'add',                data: JSON.parse(data)            }));        });
    });    // 当有socket实例断开与ws服务端的连接时    // 重新更新一下clients数组,去掉断开的用户    socket.on('close', () => {        clients = clients.filter(client => client !== socket);    });});

服务端的内容已经全部完事了,接下来我们再稍微改下客户端的代码,回到熟悉的index.js中。

代码语言:javascript
复制
// index.js文件class CanvasBarrage {    ...省略}+++++++++++++++++++++++++++++++// 创建CanvasBarrage实例// let canvasBarrage = new CanvasBarrage(canvas, video, { data });let canvasBarrage;let ws = new WebSocket('ws://localhost:9999');
// 监听与ws服务端的连接ws.onopen = function () {    // 监听ws服务端发来的消息    ws.onmessage = function (e) {        let msg = JSON.parse(e.data); //e.data里是真正的数据
        // 判断如果type为init就初始化弹幕的数据        if (msg.type === 'init') {            canvasBarrage = new CanvasBarrage(canvas, video, { data: msg.data });        } else if (msg.type === 'add') { // 添加弹幕数据            canvasBarrage.add(msg.data);        }    }};+++++++++++++++++++++++++++++++
// 发送弹幕的方法function send() {    let value = $txt.value;    let time = video.currentTime;    let color = $color.value;    let fontSize = $range.value;    let obj = { value, time, color, fontSize };    // 添加弹幕数据    // canvasBarrage.add(obj);    +++++++++++++++++++++++++++++++    // 把添加的弹幕数据发给ws服务端    // 由ws服务端拿到后添加到redis数据库中    ws.send(JSON.stringify(obj));    +++++++++++++++++++++++++++++++    $txt.value = '';}

前后端都搞定了,那么我们接下来只需要连接一下redis数据库就可以了

连接redis数据库的正确方式

首先无论是windows还是mac都需要先安装一下

windows系统
  • windows:下载:redishttps://pan.baidu.com/share/init?surl=3yz475G66o_OrC80_a6Ucg (提取码:svua)

windows连接redis数据库

进入下载解压好的redis目录,在命令行工具中输入以下指令建立连接

代码语言:javascript
复制
redis-server.exe redis.windows.conf

出现如下图显示的样子就表示已经成功建立了连接

windows下的redis可视化工具(Redis Desktop Manager)

mac系统
  • mac: brew install redis
  • 连接: brew services start redis

redis数据库如果成功的连接了,那么就可以直接启动app.js的服务了,打开index.html文件,会发现可以拿到数据库里存储的弹幕数据了

好了,这下大家满足了吧,很厉害,我们每个人都可以敲出自己的弹幕了。

不断的学习会让我们一点一滴的进步下去,前端的路还很长,我们都在慢慢前行

对了,忘记重要的事情了,如果大家有什么疑问可以看下,源码地址:https://github.com/chenhongdong/keep/tree/master/%E5%BC%B9%E5%B9%95/last 进行参考

结束了

之后一段时间打算好好的研究一下canvas绘图的知识点了,也希望在研究后可以很好的梳理一下分享给大家一起来学习。

作为大前端来说,我们要学的东西实在太多了,一专多精才是王道,不负好时光,一起努力吧!谢谢大家的观看了。

参考

  • HTML5 Video元素介绍:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video
  • Canvas学习教程:https://www.yuque.com/airing/canvas
  • 珠峰架构培训公开课 实现弹幕系统:https://www.bilibili.com/video/av35838421/
  • ES6语法学习:http://es6.ruanyifeng.com/
  • WebSocket学习参考:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket

- end -

用心分享 一起成长 做有温度的攻城狮

不要因为走了很远,就忘记了为何而出发

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 画漫画的程序员 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 与您一起成长~
    • 让弹幕飞
      • 模拟数据
      • 获取dom元素
      • Canvas渲染弹幕
      • 实现CanvasBarrage
      • render 一下
      • renderBarrage才是主角
      • 别急,让弹幕再飞一会儿
      • 发弹幕
      • 暂停和拖动
    • 尽善尽美一下
      • WebSocket通信和redis存储
      • 连接redis数据库的正确方式
    • 结束了
      • 参考
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档