管理杂谈OA答疑ERP答疑教程搜索

Fabric.js 原理与源码解析


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 但是开发理念和用到的编程范式完全不同。

这两种开发方式各有各的优势,比如:

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 有如下区别:

内部叫法文件路径作用
upperCanvassrc/canvas.class.js上层画布,只处理 分组选择事件绑定
lowerCanvassrc/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.superclassklass.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 类自己的方法,renderAndResetBoundrequestRenderAllBound_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.Observablefabric.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 的一种混入模式实现的继承。

从图中我们可以得出以下几点:

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);

    });

  });

};


这段代码做了下面一些事情:

  1. 通过类名(className 在Object的子类fromObject中指定)找到挂载在fabric命名空间上的对象的所属类

  2. 深拷贝当前对象,避免操作过程对修改源对象

  3. 处理、修正对象的一些特殊属性,比如:fill, stroke, clipPath 等

  4. 用所属类按新的对象属性构建一个新的对象实例(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,它的作用有以下几种:

  1. 为上层 Canvas 绑定原生浏览器事件

  2. 在合适的时机触发自定义事件

  3. 使用第三方库(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 种事件
画布上的 5 种事件

明白了上面这几个核心模块的工作原理,再使用 fabric.js 来进行 Canvas 开发就能很快入门, 实际上 Canvas 开发并不难,难的是编程思想和方式的转变。

几个需要注意的地方

  1. fabric.js 源码没有使用 ES 6,没使用 Typescript,所以在看代码的时候还是很不方便的,推荐使用 jetbrains 家的 IDE:IntelliJ IDEA 或 Webstorm 都是支持对 ES 6 以下的 Javascript 代码进行 静态分析的,可以使用跳转到定义、调用层级等功能,看源代码会很方便。

  2. fabric.js 源码中很多地方用到 Canvas 的 save() 和 restore() 方法,可以查看这个链接了解更多:查看

  3. 如果你之前从来没有接触过 Canvas 开发,那我建议去看看 bilibili 上萧井陌录的一节的关于入门游戏开发的视频教程,不要一开始就去学习 Canvas 的 API,先了解概念原理性的东西,最后再追求细节。



更多精彩文章浏览...
点击右上角图标分享到朋友圈
官方网站:http://www.clicksun.cn
咨询热线:400-186-1886
服务邮箱:service@clicksun.cn