:-: **介紹一**
[TOC]

`Fabric.js`是一款可以更簡便的操作`HTML5 canvas`畫布的強大的`javascript`庫;`Fabric`提供了對象模型,`svg`解析器,交互層以及一整套完整的工具組件.基于`MIT`協議完全開源,每年都有很多開發者參與貢獻.
### Why?
首先,`Canvas`現在允許我們創造強大的圖形界面.但是`API`可能比較差強人意.我們很容易忘記它們,即便是要畫一些基本的形狀,更何況各種交互,更換圖片或者更復雜的形狀.
`Fabric`的目的就是解決這些問題.
原生的`canvas`方法只允許我們觸發簡單的圖形命令,然后盲目地修改整個畫布;
取代了如此`low`的方法,`Fabric`在原生基礎上提供了簡單且強大的對象模型.它負責畫布的狀態和渲染,而我們直接面向對象.
下面是一些示例:
> 使用canvas API
```js
// reference canvas element (with id="c")
var canvasEl = document.getElementById('c');
// get 2d context to draw on (the "bitmap" mentioned earlier)
var ctx = canvasEl.getContext('2d');
// set fill color of context
ctx.fillStyle = 'red';
// create rectangle at a 100,100 point, with 20x20 dimensions
ctx.fillRect(100, 100, 20, 20);
```
> 使用`Fabric.js`
```js
// create a wrapper around native canvas element (with id="c")
var canvas = new fabric.Canvas('c');
// create a rectangle object
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 20,
height: 20
});
// "add" rectangle onto canvas
canvas.add(rect);
```
> 效果圖

在這點上,從代碼量上看幾乎沒有差別-上述兩個例子基本相同。但是你已經發現了在使用`canvas`上的差別了。原生方法中,我們操作代表整個`canvas`位圖的對象`context`,而在`fabric`中我們操作對象去實例化它們,改變它們的屬性,然后將它們添加到`canvas`中去。由此可見,`fabric`是面向對象的。
上述渲染一個紅色的矩形略顯無聊,下面我們來進行一些有趣的操作,比方說輕微的旋轉一下?
> 先看原生的方法
```js
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`
```js
var canvas = new fabric.Canvas('c');
// create a rectangle with angle=45
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 20,
height: 20,
angle: 45
});
canvas.add(rect);
```

我們在`fabric`中只需要改變`angle`到45度即可,使用原生的方法,事情反而不是那么有趣了。首先我們得記住不能直接操作對象,我們通過操作`canvas`的上下文對象來改變位置,調整角度以滿足我們的需求。接著我們畫矩形,記住偏移位置,讓其可以在準確的點渲染出來。額外地,我們還得將旋轉度數轉為弧度。
下面我們看下其他的例子,如何跟蹤記錄`canvas`的狀態。
如果現在我們要將熟悉的紅色矩形移動到`canvas`上的另外一個位置,該怎么辦呢?我們如何不操作對象來實現呢?我們是在畫布上再調用一個`fillRect`方法嗎?
不完全正確!重新調用另一個`fillRect`命令其實是在原先的矩形上再繪制一個矩形。就像畫畫一樣,為了移動它,我們需要先擦掉之前畫的矩形,再在一個新的位置畫一個矩形。
```js
var canvasEl = document.getElementById('c');
...
ctx.strokRect(100, 100, 20, 20);
...
// erase entire canvas area
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);
```
> `fabric`怎么做的呢?
```js
var canvas = new fabric.Canvas('c');
...
canvas.add(rect);
...
rect.set({ left: 20, top: 50 });
canvas.renderAll();
```
注意到這個變化,在使用`fabric`的時候我們不再需要在我們嘗試修改任何內容的時候,先擦拭掉內容。我們仍然只需要改變對象的屬性達到刷新視圖的目的。
### Objects
我們已經知道了如何實例化`fabric.Rect`構造函數來畫矩形了。`fabric`自然也提供了其他基礎形狀的構造器了--圓,三角,橢圓等等。它們都是通過`fabric`下的命名空間導出的,例如`fabric.Circle`,`fabric.Triangle`,`fabric.Ellipse`。
`Fabric`提供了7種基本圖形的接口
* `farbic.Circle`
* `fabric.Ellipse`
* `fabric.Line`
* `farbic.Polygon`
* `fabric.Polyline`
* `fabric.Rect`
* `fabric.Triangle`
下面給些示例:
```js
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);
```

