全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2023-10-10_「转」leaferjs,全新的 Canvas 渲染引擎

您的位置:首页 >> 新闻 >> 行业资讯

leaferjs,全新的 Canvas 渲染引擎 点击关注公众号,回复”福利”即可参与文末抽奖1. 前言前几天群里有人发了一个新 Canvas 渲染引擎的图片,看数据和宣传口号相当炸裂,号称只用 1.5s 可以渲染 100 万个矩形,还是个国产的。 出于个人兴趣,就花了一点儿时间研究了一下感兴趣的点,如果有错误,希望可以指出。 2. 架构设计从火焰图上可以看出,leaferjs 创建节点非常轻量,只做了setAttr的操作。 大部分耗时集中在创建节点和布局,渲染仅仅花了3ms。 那 leaferjs 为什么有这么好的性能呢?我简单去看了一下源码。 leafer 主要包括了 leafer 和 ui 两个 git 仓库,核心渲染能力在 leafer 里面,ui 封装了一些绘制类,比如 Image、Line 等等。 先看一下官网上一个详细的用法: import{App,Leafer,Rect}from'leafer-ui' constapp=newApp({view:window,type:'user'}) constbackgroundLayer=newLeafer()constcontentLayer=newLeafer({type:'design'})constwireframeLayer=newLeafer() app.add(backgroundLayer)app.add(contentLayer)app.add(wireframeLayer) constbackground=newRect({width:800,height:600,fill:'gray'})constrect=newRect({x:100,y:100,fill:'#32cd79',draggable:true})constborder=newRect({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 作为切入点,发现上面挂了很多装饰器。 @registerUI()exportclassLeaferextendsGroupimplementsILeafer{ publicget__tag(){return'Leafer'} @dataProcessor(LeaferData)public__:ILeaferData @boundsType()publicpixelRatio:number publicgetisApp():boolean{returnfalse} publicparent?: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 这个类,它的实现非常简单。 @useModule(RectRender)@registerUI()exportclassRectextendsUIimplementsIRect{ publicget__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方法。 exportconstLeafDataProxy:ILeafDataProxyModule={ __setAttr(name:string,newValue:unknown):void{if(this.leaferthis.leafer.ready){this.__[name]=newValueconst{CHANGE}=PropertyEventconstevent=newPropertyEvent(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{returnthis.__.__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 的例子: constrect=newRect({x:0,y:0,width:100,height:100}); rect.x=上面这个矩形的位置发生了变化,它在这次更新中的包围盒信息就是{ 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 库。 点击小卡片,参与粉丝专属福利!! 如果文章对你有帮助的话欢迎「关注+点赞+收藏」 阅读原文

上一篇:2025-01-13_老婆饼里没有老婆,RLHF里也没有真正的RL 下一篇:2024-06-15_一张长图透彻理解SpringBoot 启动原理,架构师必备知识,不为应付面试!

TAG标签:

12
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价