弹幕,是怎样练成的?

栏目: 数据库 · 发布时间: 6年前

内容简介:说起弹幕看过视频的都不会陌生,那满屏充满着飘逸评论的效果,让人如痴如醉,无法自拔最近也是因为在学习关于那么究竟弹幕是怎样炼成的呢? 我们且往下看(

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

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

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

什么 ?看 效果

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

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

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

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

弹幕,是怎样练成的?

项目整体已经给出,那么我们就撸起袖子加油干吧。

让弹幕飞

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

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

// 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>
复制代码

如需要 视频资源 的,就点这里吧(提取码:tsei)

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

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

模拟数据

// 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元素

// 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来渲染整个弹幕

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

// index.js文件
// 模拟数据
...省略
// 获取到所有需要的dom元素
...省略

// 创建CanvasBarrage类
class CanvasBarrage {
    // todo
}
// 创建CanvasBarrage实例
let canvasBarrage = new CanvasBarrage(canvas, video, { data });
复制代码

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

实现CanvasBarrage

// 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方法里直接返回一个{}这种形式

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

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

// index.js文件

++++++++++++++++++++++
// 创建Barrage类,用来实例化每一个弹幕元素
class Barrage {
    constructor(obj, ctx) {
        // todo
    }
}
++++++++++++++++++++++

class CanvasBarrage {
    ...省略
}
复制代码

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

弹幕,是怎样练成的?

render一下

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

// 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吧

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

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

renderBarrage才是主角

// 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下次就不再渲染
                }
            }
        });
    }
}
复制代码

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

// index.js文件
class CanvasBarrage {
    ...省略
}

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

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

弹幕,是怎样练成的?

别急,让弹幕再飞一会儿

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

发弹幕

// 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类里继续写

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

完成,漂亮,看看效果吧

弹幕,是怎样练成的?

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

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

暂停和拖动

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

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

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

// 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

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

// 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中

// 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:下载redis (提取码:gw36)

windows连接redis数据库

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

redis-server.exe redis.windows.conf
复制代码

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

弹幕,是怎样练成的?

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

弹幕,是怎样练成的?

mac系统

brew install redis
brew services start redis

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

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

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

对了,忘记重要的事情了,如果大家有什么疑问可以看下 源码地址 进行参考

结束了

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C#本质论

C#本质论

米凯利斯 / 周靖 / 人民邮电出版社 / 2010-9 / 99.00元

《C#本质论(第3版)》是一部好评如潮的语言参考书,作者用一种非常合理的方式来组织《C#本质论(第3版)》的内容,由浅人深地介绍了C#语言的各个方面。全书共包括21章及6个附录,每章开头的“思维导图”指明了本章要讨论的主题,以及各个主题之间的层次关系。书中所包含的丰富的示例代码和精要的语言比较,都有助于读者理解C#语言。《C#本质论(第3版)》首先介绍了C#语言的基础知识,随后深人讲解了泛型、迭代......一起来看看 《C#本质论》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具