Fabric.js 简介
我们先来看看官方的定义:
Fabric.js is a framework that makes it easy to work with HTML5 canvas element. It is an interactive object model on top of canvas element. It is also an SVG-to-canvas parser.
Fabric.js 是一个可以让 HTML5 Canvas 开发变得简单的框架。它是一种基于 Canvas 元素的可交互对象模型,也是一个 SVG 到 Canvas 的解析器(让SVG 渲染到 Canvas 上)。
Fabric.js 的代码不算多,源代码(不包括内置的三方依赖)大概 1.7 万行。最初是在 2010 年开发的,从源代码就可以看出来,都是很老的代码写法。没有构建工具,没有依赖,甚至没使用 ES6,代码中模块都是用 IIFE 的方式包装的。
但是这个并不影响我们学习它,相反正因为它没引入太多的概念,使用起来相当方便。不需要构建工具,直接在一个 HTML 文件中引入库文件就可以开发了,甚至官方都提供了一个 HTML 模板代码:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="https://rawgit.com/fabricjs/fabric.js/master/dist/fabric.js"></script>
  </head>
  <body>
    <canvas id="c" width="300" height="300" style="border:1px solid #ccc"></canvas>
    <script>
      (function() {
        var canvas = new fabric.Canvas('c');
      })();
    </script>
  </body>
</html>这就够了,不是吗?
使用场景
从它的官方定义可以看出来,它是一个用 Canvas 实现的对象模型。如果你需要用 HTML Canvas 来绘制一些东西,并且这些东西可以响应用户的交互,比如:拖动、变形、旋转等操作。那用 fabric.js 是非常合适的,因为它内部不仅实现了 Canvas 对象模型,还将一些常用的交互操作封装好了,可以说是开箱即用。
内部集成的主要功能如下:
Canvas 开发原理
如果你之前没有过 Canvas 的相关开发经验(只有 Javascript 网页开发经验),刚开始入门会觉得不好懂,不理解 Canvas 开发的逻辑。这个很正常,因为这表示你正在从传统的 Javascript 开发转到图形图像 GUI 图形图像、动画开发。虽然语言都是 Javascript 但是开发理念和用到的编程范式完全不同。
这两种开发方式各有各的优势,比如:
- 有的功能在 HTML 里一行代码就能实现的功能放到 Canvas 中需要成千行的代码去实现。比如:textarea, contenteditable 
- 相反,有的功能在 Canvas 里面只需要一行代码实现的,使用 HTML 却几乎无法实现。比如:截图、录制 
Canvas 开发的本质其实很简单,想像下面这种少儿画板:

Canvas 的渲染过程就是不断的在画板(Canvas)上面擦了画,画了擦。
动画就更简单了,只要渲染帧率超过人眼能识别的帧率(60fps)即可:
<canvas id="canvas" width="500" height="500" style="border:1px solid black"></canvas>
<script>
    var canvas = document.getElementById("canvas")
    var ctx = canvas.getContext('2d');
    var left = 0
 
    setInterval(function() {
        ctx.clearRect(0, 0, 500, 500);
        ctx.fillRect(left++, 100, 100, 100);
    }, 1000 / 60)
</script>
当然你也可以用requestAnimationFrame,不过这不是我想说明的重点。
Fabric.js 源码解析
模块结构图
fabric.js 的模块我大概画了个图,方便理解。

