JS前端輕量fabric.js系列之畫佈初始化
前言
從這個章節開始我們就步入正題瞭,那一開始要做啥子呢,回憶下上個章節中 fabric.js 的使用過程,先是創建畫佈,再添加物體,然後開始動畫和交互。顯然畫佈是一切物體的開端🚀,所以首先要搞定的就是它,也就是 const canvas = new fabric.Canvas('canvas') 這一步要做的事情。
畫佈的前置知識
在說 fabric.js 如何初始化畫佈之前,先鞏固下畫佈的相關知識點。創建畫佈要做的事情通常比較簡單,就是單純的獲取畫佈(或動態創建畫佈)並重新設置畫佈寬高,就像下面這個樣子:
const canvas = document.getElementById('canvas') || document.createElement('canvas'); const width = canvas.width; const height = canvas.height; canvas.style.width = width + 'px'; canvas.style.height = height + 'px';
為什麼要重新設置寬高,這是個很容易混淆的點。看看下面的代碼👇🏻:
#canvas { width: 200px; height: 100px; } <canvas id="canvas" width="100" height="100"></canvas>
可以看到上面的 canvas 有兩個寬高大小,一個是 canvas 上的屬性值,一個是 css 的樣式值,那應該以哪個為準呢🤔?
我們可以先拋棄 css 大小的概念,請記住:所有的繪圖操作都是在 canvas 這個畫佈大小上進行的,就上面的代碼來說不論你繪制什麼東西,都是在 100*100
的畫佈中進行的,當你在 canvas 繪制完所有東西之後要在頁面上某個區域渲染瞭,才和 css 大小有關,就上面的例子來說就是你要把 100*100
的 canvas 畫佈放到頁面上 200*100
的區域,但是它們大小不一致要怎麼處理呢?
你可以把 canvas 繪制的內容想象成一張大小固定的照片,把 css 大小想象成一個容器,不管 css 尺寸如何,這張照片都會鋪滿整個容器(機制就是這樣,沒有為啥😒)。所以如果長寬比例相同就會等比縮放;
如果長寬比例不同就會拉伸變形;如果大小一樣就剛剛好。就我們的例子來說 100*100
的繪制內容水平方向會被拉伸成 200*100
,就產生瞭變形,因此通常情況下需要把 canvas 和 css 設置成一樣大,確保不拉伸變形,看下面的示意圖能幫你加深理解:
另外還有一個常見問題就是設備像素比(devicePixelRatio)的影響,如果不處理在高清屏上就會導致模糊(比如 Mac 電腦),大傢應該有看過類似問題的文章,但大多都是各種名詞詞匯,看完就忘的那種。
關於這個問題我在另一篇文章🔥關於 canvas 模糊的問題(高清圖解)有解釋過,有需要的可以去看下,這裡就簡單介紹下(溫馨提示:實在不好記可以跳過這一趴😬,因為它並不妨礙我們進行接下來的開發)。我們知道畫的東西最終是要展現在屏幕上的,而屏幕又是由很多小格子構成的,通常情況下:
- 如果 dpr = 1,就說明 1px 對應屏幕上的 1 個小格子(亦即 1 個 css 像素對應 1 個物理像素)
如果 dpr = 2,就說明 1px 對應屏幕上的 2 個小格子(亦即 1 個 css 像素對應 1 個物理像素) 順便看下圖解👇🏻:
圖沒看懂😂?那就來看看文字解說:假設我們現在 canvas 和 css 的大小都是 10 * 10,那麼 canvas 畫完的照片中就會有 100 個(像素)點,也就是隻有 100 個點的信息;但是到瞭高清屏中(如 dpr = 2),我們需要 400 個點的信息,原來的點不夠用怎麼辦?
於是就會有一套算法來自動生成這些點的信息,從而造成瞭模糊。那應該怎麼辦呢🤔?我們需要更多的點,所以可以這樣子搞,把畫佈放大 dpr 倍,也就是把 canvas 的寬高都乘以 dpr(css 的大小還是不變的),接下來的繪制都是在寬為width*dpr
、高為 height*dpr
的畫佈大小上進行的,這樣一折騰,點就變多瞭。
但是要註意什麼呢,畫佈變大瞭,相應的繪制操作(畫圓、畫矩形等)也需要相應放大,這個我會在最後一章加上這個功能,一開始有個印象就行,不然容易犯暈🤯。
畫佈初始化
在 fabric.js 中我們總共會創建兩個畫佈,一個是上層畫佈(upper-canvas),一個是下層畫佈(lower-canvas),兩個畫佈是一樣大的,還有一個外層 div 將這兩個 canvas 包起來。
- 上層畫佈主要用於處理一些交互事件,比如鼠標事件、塗鴉模式(畫板)、左鍵拖拽產生的框選區域等;
- 下層畫佈則單純的用於繪制所有物體,簡單粗暴的遍歷所有物體進行繪制,沒有其他多餘的操作。
如果通過上層畫佈的交互後,某些物體的某些屬性值被改變瞭,這時候就會清空下層畫佈,重新繪制所有物體,兩層畫佈各司其職,典型的數據驅動視圖。
除瞭職責分明還有一點點單向數據流的味道,上層的交互改變瞭數據,數據的改變傳到下層畫佈,下層畫佈就單純的重新繪制;
但是反過來,下層畫佈並不會影響上層畫佈也不會影響數據,這樣問題排查起來也方便些。相信大傢都用過 vue2,如果我們要修改 props 中的值,就需要用 $emit 把數據傳出去,修改父元素的值才行;
但如果 props 是個對象,我們其實可以在子元素中直接修改 props 的屬性值,雖然方便但不是很好的寫法,關系就亂瞭,如果你有踩過這個坑的話。
扯遠瞭,回過頭來,實際上 fabric.js 一共創建瞭三層畫佈,還有一個是 cacheCanvasEl,我們就把它叫做緩沖層畫佈吧,它和另外兩個畫佈一樣大,但並沒有在頁面中顯示,所以也可以叫離屏 canvas,它主要用來提供一個臨時繪制環境,以便不時之需,後面章節會說道它的用途,這裡先知道有這麼個東西就行。
順便給些示例代碼,簡單瞟一瞟就行:
/** 畫佈類 */ class Canvas { /** 畫佈寬度 */ public width: number; /** 畫佈高度 */ public height: number; /** 包圍 canvas 的外層 div 容器 */ public wrapperEl: HTMLElement; /** 下層 canvas 畫佈,主要用於繪制所有物體 */ public lowerCanvasEl: HTMLCanvasElement; /** 上層 canvas,主要用於監聽鼠標事件、塗鴉模式、左鍵點擊拖藍框選區域 */ public upperCanvasEl: HTMLCanvasElement; /** 緩沖層畫佈 */ public cacheCanvasEl: HTMLCanvasElement; /** 上層畫佈環境 */ public contextTop: CanvasRenderingContext2D; /** 下層畫佈環境 */ public contextContainer: CanvasRenderingContext2D; /** 緩沖層畫佈環境 */ public contextCache: CanvasRenderingContext2D; /** 整個畫佈到上面和左邊的偏移量 */ private _offset: Offset; /** 畫佈中所有添加的物體 */ private _objects: FabricObject[]; constructor(el: HTMLCanvasElement, options) { // 初始化下層畫佈 lower-canvas this._initStatic(el, options); // 初始化上層畫佈 upper-canvas this._initInteractive(); // 初始化緩沖層畫佈 this._createCacheCanvas(); } // 下層畫佈初始化:參數賦值、重置寬高,並賦予樣式 _initStatic(el: HTMLCanvasElement, options) { this.lowerCanvasEl = el; Util.addClass(this.lowerCanvasEl, 'lower-canvas'); this._applyCanvasStyle(this.lowerCanvasEl); this.contextContainer = this.lowerCanvasEl.getContext('2d'); for (let prop in options) { this[prop] = options[prop]; } this.width = +this.lowerCanvasEl.width; this.height = +this.lowerCanvasEl.height; this.lowerCanvasEl.style.width = this.width + 'px'; this.lowerCanvasEl.style.height = this.height + 'px'; } // 其餘兩個畫佈同理 }
上面的代碼簡單用到瞭 Util 這個工具類,裡面主要就是封裝一些獨立的、常用的方法,大部分都比較簡單,下面簡單的列舉幾種:
const PiBy180 = Math.PI / 180; // 寫在這裡相當於緩存,因為會頻繁調用 class Util { /** 單純的創建一個新的 canvas 元素 */ static createCanvasElement() { const canvas = document.createElement('canvas'); return canvas; } /** 角度轉弧度,註意 canvas 中用的都是弧度,但是角度對我們來說比較直觀 */ static degreesToRadians(degrees: number): number { return degrees * PiBy180; } /** 弧度轉角度,註意 canvas 中用的都是弧度,但是角度對我們來說比較直觀 */ static radiansToDegrees(radians: number): number { return radians / PiBy180; } /** 從數組中溢出某個元素 */ static removeFromArray(array: any[], value: any) { let idx = array.indexOf(value); if (idx !== -1) { array.splice(idx, 1); } return array; } static clone(obj) { if (!obj || typeof obj !== 'object') return obj; let temp = new obj.constructor(); for (let key in obj) { if (!obj[key] || typeof obj[key] !== 'object') { temp[key] = obj[key]; } else { temp[key] = Util.clone(obj[key]); } } return temp; } static loadImage(url, options: any = {}) { return new Promise(function (resolve, reject) { let img = document.createElement('img'); let done = () => { img.onload = img.onerror = null; resolve(img); }; if (url) { img.onload = done; img.onerror = () => { reject(new Error('Error loading ' + img.src)); }; options && options.crossOrigin && (img.crossOrigin = options.crossOrigin); img.src = url; } else { done(); } }); } }
諸如此類,大傢可以自己去看下 Util 這個工具類,後面就不再贅述瞭,當然有些比較麻煩點的方法(比如 animate 和一些計算)可以先跳過,後面的用到的時候會再展開。
變換練習
同樣的這個章節內容不多也不難,所以這裡先為下一篇文章(物體基類)做一些熱身練習,講一些變換的基礎內容,也就是 transform(translate、rotate、scale),功能和 css 的 transform 類似。
以繪制一個紅色矩形為例 ctx.fillRect(0, 0, 50, 50)
,讓我們看看這幾個東西分別會產生什麼影響:
translate 的影響
rotate 的影響
scale 的影響
這裡對 scale 做一些補充,scale 的結果是對坐標系做瞭縮放,但是理解起來不是很直觀,所以你可以認為 scale 其實是對坐標軸的刻度做瞭縮放,比如本來畫佈的一段固定長度代表 50,scale(2, 2) 之後,同樣的固定長度就隻能代表 25,所以還需要再來一個固定長度才能表示 50,視覺上就是放大的效果。
好瞭,以上這幾種變換的結果本質都是對坐標系的變換,translate 改變瞭坐標系原點的位置,rotate 將坐標系進行瞭旋轉,scale 則將坐標軸的刻度進行瞭縮放,而畫佈的視窗大小(也就是上面圖中的 canvas 框)是不變的(可以想象成一個鏡頭),我們並不會改動到畫佈的寬高,不要混淆瞭。
單個內容的變換還是比較好理解的,但是混在一起就會有點變扭瞭,比如要畫下面這樣一個圖形(兩個箭頭和等邊三角形):
大傢可以用這三種變換畫一下上面的圖形,能畫出來應該就有點感覺瞭(這些變換效果是會累加的哦)。建議多動手練練,因為下個章節會用上。
小結
這裡是本章的知識點小結,記住這些就可以瞭:
- 我們共創建瞭三個 canvas,每個 canvas 都是一樣大的,但功能各不相同
- 邏輯和繪制是分離的,上層畫佈用來改邏輯和改數據,下層畫佈則用來繪制
- 原點始終都是在畫佈左上角,x 軸水平向右為正,y 軸豎直向下為正😂 然後這裡還是先給個簡版 fabric.js 的代碼鏈接吧,有需要的可以參考看看,會隨著文章更新不斷完善。好啦,今天的分享就到這裡,有什麼問題歡迎點贊評論留言,我們下期再見,拜拜 🏎 💨
實現一個輕量 fabric.js 系列一(概覽)💥
以上就是JS前端輕量fabric.js系列之畫佈初始化的詳細內容,更多關於fabric.js畫佈初始化的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- JS前端輕量fabric.js系列物體基類
- JS前端以輕量fabric.js實現示例理解canvas
- 前端canvas中物體邊框和控制點的實現示例
- JS前端使用canvas實現物體的點選示例
- 如何用vue實現網頁截圖你知道嗎