• 作者:老汪软件技巧
  • 发表时间:2024-08-28 21:04
  • 浏览量:

源码

/buglas/canv…

学习目标知识点1-题目解析

弹幕就是有文字飞过的屏幕,文字就是弹幕里的子弹,其效果如下:

题目的已知条件:

time 是弹幕文字出现的时间,只是一个相对于视频时长的时间。

text 是弹幕文字的内容。

弹幕数据是按照time 升序排列的。

题目要求:

根据弹幕文字的出现时间将文字显示在视频中,文字不可被其它文字遮挡。

解题思路:

如果当前的文字覆盖了上一个文字,那就换行显示当前文字。

这个题很简单,咱们接下来直接说代码。

2-BulletScreen 弹幕对象

BulletScreen对象可以根据视频的尺寸建立一张同样大小的canvas画布,然后将弹幕文字显示在canvas画布中。

之后在HTML中把canvas画布覆盖到video 上即可。

BulletScreen对象的整体代码如下:

import { Scene } from "../lmm/core/Scene"
import { TextGraph2D } from "../lmm/objects/TextGraph2D"
import { TextStyle } from "../lmm/style/TextStyle"
type BulletDataType = {
    // 出现时间
    time: number
    // 文字内容
    text: string
}
class BulletScreen{
  // dom容器
  video:HTMLVideoElement
  // 视频初次播放的时间
  startTime=0
  // 视频最近一次播放的时间
  playTime=0
  // 视频最近一次暂停播放的时间
  pauseTime=0
  // 视频累计暂停的时间长度
  pauseTimeLen=0
  // 播放状态
  state:'pause'|'play'|'finish'='pause'
  // 文字尺寸
  fontSize=18
  // 文字样式
  textStyle=new TextStyle({
    fontSize:this.fontSize,
    fillStyle: '#fff',
    strokeStyle: '#000',
    lineWidth: 2,
    textBaseline: 'top',
  })
  // 弹幕速度,像素/ms
  velocity=0.1
  // 弹幕的行高,默认字体的1.5倍
  lineHeight=1.5
  // 弹幕的右间距,默认字体的2倍
  rightPadding=2
  // 弹药库
  magazine:TextGraph2D[]=[]
  // 弹药库中要装入弹夹的子弹的索引
  lastBulletIndex=0
  // 场景,弹药库中的子弹会在特定时间飞入场景,并在飞出屏幕时,从场景中删除
  scene=new Scene()
  // 动画帧,可在暂停视频时,取消动画帧的请求,从而实现弹幕的暂停
  frame=0
  constructor(video:HTMLVideoElement,data:BulletDataType[]=[]){
    this.video=video
    this.initScene(video)
    this.createBullets(data)
  }
  // 初始化场景
  initScene(video:HTMLVideoElement){
    const {clientWidth,clientHeight}=video
    const {scene:{camera,canvas}}=this
    // 时画布尺寸与视频一致
    canvas.width=clientWidth
    canvas.height=clientHeight
    // 将弹幕显示范围设置为[0,0]到[clientWidth,clientHeight]
    camera.position.set(clientWidth/2,clientHeight/2)
  }
  // 根据弹幕数据,创建子弹对象TextGraph2D,并将其放到弹药库magazine中。
  createBullets(data:BulletDataType[]){
    const {textStyle,video:{clientWidth:videoWidth},velocity,fontSize,lineHeight,rightPadding,magazine}=this
    // 弹道,存储每个弹道中最新加入的文字完全显示的时间
    const passes: number[] = []
    // 遍历子弹数据
    data.forEach(({ time, text }) => {
      // 建立文字对象
      const text2D = new TextGraph2D(text,textStyle)
      // 文字宽度
      const textWidth=text2D.width
      // 在文字对象上挂载其它文字相关的数据
      text2D.userData={
        // 文字开始时间,文字开始飞入屏幕
        startTime:time,
        // 文字结束时间,文字飞出屏幕时结束
        endTime:time+(videoWidth+textWidth)/velocity,
      }
      // 从0开始遍历文字的弹道,寻找有空位的弹道,然后将文字放到相应弹道上
      let i=0;
      const len=passes.length
      for (i; i < len; i++) {
        // 若当前文字的显示时间大于当前弹道中最新放入的文字的完全显示的时间,
        // 说明此弹道中可以放入当前文字
        if (time > passes[i]) {
          break;
        }
      }
      // 若没有有空位的弹道,再开一个新的弹道
      (len&&i===len-1)&&(i=len)
      // 根据弹道设置文字高度
      text2D.position.y = i * fontSize*lineHeight
      // 将当前文字完全显示到屏幕中的时间放到相应弹道中
      // 以便让后面的文字判断其是否可以显示于此弹道中
      passes[i]=time+(textWidth+rightPadding*fontSize)/velocity
      // 将文字添加到弹药库中,以便在特定时间射入scene场景
      magazine.push(text2D)
    })
  }
  // 播放
  play(){
    // 记录播放状态
    this.state='play'
    const now=new Date().getTime()
    // 初始播放时间
    this.playTime||(this.playTime=now)
    // 当前播放时间
    this.startTime=now
    // 记录暂停总时长
    this.pauseTime&&(this.pauseTimeLen+=now-this.pauseTime)
    // 让子弹飞
    this.shot()
  }
  // 暂停
  pause(){
    // 记录播放状态
    this.state='pause'
    // 记录暂停时间
    this.pauseTime=new Date().getTime()
    // 取消动画帧的请求
    cancelAnimationFrame(this.frame)
  }
  // 播放结束
  ended(){
    this.playTime=0
    this.startTime=0
    this.pauseTime=0
    this.pauseTimeLen=0
    this.lastBulletIndex=0
    // 清空场景
    this.scene.clear()
  }
  // 让子弹飞一会
  shot(){
    const {lastBulletIndex,magazine,playTime,pauseTimeLen,scene,video:{clientWidth}}=this
    // 视频播放的时长
    const playTimeLen=new Date().getTime()-playTime-pauseTimeLen
    // 若弹药库中有子弹,尝试将子弹射入scene场景
    if(lastBulletIndex1){
      // 子弹
      const bullet=magazine[lastBulletIndex]
      // 子弹射出的时间
      const {userData:{startTime}}=bullet
      // 若当前子弹到了需要登场的时间
      if(playTimeLen>startTime){
        // 将子弹射入scene场景
        scene.add(bullet)
        // 记录弹药库中下一个要射出的子弹
        this.lastBulletIndex+=1
      }
    }
    
    // 让子弹飞
    scene.traverse(obj=>{
      const {scene,velocity}=this
      const {userData:{endTime,startTime}}=obj
      // 移动子弹,
      obj.position.x=clientWidth-(playTimeLen-startTime)*velocity
      // 若子弹飞出屏幕,从场景中删掉子弹
      if(playTimeLen>endTime){
        scene.remove(obj)
      }
    })
    // 渲染
    scene.render()
    // 请求动画帧
    this.frame=requestAnimationFrame(()=>{this.shot()})
  }
}
export {BulletScreen}

简单说一下其思路。

1.初始化操作。

在构造函数中传入要添加弹幕的视频和弹幕数据。

根据视频建立与视频相同尺寸的场景对象Scene。

根据弹幕数据建立文字对象TextGraph2D。

在文字对象上挂载文字出现在屏幕中的开始时间startTime 和结束时间endTime,以备后用。

开始时间和结束时间如下图所示:

b站弹幕前方高能特效__b站弹幕实现原理

根据前后两个文字的完全显示时间和开始显示时间,设置后一个文字的高度,从而避免文字的覆盖。

2.当播放视频的时候,显示相应时间的弹幕,使弹幕自视频右侧向左侧匀速移动。

当文字开始显示时间大于当前视频的视频播放时长时,即可将文字添加到scene场景中,进行匀速运动显示。

当文字结束显示时间小于当前视频的视频播放时长时,即可将文字从scene场景中删除。

在视频播放的过程中,需通过连续请求动画帧实现文字的连续位移。

文字的位置=屏幕宽度-(视频播放时长-文字开始显示时间)*速度

视频播放时长=当前时间-视频初次播放时间-视频暂停总时长

3.当暂停时视频的时候,记录暂停时间,取消请求动画帧。

暂停时间可以用于在下次播放视频的时候统计视频暂停时长;

取消请求动画帧可以暂停文字动画。

4.当视频播放完成时,将所有数据恢复到初始状态,清空场景。

3-实例化弹幕对象

建立vue页面,在其中搭建视频场景,实例化弹幕对象。

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { randChar } from '../component/Utils'
import { BulletScreen } from '../component/BulletScreen'
// 获取父级属性
defineProps({
    size: { type: Object, default: { width: 0, height: 0 } },
})
const videoRef = ref<HTMLVideoElement>()
const canvasWrapperRef = ref<HTMLDivElement>()
/* 弹幕假数据 */
const bullets = Array.from({ length: 40 }, () => {
    return {
    // 显示时间
        time: Math.floor(Math.random() * 9000),
    // 文字内容-随机长度的字符串
        text: randChar(Math.random() * 10 + 3),
    }
}).sort((a, b) => {
  // 按显示时间排序
    return a.time - b.time
})
// 弹幕对象
let bulletScreen:BulletScreen
onMounted(() => {
    const video = videoRef.value
    const canvasWrapper = canvasWrapperRef.value
    if (video&&canvasWrapper) {
    // 实例化弹幕
    bulletScreen=new BulletScreen(video,bullets)
    // 将弹幕的canvas画布添加到canvas容器中
    canvasWrapper.append(bulletScreen.scene.canvas)
  }
})
script>
<template>
    <div id="videoCont" >
    
        <video 
      controls
      name="media"
      width="640"
      ref="videoRef"
      @play="bulletScreen.play"
      @pause="bulletScreen.pause"
      @ended="bulletScreen.ended"
    >
      <source 
        src="https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/canvas-lesson/move01.mp4" 
        type="video/mp4"
      >
    video>
    
    <div id="canvasWrapper" ref="canvasWrapperRef">div>
    div>
template>
<style scoped>
#videoCont {
    position: relative;
  display: flex;
  justify-content:center;
  margin-top: 36px;
}
#canvasWrapper {
    position: absolute;
  pointer-events: none;
}
style>

解释一下上面的代码。

1.弹幕文字是通过randChar() 方法生成的随机长度的字符串,没啥好说的。

function randChar(
    length: number,
    characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
) {
    const arr = characters.split('')
    let result = ''
    while (result.length < length)
        result += arr[Math.round(Math.random() * arr.length)]
    return result
}

2.弹幕对象在onMounted中进行了实例化,并将其中的canvas画布放到提前准备好的覆盖在视频上的canvas容器中。

onMounted(() => {
    const video = videoRef.value
    const canvasWrapper = canvasWrapperRef.value
    if (video&&canvasWrapper) {
    // 实例化弹幕
    bulletScreen=new BulletScreen(video,bullets)
    // 将弹幕的canvas画布添加到canvas容器中
    canvasWrapper.append(bulletScreen.scene.canvas)
  }
})

3.在视频的play、pause和ended事件中执行弹幕的相应方法。

        

总结

这个题考的主要是大家对于时间和动画的基本掌控力,整体并不算难。

我在滴滴参与的智驾项目中,动画是必不可少的,比如汽车、相机、标注的运动和暂停等。

现在3个滴滴面试题就算是说完了,但为了精益求精,下一篇我们会说一下第2个面试中的选择优化-包围盒。