fly-barrage 前端弹幕库(2):弹幕内容支持混入渲染图片的设计与实现

如果弹幕内容只支持文字的话,只需要借助 canvas 绘图上下文的 fillText 方法就可以实现功能了。 但如果想同时支持渲染图片和文字的话,需要以下几个步骤:

  1. 设计一个面向用户的数据结构,用于描述弹幕应该渲染哪些文字和图片;
  2. 框架内部对上述数据结构进行解析,解析出文字部分和图片部分;
  3. 计算出各个部分相对于弹幕整体左上角的 top 偏移量和 left 偏移量;
  4. 弹幕渲染时,首先计算出弹幕整体左上角距离 canvas 原点的 top 和 left(这块的计算是后续的内容,后续再说),然后再根据弹幕整体的 top 和 left 结合各个部分的 top、left 偏移量循环渲染各个部分。

整体逻辑如下图所示:

相关 API 可以看官网的这里:fly-barrage.netlify.app/guide/barra...

下面着重说说上面几点具体是如何实现的。

1:面向用户的数据结构,用于描述弹幕应该渲染哪些文字和图片

设计的数据结构如下所示:

typescript 复制代码
export type BaseBarrageOptions = {
  // 弹幕的内容(eg:文本内容[图片id]文本内容[图片id]文本内容)
  text: string;
}

例如:"0001新年快乐0003",它的渲染效果就是如下这样子的。

2:对上述结构进行解析,解析出文字以及图片部分

这块对应源码中的 class BaseBarrage --> analyseText 方法,源码如下所示:

typescript 复制代码
/**
 * 弹幕类
 */
export default abstract class BaseBarrage {
  /**
   * 解析 text 内容
   * 文本内容[图片id]文本内容[图片id] => ['文本内容', '[图片id]', '文本内容', '[图片id]']
   * @param barrageText 弹幕文本
   */
  analyseText(barrageText: string): Segment[] {
    const segments: Segment[] = [];

    // 字符串解析器
    while (barrageText) {
      // 尝试获取 ]
      const rightIndex = barrageText.indexOf(']');
      if (rightIndex !== -1) {
        // 能找到 ],尝试获取 rightIndex 前面的 [
        const leftIndex = barrageText.lastIndexOf('[', rightIndex);
        if (leftIndex !== -1) {
          // [ 能找到
          if (leftIndex !== 0) {
            // 如果不等于 0 的话,说明前面是 text
            segments.push({
              type: 'text',
              value: barrageText.slice(0, leftIndex),
            })
          }
          segments.push({
            type: rightIndex - leftIndex > 1 ? 'image' : 'text',
            value: barrageText.slice(leftIndex, rightIndex + 1),
          });
          barrageText = barrageText.slice(rightIndex + 1);
        } else {
          // [ 找不到
          segments.push({
            type: 'text',
            value: barrageText.slice(0, rightIndex + 1),
          })
          barrageText = barrageText.slice(rightIndex + 1);
        }
      } else {
        // 不能找到 ]
        segments.push({
          type: 'text',
          value: barrageText,
        });
        barrageText = '';
      }
    }

    // 相邻为 text 类型的需要进行合并
    const finalSegments: Segment[] = [];
    let currentText = '';
    for (let i = 0; i < segments.length; i++) {
      if (segments[i].type === 'text') {
        currentText += segments[i].value;
      } else {
        if (currentText !== '') {
          finalSegments.push({ type: 'text', value: currentText });
          currentText = '';
        }
        finalSegments.push(segments[i]);
      }
    }
    if (currentText !== '') {
      finalSegments.push({ type: 'text', value: currentText });
    }

    return finalSegments;
  }
}

/**
 * 解析完成的片段
 */
export type Segment = {
  type: 'text' | 'image',
  value: string
}

analyseText 方法的作用就是将 "0001新年快乐0003" 解析成如下数据:

typescript 复制代码
[
  {
    type: 'image',
    value: '[0001]'
  },
  {
    type: 'text',
    value: '新年快乐'
  },
  {
    type: 'image',
    value: '[0003]'
  },
]

这块的核心逻辑是字符串解析器,这里我借鉴了 Vue2 模板编译中解析器的实现(Vue 解析器的解析可以看我的这篇博客:blog.csdn.net/f1885566666...)。

这里我使用 while 不断的循环解析 barrageText 字符串,一旦解析出一块内容,便将其从 barrageText 字符串中裁剪出去,并且将对应的数据 push 到 segments 数组中,当 barrageText 变成一个空字符串的时候,整个字符串的解析也就完成了。

具体的解析过程大家看我的注释即可,很容易理解。

3:计算出各个部分相对于弹幕整体左上角的 top 偏移量和 left 偏移量

这块对应源码中的 class BaseBarrage --> initBarrage 方法,源码如下所示:

typescript 复制代码
/**
 * 弹幕类
 */
