亚洲乱色熟女一区二区三区丝袜,天堂√中文最新版在线,亚洲精品乱码久久久久久蜜桃图片,香蕉久久久久久av成人,欧美丰满熟妇bbb久久久

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開(kāi)發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

Fabric.js 原理與源碼解析

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

Fabric.js 簡(jiǎn)介

我們先來(lái)看看官方的定義:

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 是一個(gè)可以讓 HTML5 Canvas 開(kāi)發(fā)變得簡(jiǎn)單的框架。它是一種基于 Canvas 元素的可交互對(duì)象模型,也是一個(gè) SVG 到 Canvas 的解析器(讓SVG 渲染到 Canvas 上)。

Fabric.js 的代碼不算多,源代碼(不包括內(nèi)置的三方依賴)大概 1.7 萬(wàn)行。最初是在 2010 年開(kāi)發(fā)的,從源代碼就可以看出來(lái),都是很老的代碼寫法。沒(méi)有構(gòu)建工具,沒(méi)有依賴,甚至沒(méi)使用 ES6,代碼中模塊都是用 IIFE 的方式包裝的。

但是這個(gè)并不影響我們學(xué)習(xí)它,相反正因?yàn)樗鼪](méi)引入太多的概念,使用起來(lái)相當(dāng)方便。不需要構(gòu)建工具,直接在一個(gè) HTML 文件中引入庫(kù)文件就可以開(kāi)發(fā)了,甚至官方都提供了一個(gè) 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>

這就夠了,不是嗎?

使用場(chǎng)景

從它的官方定義可以看出來(lái),它是一個(gè)用 Canvas 實(shí)現(xiàn)的對(duì)象模型。如果你需要用 HTML Canvas 來(lái)繪制一些東西,并且這些東西可以響應(yīng)用戶的交互,比如:拖動(dòng)、變形、旋轉(zhuǎn)等操作。那用 fabric.js 是非常合適的,因?yàn)樗鼉?nèi)部不僅實(shí)現(xiàn)了 Canvas 對(duì)象模型,還將一些常用的交互操作封裝好了,可以說(shuō)是開(kāi)箱即用。

內(nèi)部集成的主要功能如下:

  • 幾何圖形繪制,如:形狀(圓形、方形、三角形)、路徑

  • 位圖加載、濾鏡

  • 自由畫筆工具,筆刷

  • 文本、富文本渲染

  • 模式圖像

  • 對(duì)象動(dòng)畫

  • Canvas 對(duì)象之間的序列化與反序列化

Canvas 開(kāi)發(fā)原理

如果你之前沒(méi)有過(guò) Canvas 的相關(guān)開(kāi)發(fā)經(jīng)驗(yàn)(只有 Javascript 網(wǎng)頁(yè)開(kāi)發(fā)經(jīng)驗(yàn)),剛開(kāi)始入門會(huì)覺(jué)得不好懂,不理解 Canvas 開(kāi)發(fā)的邏輯。這個(gè)很正常,因?yàn)檫@表示你正在從傳統(tǒng)的 Javascript 開(kāi)發(fā)轉(zhuǎn)到圖形圖像 GUI 圖形圖像、動(dòng)畫開(kāi)發(fā)。雖然語(yǔ)言都是 Javascript 但是開(kāi)發(fā)理念和用到的編程范式完全不同。

  • 傳統(tǒng)的客戶端 Javascript 開(kāi)發(fā)一般可以認(rèn)為是事件驅(qū)動(dòng)的編程模型(Event-driven programming),這個(gè)時(shí)候你需要關(guān)注事件的觸發(fā)者和監(jiān)聽(tīng)者

  • Canvas 開(kāi)發(fā)通常是面向?qū)ο蟮木幊棠P?/span>,需要把繪制的物體抽象為對(duì)象,通過(guò)對(duì)象的方法維護(hù)自身的屬性,通常會(huì)使用一個(gè)全局的事件總線來(lái)處理對(duì)象之間的交互

