视频弹幕案例:JavaScript 类与 canvas 的应用

1,517 阅读6分钟

弹幕

在完成视频弹幕案例的过程中运用了JavaScript中的类和canvas标签,首先对它们进行了解后再进行实战。

在ES6之前,JavaScript中没有类的概念,而是通过使用构造函数和原型链模拟类的行为。然而在ES6中,引入了类的语法。

类的用法

例1:

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.eat = function () {
    console.log('I like food')
}
Person.prototype.sex= 'girl'
Person.prototype.addAge = function () {
    this.age++
}
Person.prototype.say = function () {
    console.log('hello');
}
let person =  new Person('张三', 18)

通过创建一个类达到和例1代码所具有一样效果,这样可以快速了解类的用法。

  1. 定义一个Person类。其中 constructor是类的一个特殊方法,用于初始化新创建的实例。在类中定义方法不需要function关键字。

    class Person(){
        constructor(name, age) {//构造函数,会自动被调用
            this.name = name
            this.age = age
        }
    }
    
  2. 为类本身创建静态方法需要使用static 关键字。

    static eat(){
        console.log('I like food')
    }
    
  3. 在类中达到例1中在构造函数的原型上面添加一个属性的效果,可以使用get关键字实现。

    get sex() {//函数名当作属性名
        return 'girl'
    }
    
  4. 在类中达到例1中在构造函数的原型上面添加一个addAge方法的效果,可以使用set关键字实现。但是使用set关键字后,效果会有些不一样。addAge会成为实例对象的属性使用,并且必须要有参数。

    set addAge(val) {//函数名当作属性名
        this.age = val
    }
    
  5. 在类中到达例1中在构造函数的原型上面添加一个方法的效果,可以直接在类中添加一个方法来实现。

    say() {
            console.log('hello');
        }
    
  6. 这两种方法创建实例对象的方法是一样的,都是通过new Person()创建。

    let person =  new Person('张三', 18)
    

综上所述Person类的代码如下:

class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    static eat() {
        console.log('I like food');
    }
    get sex() {//函数名当作属性名
        return 'girl'
    }
    set addAge(val) {//函数名当作属性名
        this.age = val
    }
    say() {
        console.log('hello');
    }
}
let person =  new Person('张三', 18)

在类中还可以通过#定义私有变量,这样的变量只能在类中使用。

class Person{
    #count = 1
}

canvas

传送门--拿捏canvas,从实现环形进度条与随机验证码开始 - 掘金 (juejin.cn)

在这篇文章里介绍了一些常用的canvas用法。如果看一遍没了解,那就再看一遍。

实战

html部分

<div class="wrap">
     <h1>周杰伦--听妈妈的话</h1>
     <div class="main">
         <canvas id="canvas" style="position: absolute">></canvas>
         <video id="video" src="./mv.mp4" controls width="720" height="480"></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>

效果如图:

屏幕截图 2024-06-11 124149.png

  1. 通过给canvas标签添加绝对定位的样式,让其脱离文档流,这样才能使通过canvas标签绘制的弹幕可以漂浮在视频上。
  2. 设置一个文本输入框接收要发送的弹幕。
  3. 设置一个按钮,当点击按钮时发送弹幕。
  4. 设置一个颜色选择输入框,用于选择设置要发送的弹幕颜色。
  5. 设置一个滑动输入框,用于选择设置要发送的弹幕字体大小。其中字体大小最小值为20,最大值为40。

JavaScript部分

//定义弹幕的数据结构,每一条弹幕可以包含内容、开始时间、弹幕颜色、移动速度和弹幕字体大小。
let data = [
    { value: '周杰伦的听妈妈的话我听了10年', 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: 30 },
]