基本原理
fabric.js 在初始化的时候会将你指定的 Canvas 元素(叫做 lowerCanvas)外面包裹上一层 div 元素,然后内部会插入另外一个上层的 Canvas 元素(叫做 upperCanvas),这两个 Canvas 有如下区别:
| 内部叫法 | 文件路径 | 作用 | 
|---|
| upperCanvas | src/canvas.class.js | 上层画布,只处理 分组选择,事件绑定 | 
| lowerCanvas | src/static_canvas.class.js | 真正 绘制 元素对象(Object)的画布 | 
核心模块详解
上图中,灰色的模块对于理解 fabric.js 核心工作原理没多大作用,可以不看。其它核心模块我按自己的理解来解释一下。
所有模块都被挂载到一个 fabric 的命名空间上面,都可以用fabric.XXX的形式访问。
fabric.util工具包
工具包中一个最重要的方法是createClass,它可以用来创建一个类。我们来看看这个方法:
function createClass() {
  var parent = null,
      properties = slice.call(arguments, 0);
  if (typeof properties[0] === 'function') {
    parent = properties.shift();
  }
  function klass() {
    this.initialize.apply(this, arguments);
  }
  // 关联父子类之间的关系
  klass.superclass = parent;
  klass.subclasses = [];
  if (parent) {
    Subclass.prototype = parent.prototype;
    klass.prototype = new Subclass();
    parent.subclasses.push(klass);
  }
  // ...
}
为什么不用 ES 6 的类写法呢?主要是因为这个库写的时候 ES 6 还没出来。作者沿用了老式的基于Javascript prototype 实现的类继承的写法,这个方法封装了类的继承、构造方法、父子类之前的关系等功能。注意klass.superclass和klass.subclasses这两行,后面会讲到。
添加这两个引用关系后,我们就可以在 JS 运行时动态获取类之间的关系,方便后续序列化及反序列化操作,这种做法类似于其它编程语言中的反射机制,可以让你在代码运行的时候动态的构建、操作对象
initialize()方法(构造函数)会在类被 new 出来的时候自动调用:
function klass()
{
  this.initialize.apply(this, arguments);
}
fabric 通用类
fabric.Canvas类
上层画布类,如上面表格所述,它并不渲染对象。它只来处理与用户交互的逻辑。 比如:全局事件绑定、快捷键、鼠标样式、处理多(分组)选择逻辑。
我们来看看这个类初始化时具体干了些什么。
fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, {
    initialize: function (el, options) {
        options || (options = {});
        this.renderAndResetBound = this.renderAndReset.bind(this);
        this.requestRenderAllBound = this.requestRenderAll.bind(this);
        this._initStatic(el, options);
        this._initInteractive();
        this._createCacheCanvas();
    },
    // ...
})
注意:由于createClass中第一个参数是StaticCanvas,所以我们可以知道 Canvas 的父类是StaticCanvas。
从构造方法initialize中我们可以看出:
只有_initInteractive和_createCacheCanvas是 Canvas 类自己的方法,renderAndResetBound,requestRenderAllBound,_initStatic都继承自父类StaticCanvas
这个类的使用也很简单,做为 fabric.js 程序的入口,我们只需要 new 出来即可:
// c 就是 HTML 中的 canvas 元素 id
const canvas = new fabric.Canvas("c", { /* 属性 */ })
fabric.StaticCanvas类
fabric 的核心类,控制着 Canvas 的渲染操作,所有的画布对象都必须在它上面绘制出来。我们从构造函数中开始看:
fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, {
    initialize: function (el, options) {
        options || (options = {});
        this.renderAndResetBound = this.renderAndReset.bind(this);
        this.requestRenderAllBound = this.requestRenderAll.bind(this);
        this._initStatic(el, options);
    },
})
注意:StaticCanvas 不仅继承了fabric.CommonMethods中的所有方法,还继承了fabric.Observable和fabric.Collection,而且它的实现方式很 Javascript,在 StaticCanvas.js 最下面一段:
extend(fabric.StaticCanvas.prototype, fabric.Observable);
extend(fabric.StaticCanvas.prototype, fabric.Collection);
fabric.js 的画布渲染原理
requestRenderAll()方法
从下面的代码可以看出来,这个方法的主要任务就是不断调用renderAndResetBound方法,renderAndReset方法会最终调用renderCanvas来实现绘制。
requestRenderAll: function () {
  if (!this.isRendering) {
    this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound);
  }
  return this;
}
renderCanvas()方法
renderCanvas 方法中代码比较多:
renderCanvas: function(ctx, objects) {
    var v = this.viewportTransform, path = this.clipPath;
    this.cancelRequestedRender();
    this.calcViewportBoundaries();
    this.clearContext(ctx);
    fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled);
    this.fire('before:render', {ctx: ctx,});
    this._renderBackground(ctx);
    ctx.save();
    //apply viewport transform once for all rendering process
    ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
    this._renderObjects(ctx, objects);
    ctx.restore();
    if (!this.controlsAboveOverlay && this.interactive) {
        this.drawControls(ctx);
    }
    if (path) {
        path.canvas = this;
        // needed to setup a couple of variables
        path.shouldCache();
        path._transformDone = true;
        path.renderCache({forClipping: true});
        this.drawClipPathOnCanvas(ctx);
    }
    this._renderOverlay(ctx);
    if (this.controlsAboveOverlay && this.interactive) {
        this.drawControls(ctx);
    }
    this.fire('after:render', {ctx: ctx,});
}
我们删掉一些不重要的,精简一下,其实最主要的代码就两行:
renderCanvas: function(ctx, objects) {
    this.clearContext(ctx);
    this._renderObjects(ctx, objects);
}
clearContext 里面会调用 canvas 上下文的clearRect方法来清空画布:
ctx.clearRect(0, 0, this.width, this.height)
_renderObjects就是遍历所有的objects调用它们的render()方法,把自己绘制到画布上去:
for (i = 0, len = objects.length; i < len; ++i) {
    objects[i] && objects[i].render(ctx);
}
现在你是不是明白了文章最开始那段setInterval实现的 Canvas 动画原理了?
fabric 形状类
fabric.Object对象根类型
虽然我们已经明白了 canvas 的绘制原理,但是一个对象(2d元素)到底是怎么绘制到 canvas 上去的,它们的移动怎么实现的?具体细节我们还不是很清楚,这就要从fabric.Object根类型看起了。
由于 fabric 中的 2d 元素都是以面向对象的形式实现的,所以我画了一张内部类之间的继承关系,可以清楚的看出它们之间的层次结构:

不像传统的 UML 类图那样,这个图看起来还稍有点乱,因为 fabric.js 内部实现的是多重继承,或者说类似于 mixin 的一种混入模式实现的继承。
从图中我们可以得出以下几点:
- 底层 StaticCanvas 继承了- Collection对象和- Observable对象,这就意味着 StaticCanvas 有两种能力:
 
- 所有的 2d 形状(如:矩形、圆、线条、文本)都继承了- Object类。Object 有的属性、方法,所有的 2d 形状都会有
 
- 所有的 2d 形状都具有自定义事件发布/订阅的能力 
Object 类常用属性
下面的注释中,边角控制器是 fabric.js 内部集成的用户与对象交互的一个手柄,当某个对象处于激活状态的时候,手柄会展示出来。如下图所示:

常用属性解释:
// 对象的类型(矩形,圆,路径等),此属性被设计为只读,不能被修改。修改后 fabric 的一些部分将不能正常使用。
type:                     'object',
// 对象变形的水平中心点的位置(左,右,中间)
// 查看 http://jsfiddle.net/1ow02gea/244/ originX/originY 在分组中的使用案例
originX:                  'left',
// 对象变形的垂直中心点的位置(上,下,中间)
// 查看 http://jsfiddle.net/1ow02gea/244/ originX/originY 在分组中的使用案例
originY:                  'top',
// 对象的顶部位置,默认**相对于**对象的上边沿,你可以通过设置 originY={top/center/bottom} 改变它的参数参考位置
top:                      0,
// 对象的左侧位置,默认**相对于**对象的左边沿,你可以通过设置 originX={top/center/bottom} 改变它的参数参考位置
left:                     0,
// 对象的宽度
width:                    0,
// 对象的高度
height:                   0,
// 对象水平缩放比例(倍数:1.5)
scaleX:                   1,
// 对象水平缩放比例(倍数:1.5)
scaleY:                   1,
// 是否水平翻转渲染
flipX:                    false,
// 是否垂直翻转渲染
flipY:                    false,
// 透明度
opacity:                  1,
// 对象旋转角度(度数)
angle:                    0,
// 对象水平倾斜角度(度数)
skewX:                    0,
// 对象垂直倾斜角度(度数)
skewY:                    0,
// 对象的边角控制器大小(像素)
cornerSize:               13,
// 当检测到 touch 交互时对象的边角控制器大小
touchCornerSize:               24,
// 对象边角控制器是否透明(不填充颜色),默认只保留边框、线条
transparentCorners:       true,
// 鼠标 hover 到对象上时鼠标形状
hoverCursor:              null,
// 鼠标拖动对象时鼠标形状
moveCursor:               null,
// 对象本身与边角控制器之间的间距(像素)
padding:                  0,
// 对象处于活动状态下边角控制器**包裹对象的边框**颜色
borderColor:              'rgb(178,204,255)',
// 指定边角控制器**包裹对象的边框**虚线边框的模式元组(hasBorder 必须为 true)
// 第一个元素为实线,第二个为空白
borderDashArray:          null,
// 对象处于活动状态下边角控制器颜色
cornerColor:              'rgb(178,204,255)',
// 对象处于活动状态且 transparentCorners 为 false 时边角控制器本身的边框颜色
cornerStrokeColor:        null,
// 边角控制器的样式,正方形或圆形
cornerStyle:          'rect',
// 指定边角控制器本身的虚线边框的模式元组(hasBorder 必须为 true)
// 第一个元素为实线,第二个为空白
cornerDashArray:          null,
// 如果为真,通过边角控制器来对对象进行缩放会以对象本身的中心点为准
centeredScaling:          false,
// 如果为真,通过边角控制器来对对象进行旋转会以对象本身的中心点为准
centeredRotation:         true,
// 对象的填充颜色
fill:                     'rgb(0,0,0)',
// 填充颜色的规则:nonzero 或者 evenodd
// @see https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/fill-rule
fillRule:                 'nonzero',
// 对象的背景颜色
backgroundColor:          '',
// 可选择区域被选择时(对象边角控制器区域),层级低于对象背景颜色
selectionBackgroundColor:          '',
// 设置后,对象将以笔触的方式绘制,此属性值即为笔触的颜色
stroke:                   null,
// 笔触的大小
strokeWidth:              1,
// 指定笔触虚线的模式元组(hasBorder 必须为 true)
// 第一个元素为实线,第二个为空白
strokeDashArray:          null,
Object 类常用方法
drawObject()对象的绘制方法
drawObject()方法内部会调用_render()方法,但是在fabric.Object基类中它是个空方法。这意味着对象具体的绘制方法需要子类去实现。即子类需要重写父类的空_render()方法。
_onObjectAdded()对象被添加到 Canvas 事件
这个方法非常重要,只要当一个对象被添加到 Canvas 中的时候,对象才可以具有 Canvas 的引用上下文,对象的一些常用方法才能起作用。比如:Object.center()方法,调用它可以让一个对象居中到画布中央。下面这段代码可以实现这个功能:
const canvas = new fabric.Canvas("canvas", {
  width: 500, height: 500,
})
const box = new fabric.Rect({
  left: 10, top: 10,
  width: 100, height: 100,
})
console.log(box.top, box.left)  // => 10, 10
box.center()
console.log(box.top, box.left)  // => 10, 10
canvas.add(box)
但是你会发现 box 并没有被居中,这就是因为:当一个对象(box)还没被添加到 Canvas 中的时候,对象上面还不具有 Canvas 的上下文,所以调用的对象并不知道应该在哪个 Canvas 上绘制。我们可以看下center()方法的源代码:
center: function () {
  this.canvas && this.canvas.centerObject(this);
  return this;
},
正如上面所说,没有 canvas 的时候是不会调用到canvas.centerObject()方法,也就实现不了居中。
所以解决方法也很简单,调换下 center() 和 add() 方法的先后顺序就好了:
const canvas = new fabric.Canvas("canvas", {
  width: 500, height: 500,
})
const box = new fabric.Rect({
  left: 10, top: 10,
  width: 100, height: 100,
})
canvas.add(box)
console.log(box.top, box.left)  // => 10, 10
box.center()
console.log(box.top, box.left)  // => 199.5, 199.5
「为什么不是 200,而是 199.5」—— 好问题,但是我不准备讲这个。有兴趣可以自己研究 下。
toObject()对象的序列化
正向的把对象序列化是很简单的,只需要把你关注的对象上的属性拼成一个 JSON 返回即可 :
toObject: function(propertiesToInclude) {
  var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS,
      object = {
        type:                     this.type,
        version:                  fabric.version,
        originX:                  this.originX,
        originY:                  this.originY,
        left:                     toFixed(this.left, NUM_FRACTION_DIGITS),
        top:                      toFixed(this.top, NUM_FRACTION_DIGITS),
        width:                    toFixed(this.width, NUM_FRACTION_DIGITS),
        height:                   toFixed(this.height, NUM_FRACTION_DIGITS),
        // 省略其它属性
      };
  return object;
},
当调用对象的toJSON()方法时会使用JSON.stringify(toObject())来将对象的属性转换成 JSON 字符串
fromObject()对象的反序列化
fromObject()是 Object 的子类需要实现的反序列化方法,通常会调用 Object 类的默认方法_fromObject()
fabric.Object._fromObject = function(className, object, callback, extraParam) {
  var klass = fabric[className];
  object = clone(object, true);
  fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) {
    if (typeof patterns[0] !== 'undefined') {
      object.fill = patterns[0];
    }
    if (typeof patterns[1] !== 'undefined') {
      object.stroke = patterns[1];
    }
    fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) {
      object.clipPath = enlivedProps[0];
      var instance = extraParam ? new klass(object[extraParam], object) : new klass(object);
      callback && callback(instance);
    });
  });
};
这段代码做了下面一些事情:
- 通过类名(className 在- Object的子类- fromObject中指定)找到挂载在- fabric命名空间上的对象的所属类
 