這兩種開(kāi)發(fā)方式各有各的優(yōu)勢(shì),比如:

  • 有的功能在 HTML 里一行代碼就能實(shí)現(xiàn)的功能放到 Canvas 中需要成千行的代碼去實(shí)現(xiàn)。比如:textarea, contenteditable

  • 相反,有的功能在 Canvas 里面只需要一行代碼實(shí)現(xiàn)的,使用 HTML 卻幾乎無(wú)法實(shí)現(xiàn)。比如:截圖、錄制

Canvas 開(kāi)發(fā)的本質(zhì)其實(shí)很簡(jiǎn)單,想像下面這種少兒畫板:

Canvas 的渲染過(guò)程就是不斷的在畫板(Canvas)上面擦了畫,畫了擦。

動(dòng)畫就更簡(jiǎn)單了,只要渲染幀率超過(guò)人眼能識(shí)別的幀率(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>


當(dāng)然你也可以用requestAnimationFrame,不過(guò)這不是我想說(shuō)明的重點(diǎn)。

Fabric.js 源碼解析

模塊結(jié)構(gòu)圖

fabric.js 的模塊我大概畫了個(gè)圖,方便理解。

基本原理

fabric.js 在初始化的時(shí)候會(huì)將你指定的 Canvas 元素(叫做 lowerCanvas)外面包裹上一層 div 元素,然后內(nèi)部會(huì)插入另外一個(gè)上層的 Canvas 元素(叫做 upperCanvas),這兩個(gè) Canvas 有如下區(qū)別:

內(nèi)部叫法文件路徑作用
upperCanvassrc/canvas.class.js上層畫布,只處理 分組選擇事件綁定
lowerCanvassrc/static_canvas.class.js真正 繪制 元素對(duì)象(Object)的畫布

核心模塊詳解

上圖中,灰色的模塊對(duì)于理解 fabric.js 核心工作原理沒(méi)多大作用,可以不看。其它核心模塊我按自己的理解來(lái)解釋一下。

所有模塊都被掛載到一個(gè) fabric 的命名空間上面,都可以用fabric.XXX的形式訪問(wèn)。

fabric.util工具包

工具包中一個(gè)最重要的方法是createClass,它可以用來(lái)創(chuàng)建一個(gè)類。我們來(lái)看看這個(gè)方法:

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

  }


  // 關(guān)聯(lián)父子類之間的關(guān)系

  klass.superclass = parent;

  klass.subclasses = [];


  if (parent) {

    Subclass.prototype = parent.prototype;

    klass.prototype = new Subclass();

    parent.subclasses.push(klass);

  }

  // ...

}


為什么不用 ES 6 的類寫法呢?主要是因?yàn)檫@個(gè)庫(kù)寫的時(shí)候 ES 6 還沒(méi)出來(lái)。作者沿用了老式的基于Javascript prototype 實(shí)現(xiàn)的類繼承的寫法,這個(gè)方法封裝了類的繼承、構(gòu)造方法、父子類之前的關(guān)系等功能。注意klass.superclassklass.subclasses這兩行,后面會(huì)講到。

添加這兩個(gè)引用關(guān)系后,我們就可以在 JS 運(yùn)行時(shí)動(dòng)態(tài)獲取類之間的關(guān)系,方便后續(xù)序列化及反序列化操作,這種做法類似于其它編程語(yǔ)言中的反射機(jī)制,可以讓你在代碼運(yùn)行的時(shí)候動(dòng)態(tài)的構(gòu)建、操作對(duì)象

initialize()方法(構(gòu)造函數(shù))會(huì)在類被 new 出來(lái)的時(shí)候自動(dòng)調(diào)用:

function klass()

{

  this.initialize.apply(this, arguments);

}


fabric 通用類

fabric.Canvas

上層畫布類,如上面表格所述,它并不渲染對(duì)象。它只來(lái)處理與用戶交互的邏輯。 比如:全局事件綁定、快捷鍵、鼠標(biāo)樣式、處理多(分組)選擇邏輯。

我們來(lái)看看這個(gè)類初始化時(shí)具體干了些什么。

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中第一個(gè)參數(shù)是StaticCanvas,所以我們可以知道 Canvas 的父類是StaticCanvas。

從構(gòu)造方法initialize中我們可以看出:

只有_initInteractive_createCacheCanvas是 Canvas 類自己的方法,renderAndResetBound,requestRenderAllBound_initStatic都繼承自父類StaticCanvas

這個(gè)類的使用也很簡(jiǎn)單,做為 fabric.js 程序的入口,我們只需要 new 出來(lái)即可:

// c 就是 HTML 中的 canvas 元素 id

const canvas = new fabric.Canvas("c", { /* 屬性 */ })


fabric.StaticCanvas

fabric 的核心類,控制著 Canvas 的渲染操作,所有的畫布對(duì)象都必須在它上面繪制出來(lái)。我們從構(gòu)造函數(shù)中開(kāi)始看:

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,而且它的實(shí)現(xiàn)方式很 Javascript,在 StaticCanvas.js 最下面一段:

extend(fabric.StaticCanvas.prototype, fabric.Observable);

extend(fabric.StaticCanvas.prototype, fabric.Collection);


fabric.js 的畫布渲染原理
requestRenderAll()方法

從下面的代碼可以看出來(lái),這個(gè)方法的主要任務(wù)就是不斷調(diào)用renderAndResetBound方法,renderAndReset方法會(huì)最終調(diào)用renderCanvas來(lái)實(shí)現(xiàn)繪制。

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

}


我們刪掉一些不重要的,精簡(jiǎn)一下,其實(shí)最主要的代碼就兩行:

renderCanvas: function(ctx, objects) {

    this.clearContext(ctx);

    this._renderObjects(ctx, objects);

}


clearContext 里面會(huì)調(diào)用 canvas 上下文的clearRect方法來(lái)清空畫布:

ctx.clearRect(0, 0, this.width, this.height)


_renderObjects就是遍歷所有的objects調(diào)用它們的render()方法,把自己繪制到畫布上去:

for (i = 0, len = objects.length; i < len; ++i) {

    objects[i] && objects[i].render(ctx);

}


現(xiàn)在你是不是明白了文章最開(kāi)始那段setInterval實(shí)現(xiàn)的 Canvas 動(dòng)畫原理了?

fabric 形狀類

fabric.Object對(duì)象根類型

雖然我們已經(jīng)明白了 canvas 的繪制原理,但是一個(gè)對(duì)象(2d元素)到底是怎么繪制到 canvas 上去的,它們的移動(dòng)怎么實(shí)現(xiàn)的?具體細(xì)節(jié)我們還不是很清楚,這就要從fabric.Object根類型看起了。

由于 fabric 中的 2d 元素都是以面向?qū)ο蟮男问綄?shí)現(xiàn)的,所以我畫了一張內(nèi)部類之間的繼承關(guān)系,可以清楚的看出它們之間的層次結(jié)構(gòu):

不像傳統(tǒng)的 UML 類圖那樣,這個(gè)圖看起來(lái)還稍有點(diǎn)亂,因?yàn)?fabric.js 內(nèi)部實(shí)現(xiàn)的是多重繼承,或者說(shuō)類似于 mixin 的一種混入模式實(shí)現(xiàn)的繼承。

從圖中我們可以得出以下幾點(diǎn):

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

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

    • 自定義事件發(fā)布/訂閱的能力

  • 所有的 2d 形狀(如:矩形、圓、線條、文本)都繼承了Object類。Object 有的屬性、方法,所有的 2d 形狀都會(huì)有

  • 所有的 2d 形狀都具有自定義事件發(fā)布/訂閱的能力

Object 類常用屬性

下面的注釋中,邊角控制器是 fabric.js 內(nèi)部集成的用戶與對(duì)象交互的一個(gè)手柄,當(dāng)某個(gè)對(duì)象處于激活狀態(tài)的時(shí)候,手柄會(huì)展示出來(lái)。如下圖所示:

常用屬性解釋:

// 對(duì)象的類型(矩形,圓,路徑等),此屬性被設(shè)計(jì)為只讀,不能被修改。修改后 fabric 的一些部分將不能正常使用。

type:                     'object',

// 對(duì)象變形的水平中心點(diǎn)的位置(左,右,中間)

// 查看 http://jsfiddle.net/1ow02gea/244/ originX/originY 在分組中的使用案例

originX:                  'left',

// 對(duì)象變形的垂直中心點(diǎn)的位置(上,下,中間)

// 查看 http://jsfiddle.net/1ow02gea/244/ originX/originY 在分組中的使用案例

originY:                  'top',

// 對(duì)象的頂部位置,默認(rèn)**相對(duì)于**對(duì)象的上邊沿,你可以通過(guò)設(shè)置 originY={top/center/bottom} 改變它的參數(shù)參考位置

top:                      0,

// 對(duì)象的左側(cè)位置,默認(rèn)**相對(duì)于**對(duì)象的左邊沿,你可以通過(guò)設(shè)置 originX={top/center/bottom} 改變它的參數(shù)參考位置

left:                     0,

// 對(duì)象的寬度

width:                    0,

// 對(duì)象的高度

height:                   0,

// 對(duì)象水平縮放比例(倍數(shù):1.5)

scaleX:                   1,

// 對(duì)象水平縮放比例(倍數(shù):1.5)

scaleY:                   1,

// 是否水平翻轉(zhuǎn)渲染

flipX:                    false,

// 是否垂直翻轉(zhuǎn)渲染

flipY:                    false,

// 透明度

opacity:                  1,

// 對(duì)象旋轉(zhuǎn)角度(度數(shù))

angle:                    0,

// 對(duì)象水平傾斜角度(度數(shù))

skewX:                    0,

// 對(duì)象垂直傾斜角度(度數(shù))

skewY:                    0,

// 對(duì)象的邊角控制器大?。ㄏ袼兀?/p>

cornerSize:               13,

// 當(dāng)檢測(cè)到 touch 交互時(shí)對(duì)象的邊角控制器大小

touchCornerSize:               24,

// 對(duì)象邊角控制器是否透明(不填充顏色),默認(rèn)只保留邊框、線條

transparentCorners:       true,

// 鼠標(biāo) hover 到對(duì)象上時(shí)鼠標(biāo)形狀

hoverCursor:              null,

// 鼠標(biāo)拖動(dòng)對(duì)象時(shí)鼠標(biāo)形狀

moveCursor:               null,

// 對(duì)象本身與邊角控制器之間的間距(像素)

padding:                  0,

// 對(duì)象處于活動(dòng)狀態(tài)下邊角控制器**包裹對(duì)象的邊框**顏色

borderColor:              'rgb(178,204,255)',

// 指定邊角控制器**包裹對(duì)象的邊框**虛線邊框的模式元組(hasBorder 必須為 true)

// 第一個(gè)元素為實(shí)線,第二個(gè)為空白

borderDashArray:          null,

// 對(duì)象處于活動(dòng)狀態(tài)下邊角控制器顏色

cornerColor:              'rgb(178,204,255)',

// 對(duì)象處于活動(dòng)狀態(tài)且 transparentCorners 為 false 時(shí)邊角控制器本身的邊框顏色

cornerStrokeColor:        null,

// 邊角控制器的樣式,正方形或圓形

cornerStyle:          'rect',

// 指定邊角控制器本身的虛線邊框的模式元組(hasBorder 必須為 true)

// 第一個(gè)元素為實(shí)線,第二個(gè)為空白

cornerDashArray:          null,

// 如果為真,通過(guò)邊角控制器來(lái)對(duì)對(duì)象進(jìn)行縮放會(huì)以對(duì)象本身的中心點(diǎn)為準(zhǔn)

centeredScaling:          false,

// 如果為真,通過(guò)邊角控制器來(lái)對(duì)對(duì)象進(jìn)行旋轉(zhuǎn)會(huì)以對(duì)象本身的中心點(diǎn)為準(zhǔn)

centeredRotation:         true,

// 對(duì)象的填充顏色

fill:                     'rgb(0,0,0)',

// 填充顏色的規(guī)則:nonzero 或者 evenodd

// @see https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/fill-rule

fillRule:                 'nonzero',

// 對(duì)象的背景顏色

backgroundColor:          '',

// 可選擇區(qū)域被選擇時(shí)(對(duì)象邊角控制器區(qū)域),層級(jí)低于對(duì)象背景顏色

selectionBackgroundColor:          '',

// 設(shè)置后,對(duì)象將以筆觸的方式繪制,此屬性值即為筆觸的顏色

stroke:                   null,

// 筆觸的大小

strokeWidth:              1,

// 指定筆觸虛線的模式元組(hasBorder 必須為 true)

// 第一個(gè)元素為實(shí)線,第二個(gè)為空白

strokeDashArray:          null,


Object 類常用方法
drawObject()對(duì)象的繪制方法

drawObject()方法內(nèi)部會(huì)調(diào)用_render()方法,但是在fabric.Object基類中它是個(gè)空方法。這意味著對(duì)象具體的繪制方法需要子類去實(shí)現(xiàn)。即子類需要重寫父類的空_render()方法。

_onObjectAdded()對(duì)象被添加到 Canvas 事件

這個(gè)方法非常重要,只要當(dāng)一個(gè)對(duì)象被添加到 Canvas 中的時(shí)候,對(duì)象才可以具有 Canvas 的引用上下文,對(duì)象的一些常用方法才能起作用。比如:Object.center()方法,調(diào)用它可以讓一個(gè)對(duì)象居中到畫布中央。下面這段代碼可以實(shí)現(xiàn)這個(gè)功能:

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)


但是你會(huì)發(fā)現(xiàn) box 并沒(méi)有被居中,這就是因?yàn)椋寒?dāng)一個(gè)對(duì)象(box)還沒(méi)被添加到 Canvas 中的時(shí)候,對(duì)象上面還不具有 Canvas 的上下文,所以調(diào)用的對(duì)象并不知道應(yīng)該在哪個(gè) Canvas 上繪制。我們可以看下center()方法的源代碼:

center: function () {

  this.canvas && this.canvas.centerObject(this);

  return this;

},


正如上面所說(shuō),沒(méi)有 canvas 的時(shí)候是不會(huì)調(diào)用到canvas.centerObject()方法,也就實(shí)現(xiàn)不了居中。

所以解決方法也很簡(jiǎn)單,調(diào)換下 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」—— 好問(wèn)題,但是我不準(zhǔn)備講這個(gè)。有興趣可以自己研究 下。

toObject()對(duì)象的序列化

正向的把對(duì)象序列化是很簡(jiǎn)單的,只需要把你關(guān)注的對(duì)象上的屬性拼成一個(gè) 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;

},


當(dāng)調(diào)用對(duì)象的toJSON()方法時(shí)會(huì)使用JSON.stringify(toObject())來(lái)將對(duì)象的屬性轉(zhuǎn)換成 JSON 字符串

fromObject()對(duì)象的反序列化

fromObject()是 Object 的子類需要實(shí)現(xiàn)的反序列化方法,通常會(huì)調(diào)用 Object 類的默認(rèn)方法_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. 通過(guò)類名(className 在Object的子類fromObject中指定)找到掛載在fabric命名空間上的對(duì)象的所屬類

  2. 深拷貝當(dāng)前對(duì)象,避免操作過(guò)程對(duì)修改源對(duì)象

  3. 處理、修正對(duì)象的一些特殊屬性,比如:fill, stroke, clipPath 等

  4. 用所屬類按新的對(duì)象屬性構(gòu)建一個(gè)新的對(duì)象實(shí)例(instance),返回給回調(diào)函數(shù)

噫,好像不對(duì)勁?反序列化入?yún)⒉坏檬莻€(gè) JSON 字符串嗎。是的,不過(guò) fabric.js 中并沒(méi)有在 Object 類中提供這個(gè)方法,這個(gè)自己實(shí)現(xiàn)也很簡(jiǎn)單,將目標(biāo) JSON 字符串 parse 成 普通的 JSON 對(duì)象傳入即可。

Canvas 類上面到是有一個(gè)畫布整體反序列化的方法:loadfromJSON(),它做的事情就是把一段靜態(tài)的 JSON 字符串轉(zhuǎn)成普通對(duì)象后傳給每個(gè)具體的對(duì)象,調(diào)用對(duì)象上面的fromObject()方法,讓對(duì)象具有真正的渲染方法,再回繪到 Canvas 上面。

序列化主要用于 持久存儲(chǔ),反序列化則主要用于將持久存儲(chǔ)的靜態(tài)內(nèi)容轉(zhuǎn)換為 Canvas 中可操作的 2d 元素,從而可以實(shí)現(xiàn)將某個(gè)時(shí)刻畫布上的狀態(tài)還原的目的

如果你的存儲(chǔ)夠用的話,甚至可以將整個(gè)在 Canvas 上的繪制過(guò)程進(jìn)行錄制/回放

一些繪制過(guò)程中常見(jiàn)的功能也是通過(guò)序列化/反序列化來(lái)實(shí)現(xiàn)的,比如:撤銷/重做

fabric 混入類

混入類(mixin)通常用來(lái)給對(duì)象添加額外的方法,通常這些方法和畫布關(guān)系不大,比如:一些無(wú)參方法,事件綁定等。通?;烊腩悤?huì)通過(guò)調(diào)用fabric.util.object.extend()方法來(lái)給對(duì)象的 prototype 上添加額外的方法。

fabric.js 的事件綁定

混入類里面有一個(gè)很重要的文件:canvas_event.mixin.js,它的作用有以下幾種:

  1. 為上層 Canvas 綁定原生瀏覽器事件

  2. 在合適的時(shí)機(jī)觸發(fā)自定義事件

  3. 使用第三方庫(kù)(event.js)綁定、模擬移動(dòng)端手勢(shì)操作事件

fabric.js 的鼠標(biāo)移動(dòng)(__onMouseMove())事件

__onMouseMove() 可以說(shuō)是一個(gè)核心事件,對(duì)象的變換基本上都要靠它來(lái)計(jì)算距離才能實(shí) 現(xiàn),我們來(lái)看看它是如何實(shí)現(xiàn)的

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

},


注意看源碼的時(shí)候要把握到重點(diǎn),一點(diǎn)不重要的就先忽略,比如:緩存處理、狀態(tài)標(biāo)識(shí)。我們只看最核心的部分,上面這段代碼里面顯然_transformObject()才是一個(gè)核心方法,我們深入學(xué)習(xí)下。

