Fabric.js - 輕而易舉操作canvas的神奇庫

阿古達木發表於2021-11-19

本文來自於Fabricjs中文版,這個站點是我經過社群允許後部署的一份國內版,希望招募同樣對fabricjs感興趣的人一起來翻譯建設,在每篇文章下面都有提PR的連結,只要使用github在寫編輯即可


image.png
今天 我要向你們介紹 Fabric.js — 一個能夠讓你輕而易舉操作canvas的神奇的庫. Fabric 不僅提供了一個虛擬canvas物件, 還有svg渲染器, 互動層, 還有一整套十分有用的工具. 這是一個完全開源的專案, MIT協議, 多年以來依靠許多貢獻者共同維護.

Fabric 開始與2010, 在經歷過原生canvas 繁瑣的API操作之後. 原作者就寫了一個可互動的編輯器 printio.ru — 允許使用者自定義外觀. 那時候只有flash app需要這種互動. 光陰似箭,經過一點點積累形成了現在的 Fabric.

讓我們進一步看一看!

為什麼選擇fabric?
現在的Canvas 支援我們去創造一些 充滿創造力 神奇的 圖形 但是它提供的api實在是 水平低到令人髮指. 如果我們只是想畫一些簡單的圖形. 但是卻需要一系列操作, 各種修改中心點, 如果要畫一個複雜圖形 — 那操作就“更有意思了”.

Fabric 的目標就是解決這些問題.

原生 canvas 方法 只允許我們使用一些簡單的圖形操作, 然後在畫布上瞎子摸象. 想畫一個矩形? 使用 fillRect(left, top, width, height). 想畫一條線? 用 moveTo(left, top) 和 lineTo(x, y)組合. 這感覺就像 用畫筆在畫布上畫畫, 隨著畫的越來越多, 畫布內容的可控性就越差.

為了避免這種低水平操作, Fabric 在基礎上提供了一個簡單且強大的物件模型. 更注重於畫布的狀態和渲染, 現在, 讓我們開始學習使用 “物件”吧.

讓我們通過一個簡單的例子 畫一個紅色的矩形 來看看兩者有什麼不同. 這是原生 <canvas> API的實現


// 獲取畫布的引用
var canvasEl = document.getElementById('c');

// 獲取2d context 物件 用來操作 (之前提到的bitmap)
var ctx = canvasEl.getContext('2d');

// 給當前上下文設定顏色
ctx.fillStyle = 'red';

// 在100, 100的位置建立一個20*20的矩形
ctx.fillRect(100, 100, 20, 20);
現在,讓我們看看fabricjs怎麼實現同樣的效果:

// 包裹一下canvas (with id="c")
var canvas = new fabric.Canvas('c');

// 新建一個矩形物件
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20
});

// 將矩形新增到canvas裡
canvas.add(rect);

image.png
目前為止, 最不同的地方在尺寸的設定 — 兩個例子很像. 但是你應該也意識到兩種操作思想的不同了吧. 用原生方法, 我們 操作context上下文 — 代表著整個canvas. 用 Fabric, 我們 在具體物件上操作 — 例項化它們, 修改他們的屬性, 然後給它們新增到canvase上. 這些物件是fabricjs世界的第一公民.

但是畫一個紅色的矩形沒啥難度. 我們來整點有意思的! 比如, 稍微的旋轉一下?

我們試試旋轉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');

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

image.png

發生了什麼?

我們只需要修改物件的's “angle” 為 45. 使用原生方法, 事情變得越來越 “有趣了”. 我們不能操作物件. 反而為了實現需求, 我們給整個 canvas bitmap旋轉了 (ctx.translate, ctx.rotate). 然後在畫矩形上去, 別忘了原點定位為 (-10, -10), 這樣才能看起來是在(100, 100).

現在我確信你已經清楚了fabricjs存在的意義,還有他幫助我門減少了多少低階程式碼.

讓我們再看另一個例子 — 追蹤canvas狀態.