- 深拷贝当前对象,避免操作过程对修改源对象 
- 处理、修正对象的一些特殊属性,比如:fill, stroke, clipPath 等 
- 用所属类按新的对象属性构建一个新的对象实例(instance),返回给回调函数 
噫,好像不对劲?反序列化入参不得是个 JSON 字符串吗。是的,不过 fabric.js 中并没有在 Object 类中提供这个方法,这个自己实现也很简单,将目标 JSON 字符串 parse 成 普通的 JSON 对象传入即可。
Canvas 类上面到是有一个画布整体反序列化的方法:loadfromJSON(),它做的事情就是把一段静态的 JSON 字符串转成普通对象后传给每个具体的对象,调用对象上面的fromObject()方法,让对象具有真正的渲染方法,再回绘到 Canvas 上面。
序列化主要用于 持久存储,反序列化则主要用于将持久存储的静态内容转换为 Canvas 中可操作的 2d 元素,从而可以实现将某个时刻画布上的状态还原的目的
如果你的存储够用的话,甚至可以将整个在 Canvas 上的绘制过程进行录制/回放
一些绘制过程中常见的功能也是通过序列化/反序列化来实现的,比如:撤销/重做
fabric 混入类
混入类(mixin)通常用来给对象添加额外的方法,通常这些方法和画布关系不大,比如:一些无参方法,事件绑定等。通常混入类会通过调用fabric.util.object.extend()方法来给对象的 prototype 上添加额外的方法。
fabric.js 的事件绑定
混入类里面有一个很重要的文件:canvas_event.mixin.js,它的作用有以下几种:
- 为上层 Canvas 绑定原生浏览器事件 
- 在合适的时机触发自定义事件 
- 使用第三方库(event.js)绑定、模拟移动端手势操作事件 
fabric.js 的鼠标移动(__onMouseMove())事件
__onMouseMove() 可以说是一个核心事件,对象的变换基本上都要靠它来计算距离才能实 现,我们来看看它是如何实现的
__onMouseMove: function (e) {
  this._handleEvent(e, 'move:before');
  this._cacheTransformEventData(e);
  var target, pointer;
  if (this.isDrawingMode) {
    this._onMouseMoveInDrawingMode(e);
    return;
  }
  if (!this._isMainEvent(e)) {
    return;
  }
  var groupselector = this._groupselector;
  // We initially clicked in an empty area, so we draw a box for multiple selection
  if (groupselector) {
    pointer = this._pointer;
    groupselector.left = pointer.x - groupselector.ex;
    groupselector.top = pointer.y - groupselector.ey;
    this.renderTop();
  }
  else if (!this._currentTransform) {
    target = this.findTarget(e) || null;
    this._setCursorfromEvent(e, target);
    this._fireOverOutEvents(target, e);
  }
  else {
    this._transformObject(e);
  }
  this._handleEvent(e, 'move');
  this._resetTransformEventData();
},
注意看源码的时候要把握到重点,一点不重要的就先忽略,比如:缓存处理、状态标识。我们只看最核心的部分,上面这段代码里面显然_transformObject()才是一个核心方法,我们深入学习下。
/**
 * 对对象进行转换(变形、旋转、拖动)动作,e 为当前鼠标的 mousemove 事件,
 * **transform** 表示要进行转换的对象(mousedown 时确定的)在 `_setupCurrentTransform()` 中封装过,
 * 可以理解为对象 **之前** 的状态,再调用 transform 对象中对应的 actionHandler
 * 来操作画布中的对象,`_performTransformAction()` 可以对 action 进行检测,如果对象真正发生了变化
 * 才会触发最终的渲染方法 requestRenderAll()
 * @private
 * @param {Event} e 鼠标的 mousemove 事件
 */
