今天我開始像您介紹Fabric,一個功能強大的Javascript庫,運行在HTML5 canvas上,F(xiàn)abric為畫布提供了一個缺失的對象模型,以及一個SVG解析器,一個交互層,以及一整套其他必不可少的工具。它是一個完全開放源碼的項目,在MIT許可,多年來做出了許多貢獻。
三年前我開始開發(fā)Fabric,在發(fā)現(xiàn)使用原生canvas的API之后,我正在為printio.ru創(chuàng)建一個互動設計編輯器:我的創(chuàng)業(yè)公司允許用戶設計自己的服裝。在這些日子里,我們想要的那種交互性只存在于Flash應用中。而現(xiàn)在,"Fabric"成為可能。
讓我們來看看吧!
為什么要做fabric
Canvas可以讓我們在網(wǎng)絡上創(chuàng)造出絕對驚人的圖形。但它提供的API是令人失望的。如果我們只想在畫布上畫幾條基本的形狀,不會覺得有什么繁瑣。但是一旦需要任何形式的互動,任何時候改變圖片或繪制更復雜的形狀,代碼復雜度會急劇增加。
Fabric旨在解決這個問題。
原生canvas方法只允許我們觸發(fā)簡單的圖形命令,盲目的修改canvas的位圖,想畫一個矩形?使用fillRect(left, top, width, height).,想畫一條線?使用moveTo(left, top) 和 lineTo(x, y)的組合命令,就好像我們用刷子畫畫,上層涂上越來越多的顏料,幾乎沒有控制。
Fabric不是在這么低的層次上運行,而是在原生方法之上提供簡單而強大的對象模型。它負責畫布狀態(tài)和渲染,并讓我們直接使用繪制后的“對象”。
讓我們來看一個簡單的例子來說明這個差異。假設我們想在畫布上畫一個紅色的矩形。以下是我們如何使用原生的<canvas> API。
// 有一個id是c的canvas元素
var canvasEl = document.getElementById('c');
// 獲取2d位圖模型
var ctx = canvasEl.getContext('2d');
// 設置填充顏色
ctx.fillStyle = 'red';
// 創(chuàng)建一個坐標100,190,尺寸是20,20的矩形
ctx.fillRect(100, 100, 20, 20);
現(xiàn)在使用Fabric做同樣的事情:
// 用原生canvas元素創(chuàng)建一個fabric實例
var canvas = new fabric.Canvas('c');
// 創(chuàng)建一個矩形對象
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 20,
height: 20
});
// 將矩形添加到canvas畫布上
canvas.add(rect);

在這種情況下,這兩個例子非常相似,大小幾乎沒有差別。但是,您可以看到使用canvas的方法有多么不同。使用原生方法,我們在上下文中操作(表示整個畫布位圖的對象),在Fabric中,我們操作對象,實例化它們,更改其屬性,并將其添加到畫布。你可以看到這些對象是Fabric中的第一等公民。
但渲染純正的紅色矩形就如此無聊。我們至少可以做一些有趣的事情!也許,稍稍旋轉?
旋轉45度,首先使用原生的canvas方法:
var canvasEl = document.getElementById('c');
var ctx = canvasEl.getContext('2d');
ctx.fillStyle = 'red';
ctx.translate(100, 100);
ctx.rotate(Math.PI / 180 * 45);
ctx.fillRect(-10, -10, 20, 20);
使用Fabric:
var canvas = new fabric.Canvas('c');
// 創(chuàng)建一個45度的矩形
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 20,
height: 20,
angle: 45
});
canvas.add(rect);

這里發(fā)生了什么?
我們在Fabric中所做的一切都是將對象的“角度”值更改為45。然而使用原生的方法,事情變得更加有趣,記住我們無法對對象進行操作,相反,我們調整整個畫布位圖(ctx.translate,ctx.rotate)的位置和角度,以適應我們的需要。然后,我們再次繪制矩形,但記住要正確地偏移位圖(-10,-10),所以它仍然呈現(xiàn)在100,100點。作為練習,我們不得不在旋轉畫布位圖時將度數(shù)轉換為弧度。
我相信你剛剛開始明白為什么面料存在,以及它解決了多少低級寫法。
如果在某些時候,我們想將現(xiàn)在熟悉的紅色矩形移動到畫布上稍微不同的位置怎么辦?我們如何在無法操作對象的情況下執(zhí)行此操作?我們會在canvas位圖上調用另一個fillRect嗎?
不完全的。調用另一個fillRect命令實際上在畫布上繪制的東西所有之上繪制矩形。還記得我前邊說的用刷子畫畫嗎?為了“移動”它,我們需要先擦除以前繪制的內容,然后在新的位置繪制矩形。
var canvasEl = document.getElementById('c');
...
ctx.strokRect(100, 100, 20, 20);
...
// 擦除整個畫布
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);
我們如何用Fabric完成這個?
var canvas = new fabric.Canvas('c');
...
canvas.add(rect);
...
rect.set({ left: 20, top: 50 });
canvas.renderAll();