假設在某一個點, 我們想要移動剛才的矩形到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();

image.png
注意最重要的區別. 用過Fabricjs, 我們不用再為了實現“移動”清除上一個畫布. 只需要操作物件, 簡單的修改它們的屬性, 然後 re-render canvas 獲取 “最新的畫面”.

物件
現在我們已經瞭解了怎麼操作 fabric.Rect 建構函式生成例項. 當然Fabric預設包含了許多基礎形狀 — 原, 三角, 橢圓, 等等. 這些都掛載在 fabric “變數下” 像 fabric.Circle, fabric.Triangle, fabric.Ellipse, 等等.

Fabric 提供的7中基礎圖形:

fabric.Circle 圓
fabric.Ellipse 橢圓
fabric.Line 線段
fabric.Polygon 多邊形
fabric.Polyline 折線
fabric.Rect 矩形
fabric.Triangle 三角形
想要畫一個圓? 只需要建立圓物件, 然後新增到canvas中. 和其他基礎圖形一樣:


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

image.png

..現在我們在100, 100有了一個綠色的原型, 50, 50有了一個藍色的三角形.

操縱 物件
建立幾何圖形 — 矩形, 圓, 或者其他 — 僅僅是開始. 以後, 我們可能需要修改這些物件. 也許是某些動作觸發的改變, 或播放某種動畫. 或者想要在滑鼠事件時修改物件某些屬性 (顏色, 透明度, 尺寸, 位置).

Fabric 替我們關心了渲染和狀態維護的事情. 我們只需要在物件上做手腳就行.

之前的例子展示了 set 方法 並且呼叫 set({ left: 20, top: 50 })從之前的位置 “移動”走了. 類似的方式, 我們可以修改任何屬性. 但是有啥屬性呢?

正如你期望的,與位置相關 — left, top; 尺寸相關 — width, height; 渲染相關 — fill, opacity, stroke, strokeWidth; 縮放和旋轉 — scaleX, scaleY, angle; 甚至有翻轉 — flipX, flipY 和傾斜 skewX, skewY

是的, 在fabric中建立翻轉物件 只需要給flip*屬性設定為 true.

你可通過 get method讀取所有屬性, 然後使用 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);

image.png

首先, 我們設定 “fill” 為 “red”, 將影像變成紅色的了. 下一行設定了 “畫筆寬度” 和 “畫筆顏色” 的值, 給矩形一個5px寬的淺綠色邊框. 最後, 我們修改 “angle” 和 “flipY” 屬性. 注意三行不同的設定語法,都支援.

這個例子展示了 set方法的通用性. 你以後會經常用到它, 所以這個方法儘量的支援各種使用方法.

講完了設定屬性, 那麼如果獲取呢? 只需要使用通用的 get 方法 , 當然還有各種特殊的 get* ,來獲取一個屬性. 想要獲取一個物件的“width”, 可以用 get('width') 或者 getWidth(). 想要獲取 “scaleX” 屬性 — get('scaleX') 或者 getScaleX(), 等等. 物件 “公共” 屬性都有 getWidth 或 getScaleX這種方法 (“stroke”, “strokeWidth”, “angle”, 等.)

你可能注意到了,在之前的例子裡 寫初始化配置生成的物件和使用 set 方法建立的沒什麼區別.這是因為他們就是完全一樣. 你可以在初始化的時候使用 “配置”, 或者在建立物件之後再使用 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 });

預設配置
到這裡, 你可能想問 — 不傳 “配置”建立物件發生了什麼. 還有那些屬性嗎?

當然有. Fabricjs中的物件一直會有預設屬性的. 如果在建立的時候省略, 就是使用預設值. 試試看:

var rect = new fabric.Rect(); // 沒有配置傳入

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). 因為都是0, 所以我們看不見. 只要給寬高附上正整數後,我們就能看在一個黑色矩形在畫布左上角.

image.png

Hierarchy and Inheritance
Fabric 物件並不是獨立存在的.他們都繼承自一個源物件

