从canvas到B站弹幕

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()
    }
    
})
  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. 使用:
    • 当视频播放时,弹幕系统开始渲染,并且用户可以使用提供的输入元素添加新的弹幕

效果如下:


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

相关推荐
qq_364371721 小时前
Vue 内置组件 keep-alive 中 LRU 缓存淘汰策略和实现
前端·vue.js·缓存
y先森2 小时前
CSS3中的弹性布局之侧轴的对齐方式
前端·css·css3
new出一个对象5 小时前
uniapp接入BMapGL百度地图
javascript·百度·uni-app
你挚爱的强哥6 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森7 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy7 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189117 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿8 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡9 小时前
commitlint校验git提交信息
前端
虾球xz10 小时前
游戏引擎学习第20天
前端·学习·游戏引擎