V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
imtaotao
V2EX  ›  前端开发

这可能是见过的最好用的弹幕库 🔥🔥

  •  1
     
  •   imtaotao · 89 天前 · 1289 次点击
    这是一个创建于 89 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近把五年前写的一个弹幕库给重构了一下,本来两年前就想着做这件事,但是其中有一段工作时间压力很大,所以就搁置了,导致没有时间来做这件事情,最近周末 + 晚上花一些时间重构了下,并把文档好好写了一下。言归正传,这篇文章会介绍部分这个弹幕库有的能力,如果正好符合你的需求或者感兴趣,可以帮忙点点 star 来支持一下。

    我们有哪些能力

    我们提供了灵活调整轨道自定义弹幕和容器样式弹幕运动算法等能力,还在提供非常丰富的钩子来让用户处理自定义的行为,只要你想要的,都能做到,本文档会简单介绍一些能力和一些功能的实现。

    快速开始

    对于一个开箱即用的 demo ,可以非常简单的接入,如下所示:

    import { create } from 'danmu';
    
    const manager = create();
    
    manager.mount('#root');
    manager.startPlaying();
    
    // 发送弹幕
    manager.push('弹幕内容')
    

    对轨道进行调整

    我们对支持类似 CSS calc 表达式的能力,一些位置/宽高等信息都可以用表达式来计算。所以对于轨道来说可以很方便的进行调整。

    1. **number**:默认单位为 px
    2. **string**:表达式计算。支持(+-*/)数学计算,只支持 % 和 px 两种单位。
    // 例如,这里的 100% 是指容器宽度(如果是高度相关的配置 100%  就是容器的高度)
    manager.setGap('(100% - 10px) / 5');
    

    限制为顶部 3 条弹幕

    // 如果我们希望轨道高度为 50px
    manager.setTrackHeight('100% / 3');
    
    // 如果不设置渲染区域,轨道的高度会根据默认的 container.height / 3 得到,
    // 这可能导致轨道高度不是你想要的
    manager.setArea({
      y: {
        start: 0,
        // 3 条轨道的总高度为 150px
        end: 150,
      },
    });
    

    限制为中间 3 条弹幕

    manager.setTrackHeight('100% / 3');
    
    manager.setArea({
      y: {
        start: `50%`,
        end: `50% + 150`,
      },
    });
    

    限制为几条不连续的轨道

    限制为几条不连续的轨道,除了要做和连续轨道的操作之外,还需要借助 willRender 这个钩子来实现。

    // 如果我们希望轨道高度为 50px ,并渲染 0 ,2 ,4 这几条轨道
    manager.setTrackHeight('100% / 6');
    
    // 设置容器的渲染区域
    manager.setArea({
      y: {
        start: 0,
        // 6 条轨道的总高度为 300px
        end: 300,
      },
    });
    
    manager.use({
      willRender(ref) {
        // 高级弹幕和轨道不强相关,没有 trackIndex 这个属性
        if (ref.trackIndex === null) return ref;
    
        // 如果为 1 ,3 ,5 这几条轨道就阻止渲染,并重新添加等待下次渲染
        if (ref.trackIndex % 2 === 1) {
          ref.prevent = true;
          manager.unshift(ref.danmaku);
        }
        return ref;
      },
    });
    

    自定义渲染

    弹幕和容器都允许自定义的渲染样式,你可以很方便的做到。

    自定义弹幕的样式

    1. 通过 manager.setStyle 来设置

    import { create } from 'danmu';
    
    // 需要添加的样式
    const styles = {
      color: 'red',
      fontSize: '15px',
      // .
    };
    
    const manager = create();
    
    // 后续渲染的弹幕和当前已经渲染的弹幕会设置上这些样式。
    for (const key in styles) {
      manager.setStyle(key, styles[key]);
    }
    

    2. 通过 danamaku.setStyle 来设置

    import { create } from 'danmu';
    
    // 需要添加的样式
    const styles = {
      color: 'red',
      fontSize: '15px',
      // .
    };
    
    // 初始化的时候添加钩子处理,这样当有新的弹幕渲染时会自动添加上这些样式
    const manager = create({
      plugin: {
        $moveStart(danmaku) {
          for (const key in styles) {
            danmaku.setStyle(key, styles[key]);
          }
          // 你也可以在这里给弹幕 DOM 添加 className
          danmaku.node.classList.add('className');
        },
      },
    });
    
    // 对当前正在渲染的弹幕添加样式
    manager.asyncEach((danmaku) => {
      for (const key in styles) {
        danmaku.setStyle(key, styles[key]);
      }
    });
    

    自定义容器样式

    import { create } from 'danmu';
    
    // 需要添加的样式
    const styles = {
      background: 'red',
      // .
    };
    
    const manager = create({
      plugin: {
        // 你可以在初始化的时候添加钩子处理
        init(manager) {
          for (const key in styles) {
            manager.container.setStyle(key, styles[key]);
          }
          // 你也可以在这里给容器 DOM 添加 className
          manager.container.node.classList.add('className');
        },
      },
    });
    
    // 或者直接调用 api
    for (const key in styles) {
      manager.container.setStyle(key, styles[key]);
    }
    

    高级弹幕的示例

    本章节将介绍如何将弹幕固定在某一位置,以 top 和 left 这两个位置举例。由于我们需要自定义位置,所以我们需要使用高级弹幕的能力。

    将弹幕固定在顶部

    // 这条弹幕将会居中距离顶部 10px 的位置悬停 5s
    manager.pushFlexibleDanmaku('弹幕内容', {
      duration: 5000,
      direction: 'none',
      position(danmaku, container) {
        return {
          x: `50% - ${danmaku.getWidth() / 2}`,
          y: 10, // 具体容器顶部的距离为 10px
        };
      },
    });
    

    固定在顶部第 2 条轨道上

    // 这条弹幕将会在第二条轨道居中的位置悬停 5s
    manager.pushFlexibleDanmaku('弹幕内容', {
      duration: 5000,
      direction: 'none',
      position(danmaku, container) {
        // 渲染在第 3 条轨道中
        const { middle } = manager.getTrackLocation(2);
        return {
          x: `50% - ${danmaku.getWidth() / 2}`,
          y: middle - danmaku.getHeight() / 2,
        };
      },
    });
    

    将弹幕固定在左边

    // 这条弹幕将会在容器中间距离左边 10px 的地方停留 5s
    manager.pushFlexibleDanmaku('弹幕内容', {
      duration: 5000,
      direction: 'none',
      position(danmaku, container) {
        // 渲染在第 3 条轨道中
        const { middle } = manager.getTrackLocation(2);
        return {
          x: 10,
          y: `50% - ${danmaku.getHeight() / 2}`,
        };
      },
    });
    

    发送带图片的弹幕

    要让弹幕里面能够携带图片,要在弹幕的节点内部添加自定义的内容,实际上不止图片,你可以往弹幕的节点里面添加任何的内容。

    本章节的组件以 React 来实现演示。

    开发弹幕组件

    export function Danmaku({ danmaku }) {
      return (
        <div>
          <img src="https://abc.jpg" />
          {danmaku.data}
        </div>
      );
    }
    

    渲染弹幕

    import ReactDOM from 'react-dom/client';
    import { create } from 'danmu';
    import { Danmaku } from './Danmaku';
    
    const manager = create<string>({
      plugin: {
        // 将组件渲染到弹幕的内置节点上
        $createNode(danmaku) {
          ReactDOM.createRoot(danmaku.node).render(<Danmaku danmaku={danmaku} />);
        },
      },
    });
    

    编写一个插件

    编写一个插件是很简单的,但是借助内核暴露出来的钩子和 API,你可以很轻松的实现强大且定制化的需求。由于内核没有暴露出来根据条件来实现过滤弹幕的功能,原因在于内核不知道弹幕内容的数据结构,这和业务的诉求强相关,所以我们在此通过插件来实现精简弹幕的功能用来演示。

    编写一个插件

    • 你编写的插件应当取一个 name,以便于调试定位问题(注意不要和其他插件冲突了)。
    • 插件可以选择性的声明一个 version,这在你的插件作为独立包发到 npm 上时很有用。
    export function filter({ userIds, keywords }) {
      return (manager) => {
        return {
          name: 'filter-keywords-or-user',
          version: '1.0.0', // version 字段不是必须的
          willRender(ref) {
            const { userId, content } = ref.danmaku.data.value;
            console.log(ref.type); // 可以根据此字段来区分是普通弹幕还是高级弹幕
    
            if (userIds && userIds.includes(userId)) {
              ref.prevent = true;
            } else if (keywords) {
              for (const word of keywords) {
                if (content.includes(word)) {
                  ref.prevent = true;
                  break;
                }
              }
            }
            return ref;
          },
        };
      };
    }
    

    注册插件

    你需要通过 mananger.use() 来注册插件。

    import { create } from 'danmu';
    
    const manager = create<{
      userId: number;
      content: string;
    }>();
    
    manager.use(
      filter({
        userIds: [1],
        keywords: ['菜'],
      }),
    );
    

    发送弹幕

    • ❌ 被插件阻止渲染
    manager.push({
      userId: 1,
      content: '',
    });
    
    • ❌ 被插件阻止渲染
    manager.push({
      userId: 2,
      content: '你真菜',
    });
    
    • ✔️ 不会被插件阻止渲染
    manager.push({
      userId: 2,
      content: '',
    });
    
    • ✔️ 不会被插件阻止渲染
    manager.push({
      userId: 2,
      content: '你真棒',
    });
    

    总结

    本文档只是简单介绍了下现在的部分能力,更详细的文档在官网可以查看,如果对你的业务或者学习有帮助的,给个 star 支持一下作者,也欢迎大家评论探讨(不止弹幕,哈哈)。

    3 条回复    2024-09-03 14:06:04 +08:00
    kk2syc
        1
    kk2syc  
       89 天前
    +1s
    Tubbs
        2
    Tubbs  
       88 天前 via iPhone
    看着很不错,支持一下
    imtaotao
        3
    imtaotao  
    OP
       88 天前
    @Tubbs 感谢 ❀
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2473 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 02:23 · PVG 10:23 · LAX 18:23 · JFK 21:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.