大多數物件都繼承自根物件 fabric.Object. fabric.Object 代表著一個二維,有著座標和寬高, 以及一系列其他圖形特徵. 這些就是之前看到的物件屬性 — fill, stroke, angle, opacity, flip*, 等.

使用繼承,可以讓我們在fabric.Object上定義方法,然後提供給所有子類. 比如, 你想給所有物件都加一個自定義 getAngleInRadians 方法, 你可以直接在 fabric.Object.prototype上定義


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.Circle 需要 “radius” 屬性. fabric.Image — 在下面會講到 — 需要 getElement/setElement 方法 用於訪問/設定HTML元素.
在高階專案中 使用原型來獲取自定義渲染和行為非常常見.

Canvas
現在已經詳細的說完了Fabricjs物件, 讓我門回過頭來說一下canvas.

在所有的Fabricjs例子中,你看到的第一行是不是建立canvas物件? — new fabric.Canvas('...'). fabric.Canvas 包裹著 <canvas> 元素, 它負責著所有Fabric.Object物件. 傳入一個id, 返回 fabric.Canvas 例項.

我們可以將物件 add 進去, 通過引用關係, 也可以刪除他們:


var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();

canvas.add(rect); // 新增進去

canvas.item(0); //  獲取剛才新增的fabric.Rect
canvas.getObjects(); // 獲取畫布中所有的物件

canvas.remove(rect); // 刪除fabric.Rect

所以 fabric.Canvas的主要作用就是管理物件, 它還可以寫一些 配置 . 想要給畫布設定背景? 對所有內容進行裁剪? 設定不同的寬/高? 是否可互動? 包括但不限於這些屬性可以傳給 fabric.Canvas, 同物件一樣,在任何時候都可以:

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(){ /* ... */ };
// ...

互動功能
我們現在來說一說互動功能. 一個獨特的Fabricjs功能 — 內建的 — 在物件層之上的互動層.

物件模型的存在允許以程式設計方式訪問和操作畫布上的物件. 但是對於使用者來說, 需要用滑鼠或者手指操作. 當你通過 new fabric.Canvas('...')初始化畫布之後, 可以選擇 拖動 旋轉 縮放等,甚至是 群選 之後一起操作!

image.pngimage.png

如果我們想讓用在在畫布上拖拽一些東西 — 比如一張圖片 — 我們只需要建立畫布,加一個圖片進去. 不需要任何額外的操作.

我們可以使用把布林值傳給 Fabric's “selection” 或者物件的穿一個布林值給物件的“selectable”欄位 來控制是否可互動.


var canvas = new fabric.Canvas('c');
...
canvas.selection = false; // 關閉群選
rect.set('selectable', false); // 單個物件不可選

如果不想要這個互動功能? 你可以使用fabric.StaticCanvas 代替 fabric.Canvas . 別的都一樣.


var staticCanvas = new fabric.StaticCanvas('c');

staticCanvas.add(
  new fabric.Rect({
    width: 10, height: 20,
    left: 100, top: 100,
    fill: 'yellow',
    angle: 30
  }));

這樣建立了一個 “輕量” 版本的 canvas, 沒有任何事件處理邏輯. 你還是可以操作 全部的物件模型 — 新增物件, 刪除或修改他們, 或者修改 canvas 配置 — 這些都還是一如既往能用. 只是事件系統沒了.

總之,如果你需要一個不需要互動的畫布,就要擇更輕量的 StaticCanvas 就夠了.

圖片
說到圖片…

在畫布上玩矩形圓形沒什麼意思,我們來試試玩圖片? 正如你認為的, Fabric 也讓這個變得簡單. 讓我們例項化一個 fabric.Image 物件然後把它新增進canvas:

