JS前端輕量fabric.js系列之畫(huà)布初始化
前言
從這個(gè)章節(jié)開(kāi)始我們就步入正題了,那一開(kāi)始要做啥子呢,回憶下上個(gè)章節(jié)中 fabric.js 的使用過(guò)程,先是創(chuàng)建畫(huà)布,再添加物體,然后開(kāi)始動(dòng)畫(huà)和交互。顯然畫(huà)布是一切物體的開(kāi)端??,所以首先要搞定的就是它,也就是 const canvas = new fabric.Canvas('canvas') 這一步要做的事情。
畫(huà)布的前置知識(shí)
在說(shuō) fabric.js 如何初始化畫(huà)布之前,先鞏固下畫(huà)布的相關(guān)知識(shí)點(diǎn)。創(chuàng)建畫(huà)布要做的事情通常比較簡(jiǎn)單,就是單純的獲取畫(huà)布(或動(dòng)態(tài)創(chuàng)建畫(huà)布)并重新設(shè)置畫(huà)布寬高,就像下面這個(gè)樣子:
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';
為什么要重新設(shè)置寬高,這是個(gè)很容易混淆的點(diǎn)??纯聪旅娴拇a????:
#canvas { width: 200px; height: 100px; } <canvas id="canvas" width="100" height="100"></canvas>
可以看到上面的 canvas 有兩個(gè)寬高大小,一個(gè)是 canvas 上的屬性值,一個(gè)是 css 的樣式值,那應(yīng)該以哪個(gè)為準(zhǔn)呢???
我們可以先拋棄 css 大小的概念,請(qǐng)記?。核械睦L圖操作都是在 canvas 這個(gè)畫(huà)布大小上進(jìn)行的,就上面的代碼來(lái)說(shuō)不論你繪制什么東西,都是在 100*100
的畫(huà)布中進(jìn)行的,當(dāng)你在 canvas 繪制完所有東西之后要在頁(yè)面上某個(gè)區(qū)域渲染了,才和 css 大小有關(guān),就上面的例子來(lái)說(shuō)就是你要把 100*100
的 canvas 畫(huà)布放到頁(yè)面上 200*100
的區(qū)域,但是它們大小不一致要怎么處理呢?
你可以把 canvas 繪制的內(nèi)容想象成一張大小固定的照片,把 css 大小想象成一個(gè)容器,不管 css 尺寸如何,這張照片都會(huì)鋪滿整個(gè)容器(機(jī)制就是這樣,沒(méi)有為啥??)。所以如果長(zhǎng)寬比例相同就會(huì)等比縮放;
如果長(zhǎng)寬比例不同就會(huì)拉伸變形;如果大小一樣就剛剛好。就我們的例子來(lái)說(shuō) 100*100
的繪制內(nèi)容水平方向會(huì)被拉伸成 200*100
,就產(chǎn)生了變形,因此通常情況下需要把 canvas 和 css 設(shè)置成一樣大,確保不拉伸變形,看下面的示意圖能幫你加深理解:
另外還有一個(gè)常見(jiàn)問(wèn)題就是設(shè)備像素比(devicePixelRatio)的影響,如果不處理在高清屏上就會(huì)導(dǎo)致模糊(比如 Mac 電腦),大家應(yīng)該有看過(guò)類(lèi)似問(wèn)題的文章,但大多都是各種名詞詞匯,看完就忘的那種。
關(guān)于這個(gè)問(wèn)題我在另一篇文章??關(guān)于 canvas 模糊的問(wèn)題(高清圖解)有解釋過(guò),有需要的可以去看下,這里就簡(jiǎn)單介紹下(溫馨提示:實(shí)在不好記可以跳過(guò)這一趴??,因?yàn)樗⒉环恋K我們進(jìn)行接下來(lái)的開(kāi)發(fā))。我們知道畫(huà)的東西最終是要展現(xiàn)在屏幕上的,而屏幕又是由很多小格子構(gòu)成的,通常情況下:
- 如果 dpr = 1,就說(shuō)明 1px 對(duì)應(yīng)屏幕上的 1 個(gè)小格子(亦即 1 個(gè) css 像素對(duì)應(yīng) 1 個(gè)物理像素)
如果 dpr = 2,就說(shuō)明 1px 對(duì)應(yīng)屏幕上的 2 個(gè)小格子(亦即 1 個(gè) css 像素對(duì)應(yīng) 1 個(gè)物理像素) 順便看下圖解????:
圖沒(méi)看懂???那就來(lái)看看文字解說(shuō):假設(shè)我們現(xiàn)在 canvas 和 css 的大小都是 10 * 10,那么 canvas 畫(huà)完的照片中就會(huì)有 100 個(gè)(像素)點(diǎn),也就是只有 100 個(gè)點(diǎn)的信息;但是到了高清屏中(如 dpr = 2),我們需要 400 個(gè)點(diǎn)的信息,原來(lái)的點(diǎn)不夠用怎么辦?
于是就會(huì)有一套算法來(lái)自動(dòng)生成這些點(diǎn)的信息,從而造成了模糊。那應(yīng)該怎么辦呢???我們需要更多的點(diǎn),所以可以這樣子搞,把畫(huà)布放大 dpr 倍,也就是把 canvas 的寬高都乘以 dpr(css 的大小還是不變的),接下來(lái)的繪制都是在寬為width*dpr
、高為 height*dpr
的畫(huà)布大小上進(jìn)行的,這樣一折騰,點(diǎn)就變多了。
但是要注意什么呢,畫(huà)布變大了,相應(yīng)的繪制操作(畫(huà)圓、畫(huà)矩形等)也需要相應(yīng)放大,這個(gè)我會(huì)在最后一章加上這個(gè)功能,一開(kāi)始有個(gè)印象就行,不然容易犯暈??。
畫(huà)布初始化
在 fabric.js 中我們總共會(huì)創(chuàng)建兩個(gè)畫(huà)布,一個(gè)是上層畫(huà)布(upper-canvas),一個(gè)是下層畫(huà)布(lower-canvas),兩個(gè)畫(huà)布是一樣大的,還有一個(gè)外層 div 將這兩個(gè) canvas 包起來(lái)。
- 上層畫(huà)布主要用于處理一些交互事件,比如鼠標(biāo)事件、涂鴉模式(畫(huà)板)、左鍵拖拽產(chǎn)生的框選區(qū)域等;
- 下層畫(huà)布則單純的用于繪制所有物體,簡(jiǎn)單粗暴的遍歷所有物體進(jìn)行繪制,沒(méi)有其他多余的操作。
如果通過(guò)上層畫(huà)布的交互后,某些物體的某些屬性值被改變了,這時(shí)候就會(huì)清空下層畫(huà)布,重新繪制所有物體,兩層畫(huà)布各司其職,典型的數(shù)據(jù)驅(qū)動(dòng)視圖。
除了職責(zé)分明還有一點(diǎn)點(diǎn)單向數(shù)據(jù)流的味道,上層的交互改變了數(shù)據(jù),數(shù)據(jù)的改變傳到下層畫(huà)布,下層畫(huà)布就單純的重新繪制;
但是反過(guò)來(lái),下層畫(huà)布并不會(huì)影響上層畫(huà)布也不會(huì)影響數(shù)據(jù),這樣問(wèn)題排查起來(lái)也方便些。相信大家都用過(guò) vue2,如果我們要修改 props 中的值,就需要用 $emit 把數(shù)據(jù)傳出去,修改父元素的值才行;
但如果 props 是個(gè)對(duì)象,我們其實(shí)可以在子元素中直接修改 props 的屬性值,雖然方便但不是很好的寫(xiě)法,關(guān)系就亂了,如果你有踩過(guò)這個(gè)坑的話。
扯遠(yuǎn)了,回過(guò)頭來(lái),實(shí)際上 fabric.js 一共創(chuàng)建了三層畫(huà)布,還有一個(gè)是 cacheCanvasEl,我們就把它叫做緩沖層畫(huà)布吧,它和另外兩個(gè)畫(huà)布一樣大,但并沒(méi)有在頁(yè)面中顯示,所以也可以叫離屏 canvas,它主要用來(lái)提供一個(gè)臨時(shí)繪制環(huán)境,以便不時(shí)之需,后面章節(jié)會(huì)說(shuō)道它的用途,這里先知道有這么個(gè)東西就行。
順便給些示例代碼,簡(jiǎn)單瞟一瞟就行:
/** 畫(huà)布類(lèi) */ class Canvas { /** 畫(huà)布寬度 */ public width: number; /** 畫(huà)布高度 */ public height: number; /** 包圍 canvas 的外層 div 容器 */ public wrapperEl: HTMLElement; /** 下層 canvas 畫(huà)布,主要用于繪制所有物體 */ public lowerCanvasEl: HTMLCanvasElement; /** 上層 canvas,主要用于監(jiān)聽(tīng)鼠標(biāo)事件、涂鴉模式、左鍵點(diǎn)擊拖藍(lán)框選區(qū)域 */ public upperCanvasEl: HTMLCanvasElement; /** 緩沖層畫(huà)布 */ public cacheCanvasEl: HTMLCanvasElement; /** 上層畫(huà)布環(huán)境 */ public contextTop: CanvasRenderingContext2D; /** 下層畫(huà)布環(huán)境 */ public contextContainer: CanvasRenderingContext2D; /** 緩沖層畫(huà)布環(huán)境 */ public contextCache: CanvasRenderingContext2D; /** 整個(gè)畫(huà)布到上面和左邊的偏移量 */ private _offset: Offset; /** 畫(huà)布中所有添加的物體 */ private _objects: FabricObject[]; constructor(el: HTMLCanvasElement, options) { // 初始化下層畫(huà)布 lower-canvas this._initStatic(el, options); // 初始化上層畫(huà)布 upper-canvas this._initInteractive(); // 初始化緩沖層畫(huà)布 this._createCacheCanvas(); } // 下層畫(huà)布初始化:參數(shù)賦值、重置寬高,并賦予樣式 _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'; } // 其余兩個(gè)畫(huà)布同理 }
上面的代碼簡(jiǎn)單用到了 Util 這個(gè)工具類(lèi),里面主要就是封裝一些獨(dú)立的、常用的方法,大部分都比較簡(jiǎn)單,下面簡(jiǎn)單的列舉幾種:
const PiBy180 = Math.PI / 180; // 寫(xiě)在這里相當(dāng)于緩存,因?yàn)闀?huì)頻繁調(diào)用 class Util { /** 單純的創(chuàng)建一個(gè)新的 canvas 元素 */ static createCanvasElement() { const canvas = document.createElement('canvas'); return canvas; } /** 角度轉(zhuǎn)弧度,注意 canvas 中用的都是弧度,但是角度對(duì)我們來(lái)說(shuō)比較直觀 */ static degreesToRadians(degrees: number): number { return degrees * PiBy180; } /** 弧度轉(zhuǎn)角度,注意 canvas 中用的都是弧度,但是角度對(duì)我們來(lái)說(shuō)比較直觀 */ static radiansToDegrees(radians: number): number { return radians / PiBy180; } /** 從數(shù)組中溢出某個(gè)元素 */ 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(); } }); } }
諸如此類(lèi),大家可以自己去看下 Util 這個(gè)工具類(lèi),后面就不再贅述了,當(dāng)然有些比較麻煩點(diǎn)的方法(比如 animate 和一些計(jì)算)可以先跳過(guò),后面的用到的時(shí)候會(huì)再展開(kāi)。
變換練習(xí)
同樣的這個(gè)章節(jié)內(nèi)容不多也不難,所以這里先為下一篇文章(物體基類(lèi))做一些熱身練習(xí),講一些變換的基礎(chǔ)內(nèi)容,也就是 transform(translate、rotate、scale),功能和 css 的 transform 類(lèi)似。
以繪制一個(gè)紅色矩形為例 ctx.fillRect(0, 0, 50, 50)
,讓我們看看這幾個(gè)東西分別會(huì)產(chǎn)生什么影響:
translate 的影響
rotate 的影響
scale 的影響
這里對(duì) scale 做一些補(bǔ)充,scale 的結(jié)果是對(duì)坐標(biāo)系做了縮放,但是理解起來(lái)不是很直觀,所以你可以認(rèn)為 scale 其實(shí)是對(duì)坐標(biāo)軸的刻度做了縮放,比如本來(lái)畫(huà)布的一段固定長(zhǎng)度代表 50,scale(2, 2) 之后,同樣的固定長(zhǎng)度就只能代表 25,所以還需要再來(lái)一個(gè)固定長(zhǎng)度才能表示 50,視覺(jué)上就是放大的效果。
好了,以上這幾種變換的結(jié)果本質(zhì)都是對(duì)坐標(biāo)系的變換,translate 改變了坐標(biāo)系原點(diǎn)的位置,rotate 將坐標(biāo)系進(jìn)行了旋轉(zhuǎn),scale 則將坐標(biāo)軸的刻度進(jìn)行了縮放,而畫(huà)布的視窗大?。ㄒ簿褪巧厦鎴D中的 canvas 框)是不變的(可以想象成一個(gè)鏡頭),我們并不會(huì)改動(dòng)到畫(huà)布的寬高,不要混淆了。
單個(gè)內(nèi)容的變換還是比較好理解的,但是混在一起就會(huì)有點(diǎn)變扭了,比如要畫(huà)下面這樣一個(gè)圖形(兩個(gè)箭頭和等邊三角形):
大家可以用這三種變換畫(huà)一下上面的圖形,能畫(huà)出來(lái)應(yīng)該就有點(diǎn)感覺(jué)了(這些變換效果是會(huì)累加的哦)。建議多動(dòng)手練練,因?yàn)橄聜€(gè)章節(jié)會(huì)用上。
小結(jié)
這里是本章的知識(shí)點(diǎn)小結(jié),記住這些就可以了:
- 我們共創(chuàng)建了三個(gè) canvas,每個(gè) canvas 都是一樣大的,但功能各不相同
- 邏輯和繪制是分離的,上層畫(huà)布用來(lái)改邏輯和改數(shù)據(jù),下層畫(huà)布則用來(lái)繪制
- 原點(diǎn)始終都是在畫(huà)布左上角,x 軸水平向右為正,y 軸豎直向下為正?? 然后這里還是先給個(gè)簡(jiǎn)版 fabric.js 的代碼鏈接吧,有需要的可以參考看看,會(huì)隨著文章更新不斷完善。好啦,今天的分享就到這里,有什么問(wèn)題歡迎點(diǎn)贊評(píng)論留言,我們下期再見(jiàn),拜拜 ?? ??
實(shí)現(xiàn)一個(gè)輕量 fabric.js 系列一(概覽)??
以上就是JS前端輕量fabric.js系列之畫(huà)布初始化的詳細(xì)內(nèi)容,更多關(guān)于fabric.js畫(huà)布初始化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Dom-api MutationObserver使用方法詳解
這篇文章主要為大家介紹了Dom-api MutationObserver使用方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11JS前端實(shí)現(xiàn)解除頁(yè)面禁止復(fù)制功能方法詳解
這篇文章主要為大家介紹了JS前端實(shí)現(xiàn)解除頁(yè)面禁止復(fù)制功能方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08vue框架通用化解決個(gè)性化文字換行問(wèn)題實(shí)例詳解
這篇文章主要為大家介紹了通用化解決個(gè)性化文字換行問(wèn)題實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09微信公眾號(hào) 提示:Unauthorized API function 問(wèn)題解決方法
這篇文章主要介紹了微信公眾號(hào) 提示:Unauthorized API function 問(wèn)題解決方法的相關(guān)資料,這里提供了出現(xiàn)提示的解決方法,需要的朋友可以參考下2016-12-12js類(lèi)型判斷內(nèi)部實(shí)現(xiàn)原理示例詳解
這篇文章主要為大家介紹了js類(lèi)型判斷內(nèi)部實(shí)現(xiàn)原理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07