Fabric.js 簡(jiǎn)介
我們先來看看官方的定義:
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 開發(fā)變得簡(jiǎn)單的框架。它是一種基于 Canvas 元素的可交互對(duì)象模型,也是一個(gè) SVG 到 Canvas 的解析器(讓SVG 渲染到 Canvas 上)。
Fabric.js 的代碼不算多,源代碼(不包括內(nèi)置的三方依賴)大概 1.7 萬行。最初是在 2010 年開發(fā)的,從源代碼就可以看出來,都是很老的代碼寫法。沒有構(gòu)建工具,沒有依賴,甚至沒使用 ES6,代碼中模塊都是用 IIFE 的方式包裝的。
但是這個(gè)并不影響我們學(xué)習(xí)它,相反正因?yàn)樗鼪]引入太多的概念,使用起來相當(dāng)方便。不需要構(gòu)建工具,直接在一個(gè) HTML 文件中引入庫文件就可以開發(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)景
從它的官方定義可以看出來,它是一個(gè)用 Canvas 實(shí)現(xiàn)的對(duì)象模型。如果你需要用 HTML Canvas 來繪制一些東西,并且這些東西可以響應(yīng)用戶的交互,比如:拖動(dòng)、變形、旋轉(zhuǎn)等操作。那用 fabric.js 是非常合適的,因?yàn)樗鼉?nèi)部不僅實(shí)現(xiàn)了 Canvas 對(duì)象模型,還將一些常用的交互操作封裝好了,可以說是開箱即用。
內(nèi)部集成的主要功能如下:
Canvas 開發(fā)原理
如果你之前沒有過 Canvas 的相關(guān)開發(fā)經(jīng)驗(yàn)(只有 Javascript 網(wǎng)頁開發(fā)經(jīng)驗(yàn)),剛開始入門會(huì)覺得不好懂,不理解 Canvas 開發(fā)的邏輯。這個(gè)很正常,因?yàn)檫@表示你正在從傳統(tǒng)的 Javascript 開發(fā)轉(zhuǎn)到圖形圖像 GUI 圖形圖像、動(dòng)畫開發(fā)。雖然語言都是 Javascript 但是開發(fā)理念和用到的編程范式完全不同。
傳統(tǒng)的客戶端 Javascript 開發(fā)一般可以認(rèn)為是事件驅(qū)動(dòng)的編程模型(Event-driven programming),這個(gè)時(shí)候你需要關(guān)注事件的觸發(fā)者和監(jiān)聽者
Canvas 開發(fā)通常是面向?qū)ο蟮木幊棠P?/span>,需要把繪制的物體抽象為對(duì)象,通過對(duì)象的方法維護(hù)自身的屬性,通常會(huì)使用一個(gè)全局的事件總線來處理對(duì)象之間的交互
這兩種開發(fā)方式各有各的優(yōu)勢(shì),比如:
有的功能在 HTML 里一行代碼就能實(shí)現(xiàn)的功能放到 Canvas 中需要成千行的代碼去實(shí)現(xiàn)。比如:textarea, contenteditable
相反,有的功能在 Canvas 里面只需要一行代碼實(shí)現(xiàn)的,使用 HTML 卻幾乎無法實(shí)現(xiàn)。比如:截圖、錄制
Canvas 開發(fā)的本質(zhì)其實(shí)很簡(jiǎn)單,想像下面這種少兒畫板:
Canvas 的渲染過程就是不斷的在畫板(Canvas)上面擦了畫,畫了擦。
動(dòng)畫就更簡(jiǎn)單了,只要渲染幀率超過人眼能識(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
,不過這不是我想說明的重點(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)部叫法 | 文件路徑 | 作用 |
---|
upperCanvas | src/canvas.class.js | 上層畫布,只處理 分組選擇,事件綁定 |
lowerCanvas | src/static_canvas.class.js | 真正 繪制 元素對(duì)象(Object)的畫布 |
核心模塊詳解
上圖中,灰色的模塊對(duì)于理解 fabric.js 核心工作原理沒多大作用,可以不看。其它核心模塊我按自己的理解來解釋一下。
所有模塊都被掛載到一個(gè) fabric 的命名空間上面,都可以用fabric.XXX
的形式訪問。
fabric.util
工具包
工具包中一個(gè)最重要的方法是createClass
,它可以用來創(chuàng)建一個(gè)類。我們來看看這個(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è)庫寫的時(shí)候 ES 6 還沒出來。作者沿用了老式的基于Javascript prototype 實(shí)現(xiàn)的類繼承的寫法,這個(gè)方法封裝了類的繼承、構(gòu)造方法、父子類之前的關(guān)系等功能。注意klass.superclass
和klass.subclasses
這兩行,后面會(huì)講到。
添加這兩個(gè)引用關(guān)系后,我們就可以在 JS 運(yùn)行時(shí)動(dòng)態(tài)獲取類之間的關(guān)系,方便后續(xù)序列化及反序列化操作,這種做法類似于其它編程語言中的反射機(jī)制,可以讓你在代碼運(yùn)行的時(shí)候動(dòng)態(tài)的構(gòu)建、操作對(duì)象
initialize()
方法(構(gòu)造函數(shù))會(huì)在類被 new 出來的時(shí)候自動(dòng)調(diào)用:
function klass()
{
this.initialize.apply(this, arguments);
}
fabric 通用類
fabric.Canvas
類
上層畫布類,如上面表格所述,它并不渲染對(duì)象。它只來處理與用戶交互的邏輯。 比如:全局事件綁定、快捷鍵、鼠標(biāo)樣式、處理多(分組)選擇邏輯。
我們來看看這個(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 出來即可:
// c 就是 HTML 中的 canvas 元素 id
const canvas = new fabric.Canvas("c", { /* 屬性 */ })
fabric.StaticCanvas
類
fabric 的核心類,控制著 Canvas 的渲染操作,所有的畫布對(duì)象都必須在它上面繪制出來。我們從構(gòu)造函數(shù)中開始看:
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
,而且它的實(shí)現(xiàn)方式很 Javascript,在 StaticCanvas.js 最下面一段:
extend(fabric.StaticCanvas.prototype, fabric.Observable);
extend(fabric.StaticCanvas.prototype, fabric.Collection);
fabric.js 的畫布渲染原理
requestRenderAll()
方法
從下面的代碼可以看出來,這個(gè)方法的主要任務(wù)就是不斷調(diào)用renderAndResetBound
方法,renderAndReset
方法會(huì)最終調(diào)用renderCanvas
來實(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
方法來清空畫布:
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)在你是不是明白了文章最開始那段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è)圖看起來還稍有點(diǎn)亂,因?yàn)?fabric.js 內(nèi)部實(shí)現(xiàn)的是多重繼承,或者說類似于 mixin 的一種混入模式實(shí)現(xiàn)的繼承。
從圖中我們可以得出以下幾點(diǎn):
底層 StaticCanvas 繼承了Collection
對(duì)象和Observable
對(duì)象,這就意味著 StaticCanvas 有兩種能力:
所有的 2d 形狀(如:矩形、圓、線條、文本)都繼承了Object
類。Object 有的屬性、方法,所有的 2d 形狀都會(huì)有
所有的 2d 形狀都具有自定義事件發(fā)布/訂閱的能力
Object 類常用屬性
下面的注釋中,邊角控制器是 fabric.js 內(nèi)部集成的用戶與對(duì)象交互的一個(gè)手柄,當(dāng)某個(gè)對(duì)象處于激活狀態(tài)的時(shí)候,手柄會(huì)展示出來。如下圖所示:
常用屬性解釋:
// 對(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ì)象的上邊沿,你可以通過設(shè)置 originY={top/center/bottom} 改變它的參數(shù)參考位置
top: 0,
// 對(duì)象的左側(cè)位置,默認(rèn)**相對(duì)于**對(duì)象的左邊沿,你可以通過設(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ì)象的邊角控制器大小(像素)
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,
// 如果為真,通過邊角控制器來對(duì)對(duì)象進(jìn)行縮放會(huì)以對(duì)象本身的中心點(diǎn)為準(zhǔn)
centeredScaling: false,
// 如果為真,通過邊角控制器來對(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 并沒有被居中,這就是因?yàn)椋寒?dāng)一個(gè)對(duì)象(box)還沒被添加到 Canvas 中的時(shí)候,對(duì)象上面還不具有 Canvas 的上下文,所以調(diào)用的對(duì)象并不知道應(yīng)該在哪個(gè) Canvas 上繪制。我們可以看下center()
方法的源代碼:
center: function () {
this.canvas && this.canvas.centerObject(this);
return this;
},
正如上面所說,沒有 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」—— 好問題,但是我不準(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())
來將對(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);
});
});
};
這段代碼做了下面一些事情:
通過類名(className 在Object
的子類fromObject
中指定)找到掛載在fabric
命名空間上的對(duì)象的所屬類
深拷貝當(dāng)前對(duì)象,避免操作過程對(duì)修改源對(duì)象
處理、修正對(duì)象的一些特殊屬性,比如:fill, stroke, clipPath 等
用所屬類按新的對(duì)象屬性構(gòu)建一個(gè)新的對(duì)象實(shí)例(instance),返回給回調(diào)函數(shù)
噫,好像不對(duì)勁?反序列化入?yún)⒉坏檬莻€(gè) JSON 字符串嗎。是的,不過 fabric.js 中并沒有在 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 上的繪制過程進(jìn)行錄制/回放
一些繪制過程中常見的功能也是通過序列化/反序列化來實(shí)現(xiàn)的,比如:撤銷/重做
fabric 混入類
混入類(mixin)通常用來給對(duì)象添加額外的方法,通常這些方法和畫布關(guān)系不大,比如:一些無參方法,事件綁定等。通?;烊腩悤?huì)通過調(diào)用fabric.util.object.extend()
方法來給對(duì)象的 prototype 上添加額外的方法。
fabric.js 的事件綁定
混入類里面有一個(gè)很重要的文件:canvas_event.mixin.js
,它的作用有以下幾種:
為上層 Canvas 綁定原生瀏覽器事件
在合適的時(shí)機(jī)觸發(fā)自定義事件
使用第三方庫(event.js)綁定、模擬移動(dòng)端手勢(shì)操作事件
fabric.js 的鼠標(biāo)移動(dòng)(__onMouseMove())事件
__onMouseMove()
可以說是一個(gè)核心事件,對(duì)象的變換基本上都要靠它來計(jì)算距離才能實(shí) 現(xiàn),我們來看看它是如何實(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()` 中封裝過,
* 可以理解為對(duì)象 **之前** 的狀態(tài),再調(diào)用 transform 對(duì)象中對(duì)應(yīng)的 actionHandler
* 來操作畫布中的對(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 開始與結(jié)束正好對(duì)應(yīng)著鼠標(biāo)的按下(onMouseDown)與松開(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ì)照上面的邊角控制器圖片更好理解。
這里我想多說一點(diǎn),一般來講,像這種上層的交互功能,做為一個(gè) Canvas 庫通常是不會(huì)封裝好的。 但是 fabric.js 卻幫我們做好了,這也驗(yàn)證了它自己定義里面的一個(gè)關(guān)鍵詞:** 可交互的**,正是因?yàn)樗ㄟ^邊角控制器封裝了可見的對(duì)象操作,才使得 Canvas 對(duì)象可以與用戶進(jìn)行交互。我們普通開發(fā)者不需要關(guān)心細(xì)節(jié),配置一些通用參數(shù)就能實(shí)現(xiàn)功能。
fabric.js 的自定義事件
fabric.js 中內(nèi)置了很多自定義事件,這些事件都是我們常用的,非原子事件。對(duì)于日常開發(fā)來說非常方便。
對(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 來進(jìn)行 Canvas 開發(fā)就能很快入門, 實(shí)際上 Canvas 開發(fā)并不難,難的是編程思想和方式的轉(zhuǎn)變。
幾個(gè)需要注意的地方
fabric.js 源碼沒有使用 ES 6,沒使用 Typescript,所以在看代碼的時(shí)候還是很不方便的,推薦使用 jetbrains 家的 IDE:IntelliJ IDEA 或 Webstorm 都是支持對(duì) ES 6 以下的 Javascript 代碼進(jìn)行 靜態(tài)分析的,可以使用跳轉(zhuǎn)到定義、調(diào)用層級(jí)等功能,看源代碼會(huì)很方便。
fabric.js 源碼中很多地方用到 Canvas 的 save() 和 restore() 方法,可以查看這個(gè)鏈接了解更多:查看。
如果你之前從來沒有接觸過 Canvas 開發(fā),那我建議去看看 bilibili 上蕭井陌錄的一節(jié)的關(guān)于入門游戲開發(fā)的視頻教程,不要一開始就去學(xué)習(xí) Canvas 的 API,先了解概念原理性的東西,最后再追求細(xì)節(jié)。
該文章在 2023/5/23 11:56:36 編輯過