[小Demo]Canvas手写b站弹幕效果!塔塔开哟!

最近,《进击的巨人》动漫也是迎来了大结局!不知道这样的结局是大家所期望的结局,还是心中的"意难平!"

今天我们就来做一个Demo,来和我们的艾伦塔塔开哟!

大家都看过B站上的"弹幕飞天","弹幕护体"吧

我们今天就来带大家用JS中的canvas画布手搓一个弹幕效果!

其实要这个效果就是在我们的视频上方添加了一层我们看不到的canvas画布!然后通过canvas实现这样一个弹幕满天飞的效果!

什么是canvas?

Canvas API 提供了一个通过JavaScriptHTML``元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

Canvas API 主要聚焦于 2D 图形。而同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的 2D 和 3D 图形。

我们可以通过canvas在页面上绘制我们想要的图形!今天我们先浅浅介绍一下我们实现弹幕需要了解的内容!

首先,我们可以在htmlbody中加入一个canvas闭标签

html 复制代码
<canvas id="canvas" width="300" height="300">

canvas是一个闭标签,因为它也是媒体标签 ,所以可以直接在标签里面设置宽和高 ,并且不需要加单位!

我们可以实现Dom结构->Html结构来实现我们的弹幕效果!

并且canvas自身带了一些方法!

DOM结构: DOM是在浏览器中生成的文档对象模型,它通过JavaScript等脚本语言提供了一种可以动态访问和修改HTML文档结构的方式。DOM以树形结构表示文档,每个HTML元素都是DOM树中的一个节点。通过DOM,你可以通过JavaScript来操纵HTML文档的内容、结构和样式。

HTML结构: HTML(Hypertext Markup Language)是一种标记语言,用于创建网页的结构。HTML文档的基本结构通常包含DOCTYPE声明、html、head和body标签等。

注意!画布没有文档流的规则,得依靠坐标!

js 复制代码
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");/*创建一个2d的画布*/

我们要获取到canvas的上下文才能对它进行操作,同时用getContext规定使用2D画布

2D绘图

1、绘制文本

js 复制代码
ctx.font = '30px sans-serif';//字体大小 和 字体类型
ctx.fillStyle ='red'//颜色
//实心文本
ctx.fillText('今天,你学习了嘛?',10,100)//填充文本 内容 点的位置(x,y)
//空心文本   注意 ctx.stroke描绘没有填充色的内容
ctx.strokeText('我学习了',10,200)

2、绘制矩形

js 复制代码
// 绘制矩形
ctx.fillStyle = "red";
// 绘制实心矩形
ctx.fillRect(50, 50, 100, 80);
// 绘制空心矩形
ctx.strokeRect(200, 200, 100, 80);

3、绘制圆

js 复制代码
// 绘制圆形
ctx.beginPath();
ctx.arc(200, 50, 30, 0, 2 * Math.PI);
ctx.fillStyle = "blue";
ctx.fill();

4、连线

js 复制代码
// 路径的开始
ctx.beginPath();
// moveTO 50,50是起始位置
ctx.moveTo(50,50)
//lineTo 目标位置
ctx.lineTo(200,50)
ctx.lineTo(200,200)
ctx.closePath();//闭合路径,闭合三个点
ctx.stroke()//绘制,描绘一下,之前是临摹
ctx.fill()//为封闭的图形填充颜色

好了,关于canvas的用法就介绍到这里!接下来就开始,我们今天的正题!

实现弹幕效果

整个效果的实现只需要一个html文件和一个js文件

1、HTML文件

html文件还是很简单的!

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;
        }
    </style>
</head>
<body>
    <div class="wrap">
        <h1>进击的巨人</h1>
        <div class="main">
            <canvas id="canvas"></canvas>
            <video src="./进击的巨人.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>
    <!-- js的执行会阻塞html的执行 -->
    <script src="./index.js">

    </script>
</body>
</html>

这里,我们唯一要注意的就是:我们要在最后再导入js,因为js的存在会阻塞html的执行!

我们body当中的主要成分:

  • 包含一个标题 <h1> 和三个 div 元素。
  • 第一个 div 元素有类名为 "wrap",包含页面的主要内容。
  • 第二个 div 元素有类名为 "main",包含一个 <canvas> 元素和一个 <video> 元素,用于显示视频和弹幕。
  • <canvas> 元素用于绘制弹幕。
  • <video> 元素用于显示视频,设置了视频源、控件(播放器控制条)以及宽度和高度。
  • 第三个 div 元素有类名为 "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"> 用于调整弹幕大小。

2、JS文件

js 复制代码
// window.onload保证页面加载完再执行js
// window.onload = function() {

// }

//用new每一次运行一次都会new一下,工厂模式!