注意一個非常重要的區(qū)別。使用Fabric,在嘗試“修改”任何內容之前,我們不再需要擦除內容。我們仍然使用對象,只需更改其屬性,然后重新繪制畫布即可獲得“最新圖片”。
Fabric中的對象
我們已經(jīng)看到如何通過實例化fabric.Rect構造函數(shù)來處理矩形。當然,F(xiàn)abric也涵蓋了所有其他的基本形狀:圓,三角形,橢圓等。所有的這些在就是Fabric“命名空間”下的:fabric.Circle,fabric.Triangle,fabric.Ellipse等。
Fabric提供了7種基礎形狀:
想畫一個圈子?只需創(chuàng)建一個圓形對象,并將其添加到畫布。與任何其他基本形狀相同:
var circle = new fabric.Circle({
radius: 20, fill: 'green', left: 100, top: 100
});
var triangle = new fabric.Triangle({
width: 20, height: 30, fill: 'blue', left: 50, top: 50
});
canvas.add(circle, triangle);

這是一個綠色的圓形在100,100的位置,藍色的三角形在50,50的位置。
操作對象
創(chuàng)建圖形對象矩形,圓形或其他東西,當然只是開始。在某些時候,我們可能想修改這些對象?;蛟S某些行為需要觸發(fā)狀態(tài)的變化,或進行某種動畫?;蛘呶覀兛赡芟M谀承┦髽私换ブ懈膶ο髮傩裕伾煌该鞫?,大小,位置)。
Fabric為我們處理畫布渲染和狀態(tài)管理。我們只需要自己修改對象。
以前的例子演示了set方法,以及如何從對象前一個位置調用set({left:20,top:50})來移動對象。以類似的方式,我們可以改變對象的任何其他屬性。但這些屬性是什么?
那么,正如你所期望的那樣,可以改變定位:top,left。尺寸:width,height。渲染: fill, opacity, stroke, strokeWidth??s放和旋轉:scaleX, scaleY, angle;甚至可以翻轉:flipX,flipY。歪斜:skewX,skewY。
是的,在Fabric中翻轉對象非常簡單,將flip[X|Y]設置為true即可。
您可以通過get方法讀取任何這些屬性,并通過set進行設置。我們嘗試改變一些紅色矩形的屬性:
var canvas = new fabric.Canvas('c');
...
canvas.add(rect);
rect.set('fill', 'red');
rect.set({ strokeWidth: 5, stroke: 'rgba(100,200,200,0.5)' });
rect.set('angle', 15).set('flipY', true);

首先,我們將“fill”值設置為“red”,使對象成為紅色。下一個語句設置“strokeWidth”和“stroke”值,給出矩形為淡綠色的5px筆畫。最后,我們正在改變“angle”和“flipY”的屬性。注意每個3個語句中的每個語句使用的語法略有不同。
這表明set是一種通用的方法。你可能經(jīng)常使用它,所以它被設計得盡可能的方便。
說了如何set的方法,那么如何獲取呢?這就有了與之對應的get方法,h還有一些具體的get*,要讀取對象的“width”值,可以使用get('width')或getWidth()。獲取“scaleX”值使用get('scaleX')或getScaleX(),等等。對于每個“公有”對象屬性(“stroke”,“strokeWidth”,“angle”等),都可以使用getWidth或getScaleX等方法。
您可能會注意到,在早期的示例中,對象在實例化的時候,我們直接傳入的配置參數(shù),而上邊的例子我們才實例化對象的時候沒有傳入配置,而是使用的set方法傳遞配置。這是因為它是完全一樣的。您可以在創(chuàng)建時“配置”對象,也可以使用set方法:
var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });
// 或者這樣
var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });
默認選項
在這一點上,您可能會問,當我們創(chuàng)建對象而不傳遞任何“配置”對象時會發(fā)生什么。它還有這些屬性嗎?
當然是。 Fabric中的對象總是具有默認的屬性集。當在創(chuàng)建過程中發(fā)生變化時,這是對給定的“給定”的默認屬性集。我們可以自己試試看看:
var rect = new fabric.Rect(); // 注意沒有傳遞參數(shù)
rect.get('width'); // 0
rect.get('height'); // 0
rect.get('left'); // 0
rect.get('top'); // 0
rect.get('fill'); // rgb(0,0,0)
rect.get('stroke'); // null
rect.get('opacity'); // 1
我們的矩形有一個默認的屬性集。它位于0,0,是黑色,完全不透明,沒有描邊,沒有尺寸(寬度和高度為0)。由于沒有尺寸,我們無法在畫布上看到它。但是給它寬度/高度的任何正值肯定會在畫布的左上角顯示一個黑色矩形。

