都说自古弹幕出人才,弹幕--B站的一大特色,当我们刷B站的时候不打开弹幕好像就没内味。那么今天我们就来聊聊怎么实现弹幕的发送。
准备工作
视频
这里我们随便准备一个视频即可,我在QQ音乐随便找了个MV
关于类
学过Java的读者应该不会陌生,但在JavaScript中有一点点不同
-
类是构造方法的新语法:
- 在 JavaScript 中,
class关键字提供了一种更清晰的方式来定义对象的构造器和原型方法。这一点跟 Java 中的类定义非常相似,可以代码结构更加清晰。
- 在 JavaScript 中,
-
constructor()用于在new时生成实例对象:constructor()方法是在使用new关键字创建类的实例时调用的特殊方法。用于初始化实例的属性。相当于 Java 中的构造器。
-
类当中定义的方法相当于添加在了构造函数的原型上:
- 在类体中定义的方法实际上被添加到了类的原型 (
prototype) 对象上。也就是说所有类的实例将共享这些方法,而不是每个实例都有其自身的副本。 - 这一点与 Java 不同,在 Java 中每个实例都有自己的方法副本。
- 在类体中定义的方法实际上被添加到了类的原型 (
-
static关键字用于将函数声明为静态:- 使用
static关键字定义的方法或属性是类级别的,而不是实例级别的。我们可以直接通过类名来调用它们,而不需要先创建类的实例。 - 相当于 Java 中的静态方法和静态变量。
- 使用
-
get和set关键字可以将函数名作为属性来访问:- 在 JavaScript 中,
get和set关键字允许定义 get 和 set 方法,这些方法可以像访问普通属性一样被调用。 - 这与 Java 中的 getter 和 setter 方法大差不差,但是语法更为简洁。
- 在 JavaScript 中,
-
类中的私有属性可以定义成
#count = xx:- 私有字段(使用
#前缀定义)只能在类内部访问。这有助于封装,确保外部代码不能直接修改这些字段的值。 - 在 JavaScript 中,这是较新的功能,而在 Java 中所有的成员默认都是私有的,除非明确指定为
public或protected。
- 私有字段(使用
在这里用一个简单的例子来展示这些概念:
class MyClass {
#privateCount
constructor(initialCount) {
this.count = initialCount; // 公有属性
this.#privateCount = 0; // 私有属性
}
// 类方法(在原型上)
publicMethod() {
console.log('Called public method');
}
// 静态方法
static staticMethod() {
console.log('Called static method');
}
// get
get count() {
return this.#privateCount;
}
// set
set count(value) {
if (value >= 0) {
this.#privateCount = value;
} else {
console.log('Value must be non-negative.');
}
}
}
// 创建实例
const myInstance = new MyClass(10);
// 访问公有方法
myInstance.publicMethod();
// 访问静态方法
MyClass.staticMethod();
// 使用 get 和 set
console.log(myInstance.count); // 输出私有属性的值
myInstance.count = 20; // 修改私有属性的值
console.log(myInstance.count); // 输出新的私有属性的值
HTML部分
<style>
body, html {
margin: 0;
padding: 0;
height: 100%; /* 设置 body 和 html 的高度为 100% */
}
.wrap {
display: flex;
flex-direction: column; /* 主轴为垂直方向 */
justify-content: center; /* 垂直居中 */
align-items: center; /* 水平居中 */
height: 100%; /* 设置 wrap 的高度为 100% */
}
.main {
display: flex; /* 使用 Flexbox */
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
}
#canvas{
position: absolute;
}
</style>
<body>
<div class="wrap">
<h1>JungKook--Seven</h1>
<div class="main">
<!-- <canvas> 元素用于在网页上绘制图形和动画。 -->
<canvas id="canvas"></canvas>
<!-- controls 属性表示显示播放控件,如播放/暂停按钮等 -->
<video src="./mv.mp4" controls width="720" height="480" id="video"></video>
</div>
<div class="content">
<input type="text" id="text">
<input type="button" id="btn" value="发弹幕">
<input type="color" id="color">
<input type="range" id="range" min="20" max="40">
</div>
</div>
<script src="./index.js"></script>
</body>
</html>
input类型汇总
这里我们看到input有很多类型type,这里来整理一下:
下面是一些常见的 <input> 类型及其例子:
-
type="text"- 最常见的用途,文本输入框,用于输入一行文本。
- 示例:
<input type="text" placeholder="Enter your name">
2.
type="password"
- 密码输入框,输入的字符会被隐藏起来。
- 示例: `<input type="password" placeholder="Enter your password">`
3.
type="submit"
- 提交按钮,用于提交表单数据。
- 示例: `<input type="submit" value="Submit">`
4. type="reset"
- 重置按钮,用于清空表单中的所有字段。
- 示例: `<input type="reset" value="Reset">`
5. type="button"
- 普通按钮,通常与 JavaScript 结合使用。
- 示例: `<input type="button" value="Click Me" onclick="alert('Button clicked!')">`
-
6.
type="checkbox"
- 复选框,可以同时选择多个选项。
- 示例: `<input type="checkbox" name="option" value="A">选项A`
7.
type="radio"
- 单选按钮,同一组中只能选择一个选项。
- 示例:
```html
<input type="radio" name="gender" value="male"> 男
<input type="radio" name="gender" value="female"> 女
```
8.
type="file"
- 文件上传输入框,用于选择文件。
- 示例: `<input type="file" accept="image/*">`
9.
type="hidden"
- 隐藏字段,用于存储不希望用户看到的信息。
- 示例: `<input type="hidden" name="user_id" value="12345">`
10. type="email"
- 电子邮件地址输入框,自动验证格式是否正确。
- 示例: `<input type="email" placeholder="Enter your email">`
11. type="url"
- URL 输入框,自动验证格式是否正确。
- 示例: `<input type="url" placeholder="Enter your website URL">`
12. type="tel"
- 电话号码输入框,可以自动识别格式。
- 示例: `<input type="tel" placeholder="Enter your phone number">`
13. type="number"
- 数字输入框,只允许输入数字。
- 示例: `<input type="number" min="1" max="10">`
14. type="range"
- 滑动条,用于设置数值范围。
- 示例: `<input type="range" min="1" max="100">`
15.
type="date"
- 日期选择器,可以选择日期。
- 示例: `<input type="date">`
16. type="month"
- 月份选择器,可以选择月份和年份。
- 示例: `<input type="month">`
17.
type="week"
- 星期选择器,可以选择星期和年份。
- 示例: `<input type="week">`
18.
type="time"
- 时间选择器,可以选择时间。
- 示例: `<input type="time">`
19.
type="datetime-local"
- 日期时间选择器,可以选择日期和时间。
- 示例: `<input type="datetime-local">`
20.
type="search"
- 搜索输入框,类似于文本输入框但带有放大镜图标。
- 示例: `<input type="search" placeholder="Search">`
21.
type="color"
- 颜色选择器,可以选择颜色。
- 示例: `<input type="color">`
真的有好多类型,html部分过了之后我们看js部分:
JavaScript部分
数据分析
首先我们知道,弹幕有别人发的,有自己发的,弹幕的属性包括value(文字内容)、time(显示时间点)、color(颜色)、speed(移动速度)和 fontSize(字体大小)。
let data = [
{value: 'JJK~', time: 5, color: 'red', speed: 1, fontSize: 22 },
{value: '小妞,想你', time: 10, color: '#00a1f5', speed: 1, fontSize: 30 },
{value: '听不懂思密达', time: 6},
{value: '田柾国你小子', time: 20, color: '#fff', speed: 1, fontSize: 26 },
]
let canvas = document.getElementById('canvas')
let video = document.getElementById('video')
let $text = document.getElementById('text')
let $btn = document.getElementById('btn')
let $color = document.getElementById('color')
let $range = document.getElementById('range')
这里的data包括了之前已经有的弹幕,然后获取弹幕的html属性
类定义
定义好了属性之后我们现在就需要有一个类(CanvasBarrage)负责管理整个弹幕系统,包括初始化、绘制弹幕等,当我们输入弹幕,就就收到这个弹幕的信息进行初始化(new CanvasBarrage(canvas, video, {data}))
CanvasBarrage 类
-
构造函数:
- 初始化
canvas和video元素。 - 设置
canvas的尺寸与video相同。 - 创建
2d上下文。 - 设置默认弹幕选项,并覆盖这些选项以适应提供的数据。
- 将数据转换为
Barrage实例数组。 - 调用
render方法开始渲染过程。
- 初始化
-
render方法:- 清除画布。
- 渲染所有弹幕。
- 使用
requestAnimationFrame递归调用自身以持续更新画面,除非isPaused为true。
-
clear方法:- 清除画布的内容。
-
renderBarrage方法:- 根据视频的当前时间检查哪些弹幕应该显示,并更新它们的位置。
- 如果弹幕已经离开屏幕,则将其标记为已显示完成。
-
add方法:- 向弹幕数组添加新的弹幕实例。
// 弹幕绘制的准备工作
class CanvasBarrage {
//函数的形参是可以添加默认值的
constructor(canvas, video, opts={}) {
if (!canvas || !video) return //没有视频发个锤子弹幕,返回
//如果想让这些属性在别的函数里访问到,就得挂在this上,想要过海,就得坐船
this.video = video
this.canvas = canvas
this.canvas.width = video.width
this.canvas.height = video.height
// 创建一个2d的画布
this.ctx = canvas.getContext('2d')
// 弹幕的默认值
let defOpts = {
color: '#e91e63',
speed: 1,
opacity: 0.5,
fontSize: 20,
data: []
}
//把opts合并到defOpts上
Object.assign(this, defOpts, opts)
// 默认暂停状态为true
this.isPaused = true
// 得到所有初始化后的弹幕
this.barrages = this.data.map(item => new Barrage(item, this))
this.render()
}
render() {
this.clear()
// 渲染弹幕
this.renderBarrage()
// 递归
if (!this.isPaused) {
requestAnimationFrame(this.render.bind(this))
}
}
//画布自清洁
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
renderBarrage() {
// 获取到视频的播放时间
let time = this.video.currentTime
this.barrages.forEach(barrage => {
if (time >= barrage.time && !barrage.flag) {
if (!barrage.isInit) { // 没有初始化
barrage.init()
barrage.isInit = true
}
barrage.x -= barrage.speed
barrage.render()
// 边界
if (barrage.x < -barrage.width) {
barrage.flag = true // 弹幕已经出去了
}
}
})
}
add(obj) {
this.barrages.push(new Barrage(obj, this))
}
}
Barrage 类
Barrage 类代表单个弹幕,负责初始化弹幕的具体属性和绘制弹幕。
-
构造函数:
- 初始化弹幕的基本属性。
-
init方法:- 计算弹幕的样式属性,如颜色、速度、不透明度和字体大小。
- 计算弹幕的宽度。
- 设置弹幕的初始位置(在屏幕右侧)。
-
render方法:- 在画布上绘制弹幕。
class Barrage{
constructor(obj, context) {
this.value = obj.value
this.time = obj.time
this.obj = obj
this.context = context
}
init() {
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
// 计算每一条弹幕的宽度
let p = document.createElement('p')
p.style.fontSize = this.fontSize + 'px'
p.innerHTML = this.value
document.body.appendChild(p)
this.width = p.clientWidth
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, this.x, this.y)
}
}
事件监听器
- 视频播放时,通过监听
play事件,设置isPaused为false并重新开始渲染。
video.addEventListener('play', () => {//视频进行播放就开始渲染
canvasBarrage.isPaused = false //在播放
canvasBarrage.render() //就渲染
})
- 点击按钮时,获取用户输入的文字、颜色和字体大小,创建一个新的弹幕并添加到列表中。
$btn.addEventListener('click', () => {
let value = $text.value
let time = video.currentTime
let color = $color.value
let fontSize = $range.value
let obj = {value, time, color, fontSize}
canvasBarrage.add(obj)
})
完整js代码:
let data = [
{value: 'JJK', time: 5, color: 'red', speed: 1, fontSize: 22 },
{value: '小妞,想你', time: 10, color: '#00a1f5', speed: 1, fontSize: 30 },
{value: '听不懂思密达', time: 6},
{value: '田柾国你小子', time: 20, color: '#fff', speed: 1, fontSize: 26 },
]
let canvas = document.getElementById('canvas')
let video = document.getElementById('video')
let $text = document.getElementById('text')
let $btn = document.getElementById('btn')
let $color = document.getElementById('color')
let $range = document.getElementById('range')
// 弹幕绘制的准备工作
class CanvasBarrage {
//函数的形参是可以添加默认值的
constructor(canvas, video, opts={}) {
if (!canvas || !video) return //没有视频发个锤子弹幕,返回
//如果想让这些属性在别的函数里访问到,就得挂在this上,想要过海,就得坐船
this.video = video
this.canvas = canvas
this.canvas.width = video.width
this.canvas.height = video.height
// 创建一个2d的画布
this.ctx = canvas.getContext('2d')
// 弹幕的默认值
let defOpts = {
color: '#e91e63',
speed: 1,
opacity: 0.5,
fontSize: 20,
data: []
}
//把opts合并到defOpts上
Object.assign(this, defOpts, opts)
// 默认暂停状态为true
this.isPaused = true
// 得到所有初始化后的弹幕
this.barrages = this.data.map(item => new Barrage(item, this))
this.render()
}
render() {
this.clear()
// 渲染
this.renderBarrage()
// 递归
if (!this.isPaused) {
requestAnimationFrame(this.render.bind(this))
}
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
renderBarrage() {
// 获取到视频的播放时间
let time = this.video.currentTime
this.barrages.forEach(barrage => {
if (time >= barrage.time && !barrage.flag) {
if (!barrage.isInit) { // 没有初始化
barrage.init()
barrage.isInit = true
}
barrage.x -= barrage.speed
barrage.render()
// 边界
if (barrage.x < -barrage.width) {
barrage.flag = true // 已经出去了
}
}
})
}
add(obj) {
this.barrages.push(new Barrage(obj, this))
}
}
// 初始化每一条弹幕
class Barrage{
constructor(obj, context) {
this.value = obj.value
this.time = obj.time
this.obj = obj
this.context = context
}
init() {
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
// 计算每一条弹幕的宽度
let p = document.createElement('p')
p.style.fontSize = this.fontSize + 'px'
p.innerHTML = this.value
document.body.appendChild(p)
this.width = p.clientWidth
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, this.x, this.y)
}
}
let canvasBarrage = new CanvasBarrage(canvas, video, {data})
video.addEventListener('play', () => {//视频进行播放就开始渲染
canvasBarrage.isPaused = false //在播放
canvasBarrage.render() //就渲染
})
$btn.addEventListener('click', () => {
let value = $text.value
let time = video.currentTime
let color = $color.value
let fontSize = $range.value
let obj = {value, time, color, fontSize}
canvasBarrage.add(obj)
})
看一下最终效果:
成功实现了弹幕的发送