canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现
Canvas
我们在body中放一个canvas标签,然后在Script中添加属性
xml
<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
用来画填充矩形。其中四个参数分别为左上角坐标,和右下角坐标,此时效果如下
xml
<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.strokeRect(10,10,55,55)
</script>
strokeRect
是用来画矩形的,只有边框,不会进行填充,stroke这个单词可能大家只知道有中风的意思,其实还有笔画,轻拭的意思,此时效果如下
再来一个自定义描边
scss
<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,否则没有效果
当然,你也可以对其填充
scss
ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.fill()
默认颜色黑色
当然,你也可以画贝塞尔曲线(bezierCurve):不规则的曲线,这个内容我这里不做介绍,方法可以网上自寻搜索
再来画个圆
ini
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.arc(50,50,40,0,2 * Math.PI)
ctx.stroke()
arc
方法用于画圆或圆弧,前两个参数为圆心坐标,第三个参数为圆的半径,第四个参数是起始角度(通常为0,三点钟方向),最后一个参数为终止角度。
绘制文本
ini
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.font = '50px sans-serif'
ctx.fillText('床前明月光',10, 100)
fillText
中后两个参数为起始坐标,strokeText
绘制的是空心字
如果两个fillText
的起始坐标一样,就可以重叠在一起,我现在再加一句同其实坐标的文字
B站弹幕其实就是用的画布,但是实现起来还是比较困难,为了方便文章排版,注释都放在了代码里面
b站弹幕
html部分
xml
<!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部分
kotlin
// 这个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()
}
})
- 数据结构:
data
数组包含表示单个弹幕项的对象。每个对象具有诸如value
(文本内容)、color
、fontSize
和time
(显示时间)等属性。
- CanvasBarrage 类:
CanvasBarrage
是一个构造函数,用于初始化弹幕系统。- 它接受一个画布元素、一个视频元素和可选的配置选项。
- 默认选项(
defOpts
)包括诸如color
、fontSize
、speed
、opacity
和data
等属性。 - 根据提供的数据创建了一个
Barrage
对象的数组。 render
方法负责在画布上渲染和动画弹幕。clear
方法在渲染之前清除画布。
- Barrage 类:
Barrage
是用于单个弹幕项的构造函数。- 它接受一个对象(
obj
)和一个上下文(context
),即CanvasBarrage
的实例。 init
方法使用属性如color
、speed
、opacity
、fontSize
、width
、x
和y
初始化弹幕项。renderEach
方法在画布上渲染单个弹幕项。
- 渲染和动画:
renderBarrages
方法负责根据当前视频时间渲染所有弹幕项。render
方法使用requestAnimationFrame
不断调用自身以进行连续动画。add
方法允许向系统添加新的弹幕项。
- 事件监听器:
- 对视频的
play
事件监听器触发在视频播放时渲染弹幕。 - 对按钮(
$btn
)的click
事件监听器触发send
函数以添加新的弹幕。 - 对文本输入框(
$text
)的keyup
事件监听器在按下Enter键时触发send
函数。
- 对视频的
- 用户输入处理:
send
函数读取输入值(文本、时间、颜色、fontSize)并创建一个新的弹幕对象,然后将其添加到弹幕系统中。
- 初始化:
- 使用画布、视频和提供的数据创建了
CanvasBarrage
的实例。
- 使用画布、视频和提供的数据创建了
- 使用:
- 当视频播放时,弹幕系统开始渲染,并且用户可以使用提供的输入元素添加新的弹幕
效果如下:
如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]