熟悉 canvas 的朋友想必都使用或者據說過 Fabric.js,Fabric 算是一個元老級的 canvas 庫了,從第一個版本發佈到如今,已經有 8 年時間了。我近一年時間也在項目中使用,做爲用戶簡單說說感覺:javascript
優缺點都很鮮明,但總的來講,若是你要作一個在線編輯類的項目,好比在線 PPT,在線製圖等應用,fabric 絕對是個很好的選擇。html
那麼這一系列文章要寫什麼?這裏不會主要介紹如何使用 fabric,主要寫的內容是把在閱讀源碼過程當中,把涉及到原理相關的知識總結出來,好比相關圖形學知識、canvas 相關、fabric 中的設計思想等的相關知識。因此,若是你如今還對 fabric 不是很瞭解,建議先去官網找幾個 demo 試一下。java
下面咱們進入此次的正題,這篇文章主要介紹 fabric.canvas 涉及到的部份內容。git
fabric 建立畫布很簡單:github
const canvas = new fabric.Canvas("domId", options);
在這樣一行代碼背後,fabric 主要作了下面這幾件事情:算法
下面我把相關內容一一闡述。canvas
介紹 canvas 緩存,fabric 中的緩存也是相似的道理,簡單來講,就是使用一個離屏 canvas 來作預渲染,在真實畫布上用 drawImage 代替直接繪製圖形。緩存
咱們先來看個 例子,你們能夠把 FPS meter 打開,切換按鈕能夠看到,不使用緩存和使用緩存 FPS 值差距仍是挺大的,我電腦在使用緩存的時候基本在 60fps,不使用會降到 15fps 左右。你們能夠打開控制檯或者在 這裏 查看代碼。
下面列出主要的代碼片斷:dom
class Ball { constructor(x, y, vx, vy, useCache = true) { // ... if (useCache) { this.useCache = useCache; this.cacheCanvas = document.createElement("canvas"); // 離屏 canvas 寬高取要渲染圖形的寬高,不能夠取真實 canvas 的寬高,不然會渲染大量無用區域 this.cacheCanvas.width = 2 * (this.r + BORDER_WIDTH); this.cacheCanvas.height = 2 * (this.r + BORDER_WIDTH); this.cacheCtx = this.cacheCanvas.getContext("2d"); this.cache(); } } paint() { // 使用緩存直接使用建立的離屏canvas,不然直接繪製圖形 if (!this.useCache) { ctx.save(); ctx.lineWidth = BORDER_WIDTH; ctx.beginPath(); ctx.strokeStyle = this.color; ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI); ctx.stroke(); ctx.restore(); } else { ctx.drawImage( this.cacheCanvas, this.x - this.r, this.y - this.r, this.cacheCanvas.width, this.cacheCanvas.height ); } } move() { // ... } cache() { // 繪製圖形 this.cacheCtx.save(); this.cacheCtx.lineWidth = BORDER_WIDTH; this.cacheCtx.beginPath(); this.cacheCtx.strokeStyle = this.color; this.cacheCtx.arc( this.r + BORDER_WIDTH, this.r + BORDER_WIDTH, this.r, 0, 2 * Math.PI ); this.cacheCtx.stroke(); this.cacheCtx.restore(); } }
解釋一下兩者區別:函數
drawImage
將離屏的 canvas 渲染。使用緩存的時候,有一點須要注意的是要控制好離屏 canvas 的大小,不能夠直接取和渲染 canvas 的實際寬高,不然會渲染不少無用的空間,好比上面例子中每一個離屏 canvas 的寬高只須要和對應圖形的寬高一致。
this.cacheCanvas.width = 2 * (this.r + BORDER_WIDTH); this.cacheCanvas.height = 2 * (this.r + BORDER_WIDTH);
上述代碼中主要節省時間的地方在 paint
函數中使用 drawImage
會比直接繪製圖形節省時間,那麼是否全部場景都是這樣呢?咱們再來看下面這個 例子.
這個例子和上面的只有繪製圖形的代碼不一樣:
// 從複雜圖形變成了簡單圖形 cache() { this.cacheCtx.save(); this.cacheCtx.lineWidth = BORDER_WIDTH; this.cacheCtx.beginPath(); this.cacheCtx.strokeStyle = this.color; this.cacheCtx.arc( this.r + BORDER_WIDTH, this.r + BORDER_WIDTH, this.r, 0, 2 * Math.PI ); this.cacheCtx.stroke(); this.cacheCtx.restore(); }
只是cache
方法中把複雜圖形變成了簡單的圖形。但實際效果相差甚遠,使用緩存和不使用性能差距並不大,甚至不使用時 fps 值還更高一些。
因此看來圖形的複雜度,直接會影響 canvas 緩存的效果,咱們在開發過程當中,也不能盲目引入緩存,要權衡利弊。fabric 中緩存是默認開啓的,同時也能夠設置 objectCaching
爲 false 禁用。
若是你們細心的話應該會發現,當咱們執行new fabric.Canvas('domeId')
的時候,在頁面上 dom 元素就改變了,fabric 複製了一層 canvas 蓋在了咱們定義的 canvas 上面:
fabric 這樣設計將渲染層和交互層作了分離,lower-canvas 只負責渲染元素;全部的交互,好比框選,事件處理都在 upper-canvas 上。
順便提一下,fabric 提供了渲染靜態畫布的方法,若是你的畫布不須要任何交互,只用來展現,那麼能夠用new fabric.StaticCanvas('domId', options)
來初始化,這時候 dom 結構中就只有一個 canvas,沒有 upper-canvas 了。
說到這裏,不少同窗可能會想到,事件是怎樣綁定的呢?其實兩個 canvas 大小等屬性都是一致的,因此座標也是能夠對應上的,好比在 upper-canvas 上某個位置點擊了一下,那麼就能夠去 lower-canvas 上就能夠用這個座標去找是否點擊到了一個元素,那麼問題來了,如何判斷一個點在一個圖形中呢?
這個問題網上有個比較廣泛的方案,就是經過畫一條射線,經過交點奇偶性來判斷。以下圖:
而 fabric 中並無用這種方法,緣由很簡單,這個算法是有前提的:發出的射線不能與圖形任何頂點相交。 這個前提對於咱們主觀來判斷是很簡單的,但程序中處理可能就須要大量的代碼去判斷是否與交點相交,若是相交再從新生成一條射線。
fabric 中使用的算法對上述算法進行了改進,咱們結合下圖來解釋:
其中 e1 ~ e5 分別爲多邊形的邊,P 爲目標點,黑色實心點爲多邊形的頂點,r 爲 P 延 X 軸發出的射線(不一樣於上面的方法,這裏咱們約定 r 射線只能延 X 軸發出)。
intersectionCount = 0
遍歷多邊形的全部邊,設邊的頂點爲 p1, p2
intersectionCount
加 1intersectionCount
爲奇數,則在圖形內,反之則在圖形外。判斷的部分用代碼實現相似:
// point 目標點,lines多邊形的全部邊 function checkPoint(point, lines) { let intersectionCount = 0; let { x, y } = point; for (let i = 0; i < lines.length; i++) { let line = lines[i]; // 兩個頂點 let { p1, p2 } = line; if ((p1.y < y && p2.y < y) || (p1.y >= y && p2.y >= y)) { continue; } else { const sx = ((y - p1.y) / (p2.y - p1.y)) * (p2.x - p1.x) + p1.x; if (sx >= x) { intersectionCount++; } } } return intersectionCount % 2 === 0; }
Retina 屏幕模糊的問題,直接給出處理方法,就不展開說了。
代碼:
function initRetina(canvas, ctx) { const dpi = window.devicePixelRatio; canvas.style.width = canvas.width + "px"; canvas.style.height = canvas.height + "px"; canvas.setAttribute("width", canvas.width * dpi); canvas.setAttribute("height", canvas.height * dpi); ctx.scale(dpi, dpi); }
本篇文章主要針對fabric.canvas
模塊,介紹了相關 canvas 緩存,fabric 中判斷點在圖形中的算法以及如何處理 retina 屏幕的知識,做爲系列的第一篇文章,可能會有不少問題,若有錯誤及意見,歡迎批評指正。
參考文獻:
http://idav.ucdavis.edu/~okre...
http://www.geog.ubc.ca/course...
https://www.cnblogs.com/axes/...
http://fabricjs.com/docs/