溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JS前端輕量fabric.js系列之畫布初始化的方法

發(fā)布時間:2022-08-04 09:57:58 來源:億速云 閱讀:247 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“JS前端輕量fabric.js系列之畫布初始化的方法”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“JS前端輕量fabric.js系列之畫布初始化的方法”吧!

畫布的前置知識

在說 fabric.js 如何初始化畫布之前,先鞏固下畫布的相關(guān)知識點。創(chuàng)建畫布要做的事情通常比較簡單,就是單純的獲取畫布(或動態(tài)創(chuàng)建畫布)并重新設(shè)置畫布寬高,就像下面這個樣子:

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è)置寬高,這是個很容易混淆的點??纯聪旅娴拇a????????:

#canvas { width: 200px; height: 100px; }
<canvas id="canvas" width="100" height="100"></canvas>

可以看到上面的 canvas 有兩個寬高大小,一個是 canvas 上的屬性值,一個是 css 的樣式值,那應(yīng)該以哪個為準(zhǔn)呢?????

我們可以先拋棄 css 大小的概念,請記?。核械睦L圖操作都是在 canvas 這個畫布大小上進行的,就上面的代碼來說不論你繪制什么東西,都是在 100*100 的畫布中進行的,當(dāng)你在 canvas 繪制完所有東西之后要在頁面上某個區(qū)域渲染了,才和 css 大小有關(guān),就上面的例子來說就是你要把 100*100 的 canvas 畫布放到頁面上 200*100 的區(qū)域,但是它們大小不一致要怎么處理呢?

你可以把 canvas 繪制的內(nèi)容想象成一張大小固定的照片,把 css 大小想象成一個容器,不管 css 尺寸如何,這張照片都會鋪滿整個容器(機制就是這樣,沒有為啥????)。所以如果長寬比例相同就會等比縮放;

如果長寬比例不同就會拉伸變形;如果大小一樣就剛剛好。就我們的例子來說 100*100 的繪制內(nèi)容水平方向會被拉伸成 200*100,就產(chǎn)生了變形,因此通常情況下需要把 canvas 和 css 設(shè)置成一樣大,確保不拉伸變形,看下面的示意圖能幫你加深理解:

JS前端輕量fabric.js系列之畫布初始化的方法

另外還有一個常見問題就是設(shè)備像素比(devicePixelRatio)的影響,如果不處理在高清屏上就會導(dǎo)致模糊(比如 Mac 電腦),大家應(yīng)該有看過類似問題的文章,但大多都是各種名詞詞匯,看完就忘的那種。

關(guān)于這個問題我在另一篇文章????關(guān)于 canvas 模糊的問題(高清圖解)有解釋過,有需要的可以去看下,這里就簡單介紹下(溫馨提示:實在不好記可以跳過這一趴????,因為它并不妨礙我們進行接下來的開發(fā))。我們知道畫的東西最終是要展現(xiàn)在屏幕上的,而屏幕又是由很多小格子構(gòu)成的,通常情況下:

  • 如果 dpr = 1,就說明 1px 對應(yīng)屏幕上的 1 個小格子(亦即 1 個 css 像素對應(yīng) 1 個物理像素)

如果 dpr = 2,就說明 1px 對應(yīng)屏幕上的 2 個小格子(亦即 1 個 css 像素對應(yīng) 1 個物理像素) 順便看下圖解????????:

JS前端輕量fabric.js系列之畫布初始化的方法

圖沒看懂?????那就來看看文字解說:假設(shè)我們現(xiàn)在 canvas 和 css 的大小都是 10 * 10,那么 canvas 畫完的照片中就會有 100 個(像素)點,也就是只有 100 個點的信息;但是到了高清屏中(如 dpr = 2),我們需要 400 個點的信息,原來的點不夠用怎么辦?

于是就會有一套算法來自動生成這些點的信息,從而造成了模糊。那應(yīng)該怎么辦呢?????我們需要更多的點,所以可以這樣子搞,把畫布放大 dpr 倍,也就是把 canvas 的寬高都乘以 dpr(css 的大小還是不變的),接下來的繪制都是在寬為width*dpr、高為 height*dpr的畫布大小上進行的,這樣一折騰,點就變多了。

但是要注意什么呢,畫布變大了,相應(yīng)的繪制操作(畫圓、畫矩形等)也需要相應(yīng)放大,這個我會在最后一章加上這個功能,一開始有個印象就行,不然容易犯暈????。

畫布初始化

在 fabric.js 中我們總共會創(chuàng)建兩個畫布,一個是上層畫布(upper-canvas),一個是下層畫布(lower-canvas),兩個畫布是一樣大的,還有一個外層 div 將這兩個 canvas 包起來。

  • 上層畫布主要用于處理一些交互事件,比如鼠標(biāo)事件、涂鴉模式(畫板)、左鍵拖拽產(chǎn)生的框選區(qū)域等;

  • 下層畫布則單純的用于繪制所有物體,簡單粗暴的遍歷所有物體進行繪制,沒有其他多余的操作。

如果通過上層畫布的交互后,某些物體的某些屬性值被改變了,這時候就會清空下層畫布,重新繪制所有物體,兩層畫布各司其職,典型的數(shù)據(jù)驅(qū)動視圖。