/**

 * 對(duì)對(duì)象進(jìn)行轉(zhuǎn)換(變形、旋轉(zhuǎn)、拖動(dòng))動(dòng)作,e 為當(dāng)前鼠標(biāo)的 mousemove 事件,

 * **transform** 表示要進(jìn)行轉(zhuǎn)換的對(duì)象(mousedown 時(shí)確定的)在 `_setupCurrentTransform()` 中封裝過(guò),

 * 可以理解為對(duì)象 **之前** 的狀態(tài),再調(diào)用 transform 對(duì)象中對(duì)應(yīng)的 actionHandler

 * 來(lái)操作畫布中的對(duì)象,`_performTransformAction()` 可以對(duì) action 進(jìn)行檢測(cè),如果對(duì)象真正發(fā)生了變化

 * 才會(huì)觸發(fā)最終的渲染方法 requestRenderAll()

 * @private

 * @param {Event} e 鼠標(biāo)的 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();

},


我已經(jīng)把注釋添加上了,主要的代碼實(shí)現(xiàn)其實(shí)是在_performTransformAction()中實(shí)現(xiàn)的。

_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對(duì)象是設(shè)計(jì)得比較精妙的地方,它封裝了對(duì)象操作的幾種不同的類型,每種類型對(duì)應(yīng)的有不同的動(dòng)作處理器(actionHandler),transform 對(duì)象就充當(dāng)了一 種對(duì)于2d元素進(jìn)行操作的上下文,這樣設(shè)計(jì)可以得得事件綁定和處理邏輯分離,代碼具有更高的內(nèi)聚性。

我們?cè)倏纯瓷厦孀⑨屩刑岬降?code style="box-sizing: inherit; -webkit-font-smoothing: auto; background-color: rgba(242, 242, 242, 0.5); color: rgb(51, 51, 51); font-size: 0.9em; font-family: "Latin Modern Mono", "SF Mono", monaco, Consolas, "Noto Serif SC", "Noto Serif CJK SC", "Noto Serif CJK", "Source Han Serif SC", "Source Han Serif CN", "Source Han Serif", source-han-serif-sc; border-radius: 3px; line-height: 1.77778; padding: 0.1em 0.4em 0.2em; vertical-align: baseline;">_setupCurrentTransform()方法,一次 transform 開(kāi)始與結(jié)束正好對(duì)應(yīng)著鼠標(biāo)的按下(onMouseDown)與松開(kāi)(onMouseUp)兩個(gè)事件。

我們可以從onMouseDown()事件中順藤摸瓜,找到構(gòu)造 transform 對(duì)象的地方:

_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 上下文對(duì)象被構(gòu)造的地方

  this._currentTransform = transform;

  this._beforeTransform(e);

},


control.getActionHandler是動(dòng)態(tài)從default_controls.js中按邊角的類型獲取的:

邊角類型控制位置動(dòng)作處理器(actionHandler)作用
ml左中scalingXOrSkewingY橫向縮放或者縱向扭曲
mr右中scalingXOrSkewingY橫向縮放或者縱向扭曲
mb下中scalingYOrSkewingX縱向縮放或者橫向扭曲
mt上中scalingYOrSkewingX縱向縮放或者橫向扭曲
tl左上scalingEqually等比縮放
tr右上scalingEqually等比縮放
bl左下scalingEqually等比縮放
br右下scalingEqually等比縮放
mtr中上變形controlsUtils.rotationWithSnapping旋轉(zhuǎn)

對(duì)照上面的邊角控制器圖片更好理解。

這里我想多說(shuō)一點(diǎn),一般來(lái)講,像這種上層的交互功能,做為一個(gè) Canvas 庫(kù)通常是不會(huì)封裝好的。 但是 fabric.js 卻幫我們做好了,這也驗(yàn)證了它自己定義里面的一個(gè)關(guān)鍵詞:** 可交互的**,正是因?yàn)樗ㄟ^(guò)邊角控制器封裝了可見(jiàn)的對(duì)象操作,才使得 Canvas 對(duì)象可以與用戶進(jìn)行交互。我們普通開(kāi)發(fā)者不需要關(guān)心細(xì)節(jié),配置一些通用參數(shù)就能實(shí)現(xiàn)功能。

fabric.js 的自定義事件

fabric.js 中內(nèi)置了很多自定義事件,這些事件都是我們常用的,非原子事件。對(duì)于日常開(kāi)發(fā)來(lái)說(shuō)非常方便。

對(duì)象上的 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

明白了上面這幾個(gè)核心模塊的工作原理,再使用 fabric.js 來(lái)進(jìn)行 Canvas 開(kāi)發(fā)就能很快入門, 實(shí)際上 Canvas 開(kāi)發(fā)并不難,難的是編程思想和方式的轉(zhuǎn)變。

幾個(gè)需要注意的地方

  1. fabric.js 源碼沒(méi)有使用 ES 6,沒(méi)使用 Typescript,所以在看代碼的時(shí)候還是很不方便的,推薦使用 jetbrains 家的 IDE:IntelliJ IDEA 或 Webstorm 都是支持對(duì) ES 6 以下的 Javascript 代碼進(jìn)行 靜態(tài)分析的,可以使用跳轉(zhuǎn)到定義、調(diào)用層級(jí)等功能,看源代碼會(huì)很方便。

  2. fabric.js 源碼中很多地方用到 Canvas 的 save() 和 restore() 方法,可以查看這個(gè)鏈接了解更多:查看。

  3. 如果你之前從來(lái)沒(méi)有接觸過(guò) Canvas 開(kāi)發(fā),那我建議去看看 bilibili 上蕭井陌錄的一節(jié)的關(guān)于入門游戲開(kāi)發(fā)的視頻教程,不要一開(kāi)始就去學(xué)習(xí) Canvas 的 API,先了解概念原理性的東西,最后再追求細(xì)節(jié)。



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