#### 對象操作
以上還僅僅只是個開始,事實上,我們更希望能夠修改這些對象。可能是一些狀態改變的響應,或者是一些不同的動畫。或者我們想在某些鼠標事件下改變對象的屬性(顏色,透明度,尺寸,位置)。
`Fabric`幫助我們解決了畫布的繪制和狀態管理,我們僅僅需要操作這些對象即可。
在此之前,我們使用`set`方法證明了,如何通過方法`set({left:20,top:50})`來移動一個對象。類似的,我們還可以改變對象的其他屬性。那些屬性呢?
顯而易見,你也可以猜到,定位屬性——`top,left`,尺寸屬性——`width,height`,繪制屬性——`fill,opacity,stroke,strokeWidth`,縮放和旋轉屬性——`scaleX,scaleY,angle`,還有翻轉屬性`flipX,flipY`,彎曲屬性——`skewX,skewY`。
是的,創建翻轉元素在`Fabric`中就是如此容易,只需要設置`flip*`屬性為`true`。你可以通過`set`來設置這些屬性,通過`get`來獲取這些屬性。下面看個栗子:
```js
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`的值。
但請注意上面三行代碼之間細微的語法差別。
`set`方法是我們常用的方法,這樣定義是非常方便使用的。
與之對應,`get`方法也是獲取屬性值最方便的方法了。例如`get('width')或getWidth()`。
當然我們也可以之間定義個全量的對象,賦值為`set`方法
```js
var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });
// or functionally identical
var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });
```
#### 默認選項
也許你可能問了,加入我們不給對象設置任何構造參數,這些屬性還有嗎?
當然了。所有的`Fabric`對象都設有默認屬性。當創建對象過程中發生遺漏,就會有默認的屬性賦值給對象。我們自己可以嘗試一下:
```js
var rect = new fabric.Rect(); // notice no options passed in
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`對象并不是互相獨立存在的。它們之間形成了非常明確的層次關系。
絕大多數對象都是繼承自根對象`fabricj.Object`。`fabirc.Object`代表一個二維的形狀,呈現在二維的`canvas`的畫布上。它是一個具有寬高,偏移還有一堆其他圖形屬性的實體。我們在上面見到的對象的這些屬性都是繼承來自`fabric.Object`。
這中繼承關系,允許我們在`fabirc.Object`上定義方法,共享與`fabric`所有的類。舉個栗子,假如你希望所有的對象都有一個`getAngleInRadians`方法,你只需要在`fabric.Object.prototype`的原型上創建即可。
```js
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
```
如你所見,方法立刻就可以在所有實例上運行啦。
盡管子類繼承自`fabric.Object`,但他們也有自己的屬性和方法。舉例來說,`fabric.Circle`需要有半徑(`radius`)屬性,`fabric.Image`需要`setElement/getElement`方法來講設置源自`HTML img`標簽中的圖片。
使用原型獲取自定義渲染和行為對于牛逼項目是非常普遍的。
### 畫布`Canvas`
我們已經了解了對象的詳細信息,現在讓我們回歸`canvas`.
在所有`fabric`的栗子中,創建對象之前你首先看到的是`new fabric.Canvas('...')`,`fabric.Canvas`給`canvas`元素提供一個包裹容器,它的主要職責就是管理指定畫布上的`fabric`對象.它需要元素的`id`返回`fabric.Canvas`的一個實例.
我們可以向里面添加對象,從中引用他們.甚至刪除他們:
```js
var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();
canvas.add(rect); // add object
canvas.item(0); // reference fabric.Rect added earlier (first object)
canvas.getObjects(); // get all objects on canvas (rect will be first and only)
canvas.remove(rect); // remove previously-added fabric.Rect
```
盡管管理對象是`farbric.Canvas`的唯一宗旨,但是它還可以當作主要配置項.需要在整個`canvas`上設置一個背景色或圖片?將所有內容剪輯到指定區域?設置不同的寬高?指定畫布是不是可交互的?所有的這些選項(還有其余的)都可以設置在`fabric.Canvas`上,可以在創建對象時,也可以在此之后:
```js
var canvas = new fabric.Canvas('c', {
backgroundColor: 'rgb(100,100,200)',
selectionColor: 'blue',
selectionLineWidth: 2
// ...
});
// or
var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage('http://...');
canvas.onFpsUpdate = function(){ /* ... */ };
```
#### 交互
既然我們討論到了`canvas`元素了,那就讓我們講講交互吧.`Fabricj`中一個特性就是我們看到的,在對象模型上有一個互動層.
對象模型允許對畫布對象進行編程訪問和操作.在外面,在用戶級別上,可以通過鼠標(觸摸,觸摸設備)來操作對象.一旦你通過`new fabric.Canvas(..)`初始化畫布之后,你就可能選擇對象,來回拖拽,旋轉,縮放他們,設置將它們分到一個操作組.