//获取页面中的元素
let canvas = document.getElementById('canvas')//获取弹幕画布元素
let ctx = canvas.getContext('2d')//获取绘图的2D渲染上下文
let video = document.getElementById('video')//获取视频元素
let $text = document.getElementById('text')//获取输入弹幕的文本框元素
let $color = document.getElementById('color')//获取颜色选择输入框元素
let $range = document.getElementById('range')//获取滑动输入框元素
let $btn = document.getElementById('btn')//获取发送弹幕按钮元素

//弹幕绘制的准备工作
class CanvasBarrage {
    constructor(canvas, video, opts = {}) {
        if (!canvas || !video) return//如果创建实例对象时没有传入canvas和video参数就会直接返回
        this.canvas = canvas
        this.video = video
        //初始化canvas的尺寸,使其于video的尺寸匹配
        this.canvas.width = video.width
        this.canvas.height = video.height
        this.ctx = canvas.getContext('2d')
        //设置弹幕的默认值
        let defOpts = {
            color: '#e91e63',//默认颜色
            speed: 1, //默认弹幕移动速度
            opacity: 0.5,//默认弹幕透明度
            fontSize: 20,//默认弹幕字体大小
            data: []//默认弹幕数据
        }
        Object.assign(this, defOpts, opts)//合并弹幕的默认值和用户传入的参数
        this.isPaused = true//默认的暂停状态为true
        this.barrages = this.data.map(item => new Barrage(item, this))//创建Barrage实例集合
        this.render()//开始渲染
    }
    //渲染函数,通过requestAnimationFrame实现动画
    render() {
        this.clear()//清除画布
        this.renderBarrages()//绘制弹幕
        //递归调用,直到视频停止播放的时候结束
        if (!this.isPaused) {
            requestAnimationFrame(this.render.bind(this))
        }
    }
    //清理函数,用于清理画布的内容
    clear() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    }
    //绘制弹幕函数,绘制所有的函数
    renderBarrages() {
        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//引用CanvasBarrage实例
    }
    //初始化弹幕的属性
    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//弹幕字体大小

        //计算每一条弹幕的宽度。通过创建一个p标签,然后将弹幕文本放入p标签中然后获取宽度然后赋值给this.width,然后移除p标签。
        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.fillStyle = this.color
        this.context.ctx.font = `${this.fontSize}px Arial`
        this.context.ctx.fillText(this.value, this.x, this.y)
    }
}
//实例化CanvasBarrage,并处理视频播放事件和发送弹幕按钮的点击事件
let canvasBarrage = new CanvasBarrage(canvas, video, { data: data })
video.addEventListener('play', function () {
    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)
})

代码看起来比较复杂,如果一遍没看懂就多看几遍。

大致思路流程:

  1. 创建了一个data数组,里面存储的每一个元素都是一条弹幕的基本信息。
  2. 定义CanvasBarrage类处理弹幕相关的操作:
    • 在构造函数constructor中,将画布的大小设置为视频的大小;合并默认弹幕信息和用户自定义的弹幕信息,使弹幕信息保持完整;数据中的每条信息转换为 Barrage 对象并存入 barrages 数组。
    • 定义一个clear方法用于清空canvas,确保每次绘制前画布是干净的
    • 定义render 方法用于绘制弹幕,其中包括清除画布、根据视频当前时间绘制未出界的弹幕,并通过递归保持不断绘制,直到视频停止时结束递归。
    • 定义renderBarrages方法遍历每一条弹幕,检查其是否到达出现时间,然后更新弹幕的位置并调用render方法绘制弹幕。如果弹幕移出了画布,则标记为已经结束。
    • 定义add函数添加新的弹幕到渲染列表里。
  3. 定义Barrage类处理每条弹幕的具体操作:
    • 通过构造函数接受弹幕信息和上下文。
    • 封装init方法初始化弹幕的颜色、速度、字体大小和透明度,并且通过利用p标签计算弹幕的宽度;设置弹幕的起始位置。
    • render 方法负责在画布上渲染弹幕。
  4. 当用户播放视频时,弹幕开始渲染,并且当用户发送弹幕时动态添加新的弹幕。

最终效果

动画.gif