Better

Ethan的博客,欢迎访问交流

fabric.js 使用总结

最近的工作内容聚焦于 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.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

扩展

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. 交换主对角线位置
      2. 副对角线取反
      3. 点乘关键表达式: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 对图形进行计算和渲染,否则性能反而会下降
    • 文本的渲染非常复杂,也不会带来性能的提升
  • 其他方向
    • 优先级渲染队列
    • 渲染对象分类:动静分离
    • 纹理缓存

资料



留言