如果我們想要允許用戶拖拽畫布上的東西,比如說圖片,我們需要做的僅僅是初始化畫布,然后將對象添加到畫布上.不許其他額外的配置和安裝.
但是如果我們不想要這樣的交互呢?那這樣的話,你可以使用`fabric.StaticCanvas`替代`fabric.Canvas`.
初始化語法完全相同,你只需要使用`StaticCanvas`替換`Cavnas`.
```js
var staticCanvas = new fabric.StaticCanvas('c');
staticCanvas.add(
new fabric.Rect({
width: 10, height: 20,
left: 100, top: 100,
fill: 'yellow',
angle: 30
}));
```
這樣就創建了一個輕量級的畫布,沒有任何多余的操作邏輯.需要注意的是你仍然擁有完成的對象模型,包括添加,刪除,修改對象和修改畫布的配置,這些仍然有效.僅僅只是事件操縱沒了.
稍后,當我們自定義構建選項時,您會看到,如果`StaticCanvas`是您所需要的,那么就可以創建一個較輕的`Fabric`版本。如果您需要諸如非交互式圖表之類的東西,或者在應用程序中使用濾鏡的非交互式圖像,這可能是一個不錯的選擇。
### 圖片`Images`
說到圖片..........
在畫布上添加矩形和圓很有趣,但為什么我們沒有用到一些圖片呢?如你所想,`Fabric`讓這變得比較簡單.
讓我們使用`fabric.Image`初始化圖片對象,然后添加到畫布中.
```html
<canvas id="c"></canvas>
<img src="my_image.png" id="my-image">
```
```js
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`構造器.這個`fabric.Image`的實例看上去就像是文檔中的圖片.而且,我們立即設置了`left/top`值,角度值以及透明度值.一旦添加到畫布,圖片就被渲染在`(100,100)`的位置,順時針旋轉30度,輕微的透明度.不錯!

現在我們文檔中并沒有圖片,只有一個圖片的`url`鏈接,咋整?不是問題,讓我們看看`fabric.Image.fromURL`:
```js
fabric.Image.fromURL('my_image.png', function(oImg) {
canvas.add(oImg);
});
```
看起來是不是很直接?僅僅是通過`fabric.Image.fromURL`加載圖片的`URL`,然后當圖片加載完成時,給一個回調函數.這個回調函數接收已經創建完成的`farbic.Image`對象當做第一個參數.在這里,你可以直接將它添加到畫布上,也可以對其做一些修改然后再添加到畫布上.
```js
fabric.Image.fromURL('my_image.png', function(oImg) {
// scale image down, and flip it, before adding it onto canvas
oImg.scale(0.5).set('flipX, true);
canvas.add(oImg);
});
```
### 路徑`Paths`
我們已經了解了簡單的形狀和圖片.那么更復雜和豐富的形狀呢?
讓我們看看這強大的一對---`Path和Groups`.
`Fabric`中的路徑表示可以通過其他方法填充修改的輪廓形狀.路徑是由一系列從一點到另一點的指令組成.通過這些指令,例如`move`,`line`,`curve`,`arc`等等,路徑可以繪制出令人難以想象的復雜形狀.再加上`PathGroup`的幫助,還可以有更多的擴展.
`Fabric`中路徑類似`svg`元素.他們使用相同的一組命令,可以從`<path>`元素創建它們,并將它們序列化.稍后我們會更深入地觀察序列化和SVG解析,但是現在值得一提的是,您很可能很少手工創建`Path`實例.相反,您將使用`Fabric`的內置SVG解析器。但是為了理解什么是Path對象,讓我們嘗試手工創建一個簡單的對象:
```js
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);
```

我們在實例化時,傳入了一個路徑參數的字符串.雖然看起來神兮兮的,但還是蠻好理解的.`M`是`move`命令,告訴那支無形的畫布移動到`0,0`的位置.`L`是`Line`命令,讓畫筆畫一條線到`200,100`,然后另一個`L`命令,是使畫筆畫到`170,200`,`z`命令是強制關閉當前的路徑,完成圖形的繪制.結果我們獲得是一個三角形.
既然`fabric.Path`跟其他`fabric`對象看起很像,所以我們可以改變一些它的屬性:
```js
...
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);
```

```js
...
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 }));
```

好了,現在我們加載`SVG`元素可以使用`fabric.loadSVGFromString`or`fabric.loadSVGFromURL`.
### 后話