層次和繼承
Fabric對象不僅彼此獨立存在。它們形成一個非常精確的層次。
大多數(shù)對象從根fabric.Object繼承。fabric.Object幾乎代表二維形狀,位于二維canvas平面,它是一個具有l(wèi)eft / top和width / height屬性的實體,以及一些其他圖形特征。我們在物體上看到的那些屬性:fill,stroke,angle,opacity,flip[X|Y]等,對于從fabric.Object繼承的所有Fabric對象都是通用的。
這個繼承允許我們在fabric.Object上定義方法,并在所有的“類”之間共享它們。例如,如果您想在所有對象上使用getAngleInRadians方法,您只需在fabric.Object.prototype上創(chuàng)建它即可:
fabric.Object.prototype.getAngleInRadians = function() {
return this.get('angle') / 180 * Math.PI;
};
var rect = new fabric.Rect({ angle: 45 });
rect.getAngleInRadians(); // 0.785...
var circle = new fabric.Circle({ angle: 30, radius: 10 });
circle.getAngleInRadians(); // 0.523...
circle instanceof fabric.Circle; // true
circle instanceof fabric.Object; // true
您可以看到,方法立即在所有實例上可用。
雖然子類“class”繼承自fabric.Object,但它們通常也定義自己的方法和屬性。例如,fabric.Circle需要有“radius”屬性。而Fabric.Image(我們稍后會看),需要使用getElement / setElement方法來訪問/設置圖像實例的真實HTML<img>元素。
使用原型來獲取自定義渲染和行為對于高級項目來說是非常普遍的。
Canvas
現(xiàn)在我們更詳細地討論了對象,讓我們回到canvas。
首先你可以看到所有的Fabric例子如果創(chuàng)建canvas對象:new fabric.Canvas('...')。fabric.Canvas作為圍繞<canvas>元素的包裝器,并負責管理該canvas上的所有Fabric對象。它需要一個元素的id,并返回一個fabric.Canvas的實例。
我們可以add對象,引用它們,或者remove它們:
var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();
canvas.add(rect); // 添加對象
canvas.item(0); // 源于剛剛添加的第一個矩形對象
canvas.getObjects(); // 獲取所有對象(只有一個矩形)
canvas.remove(rect); // 移除這個矩形
管理對象是Fabric的主要用途。其實它本事也是可配置的,需要為整個畫布設置背景顏色或圖像?將所有內容剪切到某個區(qū)域?設置不同的寬度/高度?指定畫布是否互動?所有這些選項(和其他)可以在fabric.Canvas上設置,無論是在創(chuàng)建時還是之后:
var canvas = new fabric.Canvas('c', {
backgroundColor: 'rgb(100,100,200)',
selectionColor: 'blue',
selectionLineWidth: 2
// ...
});
// 或者
var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage('http://...');
canvas.onFpsUpdate = function(){ /* ... */ };
// ...
互動
雖然我們是一個canvas元素的主題,我們來談談互動。Fabric的獨特功能之一,在我們剛剛看到的所有的對象模型之上,是一層交互性。
存在對象模型以允許編程訪問和操縱畫布上的對象。但在外部,在用戶層面上,有一種方式可以通過鼠標(或觸摸,觸摸設備)來操縱這些對象。一旦您通過new fabric.Canvas('...')初始化畫布,可以選擇對象,拖動它們,縮放或旋轉它們,甚至組合在一起操縱一個組合!