(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 的例項. 並且, 我們立刻設定了圖片的座標、旋轉、透明度. 加入到畫布中後, 會看見一個圖片在100,100 的位置, 旋轉了30度, 還有輕微的透明度. 不錯吧

image.png

那麼, 如果文件中沒有這個圖片元素怎麼辦, 我們只是有一個url? 那麼就到了 fabric.Image.fromURL派上用場的時候了

fabric.Image.fromURL('my_image.png', function(oImg) {
canvas.add(oImg);
});
看起來是不是非常簡單直觀? 只是呼叫 fabric.Image.fromURL, 傳了url, 在圖片載入完成之後呼叫一下回撥函式. 回撥函式的第一個預設傳參就是 fabric.Image 物件. 在這時候, 你就可以像之前一樣操作修改屬性了, 然後加入到畫布中:

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

路徑
我們先了解了簡單的圖形, 然後是圖片. 下面看看更復雜的圖形和內容

首先看一對強力組合 — 路徑 和 分組.

在Fabric中,路徑代表著一個可以背修改,填充,描邊的形狀. 路徑是由一堆命令組成的, 本質上是在模仿一支筆從一個點到另一個點. 通過 “move”, “line”, “curve”, 或者 “arc”命令, 可以組成神奇的圖案. 藉助路徑的分組功能Paths (PathGroup's), 讓使用者發揮想象的空間就更大了.

Fabric中的Paths 與 SVG <path> elements很像. 使用相同的語法, 所以可以互相轉化. 稍後我們將更仔細地研究序列化和 SVG 解析, 先提醒你一下,你後幾乎不會手動建立Path例項. 而你會經常使用 Fabric's 內建的 SVG 渲染器. 但是為了理解Path,我們先嚐試手動建立一個簡單的:


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

image.png

我們例項化了一個 fabric.Path 物件, 給它傳了一串字串路徑指令. 雖然看起來神祕, 但是它其實很容易理解. “M” 代表 “move” 命令, 命令那隻不可見的筆指在0,0的位置. “L” 代表著 “line” 用筆畫到200,100的位置. 然後, 另一個 “L” 畫了一條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);

image.png
出於好奇, 讓我們試一試稍微複雜一點的圖形. 你會發現我之前說的對,沒有辦法手寫路徑.


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

“M” 還是代表著 “move”, 所以畫筆從 “121.32, 0” 開始. 然後 “L” 代表這畫一條直線搭到 “44.58, 0”. 目前位置還能接受. “C” 命令, 代表著 “三次貝塞爾曲線”. 命令畫筆從當前位置到 “36.67, 0”畫一條三次貝塞爾曲線. 開始控制點為“29.5, 3.22”,結束控制點為 “24.31, 8.41”. 然後再跟上一堆的貝塞爾曲線, 最終形成了這個好看的箭頭.

image.png

通常來說, 你不會直接這麼“粗暴”的使用, 你可能會用到 fabric.loadSVGFromString 或 fabric.loadSVGFromURL 這類方法來載入SVG檔案, 把這些工作全部交給Fabric.

說到整個 SVG 文件, Fabric的path 代表著 SVG <path> 元素, SVG 文件中經常出現的路徑集合, 在Fabric中就是組的概念 (fabric.Group 例項). 正如你想到的, 組不過是一組路徑和其他物件的集合. 並且fabric.Group 繼承自 fabric.Object, 所以它的新增、修改行為和其他物件一樣.

就像使用路徑一樣,您可能不會直接使用它們. 但是一旦你需要,你應該知道它的原理.

後記
我們只是介紹了Fabric一些基礎的東西. 你現在可以輕鬆的在canvas上操作簡單的複雜的圖形,圖片了。 — 位置, 尺寸, 旋轉, 顏色, 邊框, 透明度.

該系列的下一章, 我們會講組; 動畫; 文班; SVG 解析, 渲染, 序列化; 事件; 圖片濾鏡等等.

與此同時, 去看看 示例 或者 基礎資料 或者 別的地方, 或者直接看 文件, wiki, 和 原始碼.

希望Fabric能給你帶來樂趣.

閱讀 Part 2.

相關文章