leaferjs,全新的 Canvas 渲染引擎
liuian 2024-12-18 15:36 57 浏览
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 库。
相关推荐
- 赶紧收藏!编程python基础知识,本文给你全部整理好了
-
想一起学习编程Python的同学,趁我粉丝少,可以留言、私信领编程资料~Python基础入门既然学习Python,那么至少得了解下这门编程语言,知道Python代码执行过程吧。Python的历...
- 创建绩效改进计划 (PIP) 的6个步骤
-
每个经理都必须与未能达到期望的员工抗衡,也许他们的表现下降了,他们被分配了新的任务并且无法处理它们,或者他们处理了自己的任务,但他们的行为对他人造成了破坏。许多公司转向警告系统,然后在这些情况下终止。...
- PI3K/AKT信号通路全解析:核心分子、上游激活与下游效应分子
-
PI3K/AKT/mTOR(PAM)信号通路是真核细胞中高度保守的信号转导网络,作用于促进细胞存活、生长和细胞周期进程。PAM轴上生长因子向转录因子的信号传导受到与其他多条信号通路的多重交叉相互作用的...
- 互联网公司要求签PIP,裁员连N+1都没了?
-
2021年刚画上句号,令无数互联网公司从业者闻风丧胆的绩效公布时间就到了,脉脉上已然炸了锅。阿里3.25、腾讯二星、百度四挡、美团绩效C,虽然名称五花八门,实际上都代表了差绩效。拿到差绩效,非但不能晋...
- Python自动化办公应用学习笔记3—— pip工具安装
-
3.1pip工具安装最常用且最高效的Python第三方库安装方式是采用pip工具安装。pip是Python包管理工具,提供了对Python包的查找、下载、安装、卸载的功能。pip是Python官方提...
- 单片机都是相通的_单片机是串行还是并行
-
作为一个七年的从业者,单片机对于我个人而言它是一种可编程的器件,现在长见到的电子产品中几乎都有单片机的身影,它们是以单片机为核心,根据不同的功能需求,搭建不同的电路,从8位的单片机到32位的单片机,甚...
- STM32F0单片机快速入门八 聊聊 Coolie DMA
-
1.苦力DMA世上本没有路,走的人多了,便成了路。世上本没有DMA,需要搬运的数据多了,便有了DMA。大多数同学应该没有在项目中用过这个东西,因为一般情况下也真不需要这个东西。在早期的单片机中...
- 放弃51单片机,直接学习STM32开发可能会面临的问题
-
学习51单片机并非仅仅是为了学习51本身,而是通过它学习一种方法,即如何仅仅依靠Datasheet和例程来学习一种新的芯片。51单片机相对较简单,是这个过程中最容易上手的选择,而AVR单片机则更为复杂...
- STM32串口通信基本原理_stm32串口原理图
-
通信接口背景知识设备之间通信的方式一般情况下,设备之间的通信方式可以分成并行通信和串行通信两种。并行与串行通信的区别如下表所示。串行通信的分类1、按照数据传送方向,分为:单工:数据传输只支持数据在一个...
- 单片机的程序有多大?_单片机的程序有多大内存
-
之前一直很奇怪一个问题,每次写好单片机程序之后,用烧录软件进行烧录时,能看到烧录文件也就是hex的文件大小:我用的单片机芯片是STM32F103C8T6,程序储存器(flash)只有64K。从...
- 解析STM32单片机定时器编码器模式及其应用场景
-
本文将对STM32单片机定时器编码器模式进行详细解析,包括介绍不同的编码器模式、各自的优缺点以及相同点和不同点的应用场景。通过阅读本文,读者将对STM32单片机定时器编码器模式有全面的了解。一、引言...
- 两STM32单片机串口通讯实验_两个32单片机间串口通信
-
一、实验思路连接两个STM32单片机的串口引脚,单片机A进行发送,单片机B进行接收。单片机B根据接收到单片机A的指令来点亮或熄灭板载LED灯,通过实验现象来验证是否通讯成功。二、实验器材两套STM32...
- 基于单片机的智能考勤机设计_基于51单片机的指纹考勤机
-
一、设计背景随着科技水平的不断发展,在这么一个信息化的时代,智能化信息处理已是提高效率、规范管理和客观审查的最有效途径。近几年来,国内很多公司都在加强对企业人员的管理,考勤作为企业的基础管理,是公司...
- STM32单片机详细教学(二):STM32系列单片机的介绍
-
大家好,今天给大家介绍STM32系列单片机,文章末尾附有本毕业设计的论文和源码的获取方式,可进群免费领取。前言STM32系列芯片是为要求高性能、低成本、低功耗的嵌入式应用设计的ARMCortexM...
- STM32单片机的 Hard-Fault 硬件错误问题追踪与分析
-
有过单片机开发经验的人应该都会遇到过硬件错误(Hard-Fault)的问题,对于这样的问题,有些问题比较容易查找,有些就查找起来很麻烦,甚至可能很久都找不到问题到底是出在哪里。特别是有时候出现一次,后...
- 一周热门
-
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
飞牛OS入门安装遇到问题,如何解决?
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
- 最近发表
- 标签列表
-
- 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)