{"id":13670399,"url":"https://github.com/ymssx/brush","last_synced_at":"2026-03-03T22:33:03.204Z","repository":{"id":132936077,"uuid":"267037814","full_name":"ymssx/brush","owner":"ymssx","description":"Brush.js is a JavaScript framework for drawing canvas","archived":false,"fork":false,"pushed_at":"2023-02-28T03:47:27.000Z","size":83,"stargazers_count":16,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-07T04:32:04.705Z","etag":null,"topics":["brush","canvas","javascript"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ymssx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-05-26T12:33:29.000Z","updated_at":"2023-11-24T14:41:06.000Z","dependencies_parsed_at":"2023-06-04T14:30:47.011Z","dependency_job_id":null,"html_url":"https://github.com/ymssx/brush","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ymssx/brush","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ymssx%2Fbrush","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ymssx%2Fbrush/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ymssx%2Fbrush/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ymssx%2Fbrush/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ymssx","download_url":"https://codeload.github.com/ymssx/brush/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ymssx%2Fbrush/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30064361,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T18:21:05.932Z","status":"ssl_error","status_checked_at":"2026-03-03T18:20:59.341Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["brush","canvas","javascript"],"created_at":"2024-08-02T09:00:41.037Z","updated_at":"2026-03-03T22:33:03.188Z","avatar_url":"https://github.com/ymssx.png","language":"JavaScript","readme":"# Brush.js\n\nThis repo is deprecated, please move to [https://github.com/ymssx/muser](https://github.com/ymssx/muser)\n\n\n----------------------------------------------------------------------\n\nBrush.js是一个绘制canvas的JavaScript框架。\n\n* **组件化** 和React非常相似，你可以创建拥有各自状态的组件，每个组件只需要专注于自身的绘图逻辑，再由这些组件构成更加复杂的 UI。\n\n* **高性能** Brush对绘图的细节做了大量优化，将多组件绘图的时间复杂度从O(n)降到了O(log(n))，你可以放心的交给Brush。\n\n* **响应式** Brush是数据驱动的，当数据更新时，Brush会自动更新相关的部分组件。在设计好组件的绘图逻辑之后，你只需要关注于数据逻辑部分。\n\n\u003cbr/\u003e\n\n## 📦 安装\n\n### 使用 `\u003cscript\u003e` 引用\n\n```html\n\u003cdiv id=\"root\"\u003e\u003c/div\u003e\n\n\u003cscript src=\"xxx/brush.js\"\u003e\u003c/script\u003e\n```\n\n\u003cbr/\u003e\n\n## 🧲 使用\n\n我们以制作一个俄罗斯方块游戏为例。首先你需要创建一个Brush实例，并且传入尺寸 w 和 h 以及绑定的元素root。\n\n```javascript\nconst brush = new Brush({\n  w: 300,\n  h: 600,\n  root: document.getElementById('root')\n})\n\n// 如果后面的设置一切就绪，使用render方法就可以绘制\n// brush.render()\n```\n\n为了书写简便，在Brush中的宽度width、高度height、左距离left、上距离top分别被简化成了w、h、x、y。\n\n\u003cbr/\u003e\n\n### 图层\n\n在Brush中存在图层的概念。从本质上，一个图层就是一个独立的canvas元素，这是为了应对复杂的绘图场景，各个图层在绘制时保持互相不干扰，同时也可以使用webwoker多线程渲染进行优化。\n\n总之，一个图层是一个绘制的基本单位。我们首先需要创建一个图层。\n\n```javascript\nconst layer = brush.createLayer({\n  style: {\n    backgroundColor: 'white',\n    w: 300,\n    h: 600,\n    x: 0, // 默认是0\n    y: 0  // 默认是0\n  },\n  // 根级组件\n  el: new Container({\n    w: 300,\n    h: 600,\n    backgroundColor: '#ddd'\n  })\n})\n```\n\n你需要为图层指定style样式，如果不指定，那么图层将会默认占满整个Brush画板，背景默认为透明。\n\n你可能注意到了，我们设置了一个el属性，同时指定了一个入口组件，图层将会以这个组件开始，逐渐渲染整个组件树。当然，你也可以指定多个入口组件，通过数组传入多个组件即可。\n\n\u003cbr/\u003e\n\n### 组件\n\n在Brush中，**一切皆是组件**，组件是构成整个复杂图像的基本单位。在组件中，我们将数据层和视图层进行了分离，在设置好了绘制模板之后，你只需要专注于数据业务逻辑即可。同时，每个组件将维护一个属于自己的offscreenCanvas，用于将自己的视图缓存下来，这也是Brush高效的原因之一。\n\nBrush组件和React组件长得非常相似，本框架吸收了许多React的思想。因此，对于React的使用者来说，你可能使用起来非常熟悉。\n\n我们首先来创建一个方块组件，为后面的俄罗斯方块游戏打下基础。\n\n```javascript\nclass Box extends BrushElement {\n  constructor(props) {\n    super(props);\n    // 组件的默认属性\n    this.defaultProps = {\n      w: 50,\n      h: 50,\n      border: 5\n    }\n  }\n  // 绘图的部分放在这里\n  paint() {\n    this.ctx.fillStyle = this.props.bg;\n    let border = this.props.border;\n    /**\n     * 以下四个属性的两种获取方法是等同的\n     * this.w === this.props.w\n     * this.h === this.props.h\n     * this.x === this.props.x\n     * this.y === this.props.y\n     * 这是个简单的语法糖，简化对常用属性的访问\n     * 另外，brush支持使用百分比进行布局\n     */\n    this.ctx.rect(border , border, '95%', this.h);\n  }\n}\n```\n\n如何在组件中引用其它的组件呢？我们以方块“田”为例。\n\n```javascript\nclass Tian extends BrushElement {\n  constructor(props) {\n    super(props);\n    this.defaultProps = {\n      w: 100,\n      h: 100\n    }\n  }\n  /**\n   * 指定需要用到的子组件\n   * 一定要命名为elMap，不支持自定义\n   * 组件实例名可以自定义，访问方式为: this.el.box\n   */\n  elMap = {\n    box: new Box({\n      bg: 'black',\n      border: 5\n    })\n  };\n\n  paint() {\n    // 注意是el不是elMap\n    // 可以对子组件传参\n    // 在之后可以使用rotate、scale等方法进行后处理\n    // 最后一定要调用done方法表示结束\n    this.el.box({\n      x: 0,\n      y: 0\n    }).done();\n\n    this.el.box({\n      x: 50\n    }).done();\n\n    this.el.box({\n      x: 0,\n      y: 50\n    }).done();\n\n    this.el.box({\n      x: 50\n    }).done();\n  }\n}\n```\n\n现在我们有了一个“田”字组件，接下来创建一个容器吧！\n我们通过一些数据让方块动起来。\n\n```javascript\nclass Container extends BrushElement {\n  constructor(props) {\n    super(props);\n    // 设置内部状态\n    this.state = {\n      i: 0\n    }\n  }\n\n  elMap = {\n    tian: new Tian({\n      x: 0,\n      y: 0\n    })\n  };\n\n  // 生命周期钩子，在组件被初始化后执行函数\n  created() {\n    // 每隔一秒将i自增\n    setInterval(() =\u003e {\n      this.setState({\n        i: this.state.i + 1\n      })\n    }, 1000);\n  }\n\n  paint() {\n    // 我们需要先清除一下画布\n    this.clear();\n    this.el.tian({\n      y: this.state.i * 10\n    }).done();\n  }\n}\n```\n\n怎么样，是不是“有那味了”，一切都和react那么像，你只需要通过setState更新数据，Brush会自动对相关组件进行重绘，十分简单易用。\n\n接下来，你需要按下启动键，组件树就开始绘制了。\n\n```javascript\nbrush.render();\n```\n\n\u003cbr/\u003e\n\n## 📚 核心概念\n\n### 组件\n\n组件是Brush中核心的概念，你只需要将目标细化分解成一个个组件，就能轻松的构建复杂的图像。\n\nBrush的每一个组件都维护了一个私有的offscreenCanvas，用于保存自己的视图状态，只有在必要更新时，组件才会进行重绘。\n\n组件允许嵌套其它子组件，你可以在绘制函数`paint`中指定子组件的使用时机，你也可以在组件外部进行指定。\n\n```javascript\nclass Demo extends BrushElement {\n  // 指定你需要的组件们\n  elMap = {\n    box: new Box({\n      // ... 初始化参数\n    })\n  };\n\n  constructor(props) {\n    super(props);\n  }\n\n  paint() {\n    //... 绘制逻辑\n    this.el.box.paint({\n      // ... 传递参数\n    }).done();\n  }\n}\n```\n\n注意，`this.el.name`获取的是一个控制器函数，而不是组件实例本身。其功能是传递新的参数并通知更新，在子组件绘制之后，链式调用`done`方法采集其canvas内容。而`this.elMap.name`才能直接获取到组件实例本身。\n\n在paint之后你可以使用rotate、scale、translate、transform、opacity等方法对子组件进行后处理：\n\n```javascript\nthis.el.box.paint({\n    // ...\n}).rotate(45).translate(100, 100).scale(1.2, 0.8).done();\n```\n\n你可能需要一个现成的组件上进行补充，或者以一个组件为背景快速创建图形，你可以在外部指定子组件。\n\n```javascript\nclass Demo extends BrushElement {\n  elMap = {\n    // ...\n    box1: new Box(),\n    box2: new Box()\n  }\n  // ...\n  paint() {\n    this.el.box({\n      // ... 传递参数\n    }).addChild([\n      this.elMap.box1,\n      this.elMap.box2\n    ]).done();\n  }\n}\n```\n\n\u003cbr/\u003e\n\n### 绘图扩展\n\n（进行中）Brush对canvas绘制API进行了一系列的补充，其简化了绘制复杂度，扩增了一系列常用的绘图功能，同时你也拥有完全的原生canvas API。\n\n**Brush在绘制时允许你使用百分比、vw、vh等实用的动态参数。**\n\n#### `ctx.rect(x, y, w, h)`\n\n绘制矩形\n\n#### `ctx.circle(x, y, r)`\n\n绘制圆\n\n#### `ctx.plot(X: number[], Y: number[])`\n\n（计划）快速绘制折线图\n\n#### `ctx.smooth(X: number[], Y: number[])`\n\n（计划）通过三次样条插值快速绘制曲线\n\n... 待补充\n\n\u003cbr/\u003e\n\n### 动画\n\nBrush的动画是数据驱动的，你只需要指定你的目标state和过渡时间(ms)，我们会自动平滑地绘制过渡动画（仅支持数值）。\n\n```javascript\nBrushElement.smoothState(targetState, delay);\n```\n\n* **targetState** 需要渐变的目标值，会自动渐近地改变state中对应的数值部分。\n\n* **delay** 动画过渡时间。\n\nsmoothState的返回值是一个Promise对象，你可以在之后链式调用其它动画。\n\n让我们升级一下上述的容器，让它的移动更平滑！\n\n```javascript\nclass Container extends BrushElement {\n  constructor(props) {\n    super(props);\n    // 设置内部状态\n    this.state = {\n      i: 0\n    }\n  }\n\n  elMap = {\n    tian: new Tian({\n      x: 0,\n      y: 0\n    })\n  };\n\n  created() {\n    // 每隔一秒将i自增\n    let i = 1;\n    setInterval(() =\u003e {\n      // 300ms的过渡动画\n      this.smoothState({\n        i: i++\n      }, 300);\n    }, 1000)\n  }\n\n  paint() {\n    this.clear();\n    this.el.tian({\n      y: this.state.i * 10\n    })\n  }\n}\n```\n\n也许你需要数据**永不停息**地增长，你可以使用`infiniteState`方法，传入一个增长速度，我们会按照这个速度进行平滑的增加。\n\n```javascript\nBrushElement.infiniteState(stepState);\n```\n\n函数返回一个控制器，你可以使用stop、start进行暂停和启动。\n\n例如：\n\n```javascript\nlet control = this.infiniteState({\n  i: 10, // 每秒平滑地增加10\n  j: {\n    k: 1 // 每秒平滑地增加1\n  }\n});\n\nsetTimeout(() =\u003e {\n  control.stop();\n}, 5000)\n```\n\n当然了，你可以自定义你自己的动画，在通常我们使用requestAnimationFrame来请求动画帧，在Brush中，请使用nextFrame方法。\n\n```javascript\nlet animation = () =\u003e {\n  this.state.i++;\n  // 如果你希望递归调用动画，请在末尾加上nextFrame\n  this.nextFrame(animation);\n}\nthis.nextFrame(animation);\n```\n\n\u003cbr/\u003e\n\n### 事件\n\nBrush中的父子组件通信可以使用props进行。\n...\n\n### 界面交互\n\nBrush允许你轻松的创建界面交互效果，你只需要在组件中设置鼠标事件回调函数即可。\n\n```javascript\nclass Demo extends BrushElement {\n  constructor(props) {\n    super(props);\n    this.state = { i: 1 };\n  }\n\n  created() {\n    this.addEvent('click', () =\u003e {\n      this.setState({\n        i: this.state.i + 1\n      })\n    })\n  }\n}\n```\n\n支持的鼠标事件有 `click | over | in | out`\n\n同时，你也可以随时更改鼠标样式\n```javascript\n\nthis.changeCursor('pointer');\n```\n\nBrush组件中的事件同样存在事件冒泡，在捕获到最终的目标组件之后，事件会反向向父级传播，直到传播到根级组件。\n\n### store\n\n...\n\n### 状态提升\n\n...\n\n\u003cbr/\u003e\n\n## 💡 深入原理\n\n### 组件树\n\n\u003cdiv align=center\u003e\u003cimg height=\"400px\" src=\"./figure/tree.svg\"/\u003e\u003c/div\u003e\u003cbr/\u003e\n\n父子组件之间是一对多的关系，每个子组件同时拥有自己的子组件，最终形成了一个组件树。同时，每一个组件都自行维护了一个属于自己的offscreenCanvas，只有在有必要更新时，才会自行进行重绘，更新自己的canvas。\n\n也就是说，当一个组件在重绘的时候，自己的每一个子组件都会根据情况（比如props是否有变动、变动属性是否被依赖、自身状态是否过期等）判断自己是否需要重绘，如果不需要则直接返回自己的canvas内容，而父级组件本身只需要拿到这些内容进行绘制。\n\n\u003cdiv align=center\u003e\n  \u003cimg width=\"80%\" src=\"./figure/rendercompare.svg\"/\u003e\n\u003c/div\u003e\u003cbr/\u003e\n\n这样一来，比起将所有的组件都无差别的进行重绘，Brush只需要对某一条路线进行重绘就可以了，时间复杂度从O(n)降低到了O(log(n)),这也就是为什么Brush高效的原因。\n\n\u003cbr/\u003e\n\n### 更新策略\n\nBrush的更新策略非常值得一说。早期在设计Brush的架构的时候，Brush采用的是一种自下向上、反向传播通知的更新策略。\n\n首先要明确的一点是，父组件完全依赖于子组件的内容，如果子组件的内容不是最新的，那么父级组件的绘制是完全无效的。所以Brush早期的策略是：\n\n1. 每个组件都维护一个内部状态state，当使用setState更新数据时，Brush使用requestAnimationFrame将绘制函数放到异步队列，同时多次数据更新会取消之前的绘制任务，这样一来，可以只在下一个动画帧前仅执行一次绘制函数。\n\n2. 当子组件更新时，子组件会进行自我重绘，在那之后反向通知父组件，父组件根据所有子组件当前的内容进行重绘，接着继续向上传播，直到根级组件被完全更新。\n\n这样的策略看起来十分完美，但是实际上存在几个问题。\n\n\u003e 一个组件只有在其被更新完毕之后才能进行反向传播，因为父级组件提前进行更新是没有意义的。由于绘制任务是异步的，一个动画帧仅能执行一次，也就是说，只有等到一个动画帧绘制完毕之后才会通知父级进行更新，一帧仅能反向传播一个单位。\n\n这会带来什么问题呢？我们假设在组件树的不同部位的两个子组件A、B同时（或者说在同一个主任务中）进行了数据更新，其最近的公共父组件为F。由于A、B在组件树里面的深度不一样，而反向传播的速度为 1层/帧，也就是说A、B到达父组件的顺序不一样！尽管A、B是同时更新的。最终父级组件F被分别更新了2次，这就带来了性能浪费。\n\n父级组件非常依赖子组件的内容，所以渲染顺序十分重要，如果在子组件内容不是最新时，父组件进行绘制是完全无效的。由于子组件的反向传播时间取决于在树中的深度，所以绘制顺序难以控制。\n\n同时，当一个组件树的路径非常长时，可能会导致需要传播许久才能达到根节点。\n\n那么如果取消对绘制函数的异步处理呢？假设父子组件同时进行更新，父级组件会进行两次绘制，同时在子组件未更新时父组件的绘制是浪费的。所以我们需要转变思路，改为从正向传播更新，最终Brush采取了下面的策略。\n\n\u003cbr/\u003e\n\n#### Brush的策略\n\n\u003cdiv align=center\u003e\n  \u003cimg width=\"80%\" src=\"./figure/updating.svg\"/\u003e\n\u003c/div\u003e\u003cbr/\u003e\n\n1. 每个子组件更新数据之后，直接向根级元素（图层）发送更新请求，图层收集来自各个组件的更新请求，并且把来自于同一个组件的请求进行去重，只保留一个。\n\n2. 图层每次收到更新请求后，利用requestAnimationFrame执行一个异步函数（见3），多次收到更新将会取消异步任务，重新执行异步函数。\n\n\u003cdiv align=center\u003e\n  \u003cimg width=\"40%\" src=\"./figure/overdue.svg\"/\u003e\n\u003c/div\u003e\u003cbr/\u003e\n\n3. 在下一个动画帧之前，图层分析所有发起更新请求的子组件，得到从根组件到请求子组件的**更新链**，并且将更新链上所有的组件标记为 **过期**。\n\n4. 由根组件开始向下发起渲染请求，每个组件在绘制时向子组件**索要**最新的canvas内容。如果子组件的状态为未过期，且props未发生有效变化，那么直接向父组件交付自己的canvas，不进行重绘；如果子组件已过期，那么就会强制进行重绘，同时也向自己的子组件索要最新内容，对于每一个子组件都重复上述的内容。这样一来，最终所有的组件都变成了最新状态。\n\n一次整体的更新是在一次主任务中执行的，从上至下向子组件索要更新，这能保证每个组件的渲染顺序都是正确的，且最多只被绘制了一次，直接跳过无需更新的组件，所有问题都迎刃而解。\n","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fymssx%2Fbrush","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fymssx%2Fbrush","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fymssx%2Fbrush/lists"}