如果我們希望用戶允許在畫布上拖動某些東西,比如一個圖片,我們需要做的就是初始化畫布并在其上添加一個對象。不需要額外的配置或設置。
為了控制這種交互性,我們可以在畫布上使用Fabric的“selection”布爾屬性,結合各個對象的“selectable”布爾屬性來使用。
var canvas = new fabric.Canvas('c');
...
canvas.selection = false; // 禁止所有選中
rect.set('selectable', false); // 只是禁止這個矩形選中
但是如果你不想要這樣的互動層呢?如果是這樣,您可以隨時用fabric.StaticCanvas替換fabric.Canvas。初始化的語法是相同的;初始化時使用StaticCanvas而不是Canvas。
var staticCanvas = new fabric.StaticCanvas('c');
staticCanvas.add(
new fabric.Rect({
width: 10, height: 20,
left: 100, top: 100,
fill: 'yellow',
angle: 30
}));
這將創(chuàng)建一個“更輕”的畫布版本,沒有任何事件處理邏輯。請注意,您仍然有一個完整的對象模型來使用:添加對象,刪除或修改它們,以及更改任何畫布配置,所有這一切仍然有效。這只是事件處理沒有了。
當我們?yōu)g覽自定義構建選項時,您會看到,如果StaticCanvas是您需要的選項,您甚至可以創(chuàng)建一個較輕的Fabric版本。如果您需要非交互式圖表,或者在應用程序中使用過濾器的非交互式圖像,這可能是一個不錯的選擇。
圖像
說到圖像...
在畫布上添加矩形和圓圈很有趣,但為什么我們不玩某些圖像?正如你現(xiàn)在想象的那樣,F(xiàn)abric使這個很容易。我們來實例化fabric.Image對象并將其添加到畫布中:
<canvas id="c"></canvas>
<img src="my_image.png" id="my-image">
var canvas = new fabric.Canvas('c');
var imgElement = document.getElementById('my-image');
var imgInstance = new fabric.Image(imgElement, {
left: 100,
top: 100,
angle: 30,
opacity: 0.85
});
canvas.add(imgInstance);
注意我們如何將圖像元素傳遞給fabric.Image構造函數(shù)。這將創(chuàng)建一個fabric.Image的實例,就像文檔中的圖像一樣。此外,我們立即將左/頂值設置為100/100,角度為30,不透明度為0.85。一旦添加到畫布中,圖像呈現(xiàn)在100,100位置,30度角,并且稍微透明!不錯。

