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 編輯過