除了職責(zé)分明還有一點點單向數(shù)據(jù)流的味道,上層的交互改變了數(shù)據(jù),數(shù)據(jù)的改變傳到下層畫布,下層畫布就單純的重新繪制;

但是反過來,下層畫布并不會影響上層畫布也不會影響數(shù)據(jù),這樣問題排查起來也方便些。相信大家都用過 vue2,如果我們要修改 props 中的值,就需要用 $emit 把數(shù)據(jù)傳出去,修改父元素的值才行;

但如果 props 是個對象,我們其實可以在子元素中直接修改 props 的屬性值,雖然方便但不是很好的寫法,關(guān)系就亂了,如果你有踩過這個坑的話。

扯遠了,回過頭來,實際上 fabric.js 一共創(chuàng)建了三層畫布,還有一個是 cacheCanvasEl,我們就把它叫做緩沖層畫布吧,它和另外兩個畫布一樣大,但并沒有在頁面中顯示,所以也可以叫離屏 canvas,它主要用來提供一個臨時繪制環(huán)境,以便不時之需,后面章節(jié)會說道它的用途,這里先知道有這么個東西就行。

JS前端輕量fabric.js系列之畫布初始化的方法

順便給些示例代碼,簡單瞟一瞟就行:

/** 畫布類 */
class Canvas {
    /** 畫布寬度 */
    public width: number;
    /** 畫布高度 */
    public height: number;
    /** 包圍 canvas 的外層 div 容器 */
    public wrapperEl: HTMLElement;
    /** 下層 canvas 畫布,主要用于繪制所有物體 */
    public lowerCanvasEl: HTMLCanvasElement;
    /** 上層 canvas,主要用于監(jiān)聽鼠標(biāo)事件、涂鴉模式、左鍵點擊拖藍框選區(qū)域 */
    public upperCanvasEl: HTMLCanvasElement;
    /** 緩沖層畫布 */
    public cacheCanvasEl: HTMLCanvasElement;
    /** 上層畫布環(huán)境 */
    public contextTop: CanvasRenderingContext2D;
    /** 下層畫布環(huán)境 */
    public contextContainer: CanvasRenderingContext2D;
    /** 緩沖層畫布環(huán)境 */
    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();
    }
    // 下層畫布初始化:參數(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';
    }
    // 其余兩個畫布同理
}

上面的代碼簡單用到了 Util 這個工具類,里面主要就是封裝一些獨立的、常用的方法,大部分都比較簡單,下面簡單的列舉幾種:

const PiBy180 = Math.PI / 180; // 寫在這里相當(dāng)于緩存,因為會頻繁調(diào)用
class Util {
     /** 單純的創(chuàng)建一個新的 canvas 元素 */
    static createCanvasElement() {
        const canvas = document.createElement('canvas');
        return canvas;
    }
    /** 角度轉(zhuǎn)弧度,注意 canvas 中用的都是弧度,但是角度對我們來說比較直觀 */
    static degreesToRadians(degrees: number): number {
        return degrees * PiBy180;
    }
    /** 弧度轉(zhuǎn)角度,注意 canvas 中用的都是弧度,但是角度對我們來說比較直觀 */
    static radiansToDegrees(radians: number): number {
        return radians / PiBy180;
    }
    /** 從數(shù)組中溢出某個元素 */
    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 這個工具類,后面就不再贅述了,當(dāng)然有些比較麻煩點的方法(比如 animate 和一些計算)可以先跳過,后面的用到的時候會再展開。

變換練習(xí)

同樣的這個章節(jié)內(nèi)容不多也不難,所以這里先為下一篇文章(物體基類)做一些熱身練習(xí),講一些變換的基礎(chǔ)內(nèi)容,也就是 transform(translate、rotate、scale),功能和 css 的 transform 類似。

以繪制一個紅色矩形為例 ctx.fillRect(0, 0, 50, 50),讓我們看看這幾個東西分別會產(chǎn)生什么影響:

translate 的影響

JS前端輕量fabric.js系列之畫布初始化的方法

rotate 的影響

JS前端輕量fabric.js系列之畫布初始化的方法

scale 的影響

JS前端輕量fabric.js系列之畫布初始化的方法

這里對 scale 做一些補充,scale 的結(jié)果是對坐標(biāo)系做了縮放,但是理解起來不是很直觀,所以你可以認為 scale 其實是對坐標(biāo)軸的刻度做了縮放,比如本來畫布的一段固定長度代表 50,scale(2, 2) 之后,同樣的固定長度就只能代表 25,所以還需要再來一個固定長度才能表示 50,視覺上就是放大的效果。

好了,以上這幾種變換的結(jié)果本質(zhì)都是對坐標(biāo)系的變換,translate 改變了坐標(biāo)系原點的位置,rotate 將坐標(biāo)系進行了旋轉(zhuǎn),scale 則將坐標(biāo)軸的刻度進行了縮放,而畫布的視窗大?。ㄒ簿褪巧厦鎴D中的 canvas 框)是不變的(可以想象成一個鏡頭),我們并不會改動到畫布的寬高,不要混淆了。

單個內(nèi)容的變換還是比較好理解的,但是混在一起就會有點變扭了,比如要畫下面這樣一個圖形(兩個箭頭和等邊三角形):

JS前端輕量fabric.js系列之畫布初始化的方法

到此,相信大家對“JS前端輕量fabric.js系列之畫布初始化的方法”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI