Skip to content

Commit

Permalink
feat: 实现弹幕自动布局特性
Browse files Browse the repository at this point in the history
  • Loading branch information
pengan authored and pengan committed Apr 24, 2019
1 parent 2254c5a commit 1c2eab2
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 124 deletions.
60 changes: 32 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,27 @@ npm install --save barrage-ui
## 快速开始

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

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

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

// 播放弹幕
Expand All @@ -65,7 +65,7 @@ barrage.play();
| mask | string/[ImageData](https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData) | string/ImageData | 蒙版图像,用于实现蒙版弹幕效果,详见[蒙版弹幕](#蒙版弹幕) |
| 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 | false | 是否禁止弹幕重叠(有重叠部分的弹幕将不显示) |
| avoidOverlap | boolean | true | 是否禁止弹幕重叠(默认开启) |

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

Expand Down Expand Up @@ -140,7 +140,7 @@ barrage.setConfig({ opacity: 0.5 });

```js
const barrage = new Barrage({
container: "barrage",
container: 'barrage',
data: JSON_DATA, // JSON_DATA -> 你的弹幕数据
});
```
Expand All @@ -149,7 +149,7 @@ const barrage = new Barrage({

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

barrage.setData(JSON_DATA); // JSON_DATA -> 你的弹幕数据
Expand All @@ -161,11 +161,11 @@ barrage.setData(JSON_DATA); // JSON_DATA -> 你的弹幕数据

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

Expand All @@ -177,6 +177,10 @@ barrage.add({
2. 服务端将数据存储并分发给正在进行会话的客户端
3. 客户端收到数据后,使用 `.add()` 方法进行数据更新

> **说明**
`barrage.add()` 方法返回一个 `Boolean` 值,表示弹幕是否成功添加锦画布。若当前进度的画布中弹幕已经饱和,则可能添加失败。

## 动画控制接口

### barrage.play()
Expand Down Expand Up @@ -272,7 +276,7 @@ mask - 蒙版图像的 url 或 [ImageData](https://developer.mozilla.org/zh-CN/d
> **用例**
```js
barrage.setMask("mask.png"); // 通过图片 url 设置蒙版图像
barrage.setMask('mask.png'); // 通过图片 url 设置蒙版图像

barrage.setMask(imageData); // 直接设置 ImageData 类型的数据
```
Expand Down Expand Up @@ -334,29 +338,29 @@ Barrage 组件的初始化参数中的 `mask` 一项即用于处理蒙版效果
- 可通过初始化参数 `mask` 传入蒙版图像:

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

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

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

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

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

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

> **注意**
Expand All @@ -370,11 +374,11 @@ barrage.setMask("mask.png"); // 传入蒙版图像的 url
使用组件提供的 beforeRender 钩子函数,可以轻易的实现:

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

const barrage = new Barrage({
container: "barrage",
container: 'barrage',
data: example,
beforeRender: (ctx, progress) => {
const imageData = getMask(progress); // 用于获取当前进度对应蒙版的方法
Expand All @@ -386,11 +390,11 @@ const barrage = new Barrage({
当然,beforeRender 钩子也可以在弹幕初始化之后挂载:

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

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

Expand Down
Binary file modified images/barrage.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
142 changes: 46 additions & 96 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {
requestAnimationFrame,
cancelAnimationFrame,
loadImage,
MIN_SEP,
layout,
insertItem,
} from './utils';

// 支持通过 barrage.setConfig() 接口修改的配置项
Expand All @@ -11,6 +14,7 @@ const DEFAULT_CONFIG = {
fontSize: 24,
fontFamily: 'Microsoft Yahei',
textShadowBlur: 1.0,
lineHeight: 1.32,
opacity: 1.0,
defaultColor: '#fff',
};
Expand Down Expand Up @@ -42,7 +46,7 @@ export default class Barrage {
container,
data = [],
config = {},
avoidOverlap = false,
avoidOverlap = true,
mask = [],
beforeRender = () => {},
afterRender = () => {},
Expand Down Expand Up @@ -114,95 +118,31 @@ export default class Barrage {

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

for (let [prop, value] of Object.entries(config)) {
if (DEFAULT_CONFIG[prop]) this.config[prop] = value;
}
}

_randomTop() {
const LINE_HEIGHT = 1.32;

// 计算大致的行数
this.rowCount =
this.rowCount ||
Math.floor(this.canvas.height / (LINE_HEIGHT * this.config.fontSize));

// 随机产生纵向位置
const randomTop =
(this.config.fontSize * (LINE_HEIGHT - 1)) / 2 +
Math.floor(this.rowCount * Math.random()) *
LINE_HEIGHT *
this.config.fontSize;

return randomTop;
Object.assign(this.config, config);
}

// _optimizeData() {
// // 尽量避免文字重叠
// if (this.data) {
// for (let d of this.data) {
// for (let x of this.data) {
// const hasOverlap =
// (Math.abs(x.top - d.top) < this.config.fontSize * 0.1 &&
// x.left >= d.left &&
// x.left <= d.left + d.width) ||
// (Math.abs(x.top - d.top) < this.config.fontSize * 0.1 &&
// x.left + x.width >= d.left &&
// x.left + x.width <= d.left + d.width);

// if (hasOverlap) {
// x.hasOverlap = true;
// }
// }
// }

// console.log(this.data.filter(x => x.hasOverlap));
// }
// }

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

// 获取弹幕数据并计算出布局信息
this.data = data.map(
({
key,
time,
text,
fontSize = this.config.fontSize,
fontFamily = this.config.fontFamily,
color = this.config.defaultColor,
createdAt = new Date().toISOString(),
}) => {
// 若上一次数据集中已存在该数据,则直接保留
if (this.prevData && this.prevData.some(d => d.key === key)) {
return this.prevData.find(d => d.key === key);
}

// 弹幕布局
this.ctx.font = `${fontSize}px ${fontFamily}`;
const { width } = this.ctx.measureText(text);

return {
key,
time,
text,
fontSize,
fontFamily,
color,
createdAt,
left: (this.config.speed * time) / 1000 + this.canvas.width,
top: this._randomTop(),
width,
height: this.config.fontSize,
speedRatio: 0.5 * Math.random() + 1,
};
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,
Expand All @@ -211,28 +151,28 @@ export default class Barrage {
color = this.config.defaultColor,
createdAt = new Date().toISOString(),
}) {
this.ctx.font = `${fontSize}px ${fontFamily}`;
const { width } = this.ctx.measureText(text);

const record = {
const item = {
time,
text,
fontSize,
fontFamily,
color,
createdAt,
left: (this.config.speed * time) / 1000 + this.canvas.width,
top: this._randomTop(),
width,
height: this.config.fontSize,
speedRatio: Math.random() + 1,
};

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

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

// 计算播放进度,单位:毫秒
Expand Down Expand Up @@ -263,8 +203,8 @@ export default class Barrage {
let dataShown = this.data
.filter(
x =>
x.left + x.width - translateX * x.speedRatio >= 0 &&
x.left - translateX * x.speedRatio < this.canvas.width
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));

Expand Down Expand Up @@ -317,7 +257,17 @@ export default class Barrage {
dataShown.forEach(d => {
context.font = `${d.fontSize}px ${d.fontFamily}`;
context.fillStyle = d.color;
context.fillText(d.text, d.left - translateX * d.speedRatio, d.top);
context.fillText(
d.text,
d.left -
(translateX +
this.canvas.width *
d.randomRatio *
2 *
MIN_SEP *
Math.sin((Math.PI * translateX) / this.canvas.width)),
d.top
);
});

if (GLOBAL_MASK.data) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"url": "git+ssh://git@github.com/parksben/barrage.git"
},
"keywords": [
"danmaku",
"barrage",
"danmu",
"web-player",
Expand Down

0 comments on commit 1c2eab2

Please sign in to comment.