Full Code of parksben/barrage for AI

master 1004d72496a0 cached
23 files
34.0 KB
11.9k tokens
19 symbols
1 requests
Download .txt
Repository: parksben/barrage
Branch: master
Commit: 1004d72496a0
Files: 23
Total size: 34.0 KB

Directory structure:
gitextract_xz5nebjd/

├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── docs/
│   ├── .vuepress/
│   │   └── config.js
│   ├── README.md
│   └── document/
│       ├── README.md
│       ├── anim-api.md
│       ├── anim-prop.md
│       ├── canvas.md
│       ├── config.md
│       ├── dataset.md
│       ├── example.md
│       ├── implement.md
│       ├── instance.md
│       └── masking-api.md
├── example.json
├── index.js
├── package.json
└── src/
    ├── index.js
    └── utils.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules
docs/dist
dist

================================================
FILE: .npmignore
================================================
node_modules
docs
src


================================================
FILE: CHANGELOG.md
================================================
## [1.4.1](https://github.com/parksben/barrage/compare/4ceddef...v1.4.1) (2021-01-31)


### Bug Fixes

* 修复 暂停-切换进度-播放 过程的一个已知BUG ([ee8b9f1](https://github.com/parksben/barrage/commit/ee8b9f1))
* 修复新增弹幕失败的问题 ([9e0550b](https://github.com/parksben/barrage/commit/9e0550b))
* 暂停状态下切换进度画面不变的BUG ([4ceddef](https://github.com/parksben/barrage/commit/4ceddef))


### Features

* API 文档重构为静态站点 ([14a3751](https://github.com/parksben/barrage/commit/14a3751))
* 增加清楚当前蒙版数据的方法 ([bd4d7e3](https://github.com/parksben/barrage/commit/bd4d7e3))
* 实现动画状态的可访问属性 ([e9c0b00](https://github.com/parksben/barrage/commit/e9c0b00))
* 实现弹幕自动布局特性 ([1c2eab2](https://github.com/parksben/barrage/commit/1c2eab2))
* 新增弹幕头像的功能 ([f47c78c](https://github.com/parksben/barrage/commit/f47c78c))
* 生产代码构建为 es5 产物 ([9f87295](https://github.com/parksben/barrage/commit/9f87295))





================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Peng An

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<img alt="Barrage UI" src="https://github.com/parksben/barrage/raw/master/docs/images/logo.png" width="140">

# Barrage UI

![npm](https://img.shields.io/npm/l/barrage-ui.svg)
![npm](https://img.shields.io/npm/dt/barrage-ui.svg)
![npm](https://img.shields.io/npm/v/barrage-ui/latest.svg)

Best and lightest **danmaku** component for web UI.

适用于 Web 端用户界面的轻量级 **弹幕** 组件

## 🎬 Demo

Demo Online: <https://masking-danmaku-demo.netlify.com/>

Source Codes: <https://github.com/parksben/masking-danmaku-demo>

## 📄 Docs

Version Latest: <https://barrage-ui.netlify.com/>

## 📦 Install

```bash
yarn add barrage-ui # OR npm install --save barrage-ui
```

## ✏️ License

[MIT License](LICENSE)


================================================
FILE: babel.config.js
================================================
const presets = [
  [
    '@babel/env',
    {
      targets: {
        browsers: ['> 0.25%', 'not dead'],
      },
      useBuiltIns: 'usage',
      corejs: 3,
    },
  ],
];

module.exports = { presets };


================================================
FILE: docs/.vuepress/config.js
================================================
const path = require('path');

module.exports = {
  title: 'Barrage UI',
  description:
    'Best and lightest danmaku component for web UI. 适用于 Web 端用户界面和播放器的轻量级弹幕组件',
  themeConfig: {
    nav: [
      { text: '首页', link: '/' },
      { text: '项目文档', link: '/document/' },
      { text: 'Demo', link: 'https://masking-danmaku-demo.netlify.com/' },
    ],
    sidebar: {
      '/document/': [
        '',
        'instance',
        'config',
        'dataset',
        'anim-api',
        'anim-prop',
        'masking-api',
        'canvas',
        'implement',
        'example',
      ],
      '/': false,
    },
    sidebarDepth: 2,
    repo: 'parksben/barrage',
    repoLabel: 'GitHub',
  },
  dest: path.resolve(__dirname, '../dist'),
};


================================================
FILE: docs/README.md
================================================
---
home: true
title: Barrage UI
description: 可实现B站蒙版弹幕效果的轻量级前端组件
actionText: 快速上手 →
actionLink: /document/
features:
- title: 一切皆可『弹幕』化
  details: 动画基于 Canvas 实现,可用于给网页上任何元素挂载弹幕评论
- title: 简单 & 自由
  details: 项目开源(MIT License)且源码无第三方依赖,可放心安装/构建、自由地使用组件进行业务开发
- title: 可定制 & 可延展
  details: 基于相关接口,可精确定义每一帧渲染。可快速实现 B 站(bilibili.com)风格的『蒙版弹幕』效果
footer: MIT Licensed | Copyright © 2019-present Parksben
---

## :clapper: Demo Online

* 线上效果 - <https://masking-danmaku-demo.netlify.com/>
* 案例源码 - <https://github.com/parksben/masking-danmaku-demo>

## :package: 安装

```bash
yarn add barrage-ui # 或者 npm install --save barrage-ui
```

## :truck: 快速开始

html

```html
<div id="container">
  <video id="video" src="videos/demo.mp4" controls></video>
</div>
```

js

```js
import Barrage from 'barrage-ui';
import example from 'barrage-ui/example.json'; // 组件提供的示例数据

// 加载弹幕
const barrage = new Barrage({
  container: document.getElementById('container'), // 父级容器
  data: example, // 弹幕数据
  config: {
    // 全局配置项
    duration: 20000, // 弹幕循环周期(单位:毫秒)
    defaultColor: '#fff', // 弹幕默认颜色
  },
});

// 新增一条弹幕
barrage.add({
  key: 'fctc651a9pm2j20bia8j', // 弹幕的唯一标识
  time: 1000, // 弹幕出现的时间(单位:毫秒)
  text: '这是新增的一条弹幕', // 弹幕文本内容
  fontSize: 24, // 该条弹幕的字号大小(单位:像素),会覆盖全局设置
  color: '#0ff', // 该条弹幕的颜色,会覆盖全局设置
});

// 播放弹幕
barrage.play();
```

<style>
.home  .hero::before {
  content: '';
  display: block;
  width: 16rem;
  height: 16rem;
  margin: 0 auto;
  margin-top: 3.2rem;
  background: url(./images/logo.png) no-repeat center center;
  background-size: 100% auto;
}
</style>

================================================
FILE: docs/document/README.md
================================================
---
title: 快速上手
---

# 快速上手

## 安装

```bash
yarn add barrage-ui # 或者 npm install --save barrage-ui
```

## 快速开始

html

```html
<div id="container">
  <video id="video" src="videos/demo.mp4" controls></video>
</div>
```

js

```js
import Barrage from 'barrage-ui';
import example from 'barrage-ui/example.json'; // 组件提供的示例数据

// 加载弹幕
const barrage = new Barrage({
  container: document.getElementById('container'), // 父级容器
  data: example, // 弹幕数据
  config: {
    // 全局配置项
    duration: 20000, // 弹幕循环周期(单位:毫秒)
    defaultColor: '#fff', // 弹幕默认颜色
  },
});

// 新增一条弹幕
barrage.add({
  key: 'fctc651a9pm2j20bia8j', // 弹幕的唯一标识
  time: 1000, // 弹幕出现的时间(单位:毫秒)
  text: '这是新增的一条弹幕', // 弹幕文本内容
  fontSize: 24, // 该条弹幕的字号大小(单位:像素),会覆盖全局设置
  color: '#0ff', // 该条弹幕的颜色,会覆盖全局设置
});

// 播放弹幕
barrage.play();
```

================================================
FILE: docs/document/anim-api.md
================================================
---
title: 动画控制接口
---

# 动画控制接口

## 播放 - barrage.play()

**描述**

用于播放动画。若当前为暂停状态,则从当前进度继续播放

**用例**

```js
barrage.play();
```

## 暂停 - barrage.pause()

**描述**

用于暂停动画

**用例**

```js
barrage.pause();
```

## 重新播放 - barrage.replay()

**描述**

用于重新开始播放动画

**用例**

```js
barrage.replay();
```

## 跳转进度 - barrage.goto(progress)

**描述**

用于跳转播放进度。此方法在动画播放和暂停的状态下均有效

**参数**

`progress` - 待跳转的进度。值为一个毫秒数,表示跳转到动画的第几毫秒

**用例**

```js
barrage.goto(15000); // 跳转到第 15 秒
```

================================================
FILE: docs/document/anim-prop.md
================================================
---
title: 动画状态属性
---

# 动画状态属性

## 播放进度 - barrage.progress

**含义**

当前动画的播放进度

**类型**

描述播放进度的一个毫秒数

## 播放状态 - barrage.animState

**含义**

当前动画的播放状态

**类型**

描述播放状态的一个字符串

* `'ready'` - 已就绪,即:barrage 实例已创建,但从未播放
* `'paused'` - 已暂停
* `'playing'` - 播放中


================================================
FILE: docs/document/canvas.md
================================================
---
title: Canvas 画布
---

# Canvas 画布

## 画布实例 - barrage.canvas

**含义**

渲染弹幕的 canvas 画布

## 画布上下文 - barrage.ctx

**含义**

画布的上下文,相当于 `barrage.canvas.getContext('2d')`

================================================
FILE: docs/document/config.md
================================================
---
title: 全局配置项
---

# 全局配置项

## 配置项及默认值 - barrage.config

弹幕的所有全局配置项及默认值如下:

```js
{
  duration: -1, // 弹幕动画的循环周期,-1 表示不循环播放
  speed: 100, // 弹幕的运动速度
  fontSize: 24, // 文字大小,单位:像素
  fontFamily: 'Microsoft Yahei', // 字体,默认值:微软雅黑
  textShadowBlur: 1.0, // 字体阴影扩散,有效值 >= 0
  opacity: 1.0, // 透明度,有效值 0-1
  defaultColor: '#fff', // 默认颜色,与 CSS 颜色属性一致
}
```

## 更新配置项 - barrage.setConfig()

如果你的弹幕实例已创建或者正在播放,可以通过 `setConfig` 接口进行实时更新:

```js
// 更新全局透明度
barrage.setConfig({ opacity: 0.5 });
```

================================================
FILE: docs/document/dataset.md
================================================
---
title: 弹幕数据
---

# 弹幕数据

## 数据结构

弹幕数据集为一个对象数组。每个数组元素对应一条弹幕记录,其结构如下:

```js
{
  key: 'fctc651a9pm2j20bia8j',
  createdAt: '2019-01-13T13:34:47.126Z',
  time: 1200,
  text: '我膨胀了',
  fontFamily: 'SimSun',
  fontSize: 32,
  color: 'yellow',
  avatar: '/images/avatar.png',
  avatarSize: 32,
  avatarMarginRight: 8,
}
```

::: tip 数据字段的含义
* createdAt - 弹幕的创建时间 (**必须**)
* time - 弹幕的动画时间 (**必须**)
* text - 弹幕文本内容 (**必须**)
* key - 数据的唯一标示 (**推荐**)
* fontFamily - 弹幕文本的字体,默认值:`'Microsoft Yahei'` (可选)
* fontSize - 弹幕文本字号大小,单位:像素,默认值:`24` (可选)
* color - 弹幕文本的颜色,默认值:`'#fff'` (可选)
* avatar - 头像的 url,须为正方形图片,不支持跨域,默认值:`null` (可选)
* avatarSize - 头像的大小,单位:像素,默认值为字号大小的 1.2 倍 (可选)
* avatarMarginRight - 头像与文本的间距,单位:像素,默认值为字号大小的 0.2 倍 (可选)
:::

::: warning 关于 key

当动画过程中需要更新数据集时,推荐设置此字段。

动态更新数据集时,为了动画的连续性,更新前后的数据集可能存在部分相同的数据。Barrage 组件内部会对更新前后的数据的 key 进行比较,只增量渲染那些新增的数据,而不改变已经存在的弹幕布局。

综上所述,字段 key 的取值应该是稳定且唯一的。对于同一条弹幕而言,key 的值应该是不变的。
:::

## 装填弹幕 - barrage.setData()

装填弹幕有两种方式:

**方式一:初始化时传入数据**

```js
const barrage = new Barrage({
  container: 'barrage',
  data: JSON_DATA, // JSON_DATA -> 你的弹幕数据
});
```

**方式二:初始化后更新数据**

```js
const barrage = new Barrage({
  container: 'barrage',
});

barrage.setData(JSON_DATA); // JSON_DATA -> 你的弹幕数据
```

## 新增弹幕 - barrage.add()

如果你的弹幕实例已创建或者正在播放,可以通过 `add` 方法新增一条记录:

```js
barrage.add({
  key: 'fctc651a9pm2j20bia8j',
  time: 1000,
  text: '这是新增的一条弹幕',
  fontSize: 26,
  color: '#0ff',
});
```

`add` 方法一般搭配 数据提交/请求 操作进行使用,以实现真实的线上应用。

::: tip 适用场景举例
实现多终端同步的实时弹幕:
1. 某用户在客户端提交了一条弹幕到服务端
2. 服务端将数据存储并分发给正在进行会话的客户端
3. 客户端收到数据后,使用 `add` 方法进行数据更新
:::

::: warning 注意
`add` 方法会返回一个 `Boolean` 值,表示弹幕是否成功添加进画布。若当前进度的画布中弹幕已经饱和,则可能添加失败
:::

================================================
FILE: docs/document/example.md
================================================
---
title: 项目案例
---

# 项目案例

## 基于色度键控的客户端实时蒙版弹幕

![Masking Danmaku Demo](../images/barrage.gif)

<table>
  <tr>
    <td>博客</td>
    <td>
      <a target="_blank" rel="noopener noreferrer" href="https://github.com/parksben/masking-danmaku-demo/blob/master/how-to-implement-masking-danmaku.md">基于色键技术的纯客户端实时蒙版弹幕</a>
    </td>
  </tr>
  <tr>
    <td>源码</td>
    <td>
      <a target="_blank" rel="noopener noreferrer" href="https://github.com/parksben/masking-danmaku-demo">GitHub Repo</a>
    </td>
  </tr>
  <tr>
    <td>Demo</td>
    <td>
      <a target="_blank" rel="noopener noreferrer" href="https://masking-danmaku-demo.netlify.com/">Demo Online</a>
    </td>
  </tr>
</table>

================================================
FILE: docs/document/implement.md
================================================
---
title: 科普 - 如何实现蒙版弹幕
---

# 基于 Barrage UI 如何实现蒙版弹幕

Barrage 组件提供了实现 蒙版弹幕 效果的可能。基于本组件实现的 demo 效果如下:

![蒙版弹幕效果](../images/demo.png)

## 什么是“蒙版弹幕”

**蒙版弹幕** 是由知名弹幕视频网站 [bilibili](https://www.bilibili.com) 于 2018 年中推出的一种弹幕渲染效果,可以有效减少弹幕文字对视频主体信息的干扰。

详细资料可参考 bilibili 的相关文章:

[弹幕阳光计划第十弹 蒙版听说过吗,弹幕黑科技了解一下?](https://www.bilibili.com/read/cv534194/)

[不挡脸,放肆看!B 站黑科技蒙版弹幕揭秘](https://www.infoq.cn/article/2018%2F08%2Fbili-bili-mask-barrage)

## 蒙版弹幕的实现原理

如果你熟悉最著名的图像处理软件——Adobe Photoshop,那么你应该对 “蒙版” 的概念不陌生,“蒙版弹幕” 的实现原理与此类似,即:将图像的一部分 “隐藏”。

Barrage 组件的初始化参数中的 `mask` 一项即用于处理蒙版效果。对于上文截图中的效果,其使用的蒙版图像效果如下:

![蒙版图像](../images/mask.png)

弹幕渲染时,会将蒙版图像中 “镂空” 的部分(图像 RGBA 通道中 Alpha 通道为 0 的像素)去除,从而达到 “蒙版弹幕” 的效果。

## 基于 Barrage UI 的伪代码

为 barrage 实例设置蒙版图像(mask)即可实现蒙版弹幕效果。

* 可通过初始化参数 `mask` 传入蒙版图像:

```js
import Barrage from 'barrage-ui';
import example from 'barrage-ui/example.json';

const barrage = new Barrage({
  container: 'barrage',
  data: example,
  mask: 'mask.png', // 传入蒙版图像的 url
});
```

* 也可以在弹幕初始化后,通过 `setMask` 方法进行实时更新:

```js
import Barrage from 'barrage-ui';
import example from 'barrage-ui/example.json';

const barrage = new Barrage({
  container: 'barrage',
  data: example,
});

// 设置蒙版图像
barrage.setMask('mask.png'); // 传入蒙版图像的 url
```

::: warning 注意
`mask` 参数和 `setMask` 方法的参数类型一致,可接收图像的 url 或 [ImageData](https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData)
:::

## beforeRender 钩子实现实时渲染

上文的示例仅能够实现一帧蒙版图像的渲染(只设置了一次 `mask` 而没有实时更新它),要实现实时的蒙版效果(如:与视频实时同步的蒙版效果),需要对弹幕动画的每一帧进行处理。

使用组件提供的 `beforeRender` 钩子函数,可以轻易的实现:

```js
import Barrage from 'barrage-ui';
import example from 'barrage-ui/example.json';

const barrage = new Barrage({
  container: 'barrage',
  data: example,
  beforeRender: (ctx, progress) => {
    const imageData = getMask(progress); // 用于获取当前进度对应蒙版的方法
    barrage.setMask(imageData);
  },
});
```

当然,`beforeRender` 钩子也可以在弹幕初始化之后挂载:

```js
import Barrage from 'barrage-ui';
import example from 'barrage-ui/example.json';

const barrage = new Barrage({
  container: 'barrage',
  data: example,
});

barrage.beforeRender = (ctx, progress) => {
  const imageData = getMask(progress); // 用于获取当前进度对应蒙版的方法
  barrage.setMask(imageData);
};
```

================================================
FILE: docs/document/instance.md
================================================
---
title: 初始化参数
---

# 初始化参数

在 `new Barrage({ ...options })` 创建弹幕实例时,需要传入的初始化参数如下:

|   Options    |                                   Data Type                                    |          Default Value           | Notes                                                                                                                                                          |
| :----------: | :----------------------------------------------------------------------------: | :------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|  container   |                                 string/element                                 |          必传,无默认值          | 弹幕的挂载点                                                                                                                                                   |
|     data     |                                     array                                      |                []                | 弹幕数据                                                                                                                                                       |
|    config    |                                     object                                     |          详见全局配置项          | 详见 [全局配置项](/document/config.html)                                                                                                                       |
|     mask     | string/[ImageData](https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData) |         string/ImageData         | 蒙版图像,用于实现蒙版弹幕效果,详见 [蒙版弹幕](/document/implement.html)                                                                                      |
| beforeRender |                                    function                                    | (ctx, progress, animState) => {} | 帧渲染前的回调,函数实参分别为:<br/>**`ctx`** canvas 画布的上下文<br/>**`progress`** 动画的播放进度(毫秒)<br/>**`animState`** 动画状态: 'paused' 或 'playing' |
| afterRender  |                                    function                                    | (ctx, progress, animState) => {} | 帧渲染后的回调,函数实参分别为:<br/>**`ctx`** canvas 画布的上下文<br/>**`progress`** 动画的播放进度(毫秒)<br/>**`animState`** 动画状态: 'paused' 或 'playing' |
| avoidOverlap |                                    boolean                                     |               true               | 是否禁止弹幕重叠(默认开启)                                                                                                                                     |

其中,`container` 参数在初始化实例时必传,其他参数为可选,数据类型及默认值如上表所示。


================================================
FILE: docs/document/masking-api.md
================================================
---
title: 蒙版图像接口
---

# 蒙版图像接口

## 设置蒙版图像 - barrage.setMask(mask)

**描述**

用于设置蒙版图像。蒙版图像的概念见下文 [蒙版弹幕](/document/implement.html)

**参数**

mask - 蒙版图像的 url 或 [ImageData](https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData)

**用例**

```js
barrage.setMask('mask.png'); // 通过图片 url 设置蒙版图像

barrage.setMask(imageData); // 直接设置 ImageData 类型的数据
```

## 清除蒙版图像 - barrage.clearMask()

**描述**

用于清空当前的蒙版图像。清空后若不再重新设置蒙版图像,则动画将不再具有蒙版效果

**用例**

```js
barrage.clearMask();
```

================================================
FILE: example.json
================================================
[
  {
    "key": "7g43mm0rpp1l67eh6qjo8",
    "time": 500,
    "text": "绿色走一波",
    "color": "#0f0",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "n8alq5l22d8qqbuhgst68g",
    "time": 1200,
    "text": "我膨胀了",
    "fontFamily": "SimSun",
    "fontSize": 32,
    "color": "yellow",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "atb8ctt81egh8hf45u7n2g",
    "time": 2500,
    "text": "妈妈咪呀",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "bl9a602defoehsiahgi7vo",
    "time": 3300,
    "text": "富贵使我们相遇",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "p0su7gh4s88fjtqv0bmdog",
    "time": 4000,
    "text": "要想生活过得去",
    "color": "#0f0",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "udfel3ppv5ggrfqo4nlbng",
    "time": 4400,
    "text": "你且在这里不要走动,待我去买两斤橘子",
    "color": "#f00",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "68ts93rle7gtv1i9die5ig",
    "time": 4800,
    "text": "我们都一样",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "dcprmrcmdcg4btqej0mung",
    "time": 5200,
    "text": "Remember me",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "9jgn02iii6b8qmkq2v2hg",
    "time": 5680,
    "text": "LoL",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "im73i1lka0ooavb86gb048",
    "time": 6600,
    "text": "(-_-)|||",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "hrbvbcuekoqm7aacufgk8",
    "time": 7200,
    "text": "天哪噜",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "f95vsgh50qob6ip3r617b",
    "time": 8300,
    "text": "富强 民主 文明 和谐 自由 平等 公正 法治 爱国 敬业 诚信 友善",
    "color": "#f00",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "u7bo393jvv8dkccb2ml0e",
    "time": 9210,
    "text": "啦啦啦啦啦啦啦拉",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "muafgllnsg20op35brg9",
    "time": 10000,
    "text": "我是谁 我从哪里来 我为什么要看这个",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "hjv3sllm12tbh85r2qor8",
    "time": 12000,
    "text": "2333333333333333",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "r7u5mvu1l7g1hk7cvnddig",
    "time": 12000,
    "text": "2333333333333333",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "0o51j9psnj8qn3bnfq9kag",
    "time": 12000,
    "text": "2333333333333333",
    "createdAt": "2019-01-13T13:34:47.126Z"
  },
  {
    "key": "fkgm2nerakgr71jloi9d38",
    "time": 12000,
    "text": "2333333333333333",
    "createdAt": "2019-01-13T13:34:47.126Z"
  }
]


================================================
FILE: index.js
================================================
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _dist = _interopRequireDefault(require("./dist"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var _default = _dist.default;
exports.default = _default;

================================================
FILE: package.json
================================================
{
  "scripts": {
    "prepublishOnly": "npm run compile && npm run changelog",
    "changelog": "npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
    "docs:dev": "npx vuepress dev docs",
    "docs:build": "npx vuepress build docs",
    "compile": "rimraf dist && npx babel src --source-maps --out-dir dist"
  },
  "name": "barrage-ui",
  "version": "1.4.1",
  "description": "Best and lightest danmaku component for web UI.",
  "homepage": "https://barrage-ui.netlify.com/",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/parksben/barrage.git"
  },
  "keywords": [
    "danmaku",
    "barrage",
    "弹幕",
    "danmu",
    "web-player",
    "video-player",
    "canvas"
  ],
  "author": "parksben",
  "license": "MIT",
  "devDependencies": {
    "@babel/cli": "^7.12.10",
    "@babel/core": "^7.12.10",
    "@babel/polyfill": "^7.12.1",
    "@babel/preset-env": "^7.12.11",
    "conventional-changelog-cli": "^2.0.11",
    "rimraf": "^2.6.3",
    "vuepress": "^0.14.11"
  },
  "dependencies": {
    "core-js": "^3.8.3"
  }
}


================================================
FILE: src/index.js
================================================
import {
  requestAnimationFrame,
  cancelAnimationFrame,
  loadImage,
  makeImageElement,
  MIN_SEP,
  layout,
  insertItem,
} from './utils';

// 支持通过 barrage.setConfig() 接口修改的配置项
const DEFAULT_CONFIG = {
  duration: -1, // -1 表示不循环播放
  speed: 100,
  fontSize: 24,
  fontFamily: 'Microsoft Yahei',
  textShadowBlur: 1.0,
  lineHeight: 1.32,
  opacity: 1.0,
  defaultColor: '#fff',
};

// 蒙版信息
const GLOBAL_MASK = {
  type: null, // 蒙版类型:'url' 'ImageData'
  mask: null, // 蒙版数据:ImageData
};

/**
 * 弹幕组件 Barrage
 * @param {string/element} container 弹幕的挂载点
 * @param {array} data 弹幕数据,单条数据格式如 { time: 1200, text: '2333' }
 * @param {number} config.duration 弹幕的循环周期(不设置此参数时,默认弹幕仅播放一次),单位:毫秒
 * @param {number} config.speed 弹幕最小移动速度,单位:像素/秒
 * @param {number} config.fontSize 文字大小,单位:像素
 * @param {string} config.fontFamily 字体
 * @param {number} config.textShadowBlur 字体阴影扩散系数,取值范围:[0, 1]
 * @param {number} config.opacity 字体透明度,取值范围:[0, 1]
 * @param {string} config.defaultColor 字体默认颜色
 * @param {boolean} avoidOverlap 是否禁止弹幕重叠(有重叠部分的弹幕将不显示),默认值:false
 * @param {string/ImageData} mask 蒙版图像信息,每 4 个元素表示一个像素的 RGBA 信息
 * @param {function} beforeRender 帧渲染前的钩子
 * @param {function} afterRender 帧渲染后的钩子
 */
export default class Barrage {
  constructor({
    container,
    data = [],
    config = {},
    avoidOverlap = true,
    mask = [],
    beforeRender = () => {},
    afterRender = () => {},
  }) {
    // 获取父级容器
    this.parent =
      typeof container === 'string'
        ? document.getElementById(container)
        : container;
    this.parent.classList.add('barrage-container');

    // 创建画布
    this.canvas = document.createElement('canvas');
    this.canvas.className = 'barrage-canvas';
    this.canvas.width = this.parent.clientWidth;
    this.canvas.height = this.parent.clientHeight;
    this.canvas.style.pointerEvents = 'none'; // canvas 事件穿透
    this.canvas.style.letterSpacing = '1.5px'; // canvas 字符间距
    this.parent.appendChild(this.canvas);

    // 若父节点存在其他子节点,则设置画布为绝对定位
    if (this.parent.childNodes.length > 1) {
      this.parent.style.position = 'relative';
      this.canvas.style.position = 'absolute';
      this.canvas.style.left = '0px';
      this.canvas.style.top = '0px';
    }

    // 画布上下文
    this.ctx = this.canvas.getContext('2d');

    // 弹幕装填时是否启用布局优化
    this.avoidOverlap = avoidOverlap;

    // 全局参数设置
    this.setConfig({
      ...DEFAULT_CONFIG,
      ...config,
    });

    this.setMask(mask); // 设置蒙版
    this.beforeRender = beforeRender;
    this.afterRender = afterRender;

    // 数据初始化
    this.setData(data);
  }

  setMask(input) {
    if (typeof input === 'string') {
      GLOBAL_MASK.type = 'url';
      loadImage(input).then(img => {
        GLOBAL_MASK.data = img;
      });
    } else if (
      Object.prototype.toString.apply(input) === '[object ImageData]'
    ) {
      GLOBAL_MASK.type = 'ImageData';
      GLOBAL_MASK.data = input;
    } else {
      GLOBAL_MASK.type = null;
      GLOBAL_MASK.data = null;
    }
  }

  clearMask() {
    this.setMask();
  }

  setConfig(config) {
    if (!this.config) this.config = {};
    Object.assign(this.config, config);
  }

  setData(data) {
    // 保存上一版本数据集
    if (this.data) this.prevData = this.data;

    // 获取弹幕数据并计算出布局信息
    this.data = layout({
      config: this.config,
      canvas: this.canvas,
      data,
      avoidOverlap: this.avoidOverlap,
    });

    // 不更改上一版本数据集中已存在的数据
    this.data.forEach(item => {
      if (this.prevData && this.prevData.some(d => d.key === item.key)) {
        const prevItem = this.prevData.find(d => d.key === key);
        Object.assign(item, prevItem);
      }
    });
  }

  // 新建一条弹幕(方法返回一个布尔值,表示插入新弹幕是否成功)
  add({
    time,
    text,
    fontSize = this.config.fontSize,
    fontFamily = this.config.fontFamily,
    color = this.config.defaultColor,
    createdAt = new Date().toISOString(),
    avatar,
    avatarSize,
    avatarMarginRight,
  }) {
    const item = {
      time,
      text,
      fontSize,
      fontFamily,
      color,
      createdAt,
      avatar,
      avatarSize,
      avatarMarginRight,
    };

    if (this.data && this.data.length) {
      const result = insertItem({
        item,
        visibleList: this.data,
        config: this.config,
        canvas: this.canvas,
        avoidOverlap: this.avoidOverlap,
      });
      return result.visible;
    }

    this.setData([item]);
    return true;
  }

  // 计算播放进度,单位:毫秒
  get progress() {
    if (!this.startTime) return 0;
    if (this.pauseAt !== undefined) return this.pauseAt;

    let p = Date.now() - this.startTime;
    if (this.config.duration > 0) p %= this.config.duration;

    return p;
  }

  // 获取当前播放状态
  get animState() {
    if (!this.startTime) return 'ready';
    return this.pauseAt !== undefined ? 'paused' : 'playing';
  }

  _render() {
    // 弹幕整体向左移动的总距离
    const translateX = (this.config.speed * this.progress) / 1000;

    // 清空画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    // 筛选待渲染的数据
    let dataShown = this.data
      .filter(
        x =>
          x.left + x.width - translateX >= -2 * MIN_SEP * this.canvas.width &&
          x.left - translateX < (1 + 2 * MIN_SEP) * this.canvas.width
      )
      .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));

    // 是否禁止重叠
    if (this.avoidOverlap) {
      dataShown = dataShown.filter(x => !x.hasOverlap);
    }

    // 执行渲染前的回调
    if (this.beforeRender)
      this.beforeRender(this.ctx, this.progress, this.animState);

    this.ctx.save();
    if (GLOBAL_MASK.data) {
      if (GLOBAL_MASK.type === 'ImageData') {
        this.ctx.putImageData(GLOBAL_MASK.data, 0, 0);
      } else if (GLOBAL_MASK.type === 'url') {
        this.ctx.drawImage(
          GLOBAL_MASK.data,
          0,
          0,
          this.canvas.width,
          this.canvas.height
        );
      }

      if (!this.anotherCanvas) {
        this.anotherCanvas = document.createElement('canvas');
        this.anotherCanvas.width = this.canvas.width;
        this.anotherCanvas.height = this.canvas.height;
        this.anotherContext = this.anotherCanvas.getContext('2d');
      } else {
        this.anotherContext.clearRect(
          0,
          0,
          this.anotherCanvas.width,
          this.anotherCanvas.height
        );
      }
    }

    // 绘制数据
    const context = GLOBAL_MASK.data ? this.anotherContext : this.ctx;
    context.globalAlpha = this.config.opacity;
    context.shadowColor = 'rgba(0, 0, 0, 1)';
    context.shadowOffsetX = 0;
    context.shadowOffsetY = 0;
    context.shadowBlur = this.config.textShadowBlur * 2;
    context.textBaseline = 'top';

    dataShown.forEach(d => {
      const left =
        d.left -
        (translateX +
          this.canvas.width *
            d.randomRatio *
            2 *
            MIN_SEP *
            Math.sin((Math.PI * translateX) / this.canvas.width));

      if (d.avatar && typeof d.avatar === 'string') {
        context.drawImage(
          makeImageElement(d.avatar),
          left,
          d.top - (d.avatarSize - d.fontSize) / 2,
          d.avatarSize,
          d.avatarSize
        );
      }

      context.font = `${d.fontSize}px ${d.fontFamily}`;
      context.fillStyle = d.color;
      context.fillText(
        d.text,
        left + d.avatarSize + d.avatarMarginRight,
        d.top
      );
    });

    if (GLOBAL_MASK.data) {
      this.ctx.globalCompositeOperation = 'source-in';
      this.ctx.drawImage(
        this.anotherCanvas,
        0,
        0,
        this.canvas.width,
        this.canvas.height
      );
    }
    this.ctx.restore();

    // 执行渲染后的回调
    if (this.afterRender)
      this.afterRender(this.ctx, this.progress, this.animState);

    // 执行下一帧
    if (this.animation) requestAnimationFrame(() => this._render());
  }

  _play() {
    // 创建动画任务
    if (!this.animation)
      this.animation = requestAnimationFrame(() => this._render());
  }

  goto(progress) {
    if (this.pauseAt !== undefined) this.pauseAt = undefined;
    this.startTime = Date.now() - progress;
    if (!this.animation) this._render();
  }

  play() {
    if (!this.startTime) this.startTime = Date.now();
    if (this.pauseAt !== undefined) {
      this.goto(this.pauseAt);
      this.pauseAt = undefined;
    }
    this._play();
  }

  replay() {
    this.startTime = Date.now();
    if (this.pauseAt !== undefined) this.pauseAt = undefined;
    this._play();
  }

  pause() {
    if (this.animation) {
      cancelAnimationFrame(this.animation);
      this.animation = undefined;

      // 保存暂停时的进度
      this.pauseAt = Date.now() - this.startTime;
    }
  }
}


================================================
FILE: src/utils.js
================================================
// 异步加载图片(若图片已加载,则使用缓存)
const loadImageCache = {};
export const loadImage = url =>
  new Promise((resolve, reject) => {
    if (loadImageCache[url]) {
      resolve(loadImageCache[url]);
    } else {
      const picture = new Image();
      picture.src = url;

      picture.onload = () => {
        loadImageCache[url] = picture;
        resolve(picture);
      };

      picture.onerror = () => {
        reject();
      };
    }
  });

// 异步获取图像信息(若图片已加载,则使用缓存)
const imageDataCache = {};
export const getImageData = (
  url,
  renderWidth,
  renderHeight,
  dx,
  dy,
  dw,
  dh
) => {
  if (imageDataCache[url]) {
    return Promise.resolve(imageDataCache[url]);
  }

  return loadImage(url).then(picture => {
    const imgViewer = document.createElement('canvas');
    imgViewer.width = renderWidth || picture.width;
    imgViewer.height = renderHeight || picture.height;
    const context = imgViewer.getContext('2d');

    context.drawImage(picture, 0, 0, imgViewer.width, imgViewer.height);
    imageDataCache[url] = context.getImageData(
      dx || 0,
      dy || 0,
      dw || imgViewer.width,
      dh || imgViewer.height
    );

    return imageDataCache[url];
  });
};

// 生成图片的 HTMLImageElement (若图片已存在,则使用缓存)
const imageElementCache = {};
export const makeImageElement = (url, alt) => {
  if (!imageElementCache[url]) {
    imageElementCache[url] = document.createElement('img');
    imageElementCache[url].src = url;
    imageElementCache[url].alt = alt || '';
  }

  return imageElementCache[url];
};

// 考虑浏览器兼容的 requestAnimationFrame 方法
export const requestAnimationFrame =
  window.requestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.msRequestAnimationFrame;

// 考虑浏览器兼容的 cancelAnimationFrame 方法
export const cancelAnimationFrame =
  window.cancelAnimationFrame || window.mozCancelAnimationFrame;

// 同一行相邻弹幕间的最小间隔(占画布宽度的比例)
export const MIN_SEP = 0.05;

// 弹幕布局方法
export const layout = ({ config, canvas, data, avoidOverlap = false }) => {
  // 获取画布上下文
  const canvasContext = canvas.getContext('2d');

  // 弹幕布局
  canvasContext.font = `${config.fontSize}px ${config.fontFamily}`;

  // 弹幕数据按时间排序
  const listSortedByTime = data.sort((a, b) => a.time - b.time);

  // 计算弹幕布局
  const initialList = listSortedByTime.map(
    ({
      key,
      time,
      text,
      fontSize = config.fontSize,
      fontFamily = config.fontFamily,
      color = config.defaultColor,
      createdAt = new Date().toISOString(),
      avatar,
      avatarSize,
      avatarMarginRight,
    }) => {
      // 计算文本宽度
      const { width } = canvasContext.measureText(text);

      const details = {
        key,
        time,
        avatar: avatar || null,
        avatarSize: avatarSize || (avatar ? 1.2 * fontSize : 0),
        avatarMarginRight: avatarMarginRight || (avatar ? 0.2 * fontSize : 0),
        text,
        fontSize,
        fontFamily,
        color,
        createdAt,
        left: (config.speed * time) / 1000 + canvas.width,
        width,
        height: fontSize,
        randomRatio: Math.random(),
        visible: false,
      };
      details.width += details.avatarSize + details.avatarMarginRight;

      return details;
    }
  );

  // 计算跑道数
  const rowCount = Math.floor(
    canvas.height / (config.lineHeight * config.fontSize)
  );

  // 创建跑道(跑道个数为 rowCount)
  const tracks = {};
  new Array(rowCount).fill(0).forEach((n, i) => {
    tracks[i] = [];
  });

  // 填充跑道
  for (let i = 0; i < initialList.length; i++) {
    const item = initialList[i];

    if (!avoidOverlap) {
      // 若不禁止弹幕重叠,则随机分配跑道
      const randomTrackIdx = Math.floor(rowCount * Math.random());
      item.top =
        (config.fontSize * (config.lineHeight - 1)) / 2 +
        randomTrackIdx * config.lineHeight * config.fontSize;
      item.visible = true;

      tracks[randomTrackIdx].push(item);
    } else {
      // 寻找合适的跑道编号
      const suitableTrackIdx = Object.values(tracks).findIndex(t => {
        const { left = canvas.width, width = 0 } = t[t.length - 1] || {};
        return left + width + MIN_SEP * canvas.width < item.left;
      });

      // 若存在合适的跑道,则放置弹幕
      if (suitableTrackIdx >= 0) {
        item.top =
          (config.fontSize * (config.lineHeight - 1)) / 2 +
          suitableTrackIdx * config.lineHeight * config.fontSize;
        item.visible = true;

        const suitableTrack = tracks[suitableTrackIdx];
        const lastItem = suitableTrack[suitableTrack.length - 1];
        if (lastItem) {
          item.prev = lastItem;
          lastItem.next = item;
        }

        tracks[suitableTrackIdx].push(item);
      }
    }
  }

  // 返回可视的弹幕数据集
  return initialList.filter(x => x.visible);
};

// 插入新弹幕的方法
export const insertItem = ({
  item,
  visibleList,
  config,
  canvas,
  avoidOverlap,
}) => {
  const canvasContext = canvas.getContext('2d');
  canvasContext.font = `${config.fontSize}px ${config.fontFamily}`;

  // 补全布局所需属性
  const [initialItem] = [item].map(
    ({
      key,
      time,
      text,
      fontSize = config.fontSize,
      fontFamily = config.fontFamily,
      color = config.defaultColor,
      createdAt = new Date().toISOString(),
      avatar,
      avatarSize,
      avatarMarginRight,
    }) => {
      const { width } = canvasContext.measureText(text);

      const details = {
        key,
        time,
        avatar: avatar || null,
        avatarSize: avatarSize || (avatar ? 1.2 * fontSize : 0),
        avatarMarginRight: avatarMarginRight || (avatar ? 0.2 * fontSize : 0),
        text,
        fontSize,
        fontFamily,
        color,
        createdAt,
        left: (config.speed * time) / 1000 + canvas.width,
        width,
        height: fontSize,
        randomRatio: Math.random(),
        visible: false,
      };
      details.width += details.avatarSize + details.avatarMarginRight;

      return details;
    }
  );

  // 若未禁止弹幕重叠,则随机选择一条跑道插入弹幕
  if (!avoidOverlap) {
    const rowCount = Math.floor(
      canvas.height / (config.lineHeight * config.fontSize)
    );
    const randomTrackIdx = Math.floor(rowCount * Math.random());

    initialItem.visible = true;
    initialItem.top =
      (config.fontSize * (config.lineHeight - 1)) / 2 +
      randomTrackIdx * config.lineHeight * config.fontSize;

    // 更新可视的弹幕数据集
    visibleList.push(initialItem);

    return initialItem;
  }

  // 寻找弹幕可插入的位置
  const jointPos = visibleList.findIndex(
    x =>
      x.left + x.width + MIN_SEP * canvas.width < initialItem.left &&
      ((x.next &&
        initialItem.left + initialItem.width + MIN_SEP * config.fontSize <
          x.next.left) ||
        !x.next)
  );

  // 若存在可插入位置
  if (jointPos >= 0) {
    const jointLeft = visibleList[jointPos];
    const jointRight = jointLeft.next;

    initialItem.visible = true;
    initialItem.top = jointLeft.top;

    if (jointLeft.time !== initialItem.time) {
      jointLeft.next = initialItem;
      initialItem.prev = jointLeft;
    }

    if (jointRight && jointRight.time !== initialItem.time) {
      initialItem.next = jointRight;
      jointRight.prev = initialItem;
    }

    // 更新可视的弹幕数据集
    visibleList.push(initialItem);
  }

  return initialItem;
};
Download .txt
gitextract_xz5nebjd/

├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── docs/
│   ├── .vuepress/
│   │   └── config.js
│   ├── README.md
│   └── document/
│       ├── README.md
│       ├── anim-api.md
│       ├── anim-prop.md
│       ├── canvas.md
│       ├── config.md
│       ├── dataset.md
│       ├── example.md
│       ├── implement.md
│       ├── instance.md
│       └── masking-api.md
├── example.json
├── index.js
├── package.json
└── src/
    ├── index.js
    └── utils.js
Download .txt
SYMBOL INDEX (19 symbols across 3 files)

FILE: index.js
  function _interopRequireDefault (line 10) | function _interopRequireDefault(obj) { return obj && obj.__esModule ? ob...

FILE: src/index.js
  constant DEFAULT_CONFIG (line 12) | const DEFAULT_CONFIG = {
  constant GLOBAL_MASK (line 24) | const GLOBAL_MASK = {
  class Barrage (line 45) | class Barrage {
    method constructor (line 46) | constructor({
    method setMask (line 99) | setMask(input) {
    method clearMask (line 116) | clearMask() {
    method setConfig (line 120) | setConfig(config) {
    method setData (line 125) | setData(data) {
    method add (line 147) | add({
    method progress (line 186) | get progress() {
    method animState (line 197) | get animState() {
    method _render (line 202) | _render() {
    method _play (line 314) | _play() {
    method goto (line 320) | goto(progress) {
    method play (line 326) | play() {
    method replay (line 335) | replay() {
    method pause (line 341) | pause() {

FILE: src/utils.js
  constant MIN_SEP (line 79) | const MIN_SEP = 0.05;
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (45K chars).
[
  {
    "path": ".gitignore",
    "chars": 27,
    "preview": "node_modules\ndocs/dist\ndist"
  },
  {
    "path": ".npmignore",
    "chars": 22,
    "preview": "node_modules\ndocs\nsrc\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 848,
    "preview": "## [1.4.1](https://github.com/parksben/barrage/compare/4ceddef...v1.4.1) (2021-01-31)\n\n\n### Bug Fixes\n\n* 修复 暂停-切换进度-播放 过"
  },
  {
    "path": "LICENSE",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2019 Peng An\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "README.md",
    "chars": 688,
    "preview": "<img alt=\"Barrage UI\" src=\"https://github.com/parksben/barrage/raw/master/docs/images/logo.png\" width=\"140\">\n\n# Barrage "
  },
  {
    "path": "babel.config.js",
    "chars": 206,
    "preview": "const presets = [\n  [\n    '@babel/env',\n    {\n      targets: {\n        browsers: ['> 0.25%', 'not dead'],\n      },\n     "
  },
  {
    "path": "docs/.vuepress/config.js",
    "chars": 746,
    "preview": "const path = require('path');\n\nmodule.exports = {\n  title: 'Barrage UI',\n  description:\n    'Best and lightest danmaku c"
  },
  {
    "path": "docs/README.md",
    "chars": 1571,
    "preview": "---\nhome: true\ntitle: Barrage UI\ndescription: 可实现B站蒙版弹幕效果的轻量级前端组件\nactionText: 快速上手 →\nactionLink: /document/\nfeatures:\n- "
  },
  {
    "path": "docs/document/README.md",
    "chars": 797,
    "preview": "---\ntitle: 快速上手\n---\n\n# 快速上手\n\n## 安装\n\n```bash\nyarn add barrage-ui # 或者 npm install --save barrage-ui\n```\n\n## 快速开始\n\nhtml\n\n`"
  },
  {
    "path": "docs/document/anim-api.md",
    "chars": 462,
    "preview": "---\ntitle: 动画控制接口\n---\n\n# 动画控制接口\n\n## 播放 - barrage.play()\n\n**描述**\n\n用于播放动画。若当前为暂停状态,则从当前进度继续播放\n\n**用例**\n\n```js\nbarrage.play("
  },
  {
    "path": "docs/document/anim-prop.md",
    "chars": 251,
    "preview": "---\ntitle: 动画状态属性\n---\n\n# 动画状态属性\n\n## 播放进度 - barrage.progress\n\n**含义**\n\n当前动画的播放进度\n\n**类型**\n\n描述播放进度的一个毫秒数\n\n## 播放状态 - barrage."
  },
  {
    "path": "docs/document/canvas.md",
    "chars": 166,
    "preview": "---\ntitle: Canvas 画布\n---\n\n# Canvas 画布\n\n## 画布实例 - barrage.canvas\n\n**含义**\n\n渲染弹幕的 canvas 画布\n\n## 画布上下文 - barrage.ctx\n\n**含义**"
  },
  {
    "path": "docs/document/config.md",
    "chars": 490,
    "preview": "---\ntitle: 全局配置项\n---\n\n# 全局配置项\n\n## 配置项及默认值 - barrage.config\n\n弹幕的所有全局配置项及默认值如下:\n\n```js\n{\n  duration: -1, // 弹幕动画的循环周期,-1 表"
  },
  {
    "path": "docs/document/dataset.md",
    "chars": 1672,
    "preview": "---\ntitle: 弹幕数据\n---\n\n# 弹幕数据\n\n## 数据结构\n\n弹幕数据集为一个对象数组。每个数组元素对应一条弹幕记录,其结构如下:\n\n```js\n{\n  key: 'fctc651a9pm2j20bia8j',\n  creat"
  },
  {
    "path": "docs/document/example.md",
    "chars": 682,
    "preview": "---\ntitle: 项目案例\n---\n\n# 项目案例\n\n## 基于色度键控的客户端实时蒙版弹幕\n\n![Masking Danmaku Demo](../images/barrage.gif)\n\n<table>\n  <tr>\n    <td"
  },
  {
    "path": "docs/document/implement.md",
    "chars": 2180,
    "preview": "---\ntitle: 科普 - 如何实现蒙版弹幕\n---\n\n# 基于 Barrage UI 如何实现蒙版弹幕\n\nBarrage 组件提供了实现 蒙版弹幕 效果的可能。基于本组件实现的 demo 效果如下:\n\n![蒙版弹幕效果](../ima"
  },
  {
    "path": "docs/document/instance.md",
    "chars": 2646,
    "preview": "---\ntitle: 初始化参数\n---\n\n# 初始化参数\n\n在 `new Barrage({ ...options })` 创建弹幕实例时,需要传入的初始化参数如下:\n\n|   Options    |                  "
  },
  {
    "path": "docs/document/masking-api.md",
    "chars": 470,
    "preview": "---\ntitle: 蒙版图像接口\n---\n\n# 蒙版图像接口\n\n## 设置蒙版图像 - barrage.setMask(mask)\n\n**描述**\n\n用于设置蒙版图像。蒙版图像的概念见下文 [蒙版弹幕](/document/impleme"
  },
  {
    "path": "example.json",
    "chars": 2586,
    "preview": "[\n  {\n    \"key\": \"7g43mm0rpp1l67eh6qjo8\",\n    \"time\": 500,\n    \"text\": \"绿色走一波\",\n    \"color\": \"#0f0\",\n    \"createdAt\": \"2"
  },
  {
    "path": "index.js",
    "chars": 317,
    "preview": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.default = void 0;\n\nvar _dist = _"
  },
  {
    "path": "package.json",
    "chars": 1088,
    "preview": "{\n  \"scripts\": {\n    \"prepublishOnly\": \"npm run compile && npm run changelog\",\n    \"changelog\": \"npx conventional-change"
  },
  {
    "path": "src/index.js",
    "chars": 8615,
    "preview": "import {\n  requestAnimationFrame,\n  cancelAnimationFrame,\n  loadImage,\n  makeImageElement,\n  MIN_SEP,\n  layout,\n  insert"
  },
  {
    "path": "src/utils.js",
    "chars": 7188,
    "preview": "// 异步加载图片(若图片已加载,则使用缓存)\nconst loadImageCache = {};\nexport const loadImage = url =>\n  new Promise((resolve, reject) => {\n"
  }
]

About this extraction

This page contains the full source code of the parksben/barrage GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (34.0 KB), approximately 11.9k tokens, and a symbol index with 19 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!