export default abstract class BaseBarrage {
  /**
   * 进行当前弹幕相关数据的计算
   */
  initBarrage() {
    const sectionObjects = this.analyseText(this.text);
    let barrageImage;

    // 整个弹幕的宽
    let totalWidth = 0;
    // 整个弹幕的高
    let maxHeight = 0;

    // 计算转换成 sections
    const sections: Section[] = [];
    sectionObjects.forEach(sectionObject => {
      // 判断是文本片段还是图片片段
      if (sectionObject.type === 'image' && (barrageImage = this.br.barrageImages?.find(bi => `[${bi.id}]` === sectionObject.value))) {
        totalWidth += barrageImage.width;
        maxHeight = maxHeight < barrageImage.height ? barrageImage.height : maxHeight;

        // 构建图片片段
        sections.push(new ImageSection({
          ...barrageImage,
          leftOffset: Utils.Math.sum(sections.map(section => section.width)),
        }));
      } else {
        // 设置好文本状态后,进行文本的测量
        this.setCtxFont(this.br.ctx);
        const textWidth = this.br.ctx?.measureText(sectionObject.value).width || 0;
        const textHeight = this.fontSize * this.lineHeight;

        totalWidth += textWidth;
        maxHeight = maxHeight < textHeight ? textHeight : maxHeight;

        // 构建文本片段
        sections.push(new TextSection({
          text: sectionObject.value,
          width: textWidth,
          height: textHeight,
          leftOffset: Utils.Math.sum(sections.map(section => section.width)),
        }));
      }
    })
    this.sections = sections;

    // 设置当前弹幕的宽高,如果自定义中定义了的话,则取自定义中的 width 和 height,因为弹幕实际呈现出来的 width 和 height 是由渲染方式决定的
    this.width = this.customRender?.width ?? totalWidth;
    this.height = this.customRender?.height ?? maxHeight;

    // 遍历计算各个 section 的 topOffset
    this.sections.forEach(item => {
      if (item.sectionType === 'text') {
        item.topOffset = (this.height - this.fontSize) / 2;
      } else {
        item.topOffset = (this.height - item.height) / 2;
      }
    });
  }
}

initBarrage 首先调用 analyseText 方法实现弹幕字符串的解析工作,然后对 analyseText 方法的返回值进行遍历处理。

在遍历的过程中,首先判断当前遍历的片段是文本片段还是图片片段,当片段的 type 是 image 并且对应的图片 id 已有对应配置的话,则表明当前是图片片段,否则就是文本片段。

然后需要根据片段的类型去计算对应片段的宽和高,图片类型的宽高不用计算,因为图片的尺寸是用户通过 API 传递进框架的,框架内部直接取就可以了。文本片段的宽使用渲染上下文的 measureText 方法可以计算出,文本片段的高等于弹幕的字号乘以行高。

各个片段的宽高计算出来之后,开始计算各个片段的 left 偏移量,由于每个计算好的片段都会被 push 到 sections 数组中,所以当前片段的 left 偏移量等于 sections 数组中已有片段的宽度总和。

top 偏移量需要知道弹幕整体的高度,弹幕整体的高度等于最高片段的高度,所以在循环处理 sectionObjects 的过程中,使用 maxHeight 变量判断记录最高片段的高度,在 sectionObjects 循环结束之后,就可以计算各个片段的 top 偏移量了,各个片段的 top 偏移量等于弹幕整体高度减去当前片段实际渲染高度然后除以 2。

4:弹幕渲染时的操作

弹幕渲染时,首先需要计算出弹幕整体左上角的定位,这个是后面的内容,之后再说,这里先假设某个弹幕渲染时整体左上角的定位是(10px,10px),各个片段的 top、left 偏移量已经计算出来了,结合这两块数据可以计算出各个片段左上角的定位。至此,循环渲染出各个片段即可完成整体弹幕的渲染操作,相关源码如下所示:

typescript 复制代码
/**
 * 弹幕类
 */
export default abstract class BaseBarrage {
  // 用于描述渲染时弹幕整体的 top 和 left
  top!: number;
  left!: number;

  /**
   * 将当前弹幕渲染到指定的上下文
   * @param ctx 渲染上下文
   */
  render(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {
    // 设置绘图上下文
    this.setCtxFont(ctx);
    ctx.fillStyle = this.color;
    // 遍历当前弹幕的 sections
    this.sections.forEach(section => {
      if (section.sectionType === 'text') {
        ctx.fillText(section.text, this.left + section.leftOffset, this.top + section.topOffset);
      } else if (section.sectionType === 'image') {
        ctx.drawImage(
          Utils.Cache.imageElementFactory(section.url),
          this.left + section.leftOffset,
          this.top + section.topOffset,
          section.width,
          section.height,
        )
      }
    })
  }
}

5:总结

ok,以上就是弹幕内容支持混入渲染图片的设计与实现,后面说说各种类型弹幕的具体设计。

相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马11 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒11 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端