最近的工作内容聚焦于 2D 绘制多一些,公司的选型为 fabric.js,使用了一段时间后,做个入门级别总结。fabric.js 是可以简化 canvas 编写的 js 库,提供 canvas 缺少的对象模型,以及一个 SVG 解析器,一个交互层,以及一整套其他必不可少的工具。
为什么选择
fabric 相比原生 canvas 最大的优势就是提供了语义化的对象模型,同时还内置了图层交互相关的操作。
- SVG parser
- 复杂对象模型
- 均继承自:fabric.Object
- setCoords:基于 width、height、angle 重新计算外接矩形的四个角点
- left、top:物体左上角起始坐标,可以通过 originX、originY 设置该点是中心或是四周某个点
- originX、originY:可理解成绘制物体的原点,默认为 left、top。originX 值有 left/right/center,originY 值有 top/bottom/center
- 7 种基本图形:fabric.Circle、fabric.Ellipse、fabric.Line、fabric.Polygon、fabric.Polyline、fabric.Rect、fabric.Triangle
- 常见属性(get、set 机制)
- 位置:left、top
- 尺寸:width、height
- 显示:fill、opacity、stroke、strokeWidth
- 缩放与旋转:scaleX、scaleY、angle
- 翻转:flipX、flipY
- 图片类:fabric.Image | fabric.Image.fromURL
- 复杂图形:Path 和 PathGroup
- Path 规则如同 SVG path
- fabric.loadSVGFromString | fabric.loadSVGFromURL
- 均继承自:fabric.Object
- 图层交互
- 如果不需要该功能,可直接使用 fabric.StaticCanvas 类
- 分组管理
- 事件机制
- 内置图形操作交互(拖动、缩放、旋转等)
- setCursor() 设置手势图标
- getSelectionContext()获取选中的context
- getSelectionElement()获取选中的元素
- getActiveObject() 获取选中的对象
- getActiveObjects() 获取选中的多个对象
- discardActiveObject()取消当前选中对象
left 和 top 是每种 Object 都有的属性,至于它到底指图形中哪一个点的坐标,由 originX 和 originY 这组参数决定。
- originX 三种可选值:left、center、right
- originY 三种可选值:top、center、bottom
基础
fabric.js 基础与核心类
- 画布原点:左上角
- fabric.Canvas 类
- 管理所有的 objects
- 画布配置中心:画布颜色、背景图、前置图
- 相关 api
- add/remove
- setBackgroundImage
- setOverlayImage
- absolutePan:移动视口,默认在左上角
- relativePan:移动视口,相对当前视口
- dispose:清除 canvas 元素,且移除所有事件监听
- clear:清除所有上下文(background、main、top)
- requestRenderAll:合并所有 render 请求
- renderAll:render top and secondary
- renderTop:常用于渲染选择框
- calcViewportBoundaries:计算画布的四个角点
- 缓存机制
- fabric 会将 canvas 节点转变成两个 canvas
- 一个 upper-canvas 负责绘制作为缓存,lower-canvas 负责显示,使用 drawImage 将 upper-canvas 画到 lower-canvas
进阶
fabric.js 提供的其他周边类、事件机制与分组机制
- 动画机制:object.animate(property, value, options)
- options 可以指定 from、duration、onChange、onComplete、easing
- fabric.Image
- 滤镜功能:filters 属性
- fabric.Image.filters 下提供了一系列内置滤镜
- fabric.Color
- 支持多种格式
- 格式之间转换:toHex 将颜色实例转换为十六进制表示。 toRgb 可以转换为 RGB,toRgba 转换为带 Alpha 通道的 RGB
- 颜色之间转换:toGrayscale……
- 支持渐变
- object.setGradient
- fabric.Text
- object 的完整性
- 相比原生,支持更丰富的配置
- 事件机制
- on/off
- 事件分类
- 通用类:after:render
- 鼠标类:mouse:down | mouse:move | mouse:up
- 选择类:before:selection:cleared | selection:created | selection:cleared
- 物件类:object:modified | object:selected | object:moving | object:scaling | object:rotating | object:added | object:removed
- 其他常规事件类似于 dom 事件,但使用冒号分割
- 进一步提升事件系统,并允许您将侦听器直接附加到 canvas 画布中的对象上,书写省略 object 即可
- 事件参数:{ e: Event, target: fabric.Object }
- 分组机制
- 将一组对象当做一个单元进行统一管理
- 进行选择时,fabric 会隐式将选择的物件创建一个分组
- 常用 api
- getObjects
- size
- forEachObject
- item
- add/addWithUpdate
- remove/removeWithUpdate
- 通常推荐使用 addWithUpdate/removeWithUpdate(会更新尺寸和位置),除非你在进行批处理或者 group 有错误的宽高也无妨
- 序列化与反序列化:一旦你开始构建一种有状态的应用程序,也许允许用户在服务器上保存画布内容的结果,或者将内容流传输到不同的客户端,你都需要 canvas 序列化。
- 序列化:toObject/toJSON/toSVG/toDatalessJSON
- toDatalessJSON 通过链接 SVG 的方式简化数据表达
- 通过将物件的 excludeFromExport 设置为 true,则物件不会被序列化
- 反序列化:fabric.Canvas#loadFromJSON | fabric.Canvas#loadFromDatalessJSON | fabric.loadSVGFromURL | fabric.loadSVGFromString
- 序列化:toObject/toJSON/toSVG/toDatalessJSON
扩展
fabric.js 还提供了自由绘制以及定制化的功能
- 自由绘制
- 画布设置 isDrawingMode 为 true 即可
- 每次绘制都是一个 fabric.Path 实例,通过 path:created 事件通知
- 支持设置 color 以及 width
- 定制化
- 锁定功能:比如锁定某方向的平移、旋转或缩放
- 定制边框、角点:是否显示颜色、点密度、大小等
- 禁用选取:设置 selectable 为 false 即可
- 定制选取效果:选择颜色、边框颜色、边框大小、虚线点密度
- 设置先为虚线:strokeDashArray
- 定制可点击区域
- 默认是外边框
- 通过 perPixelTargetFind 为 true 指定为仅限实际物体
- 定制旋转点
- 是否启动对象的非均匀缩放
- 常用实现
- zoom:沿中心点还是鼠标点
- pan:区分选取还是平移
- rotate
- 其他
- clipPath 实现裁剪
更多功能
- 文本上下标功能
- 打组解组功能
- toGroup
- toActiveSelection
- 自定义滤镜
- 自定义控制器 - 删除、编辑、复制
- 完善的事件机制
- 完善的动画机制
- startValue
- endValue
- duration
- easing
- onChange
- onComplete
- 选择(多选、框选)
- selectable
- setActiveObject
- getActiveObject
- discardActiveObject
- getPointer
仿射变换
为什么需要矩阵
- 理论上讲我们的确可以只通过数学公式就能实现变换,但实际的情况却是在变换十分复杂时,直接使用数学表达式来进行运算也是相当繁复的
- 在现实中常常使用矩阵(由 m×n 个标量组成的长方形数组)来表示诸如平移、旋转以及缩放等线性变换
- 一个更有趣的事实是,当两个变换矩阵 A 和 B 的积为 P=AB 时,则变换矩阵 P 相当于 A 和 B 所代表的变换。主要注意矩阵乘法不符合交换律,因此 AB 和 BA 并不相等
因此理解矩阵时理解坐标系统变换最为重要一环
- Canvas
- viewportTransform = matrix:Canvas 的状态矩阵
- Objects
- matrix = fabric.Object.prototype.calcTransformMatrix():获取该图形对象的状态矩阵,可用于相对 object 的坐标到相对 canvas 坐标的转换
- matrix = fabric.Object.prototype.calcOwnMatrix()
- Utils
- point = fabric.util.transformPoint(point, matrix):执行的就是下面会重点解释的计算
- matrix = fabric.util.multiplyTransformMatrices(matrix, matrix):多个矩阵变换组合组合
- matrix = fabric.util.invertTransform(matrix):求逆矩阵,A 点通过矩阵转换到 B,B 可通过逆矩阵还原到 A
- options = fabric.util.qrDecompose(matrix):对矩阵进行 qr 分解,求得该图形对象此时的状态(相对于初始状态缩放、旋转、平移),一个可能解
- 基础知识
2 * 3
矩阵,行 * 列
,坐标点均表示成 (x, y, 1)- 单位矩阵:如同数的乘法1,行数和列数相等,从左上角到右下角的对角线(称为主对角线)上的元素均为1,除此以外全都为0
- 对称矩阵:以主对角线为对称轴,各元素对应相等的矩阵,对称矩阵是一个方形矩阵,其转置矩阵和自身相等
- 旋转是 scale 和 skew 的组合
- 逆矩阵:逆矩阵与原矩阵相乘等于单位矩阵,任何矩阵乘以单位矩阵的结果都是其本身,可用于实现矩阵的除法。计算逻辑比较复杂:比如:针对
2 * 2
的计算- 交换主对角线位置
- 副对角线取反
- 点乘关键表达式:1 / (主对角线乘积 - 负对角线乘积),如果分母为 0 则可能没有逆矩阵
- 矩阵乘法
- AB != BA
在 fabric.js 中,使用 3*3
的矩阵进行仿射变换,具体矩阵存储是一个 6 个数的数组,矩阵中书写规则为竖向书写,最后一行补 (0, 0, 1),假设 matrix 为 [a, b, c, d, tx, ty],具体点的坐标可以看成一个 nx1 的矩阵,然后执行矩阵乘法即可实现变换,具体如下
// matrix
[
a, c, tx,
b, d, ty
0, 0, 1
]
// 点
[
x,
y,
1
]
// 3 * 3 矩阵含义
[
x(scale), x(skew), x(position)
y(skew), y(scale), y(position)
0, 0, 1
]
// 比如为旋转矩阵
[
cos(angle), -sin(angle),
sin(angle), cos(angle),
0, 0, 1
]
// 矩阵计算公式
x' = x*a + y*c + tx
y' = x*b + y*d + ty
一些坑
fabric 不支持带洞的 polygon:TODO: 感觉可以利用 clipPath 属性配合 difference 实现对 hole 的支持。20200115 更新:完全不行,怎么会有这么愚蠢的想法。但是已经找到解决办法了,具体见:fabric 绘制带洞的 polygon
fabric 即使针对 line,也可以设置 fill 值,默认为黑色,会自动将首尾相连,然后使用 fill 色填充
fabric.Path vs fabric.Line vs fabric.Polyline
- fabric.Polyline:表示多线段
- fabric.Line:仅表示直线,由两个点构成
- fabric.Path:SVG 路径格式
IText vs Text vs TextBox
- IText:你可以设置样式为你选择的任意数量的字符,它将应用于选定的部分文本
- Text:你可以设置任何样式,他将被作用于整个文本
- TextBox:继承 IText,允许用户调整文本框大小,会自动换行
对于更复杂的图形,可能需要使用像 fabric.loadSVGFromString 或 fabric.loadSVGFromURL 方法来加载整个 SVG 文件,然后让 Fabric 的 SVG 解析器完成对所有 SVG 元素的遍历和创建相应的 Path 对象的工作。
关于画布旋转:https://github.com/fabricjs/fabric.js/issues/2382
性能优化
当 fabric 中渲染的元素过多时,比如 10w+ 个元素,可能会导致整理很低,查阅了一下通用的优化思路
- 图形的更新:通常对于 Canvas 而言,更新需要清除整个画布,重新绘制所有的图形
- Canvas 图形渲染的性能瓶颈
- 同一时间绘制过多的图形,会阻塞浏览器的进程,导致页面不能响应
- 鼠标在画布上移动时,如果不能及时捕捉鼠标,会导致卡顿
- 图形更新时,重绘的时间过长,则帧率非常低
- 绘制更多图形
- 首次渲染时的优化:分片渲染,每次渲染时间间隔几毫秒的间隔。增加总的渲染时长,但可以降低页面的卡顿感
- 避免频繁地渲染:只要保证 60 帧的重绘频率即可,所以绘制的间隔不能小于 16 ms,将持续渲染的同步机制,改成每 16ms 的异步延迟渲染机制,可大大降低重绘的频率。但 fabric 的 requestRenderAll 就是这么做的
- 局部更新:仅清空图形所在的包围盒,所有与这个包围盒相交的图形全部刷新,比较复杂,具体参考
- WebGL 实现 2D 渲染
- 由于 WebGL 的渲染时基于光栅(点),所以绘制线时本质上是通过一个个的点来绘制,逐点计算贝塞尔等曲线不现实,因此绘制的这些曲线不够平滑。
- 绘制图形时尽量不要直接在 CPU 中计算各个图形的几何模型,而使用 shader 对图形进行计算和渲染,否则性能反而会下降
- 文本的渲染非常复杂,也不会带来性能的提升
- 其他方向
- 优先级渲染队列
- 渲染对象分类:动静分离
- 纹理缓存