注册
web

从canvas到B站弹幕


canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现



Canvas


我们在body中放一个canvas标签,然后在Script中添加属性


<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.fillStyle = "green"
ctx.fillRect(10,10,55,50)
</script>
</body>

let ctx = canvas.getContext("2d")其实就是对canvas实例化一个对象。先得到一只2维的画笔,接下来的操作都是针对这只画笔


ctx.fillStyle = "green"给这只画笔沾上墨水,否则怎么画都画不出


fillRext用来画填充矩形。其中四个参数分别为左上角坐标,和右下角坐标,此时效果如下


1.png


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.strokeRect(10,10,55,55)
</script>

strokeRect是用来画矩形的,只有边框,不会进行填充,stroke这个单词可能大家只知道有中风的意思,其实还有笔画,轻拭的意思,此时效果如下


2.png


再来一个自定义描边


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.closePath()
ctx.stroke()
</script>

beginPath就是让画笔落在纸上


moveTo接收的一个起始位置坐标


两个lineTo是终点坐标


closePath将所有点连接起来,stroke开始画,一定要有stroke,否则没有效果


3.png


当然,你也可以对其填充


        ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.fill()

默认颜色黑色


4.png


当然,你也可以画贝塞尔曲线(bezierCurve):不规则的曲线,这个内容我这里不做介绍,方法可以网上自寻搜索


再来画个圆


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.arc(50,50,40,0,2 * Math.PI)
ctx.stroke()

arc方法用于画圆或圆弧,前两个参数为圆心坐标,第三个参数为圆的半径,第四个参数是起始角度(通常为0,三点钟方向),最后一个参数为终止角度。


5.png


绘制文本


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.font = '50px sans-serif'
ctx.fillText('床前明月光',10, 100)

fillText中后两个参数为起始坐标,strokeText绘制的是空心字


6.png


如果两个fillText的起始坐标一样,就可以重叠在一起,我现在再加一句同其实坐标的文字


7.png


B站弹幕其实就是用的画布,但是实现起来还是比较困难,为了方便文章排版,注释都放在了代码里面


b站弹幕


html部分


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#canvas {
position: absolute;
}
/* 宽高不在css设置,是因为是和视频一起变化,动态的,用js */
</style>
</head>
<body>
<div class="wrap">
<h1>Peaky Blinders</h1>
<div class="main">
<canvas id="canvas"></canvas>
<video src="./video.mp4" id="video" controls="true" 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>
<script src="./index.js"></script>
<!-- 执行js 后加载js js可能是本地文件,也可能是在线地址,如果放在开头,执行这个的时候会堵住html的加载,甚至会报错,读取canvas都不知道,如果保证js内容在页面加载完毕后执行也可以放在前面,就是window.onload这个方法 -->
</body>
</html>

js部分


// 这个js代码很难写,一般高级程序员才会这样写
// window.onload = function(){}
// 1.读取用户内容 2.把内容颜色大小放到画布上,绘制
// 历史弹幕,数组(里面放对象)还要接受新的弹幕,到了时间就绘制,要递归
let data = [
{ value: 'By order of the peaky bliears', color: 'red', fontSize: 22, time: 5 },
{ value: 'No Fucking Fighting', color: 'green', fontSize: 30, time: 10},
{ value: 'Fucking Shelby', color: 'black', fontSize: 22, time: 22}
]
// 整理弹幕数据,弹幕的y,历史弹幕问题 形参跟外面的一样没毛病,辨识度更高,代码太多了abc是啥都不知道,都可以 ,形参可以默认值,万一没有传值呢
function CanvasBarrage(canvas, video, opts = {}){
if(!canvas || !video) return
this.video = video
this.canvas = canvas
// 伪代码 canvas 宽高 和 video宽高保持一致
// canvas.width = style.width style读取宽高,js设置宽高
this.canvas.width = video.width
this.canvas.height = video.height
// 获取画布
this.ctx = canvas.getContext("2d")
// 初始化代码
// 没有认为修改弹幕的设置,默认值
let defOpts = {
color: '#e91e63',
fontSize: 20,
speed: 1.5,
// 透明度
opacity: 0.5,
data: []
//value和time不需要默认值
}
Object.assign(this, defOpts, opts)
// 视频播放,弹幕才会进行绘制
this.isPaused = true
// 默认暂停
// 获取到所有的弹幕 map(返回一个新的数组)里面是箭头函数(把item交给一个新的箭头函数) map循环了,每个弹幕都被修饰了一下
this.barrages = this.data.map((item) => new Barrage(item, this))
this.render()
}
Barrage.prototype.init = function(){
// 左边是自己新建的右边是传进来的,如果第一个是没有的,就是给出的默认的颜色
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.offsetWidth
// 放完之后要删掉
document.body.removeChild(p)
// 设置弹幕的位置
this.x = this.context.canvas.width
// y的高度是随机值
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
}
}

// Barrage 修饰一条弹幕 为箭头函数那里服务 (实例对象,this对象)
function Barrage(obj, context){
this.value = obj.value
this.time = obj.time
// 挂在构造函数中后面更方便
this.obj = obj
this.context = context
}

