leaferjs,全新的 Canvas 渲染引擎
liuian 2024-12-18 15:36 89 浏览
1. 前言
前几天群里有人发了一个信 Canvas 渲染引擎的图片,看数据和宣传口号相当炸裂,号称只用 1.5s 可以渲染 100 万个矩形,还是个国产的。
出于个人兴趣,就花了一点儿时间研究了一下感兴趣的点,如果有错误,希望可以指出。
2. 架构设计
从火焰图上可以看出,leaferjs 创建节点非常轻量,只做了 setAttr 的操作。
大部分耗时集中在创建节点和布局,渲染仅仅花了3ms。
那 leaferjs 为什么有这么好的性能呢?我简单去看了一下源码。
leafer 主要包括了 leafer 和 ui 两个 git 仓库,核心渲染能力在 leafer 里面,ui 封装了一些绘制类,比如 Image、Line 等等。
先看一下官网上一个详细的用法:
php复制代码import { App, Leafer, Rect } from 'leafer-ui'
const app = new App({ view: window, type: 'user' })
const backgroundLayer = new Leafer()
const contentLayer = new Leafer({ type: 'design' })
const wireframeLayer = new Leafer()
app.add(backgroundLayer)
app.add(contentLayer)
app.add(wireframeLayer)
const background = new Rect({ width: 800, height: 600, fill: 'gray' })
const rect = new Rect({ x: 100, y: 100, fill: '#32cd79', draggable: true })
const border = new Rect({ x: 200, y: 200, stroke: 'blue', draggable: true })
backgroundLayer.add(background)
contentLayer.add(rect)
wireframeLayer.add(border)
从 Demo 可以看到 App 作为一个应用的实例,能往里面添加 Leafer 实例,每个 Leafer 内部会创建一个 Canvas 节点,这个和 Konva 的 Layer 比较相似。
通过创建多个 Leafer,可以来做 Canvas 分层优化。
每个 leafer 作为一个容器,可以里面去添加子节点,比如 rect 等等。
2.1 Leafer
从 Leafer 作为切入点,发现上面挂了很多装饰器。
java复制代码@registerUI()
export class Leafer extends Group implements ILeafer {
public get __tag() { return 'Leafer' }
@dataProcessor(LeaferData)
public __: ILeaferData
@boundsType()
public pixelRatio: number
public get isApp(): boolean { return false }
public parent?: App
}
其中 registerUI 的用来注册当前的 Leafer 类的,会将其放入一个 UICreator.list 里面,后续可以使用 Leafer.one(data) 的形式来创建。
__tag 是用于标识当前节点的类型,比如 'Leafer'、'Rect' 等等。
boundsType 装饰器里面通过 Object.defineProperty 对 set 做了拦截,底层调用了 __setAttr 方法。
其中节点的一些信息都挂在 __ 上面,比如 fill、shadow 等等。
interaction 模块是用于处理事件监听的,它会监听 DOM 事件,将其再次分发给节点。
canvasManager 是用于管理 Canvas 节点的,可以理解为一个 Canvas 池,支持创建、销毁 Canvas 节点,也支持复用相同尺寸的 Canvas 节点。
imageManager 是用于管理图片资源的下载、获取的模块。
在 init 方法中,会根据传给 Leafer 的 config 信息创建一个新的 Canvas 节点,前提是你有设置 view 属性,所以 leaferjs 支持 Canvas 分层管理。
Creator 提供了一系列创建方法,其中 renderer 是创建了一个渲染器,里面封装了 Canvas 渲染的核心机制。
这里还调用 Creator.watcher 来创建一个 Watcher 实例,Watcher 观察节点的属性变化,从而触发重新渲染。
Creator.selector 创建了一个选择器,主要用于根据坐标点去查询对应的 Branch 分支。
Leafer 继承了 Group 类,Group 又 mixin 了 Branch 类,所以在 leaferjs 里面,所以容器类都是继承了 Group 和 Branch。
2.2 Leaf
那创建完成后,形状又是怎么绘制的呢?我们来看一下 Rect 这个类,它的实现非常简单。
kotlin复制代码@useModule(RectRender)
@registerUI()
export class Rect extends UI implements IRect {
public get __tag() { return 'Rect' }
@dataProcessor(RectData)
public __: IRectData
constructor(data?: IRectInputData) {
super(data)
}
public __drawPathByData(drawer: IPathDrawer, _data: IPathCommandData): void {
const { width, height, cornerRadius } = this.__
if (cornerRadius) {
drawer.roundRect(0, 0, width, height, cornerRadius)
} else {
drawer.rect(0, 0, width, height)
}
}
}
这里做了下面几件事:
- 使用装饰器 useModule 来将 RectRender 类的方法混合到 Rect 上面。
- 继承了 UI 类。
- 实现了 __drawPathByData 方法,看起来是绘制方法。
先来看一下 RectRender 里面做了什么,发现它只实现了一个 __drawFast 方法,那这个 __drawFast 和 __drawPathByData 有什么区别呢?
搜索了一下调用方,发现两者的区别在于当前绘制类是否有 __complex,如果是复杂的,就走 __drawPathByData,否则就走 __drawFast。
那什么是复杂,什么是简单呢?以官网 Demo 为例子,当 fill 不是字符串的时候就算是复杂绘制。复杂绘制去尝试去解析 fill、stroke 等属性,最后才调用 __drawPathByData。
Rect 继承的 UI 类封装了绘制方法的调用,以及 fill、stroke、x、y 等属性。
UI 类又继承了 Leaf 类,Leaf 是最底层的类,混合了一系列底层能力。
LeafMatrix 定义了矩阵变换的信息,LeafBounds 定义了包围盒的信息,LeafEventer 提供了事件的监听、取消监听等方法。
LeafDataProxy 提供了 get/set 的能力,前面 UI 类定义的时候通过 @opcityType、@positionType 等装饰器拦截了属性的 set。
因此当我们每次修改属性的时候,就会触发到这里的 __setAttr 方法。
kotlin复制代码
export const LeafDataProxy: ILeafDataProxyModule = {
__setAttr(name: string, newValue: unknown): void {
if (this.leafer && this.leafer.ready) {
this.__[name] = newValue
const { CHANGE } = PropertyEvent
const event = new PropertyEvent(CHANGE, this, name, this.__.__get(name), newValue)
if (this.hasEvent(CHANGE) && !this.isLeafer) this.emitEvent(event)
this.leafer.emitEvent(event)
} else {
this.__[name] = newValue
}
},
__getAttr(name: string): unknown {
return this.__.__get(name)
}
}
3. 更新机制
前面的 __setAttr 方法触发时,就会调用 this.emitEvent(CHANGE) 发送一个事件。
事件在前面说的 Watcher 里面监听,会将当前节点放到一个更新队列里面,并发送一个 RenderEvent.REQUEST 事件,开始请求渲染。
请求渲染之后,就会放入一个 requestAnimateFrame 里面进行下一帧渲染,这样做是为了提升性能做批量更新,避免大量属性修改的时候频发触发更新。
这里可以参考一下官网给的渲染生命周期,可以发现是一致的:
render 方法里面有一系列判断,核心点在于 fullRender 和 partRender 两个地方。
3.1 可视区域渲染
先来看一下 fullRender 方法,这个是全量渲染,不会去计算最小渲染区域。当初次渲染或者设置了 usePartRender 为 false 的时候就会走全量渲染。
全量渲染会调用到 this.target.__render 里面,这个 target 是指 Leafer,意思就是从 Leafer 根节点开始,往下遍历子节点来渲染。
那 __render 哪里来的呢?Leafer 继承了 Group,Group 又混合了 Branch 的方法,所以 __render 就是 Branch 类上面的。
这里的核心在于下面这句:
遍历渲染的时候,会判断当前 Branch 或者 Leaf 节点是否在给定的 Bound 内(这里的 Bound 就是可视区域,child.__world 是当前节点的位置信息,调用 bounds.hit 方法)。
如果不在可视区域,那就 continue,否则就执行子节点的 __render 方法。
在 Fabric 里面也有这种的优化,Konva 里面反而没有,所以在 leaferjs 给的对比里面,Konva 渲染速度是最低的。
3.2 局部渲染
另一个分支是 partRender 方法,partRender 的实现原理是将每个节点变化前后的包围盒进行一次合并,计算出当前节点的 Block。
然后利用 Canvas 的 clip 进行裁剪,再去遍历 Leafer 下面所有的子节点,判断其是否和 Block 相交,如果相交那么就进行重绘。
partRender 的源码如下:
updateBlocks 是这次更新涉及的所有节点的包围盒信息,其中每个节点的包围盒信息都是更新前和更新后的两个包围盒合并后的信息。
举个简单的矩形向右移动 100px 的例子:
arduino复制代码const rect = new Rect({ x: 0, y: 0, width: 100, height: 100});
rect.x = 100;
上面这个矩形的位置发生了变化,它在这次更新中的包围盒信息就是 { x: 0, y: 0, width: 200, height }。
最关键的点在于 clipRender 里面进行了局部渲染,那么它是怎么做的呢?
其实本质上还是复用了前面 fullRender 里面判断节点和 Bounds 是否相交,如果相交的话,这个节点就会进行重绘。
使用下面这个例子来讲解会更容易理解一些:
rect2 移动到了下方,虚线框就是要重绘的区域,在重绘之前先对这片区域进行 clip,然后 clear 清空这片区域。
接着对节点进行遍历,遍历后的结果是 circle1、rect2、circle2、rect3 是需要重绘的,就会调用这些节点的 __render 方法进行重绘。
这里为什么 rect4 没有被重绘呢?虽然它和 circle2 相交了,但由于提前进行了一次 clip,因此 circle2 的重绘不会影响到 rect4。
使用局部渲染,可以避免每次节点的修改都会触发整个画布的重绘,降低绘制的开销。
但由于 hit 计算也有一定的 cpu 开销,对于一些修改影响范围大的场景,性能可能反而不如全量渲染。
4. 事件拾取
事件拾取也是 Canvas 渲染引擎里面的一个核心功能,一般来说 Canvas 在 DOM 树里面的表现只是一个节点,里面的形状都是自己绘制的,因此我们无法感知到用户当前触发的是哪个形状。
在 Konva 里面采用了色值法的方式来实现,但色值法开销很大,尤其是绘制带来了两倍开销。
在 leaferjs 里面针对 Konva 的事件拾取做了一定优化。
对事件拾取感兴趣的也可以看一下 Antv/g 语雀上的一篇博客:G 4.0 的拾取方案设计
前面讲过,interaction 模块封装了事件,它将绑定在 Leafer 根节点的 DOM 事件进行了包装和分发,分发给对应的 Leaf 节点。
我们以鼠标的点击事件为例子来讲解,this.selector.getByPoint 就是根据坐标点来匹配 Leaf 节点的方法。
getByPoint 最终调用到了 FindPath 的 eachFind 里面。
在 eachFind 里面会遍历当前 Leafer 的子节点,子节点可能是个 Branch(Group),也可能是个 Leaf。
如果是个 Branch 的话,就先通过 hitRadiusPoint 来判断是否 hit 了当前的 Branch,如果命中了,那就继续递归它的子节点。
如果不是个 Branch,那么就是个普通 Leaf 节点,直接调用 __hitWorld 方法来判断 point 是否命中当前的 Leaf 节点。
__hitWorld 的原理是:
- 在离屏的一个 hitCanvas 里面将当前节点绘制一遍。
- 调用 isPointInPath 或者 isPointInStroke 来判断是否打击到了。
为什么这里要利用 isPointInPath 呢?
因为在 beginPath 之后,绘制的路径都会被添加到这个路径集合里,isPointInPath(x, y) 方法判断的就是x、y 点是否在这个路径集合的所有路径里。
画一个流程图来梳理一下事件拾取:
所以对于不规则图形来说,通过 isPointInPath 也可以简单的判断是否命中,不需要自己去写复杂的几何碰撞算法。
很显然 isPointInPath 也有缺点,那就是同样需要绘制两遍,一个是主画布,一个是 hitCanvas。
相比 Konva 在首屏就绘制了两遍,leaferjs 会在事件触发的时候,针对当前遍历的节点进行 hitCanvas 的绘制,所以首屏渲染性能比 Konva 要好很多。但这部分绘制只是延迟了,最终还是要两份的。
但由于不需要去存 colorKey 这些数据,内存占用相比 Konva 还是会少了很多。
5. 总结
leaferjs 是一个国人在工作之余写的渲染库,看文件目录未来还会支持 Canvaskit、Miniapp,也支持开发者贡献插件,野心不小。
虽然处于刚起步阶段,相信随着后续迭代,leaferjs 会变成一个非常具有竞争力的 Canvas 库。
相关推荐
-
- 驱动网卡(怎么从新驱动网卡)
-
网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...
-
2026-01-30 00:37 liuian
- win10更新助手装系统(微软win10更新助手)
-
1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...
- windows11专业版密钥最新(windows11专业版激活码永久)
-
Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...
-
- 手机删过的软件恢复(手机删除过的软件怎么恢复)
-
操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...
-
2026-01-29 23:55 liuian
- 一键ghost手动备份系统步骤(一键ghost 备份)
-
步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。 步骤...
- 怎么创建局域网(怎么创建局域网打游戏)
-
1、购买路由器一台。进入路由器把dhcp功能打开 2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。 3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...
- 精灵驱动器官方下载(精灵驱动手机版下载)
-
是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...
- 一键还原系统步骤(一键还原系统有哪些)
-
1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。 2、在弹出的“准备安装”窗口中,可...
- 电脑加速器哪个好(电脑加速器哪款好)
-
我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...
- 任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)
-
是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...
- u盘怎么恢复文件(u盘文件恢复的方法)
-
开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...
- 系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)
-
1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...
-
- 剪贴板权限设置方法(剪贴板访问权限)
-
1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...
-
2026-01-29 21:37 liuian
- 平板系统重装大师(平板重装win系统)
-
如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...
- 联想官网售后服务网点(联想官网售后服务热线)
-
联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...
- 一周热门
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