現(xiàn)在,如果我們在文檔中真的沒有圖像,而只是一個圖像的URL呢?我們來看看如何使用fabric.Image.fromURL:
fabric.Image.fromURL('my_image.png', function(oImg) {
canvas.add(oImg);
});
看起來很簡單,不是嗎?只需調用具有圖像URL的fabric.Image.fromURL,并在加載和創(chuàng)建圖像后給它一個回調函數(shù)來調用?;卣{函數(shù)接收已經(jīng)創(chuàng)建的fabric.Image對象作為第一個參數(shù)。此時,您可以將其添加到畫布中,也可以先更改,然后添加到畫布:
fabric.Image.fromURL('my_image.png', function(oImg) {
// 將其縮小,然后將其翻轉,然后再將其添加到畫布上
oImg.scale(0.5).set('flipX, true);
canvas.add(oImg);
});
路徑(Paths)
我們已經(jīng)看過簡單的形狀,然后看了圖像。那么更復雜,豐富的形狀和內容呢?
認識更強大的搭檔:路徑(Path)和組合(Group)
Fabric中的path表示可以填充,描邊和修改的形狀的輪廓。path由一系列命令組成,基本上模仿了從一個點到另一個點的筆。借助“move”,“l(fā)ine”,“curve”或“arc”等命令,path可以形成令人難以置信的復雜形狀。在Paths(路徑組合)團隊的幫助下,可能性更大。
Fabric中的路徑與SVG <path>元素非常相似。它們使用相同的命令,可以從<path>元素創(chuàng)建,并將其序列化。稍后我們將更加仔細地觀察序列化和SVG解析,但現(xiàn)在值得一提的是,您很可能很少手動創(chuàng)建Path實例,相反,您將使用Fabric的內置SVG解析器。但是要了解Path對象是什么,我們來嘗試用手創(chuàng)建一個簡單的對象:
var canvas = new fabric.Canvas('c');
var path = new fabric.Path('M 0 0 L 200 100 L 170 200 z');
path.set({ left: 120, top: 120 });
canvas.add(path);

我們通過傳遞一串路徑指令,實例化fabric.Path對象,雖然看起來很神秘,但實際上很容易理解?!癕”代表“move”命令,并告訴筆移動到0,0坐標?!癓”表示“l(fā)ine”,筆畫線為200,100坐標。然后,另一個“L”創(chuàng)建一個連接170,200坐標的線段。最后,“z”指示繪畫筆關閉當前路徑并完成形狀。結果,我們得到一個三角形。
由于fabric.Path與Fabric中的任何其他對象一樣,我們還可以更改其某些屬性。但是我們可以修改更多:
...
var path = new fabric.Path('M 0 0 L 300 100 L 200 300 z');
...
path.set({ fill: 'red', stroke: 'green', opacity: 0.5 });
canvas.add(path);

出于好奇,我們來看一下稍微更復雜的path語法。你會看到為什么手動創(chuàng)建路徑可能不是最好的主意。
...
var path = new fabric.Path('M121.32,0L44.58,0C36.67,0,29.5,3.22,24.31,8.41\
c-5.19,5.19-8.41,12.37-8.41,20.28c0,15.82,12.87,28.69,28.69,28.69c0,0,4.4,\
0,7.48,0C36.66,72.78,8.4,101.04,8.4,101.04C2.98,106.45,0,113.66,0,121.32\
c0,7.66,2.98,14.87,8.4,20.29l0,0c5.42,5.42,12.62,8.4,20.28,8.4c7.66,0,14.87\
-2.98,20.29-8.4c0,0,28.26-28.25,43.66-43.66c0,3.08,0,7.48,0,7.48c0,15.82,\
12.87,28.69,28.69,28.69c7.66,0,14.87-2.99,20.29-8.4c5.42-5.42,8.4-12.62,8.4\
-20.28l0-76.74c0-7.66-2.98-14.87-8.4-20.29C136.19,2.98,128.98,0,121.32,0z');
canvas.add(path.set({ left: 100, top: 200 }));
看一下這里發(fā)生了什么?
那么“M”仍然代表“move”的命令,所以筆在“121.32,0”點開始繪畫。然后有“L”命令使其為“44.58,0”。到現(xiàn)在為止還可以理解。下一步是什么? “C”指令,代表“cubic bezier”(貝塞爾曲線)。它使筆從當前點繪制貝塞爾曲線到“36.67,0”,它以“29.5,3.22”為起點的控制點,“24.31,8.41”為終點的控制點。這整個事情之后是十幾個其他的“cubic bezier”命令,最終創(chuàng)建一個漂亮的箭頭形狀。

不過你可能永遠不會使用這樣復雜的命令,相反,您可能需要使用像fabric.loadSVGFromString或fabric.loadSVGFromURL方法來加載整個SVG文件,然后讓Fabric的SVG解析器完成對所有SVG元素的遍歷和創(chuàng)建相應的Path對象的工作。
談到整個SVG文件,而Fabric的路徑通常表示SVG<path>元素,SVG文檔中通常存在的路徑集合表示為Group(fabric.Group實例)。你可以想像,Group(組合)只不過是一組Path實例和其他對象。而且由于fabric.Group從fabric.Object繼承,它可以像任何其他對象一樣添加到畫布中,并以相同的方式進行操作。
就像Path一樣,你可能不會直接的使用它。但是,如果您在解析SVG文檔之后偶然發(fā)現(xiàn)了一個問題,那么您將確切地知道它是什么以及它的目的是什么。
后記
我們只觸及了Fabric的表面。你現(xiàn)在可以很容易地創(chuàng)建任何一個簡單的形狀,復雜的形狀,圖像;將它們添加到畫布中,并以任何你想要的方式進行修改:位置、尺寸、角度、顏色、筆畫、不透明度等。
在本系列的下一部分,我們將看看組合,動畫,文本,SVG解析,渲染,序列化,事件,圖像,濾鏡等。
與此同時,你可以隨意查看帶注釋的演示或基準,加入Google小組或其他地方的討論,或者直接訪問docs、wiki和源代碼。
做一些有趣的實驗!我希望你喜歡這次旅行。
下一章:Fabric.js介紹 第二部分