//历史弹幕
let data = [
    {value:'口里哇,自由大',color:'red',fontSize:30,time:5},
    {value:'男神',color:'blue',fontSize:26,time:5},
    {value:'塔塔开哟',color:'red',fontSize:22,time:5},
    {value:'一抖哟',color:'green',fontSize:22,time:10},
    {value:'男神',color:'yellow',fontSize:26,time:15},
    {value:'艾伦~!!!',color:'yellow',fontSize:26,time:20},
    {value:'自由嗒',color:'red',fontSize:25,time:16},

    {},
]
//整理弹幕数据 
CanvasBarrage.prototype.render = function() {
    //清除画布
    this.clear()
    this.renderBarrage()//弹幕绘制再画布上
    if(!this.isPaused) {//播放状态
        requestAnimationFrame(this.render.bind(this))//这个也是定时器 递归,无限循环
        
    }
}
CanvasBarrage.prototype.clear = function() {
    this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)//清除画布
}

CanvasBarrage.prototype.renderBarrage = function() 
{
    //拿到视频播放的时间,当前,根据播放的时间判断要不要绘制上
    let time=this.video.currentTime

    //遍历所有的弹幕
    this.barrages.forEach(function(barrage){

    
        if(time>=barrage.time){
            //执行一遍后,数组里面所有的值都会重新被初始化,下面让被初始化的弹幕对象就不再操作
         if(!barrage.isInit&&!barrage.flag){
            barrage.init()
            barrage.isInit=true
         }
         //控制弹幕左移
         barrage.x-=barrage.speed
         barrage.renderEach()
         if(barrage.x<-barrage.width){//从左边出屏幕
            barrage.flag = true
         }
 
        }
    })
    
}
CanvasBarrage.prototype.add = function(obj){
    //添加新的弹幕
    this.barrages.push(new Barrage(obj,this))//this.barrages新增16.7ms之后会重新渲染一波
}


function CanvasBarrage(canvas,video,opts = {}) {
    if(!canvas||!video) return 
    // this是实例对象
    this.video = video
    this.canvas = canvas
    // 设置Canvas宽高和video保持一致
    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:[]
    }
    Object.assign(this,defOpts,opts)//直接往this身上挂key值

    //视频播放,弹幕才出现
    this.isPaused = true
    //获取到所有的弹幕
    //map会返回一个新的数组
    this.barrages = this.data.map((item)=>new Barrage(item,this))//将每一条弹幕都修饰一下
    //移动弹幕
    this.render()


}



// let canvas = document.getElementById('canvas')

// new CanvasBarrage(canvas,video,{value:''})

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')

$btn.addEventListener('click', send)
$text.addEventListener('keyup', function(e){
    console.log(e)
    if(e.keyCode===13)
    {
    send()
    }
})
//添加弹幕数据
//创建整理弹幕的实例对象
let canvasBarrage = new CanvasBarrage(canvas,video,{data})
video.addEventListener('play', function(){
    canvasBarrage.isPaused = false

    canvasBarrage.render()//处理每一条弹幕!
})


function send(){
    //读取文本内容
    let value = $text.value
    //当前视频播放时间
    let time = video.currentTime
    let color = $color.value
    let fontSize= $range.value
    let obj = {value:value,color:color,fontSize:fontSize,time:time}
    //add吧obj放进去,接收新的弹幕,再执行一遍
    canvasBarrage.add(obj)

    // console.log(obj)
}
//修饰一条弹幕,为箭头函数服务
//第一个参数是一条弹幕,第二个是上一个函数的this
//init修饰的意思
Barrage.prototype.init = function(){
    this.color = this.obj.color||this.context.color//如果前者为undefined则会读取第二个
    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
    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.prototype.renderEach = function(){
    //这条弹幕绘制在画布上
    //设置画布的文字字体和字号
    //设置画布的文字颜色
    //绘制文字
    this.context.ctx.font = `${this.fontSize}px Arial`
    this.context.ctx.fillStyle = this.color
    this.context.ctx.fillText(this.value,this.x,this.y)
}
function Barrage(obj,context){//context == this
    this.value = obj.value//新弹幕的内容
    this.time = obj.time
    this.obj = obj
    this.context = context
}

js文件还是比较长的!但是每一处我都标注了作用哦!大家可以直接c + v尝试!

上效果

这样我们的效果就实现啦!其实还有很多可以优化的地方!小伙伴们可以尽情去尝试哦!

看到这里了!--点个赞再走呗!🥺🥺🥺

个人gitee库:MycodeSpace: 主要应用的仓库,记录学习coding中的点点滴滴 (gitee.com)

相关推荐
-seventy-4 分钟前
对 JavaScript 原型的理解
javascript·原型
&白帝&22 分钟前
uniapp中使用picker-view选择时间
前端·uni-app
谢尔登29 分钟前
Babel
前端·react.js·node.js
ling1s29 分钟前
C#基础(13)结构体
前端·c#
卸任35 分钟前
使用高阶组件封装路由拦截逻辑
前端·react.js
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
秋沐1 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
这个需求建议不做1 小时前
vue3打包配置 vite、router、nginx配置
前端·nginx·vue
QGC二次开发1 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
云草桑1 小时前
逆向工程 反编译 C# net core
前端·c#·反编译·逆向工程