CanvasBarrage.prototype.clear = function(){
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
// 将这条弹幕会绘制在画布上
Barrage.prototype.renderEach = function(){
// canvas绘制过程
// 设置画布的文字字体,字号
// 设置画布的文字颜色
// 绘制颜色
this.context.ctx.font = `${this.fontSize}px Arial`
this.context.ctx.fillStyle = this.color
this.context.ctx.fillText(this.value, this.x, this.y)
}
// 将弹幕绘制到画布上
CanvasBarrage.prototype.renderBarrages = function(){
// 伪代码 拿到视频播放的当前时间,根据时间绘制
let time = this.video.currentTime
// 遍历所有的弹幕
this.barrages.forEach(function(barrage){
// 出屏之外之后就不用再操作了
if(time >= barrage.time && !barrage.flag){
// 这属性没有就是false 这个操作就是为了防止放过了的弹幕不需要初始化
if(!barrage.isInit){
barrage.init()
barrage.isInit = true
}
// 控制弹幕左移
barrage.x -= barrage.speed
// rednerEach相当于ctx.fillstyle
barrage.renderEach()
// 弹幕是有长度的
if(barrage.x < -barrage.width){
barrage.flag = true
}
}
})
}
// 这里就是render ,把弹幕弄到画布中
CanvasBarrage.prototype.render = function(){
// 清除画布,习惯问题
this.clear()
// 要先绘制才能操作画笔,并且要向左移动
this.renderBarrages()
// 播放状态才能移动
if(!this.isPaused){
// setInterval这里不用,下面定时器的更高级,16.7ms(内定时间)之后就执行一次,递归之后就是一直循环下去
requestAnimationFrame(this.render.bind(this))
// bind(this)以后再讲
}
}
// 添加新的弹幕
CanvasBarrage.prototype.add = function(obj){
// barrages是终极数组,data修饰之后的
// this.barrages16.7ms之后也会重修渲染一次
this.barrages.push(new Barrage(obj,this))
}
// 传的参数是canvas和video dom结构 opts是一个对象含value color time fontSize 这个会替代掉,合并对象,相同的覆盖,不同的加进去
let canvas = document.getElementById('canvas')
// video知道此时视频多少秒
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')
// 整理弹幕的实例对象
// 对象里key和value可以直接由{data: data}变成{data}
let canvasBarrage = new CanvasBarrage(canvas, video, {data})
// play是播放,处理所有弹幕实例对象
video.addEventListener('play',function(){
canvasBarrage.isPaused = false
// 处理每一条弹幕,canvasBarrage相当于一个管家
canvasBarrage.render()
})

function send(){
// 读取文本内容
let value = $text.value
// video 自带一个属性读取时间
let time = video.currentTime
let color = $color.value
let fontSize = $range.value
// 把上面的内容整理成一个对象,交给函数去操作
let obj = {
value: value,
color: color,
fontSize: fontSize,
time: time
}
// 多么希望add可以把obj放进去,接收新的弹幕,处理弹幕再走一遍send
canvasBarrage.add(obj)
}
$btn.addEventListener('click', send)
$text.addEventListener('keyup',function(e){

console.log(e);
if(e.keyCode === 13){
send()
}

})


  1. 数据结构:

    • data 数组包含表示单个弹幕项的对象。每个对象具有诸如 value(文本内容)、colorfontSizetime(显示时间)等属性。


  2. CanvasBarrage 类:

    • CanvasBarrage 是一个构造函数,用于初始化弹幕系统。
    • 它接受一个画布元素、一个视频元素和可选的配置选项。
    • 默认选项(defOpts)包括诸如 colorfontSizespeedopacitydata 等属性。
    • 根据提供的数据创建了一个 Barrage 对象的数组。
    • render 方法负责在画布上渲染和动画弹幕。
    • clear 方法在渲染之前清除画布。


  3. Barrage 类:

    • Barrage 是用于单个弹幕项的构造函数。
    • 它接受一个对象(obj)和一个上下文(context),即 CanvasBarrage 的实例。
    • init 方法使用属性如 colorspeedopacityfontSizewidthxy 初始化弹幕项。
    • renderEach 方法在画布上渲染单个弹幕项。


  4. 渲染和动画:

    • renderBarrages 方法负责根据当前视频时间渲染所有弹幕项。
    • render 方法使用 requestAnimationFrame 不断调用自身以进行连续动画。
    • add 方法允许向系统添加新的弹幕项。


  5. 事件监听器:

    • 对视频的 play 事件监听器触发在视频播放时渲染弹幕。
    • 对按钮($btn)的 click 事件监听器触发 send 函数以添加新的弹幕。
    • 对文本输入框($text)的 keyup 事件监听器在按下Enter键时触发 send 函数。


  6. 用户输入处理:

    • send 函数读取输入值(文本、时间、颜色、fontSize)并创建一个新的弹幕对象,然后将其添加到弹幕系统中。


  7. 初始化:

    • 使用画布、视频和提供的数据创建了 CanvasBarrage 的实例。


  8. 使用:

    • 当视频播放时,弹幕系统开始渲染,并且用户可以使用提供的输入元素添加新的弹幕



效果如下:


效果.gif




如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]


作者:Dolphin_海豚
来源:juejin.cn/post/7302310196311719988

0 个评论

要回复文章请先登录注册