Fabricjs 是基於 HTML5 Canvas 來撰寫的 Library,能讓我們更簡單、更方便的來操作 Canvas,在學習 Fabricjs 之前,我們就先來了解看看使用原生 Canvas api 和使用 Fabricjs 操作上有什麼差異吧。
這篇筆記可能會使用到一些 ES6 語法,有用到的相關連結我都會貼到最下面讓大家參考。
相關程式碼會放上 codepen 方便大家瀏覽和體驗 Fabricjs。
註:
- fabricjs 使用的版本是 2.4.1
- 使用 babel 編譯 es6 語法
讓我們先來建立一個矩形
// 原生 canvas 建立矩形
nativeCtx.rect(10,10,30,30)
nativeCtx.fill()
// 透過 fabric.Rect 新增一個 fabric Rect Object
const rect = new fabric.Rect({
top: 10,
left: 10,
width: 30,
height: 30
})
// 加入到 canvas 中
fabricCanvas.add(rect)
我們這樣看下來, 若只單純新增一個矩形,原生 Canvas 看起來是比較輕鬆的。
而使用 Fabricjs 雖然看起來行數比較多,但因為是透過新增物件方式來建立一個矩形,標明了每個屬性的名稱和給的數值,在閱讀程式上比較直覺一些。
再來移動一下我們剛剛新增的矩形
nativeCtx.clearRect(0, 0, nativeCanvas.width, nativeCanvas.height)
nativeCtx.beginPath()
nativeCtx.rect(50,50,30,30)
nativeCtx.fill()
rect.set({
top: 50,
left: 50
})
canvas.renderAll()
我們看到兩者差異,原生 Canvas 直接就是重畫了一個新的矩形。
而 Fabricjs 則是使用了我們剛剛 new 出來的 rect 物件的 set 方法直接改完位置後,Fabricjs 會自動重整一次畫布。
接下來我們做旋轉矩形
// 儲存一下我們初始狀態,因為要來移動整個畫布
nativeCtx.save()
// 清掉畫布
nativeCtx.clearRect(0, 0, nativeCanvas.width, nativeCanvas.height)
nativeCtx.beginPath()
// 改變一下中心位置
nativeCtx.translate(30 + 15, 30 + 15)
// 旋轉一下
nativeCtx.rotate(45 * Math.PI / 180)
// 再畫一次
nativeCtx.rect(-15,-15,30,30)
nativeCtx.fill()
nativeCtx.restore()
// 再次使用剛剛我們 new 出來的 rect 物件
rect.rotate(45)
fabricCanvas.renderAll()
看到使用原生 Canvas 時,也是先清空畫布,再去做一些座標計算和旋轉,最後在畫上一個矩形。
**註:這邊用 save & restore 因為若是今天要操控多個矩形,我們還要把整個畫布中心移來移去。
nativeCtx.save()
nativeCtx.translate(30 + 15, 30 + 15)
nativeCtx.restore()
通常會用 save 和 restore 來記錄和往返畫布沒有更動過的狀態
這邊我們若使用 Fabricjs 可以很簡單地透過操控我們剛剛所 new 出來的物件去旋轉。
原生 canvas | Fabricjs | |
---|---|---|
可讀性 | 較低且需要自己包裝 | win |
自由度 | win | 較低但也是有能控制原生的方法 |
便利性 | win |
我們能夠透過 Fabricjs 來很方便地透過修改物件的方式,來更新我們畫布上的圖形。這在修改大量圖形上十分方便,不需要自己直接接觸原生 Canvas 操作。
但別忘了 Fabricjs 也是以 canvas 為基礎,來提供的函式庫,只是它包裝了更物件化的寫法,使用起來更加直覺、簡單。
請問為什麼Fabric
要用render重新整理一次畫布
圖不是已經畫上去了嗎?
Hi, 因為當我們對 Fabric 畫布做數據更新 (etc 新增物件、變更物件屬性...)時,Fabric 畫布實際上是還沒有畫上去的。
但為什麼我們新增物件時畫布就自動更新了呢?
這是因為 Fabric 的 Canvas 有一個屬性 renderOnAddRemove
為一個 Boolean 型態,預設為 true
。
字面上的意思也就是當新增或刪除就會 render 一次畫布。
可以試著把這個參數設定成 false
,我們就可以每次都使用 renderAll
來自己 render 圖形囉!
const canvas = new fabric.Canvas('canvas',
{
...,
renderOnAddRemove: false
})
canvas.add(rect)
canvas.renderAll()
rect.rotate(45)
window.setTimeout(() => canvas.renderAll(), 5000)
Codepen 玩玩看
最後總結一下 Fabricjs 對物件繪製的流程
既然預設是renderOnAddRemove = true
新增或刪除他都會自動幫我們render一次畫布
那我們為什麼要自己再打上renderAll()呢?
主要是因為在操作 set() 和 rotate() 這些非新增或刪除的操作時,物件不會更新!
當 renderOnAddRemove = true 時
再看看下面這段程式不使用 canvas.renderAll()
為什麼還是可以讓 rect 旋轉。canvas.add(rect)
rect.rotate(45)
這邊是因為 fabricjs 再處理 canvas.add()
時,其實是將 rect 的 data,丟進 objects
裡面,過一會兒才做刷新畫布。
重點其實在刷新畫布的部分,fabircjs 是使用 requestAnimationFrame
來做更新。
所以事實上畫面不會馬上就更新,而是先執行了 rect.rotate(45)
,rect 物件在 objects 中的 rotate 資訊已經被改成 45。
requestAnimationFrame 綁定的 function 被執行時,抓出 Objects 的物件已經是被旋轉過的 rect
。
這邊如果使用我們在一秒後再做 rect.rotate
就會發現 rect 旋轉沒有被更新,就需要在呼叫一次 canvas.renderAll()
。
canvas.add(rect)
window.setTimeout(() => rect.rotate(45), 1000)
相關參考:
fabricjs doc:
Canvas.add 原始碼