狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

Fabric.js 原理與源碼解析

admin
2023年5月23日 11:50 本文熱度 773

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 開發原理

如果你之前沒有過 Canvas 的相關開發經驗(只有 Javascript 網頁開發經驗),剛開始入門會覺得不好懂,不理解 Canvas 開發的邏輯。這個很正常,因為這表示你正在從傳統的 Javascript 開發轉到圖形圖像 GUI 圖形圖像、動畫開發。雖然語言都是 Javascript 但是開發理念和用到的編程范式完全不同。

  • 傳統的客戶端 Javascript 開發一般可以認為是事件驅動的編程模型(Event-driven programming),這個時候你需要關注事件的觸發者和監聽者

  • Canvas 開發通常是面向對象的編程模型,需要把繪制的物體抽象為對象,通過對象的方法維護自身的屬性,通常會使用一個全局的事件總線來處理對象之間的交互

這兩種開發方式各有各的優勢,比如:

  • 有的功能在 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 有如下區別:

內部叫法文件路徑作用
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 的一種混入模式實現的繼承。

從圖中我們可以得出以下幾點:

  • 底層 StaticCanvas 繼承了Collection對象和Observable對象,這就意味著 StaticCanvas 有兩種能力:

    • 給 Canvas 添加(Collection.add())對象,遍歷所(Collection.forEachObject())有對象

    • 自定義事件發布/訂閱的能力

  • 所有的 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);

    });

  });

};


這段代碼做了下面一些事情:

  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 種事件
  • 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 開發并不難,難的是編程思想和方式的轉變。

幾個需要注意的地方

  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,先了解概念原理性的東西,最后再追求細節。



該文章在 2023/5/23 11:56:36 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved