# brush
**Repository Path**: ymssx/brush
## Basic Information
- **Project Name**: brush
- **Description**: Brush.js是一个绘制canvas的JavaScript框架。
- **Primary Language**: JavaScript
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 4
- **Forks**: 2
- **Created**: 2020-05-12
- **Last Updated**: 2022-06-01
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Brush.js -- 用Canvas开发复杂应用
Brush.js是一个绘制Canvas的JavaScript框架。它是一套Canvas复杂应用开发的最佳实践。
* **组件化** 它解决了Canvas组件化的难题,为复杂应用的组件资产沉淀提供了可能性。
* **响应式** 它是数据驱动的,当数据更新时,Brush会自动更新相关的部分组件。在设计好组件的绘图逻辑之后,你只需要关注于数据逻辑部分。
* **高性能** 它对绘图的细节做了大量优化,将多组件绘图的时间复杂度从O(n)降到了O(log(n)),并可以自动实现局部渲染,你可以放心地交给Brush。
* **易使用** 它很容易上手,对于没有Canvas优化经验的开发者,也能轻松实现高性能的复杂Canvas应用。
## 为什么使用Brush
Canvas应用由于其难把握性,开发者往往难以开发出可靠的复杂应用,这让现代前端UI的多元性发展受到了一定的阻扰。Brush希望通过一套简洁易用的框架,帮助开发者负责那些难以把握的性能优化环节,同时组件化的开发模式能够让复杂应用变得更简单。总而言之,Brush不需要开发者关心任何技术细节,而是可以专注于业务逻辑,极大的提升了开发效率。
## 📦 安装
### 使用 `
```
## 🧲 使用
我们以制作一个俄罗斯方块游戏为例。首先你需要创建一个Brush实例,并且传入尺寸 w 和 h 以及绑定的元素root。
```javascript
const brush = new Brush({
w: 300,
h: 600,
root: document.getElementById('root')
})
// 如果后面的设置一切就绪,使用render方法就可以绘制
// brush.render()
```
为了书写简便,在Brush中的宽度width、高度height、左距离left、上距离top分别被简化成了w、h、x、y。
### 图层
在Brush中存在图层的概念。从本质上,一个图层就是一个独立的canvas元素,这是为了应对复杂的绘图场景,各个图层在绘制时保持互相不干扰,同时也可以使用webwoker多线程渲染进行优化。
总之,一个图层是一个绘制的基本单位。我们首先需要创建一个图层。
```javascript
const layer = brush.createLayer({
style: {
backgroundColor: 'white',
w: 300,
h: 600,
x: 0, // 默认是0
y: 0 // 默认是0
},
// 根级组件
el: new Container({
w: 300,
h: 600,
backgroundColor: '#ddd'
})
})
```
你需要为图层指定style样式,如果不指定,那么图层将会默认占满整个Brush画板,背景默认为透明。
你可能注意到了,我们设置了一个el属性,同时指定了一个入口组件,图层将会以这个组件开始,逐渐渲染整个组件树。当然,你也可以指定多个入口组件,通过数组传入多个组件即可。
### 组件
在Brush中,**一切皆是组件**,组件是构成整个复杂图像的基本单位。在组件中,我们将数据层和视图层进行了分离,在设置好了绘制模板之后,你只需要专注于数据业务逻辑即可。同时,每个组件将维护一个属于自己的offscreenCanvas,用于将自己的视图缓存下来,这也是Brush高效的原因之一。
Brush组件和React组件长得非常相似,本框架吸收了许多React的思想。因此,对于React的使用者来说,你可能使用起来非常熟悉。
我们首先来创建一个方块组件,为后面的俄罗斯方块游戏打下基础。
```javascript
class Box extends BrushElement {
constructor(props) {
super(props);
// 组件的默认属性
this.defaultProps = {
w: 50,
h: 50,
border: 5
}
}
// 绘图的部分放在这里
paint() {
this.ctx.fillStyle = this.props.bg;
let border = this.props.border;
/**
* 以下四个属性的两种获取方法是等同的
* this.w === this.props.w
* this.h === this.props.h
* this.x === this.props.x
* this.y === this.props.y
* 这是个简单的语法糖,简化对常用属性的访问
* 另外,brush支持使用百分比进行布局
*/
this.ctx.fillRect(border , border, '95%', this.h);
}
}
```
如何在组件中引用其它的组件呢?我们以方块“田”为例。
```javascript
class Tian extends BrushElement {
constructor(props) {
super(props);
this.defaultProps = {
w: 100,
h: 100
}
}
/**
* 指定需要用到的子组件
* 一定要命名为elMap,不支持自定义
* 组件实例名可以自定义,访问方式为: this.el.box
*/
elMap = {
box: new Box({
bg: 'black',
border: 5
})
};
paint() {
// 注意是el不是elMap
// 可以对子组件传参
// 在之后可以使用rotate、scale等方法进行后处理
// 最后一定要调用done方法表示结束
this.el.box({
x: 0,
y: 0
}).done();
this.el.box({
x: 50
}).done();
this.el.box({
x: 0,
y: 50
}).done();
this.el.box({
x: 50
}).done();
}
}
```
现在我们有了一个“田”字组件,接下来创建一个容器吧!
我们通过一些数据让方块动起来。
```javascript
class Container extends BrushElement {
constructor(props) {
super(props);
// 设置内部状态
this.state = {
i: 0
}
}
elMap = {
tian: new Tian({
x: 0,
y: 0
})
};
// 生命周期钩子,在组件被初始化后执行函数
created() {
// 每隔一秒将i自增
setInterval(() => {
this.setState({
i: this.state.i + 1
})
}, 1000);
}
paint() {
// 我们需要先清除一下画布
this.clear();
this.el.tian({
y: this.state.i * 10
}).done();
}
}
```
怎么样,是不是“有那味了”,一切都和react那么像,你只需要通过setState更新数据,Brush会自动对相关组件进行重绘,十分简单易用。
接下来,你需要按下启动键,组件树就开始绘制了。
```javascript
brush.render();
```
## 📚 核心概念
### 组件
组件是Brush中核心的概念,你只需要将目标细化分解成一个个组件,就能轻松的构建复杂的图像。
Brush的每一个组件都维护了一个私有的offscreenCanvas,用于保存自己的视图状态,只有在必要更新时,组件才会进行重绘。
组件允许嵌套其它子组件,你可以在绘制函数`paint`中指定子组件的使用时机,你也可以在组件外部进行指定。
```javascript
class Demo extends BrushElement {
// 指定你需要的组件们
elMap = {
box: new Box({
// ... 初始化参数
})
};
constructor(props) {
super(props);
}
paint() {
//... 绘制逻辑
this.el.box.paint({
// ... 传递参数
}).done();
}
}
```
注意,`this.el.name`获取的是一个控制器函数,而不是组件实例本身。其功能是传递新的参数并通知更新,在子组件绘制之后,链式调用`done`方法采集其canvas内容。而`this.elMap.name`才能直接获取到组件实例本身。
在paint之后你可以使用rotate、scale、translate、transform、opacity等方法对子组件进行后处理:
```javascript
this.el.box.paint({
// ...
}).rotate(45).translate(100, 100).scale(1.2, 0.8).done();
```
你可能需要一个现成的组件上进行补充,或者以一个组件为背景快速创建图形,你可以在外部指定子组件。
```javascript
class Demo extends BrushElement {
elMap = {
// ...
box1: new Box(),
box2: new Box()
}
// ...
paint() {
this.el.box({
// ... 传递参数
}).addChild([
this.elMap.box1,
this.elMap.box2
]).done();
}
}
```
### 绘图扩展
(进行中)Brush对canvas绘制API进行了一系列的补充,其简化了绘制复杂度,扩增了一系列常用的绘图功能,同时你也拥有完全的原生canvas API。
**Brush在绘制时允许你使用百分比、vw、vh等实用的动态参数。**
#### `ctx.rect(x, y, w, h)`
绘制矩形
#### `ctx.circle(x, y, r)`
绘制圆
#### `ctx.plot(X: number[], Y: number[])`
(计划)快速绘制折线图
#### `ctx.smooth(X: number[], Y: number[])`
(计划)通过三次样条插值快速绘制曲线
... 待补充
### 动画
Brush的动画是数据驱动的,你只需要指定你的目标state和过渡时间(ms),我们会自动平滑地绘制过渡动画(仅支持数值)。
```javascript
BrushElement.smoothState(targetState, delay);
```
* **targetState** 需要渐变的目标值,会自动渐近地改变state中对应的数值部分。
* **delay** 动画过渡时间。
smoothState的返回值是一个Promise对象,你可以在之后链式调用其它动画。
让我们升级一下上述的容器,让它的移动更平滑!
```javascript
class Container extends BrushElement {
constructor(props) {
super(props);
// 设置内部状态
this.state = {
i: 0
}
}
elMap = {
tian: new Tian({
x: 0,
y: 0
})
};
created() {
// 每隔一秒将i自增
let i = 1;
setInterval(() => {
// 300ms的过渡动画
this.smoothState({
i: i++
}, 300);
}, 1000)
}
paint() {
this.clear();
this.el.tian({
y: this.state.i * 10
})
}
}
```
也许你需要数据**永不停息**地增长,你可以使用`infiniteState`方法,传入一个增长速度,我们会按照这个速度进行平滑的增加。
```javascript
BrushElement.infiniteState(stepState);
```
函数返回一个控制器,你可以使用stop、start进行暂停和启动。
例如:
```javascript
let control = this.infiniteState({
i: 10, // 每秒平滑地增加10
j: {
k: 1 // 每秒平滑地增加1
}
});
setTimeout(() => {
control.stop();
}, 5000)
```
或者你需要组件没有时间限制的运动,你可以使用stepState,我们会尽可能的在每次电话帧之前修改state。
```javascript
BrushElement.stepState(stepState);
```
函数返回一个控制器,你可以使用stop、start进行暂停和启动。
例如:
```javascript
let control = this.stepState({
i: 10, // 每秒平滑地增加10
j: {
k: 1 // 每秒平滑地增加1
}
});
setTimeout(() => {
control.stop();
}, 5000)
```
你也可以传入一个函数
```javascript
let control = this.stepState(state => {
return {
x: state.x + 1,
}
});
```
当然了,你可以自定义你自己的动画,在通常我们使用requestAnimationFrame来请求动画帧,在Brush中,请使用nextFrame方法。
```javascript
let animation = () => {
this.state.i++;
// 如果你希望递归调用动画,请在末尾加上nextFrame
this.nextFrame(animation);
}
this.nextFrame(animation);
```
### 事件
Brush中的父子组件通信可以使用props进行。
...
### 界面交互
Brush允许你轻松的创建界面交互效果,你只需要在组件中设置鼠标事件回调函数即可。
```javascript
class Demo extends BrushElement {
constructor(props) {
super(props);
this.state = { i: 1 };
}
created() {
this.addEvent('click', () => {
this.setState({
i: this.state.i + 1
})
})
}
}
```
支持的鼠标事件有 `click | over | in | out`
同时,你也可以随时更改鼠标样式
```javascript
this.changeCursor('pointer');
```
Brush组件中的事件同样存在事件冒泡,在捕获到最终的目标组件之后,事件会反向向父级传播,直到传播到根级组件。
### store
...
### 状态提升
...
## 💡 深入原理
### 组件树