_transformObject: function(e) {
  var pointer = this.getPointer(e),
      transform = this._currentTransform;
  transform.reset = false;
  transform.shiftKey = e.shiftKey;
  transform.altKey = e[this.centeredKey];
  this._performTransformAction(e, transform, pointer);
  transform.actionPerformed && this.requestRenderAll();
},
我已经把注释添加上了,主要的代码实现其实是在_performTransformAction()中实现的。
_performTransformAction: function(e, transform, pointer) {
  var x = pointer.x,
      y = pointer.y,
      action = transform.action,
      actionPerformed = false,
      actionHandler = transform.actionHandler;
      // actionHandle 是被封装在 controls.action.js 中的处理器
  if (actionHandler) {
    actionPerformed = actionHandler(e, transform, x, y);
  }
  if (action === 'drag' && actionPerformed) {
    transform.target.isMoving = true;
    this.setCursor(transform.target.moveCursor || this.moveCursor);
  }
  transform.actionPerformed = transform.actionPerformed || actionPerformed;
},
这里的transform对象是设计得比较精妙的地方,它封装了对象操作的几种不同的类型,每种类型对应的有不同的动作处理器(actionHandler),transform 对象就充当了一 种对于2d元素进行操作的上下文,这样设计可以得得事件绑定和处理逻辑分离,代码具有更高的内聚性。
我们再看看上面注释中提到的_setupCurrentTransform()方法,一次 transform 开始与结束正好对应着鼠标的按下(onMouseDown)与松开(onMouseUp)两个事件。
我们可以从onMouseDown()事件中顺藤摸瓜,找到构造 transform 对象的地方:
_setupCurrentTransform: function (e, target, alreadyselected) {
  var pointer = this.getPointer(e), corner = target.__corner,
      control = target.controls[corner],
      actionHandler = (alreadyselected && corner) 
              ? control.getActionHandler(e, target, control) 
              : fabric.controlsUtils.dragHandler,
      transform = {
        target: target,
        action: action,
        actionHandler: actionHandler,
        corner: corner,
        scaleX: target.scaleX,
        scaleY: target.scaleY,
        skewX: target.skewX,
        skewY: target.skewY,
      };
  // transform 上下文对象被构造的地方
  this._currentTransform = transform;
  this._beforeTransform(e);
},
control.getActionHandler是动态从default_controls.js中按边角的类型获取的:
| 边角类型 | 控制位置 | 动作处理器(actionHandler) | 作用 | 
|---|
| ml | 左中 | scalingXOrSkewingY | 横向缩放或者纵向扭曲 | 
| mr | 右中 | scalingXOrSkewingY | 横向缩放或者纵向扭曲 | 
| mb | 下中 | scalingYOrSkewingX | 纵向缩放或者横向扭曲 | 
| mt | 上中 | scalingYOrSkewingX | 纵向缩放或者横向扭曲 | 
| tl | 左上 | scalingEqually | 等比缩放 | 
| tr | 右上 | scalingEqually | 等比缩放 | 
| bl | 左下 | scalingEqually | 等比缩放 | 
| br | 右下 | scalingEqually | 等比缩放 | 
| mtr | 中上变形 | controlsUtils.rotationWithSnapping | 旋转 | 
对照上面的边角控制器图片更好理解。
这里我想多说一点,一般来讲,像这种上层的交互功能,做为一个 Canvas 库通常是不会封装好的。 但是 fabric.js 却帮我们做好了,这也验证了它自己定义里面的一个关键词:** 可交互的**,正是因为它通过边角控制器封装了可见的对象操作,才使得 Canvas 对象可以与用户进行交互。我们普通开发者不需要关心细节,配置一些通用参数就能实现功能。
fabric.js 的自定义事件
fabric.js 中内置了很多自定义事件,这些事件都是我们常用的,非原子事件。对于日常开发来说非常方便。
对象上的 24 种事件
- object:added 
- object:removed 
- object:selected 
- object:deselected 
- object:modified 
- object:modified 
- object:moved 
- object:scaled 
- object:rotated 
- object:skewed 
- object:rotating 
- object:scaling 
- object:moving 
- object:skewing 
- object:mousedown 
- object:mouseup 
- object:mouseover 
- object:mouseout 
- object:mousewheel 
- object:mousedblclick 
- object:dragover 
- object:dragenter 
- object:dragleave 
- object:drop 
画布上的 5 种事件
- before:render 
- after:render 
- canvas:cleared 
- object:added 
- object:removed 
明白了上面这几个核心模块的工作原理,再使用 fabric.js 来进行 Canvas 开发就能很快入门, 实际上 Canvas 开发并不难,难的是编程思想和方式的转变。
几个需要注意的地方
- fabric.js 源码没有使用 ES 6,没使用 Typescript,所以在看代码的时候还是很不方便的,推荐使用 jetbrains 家的 IDE:IntelliJ IDEA 或 Webstorm 都是支持对 ES 6 以下的 Javascript 代码进行 静态分析的,可以使用跳转到定义、调用层级等功能,看源代码会很方便。 
- fabric.js 源码中很多地方用到 Canvas 的 save() 和 restore() 方法,可以查看这个链接了解更多:查看。 
- 如果你之前从来没有接触过 Canvas 开发,那我建议去看看 bilibili 上萧井陌录的一节的关于入门游戏开发的视频教程,不要一开始就去学习 Canvas 的 API,先了解概念原理性的东西,最后再追求细节。 
该文章在 2023/5/23 11:56:36 编辑过