溫馨提示×

溫馨提示×

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

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

JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)

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

這篇文章主要介紹“JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)”,在日常操作中,相信很多人在JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

FabricObject 基類的實(shí)現(xiàn)

抽離共同屬性

我們要繪制某個(gè)物體,那不就是在畫布的某個(gè)位置(top、left值)根據(jù)某些屬性(寬高大小等)畫上某個(gè)物體(比如矩形、多邊形、圖片或者路徑等等)嗎,并且之后還可以對每個(gè)物體進(jìn)行一些交互操作(主要就是平移+旋轉(zhuǎn)+縮放)。這么一說,是不是好像已經(jīng)把物體的挺多共性給抽離出來呢(真的是萬物皆對象啊,前端同學(xué)在 canvas 中尤其能體會到這個(gè)思想)。

那么,自然而然的我們就需要抽象出一個(gè)物體基類(FabricObject),其它物體(如 Rect)只需要繼承這個(gè)物體基類,就能夠很方便的擁有一些通用能力,對于日后的維護(hù)和擴(kuò)展也都是很友好的,看下面的代碼理解起來應(yīng)該會更清晰????????:

class FabricObject {
    /** 物體類型標(biāo)識 */
    public type: string = 'object';
    /** 是否可見 */
    public visible: boolean = true;
    /** 是否處于激活態(tài),也就是是否被選中 */
    public active: boolean = false;
    /** 物體位置的 top 值,就是 y */
    public top: number = 0;
    /** 物體位置的 left 值,就是 x */
    public left: number = 0;
    /** 物體的原始寬度 */
    public width: number = 0;
    /** 物體的原始高度 */
    public height: number = 0;
    /** 物體當(dāng)前的縮放倍數(shù) x */
    public scaleX: number = 1;
    /** 物體當(dāng)前的縮放倍數(shù) y */
    public scaleY: number = 1;
    /** 物體當(dāng)前的旋轉(zhuǎn)角度 */
    public angle: number = 0;
    /** 默認(rèn)水平變換中心 left | right | center */
    public originX: string = 'center';
    /** 默認(rèn)垂直變換中心 top | bottom | center */
    public originY: string = 'center';
    /** 列舉常用的屬性 */
    public stateProperties: string[] = ('top left width height scaleX scaleY ' + 'angle fill originX originY ' + 'stroke strokeWidth ' + 'borderWidth visible').split(' ');
    ...
    constructor(options) {
        this.initialize(options); // 初始化各種屬性,就是簡單的賦值
    }
    initialize(options) {
        options && this.setOptions(options);
    }
    render() {} // 繪制物體的方法
    ...
}

上面代碼中有幾個(gè)比較容易混淆的點(diǎn),就是 originX、originY 和 top、left,以及為啥不用 x、y 來表示物體位置呢?

解答之前,我們先來思考一個(gè)問題,如果要在畫布的 (x, y) 處繪制一個(gè) 100*100 的矩形,這句話會有什么歧義嗎?em。。。有的,看下下面這張圖????????:

JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)

你會發(fā)現(xiàn)兩種畫法好像都沒錯(cuò),也都挺符合直覺,主要就是因?yàn)樗鼈兯x的中心點(diǎn)不一樣,所以就有了 originX 和 originY。

  • 如果 originX = 'left', originY = 'top' 就是左圖那樣;

  • 如果 originX = 'center', originY = 'center' 就是右圖那樣;

  • 如果 originX = 'left', originY = 'bottom',那矩形就會畫在點(diǎn)(x, y) 的右上方;

  • 以此類推... 新版本的 fabric.js 默認(rèn)采用的是左圖的方式,很早很早前是右圖的方式,當(dāng)然你可以自己傳參設(shè)置,靈活性杠杠滴。然后,現(xiàn)在你是不是會覺得 top、left 相比于 x、y 來說會稍微語義化點(diǎn)????。建議這幾個(gè)變量要好好理清一下,后續(xù)都是在此基礎(chǔ)上展開的。這里我覺得還是用 center 會直觀點(diǎn),所以這個(gè)系列采用的是右圖的方式,請務(wù)必記住。

抽離共同方法

物體最重要的一個(gè)方法就是 render 了,但是每個(gè)物體有各自獨(dú)特的繪制方法,能抽象出什么呢?想想好像沒啥能抽的。確實(shí)是這樣,所以我們嘗試先直接繪制幾個(gè)普通物體,再通過它們看看能不能倒推出一些通用的東西。

假設(shè)要在 (100, 100) 的地方繪制一個(gè) 50*50 的矩形,并將其放大 2 倍,之后旋轉(zhuǎn) 45°,該怎么畫呢?正常來說我們需要簡單計(jì)算一下,就像這樣:

  • 手動(dòng)算下寬高 100*100

  • 手動(dòng)算下旋轉(zhuǎn)之后各個(gè)頂點(diǎn)的坐標(biāo)

  • 連接四個(gè)頂點(diǎn) 如果是在畫布左下角畫一個(gè)邊長為 100 的、擺的比較正的等邊三角形呢,就像這樣△?那我們也需要簡單計(jì)算下:

  • 手動(dòng)算下三角形每個(gè)頂點(diǎn)的坐標(biāo)

  • 連接三個(gè)頂點(diǎn)

  • 如果加上旋轉(zhuǎn),這個(gè)計(jì)算就更復(fù)雜了一些 又或者簡單點(diǎn),我們在 (100, 100) 處畫個(gè)圓,然后將其旋轉(zhuǎn) 30°,并把半徑縮小 2 倍,那就要:

  • 因?yàn)槭莻€(gè)圓,所以不用考慮旋轉(zhuǎn),但是要算一下縮小后的半徑

  • 畫一個(gè) (0, 2 * Math.PI) 的圓弧 所以上面三個(gè)小例子的共性就是:先計(jì)算再繪制嗎?不,不是的,我們在 canvas 中要改掉這種繪制的思想,而是要通過并善用變換坐標(biāo)系來繪制物體,這個(gè)在上個(gè)章節(jié)末尾有提到,之所以這樣做,是因?yàn)樗軌蚬?jié)省很多計(jì)算和繪制成本。提到變換坐標(biāo)系,這個(gè)東西很容易讓人蒙圈,但它絕對是一把利器,所以我們必須要搞定它,如果你不熟悉,還是希望能夠多動(dòng)手練練,這樣才能拿捏它。

  • 那現(xiàn)在我們應(yīng)該怎么畫呢?就是能用變換就用變換,能不計(jì)算就不計(jì)算。來看看上面第一個(gè)畫矩形的例子,首先我們繪制矩形的方法是固定的 ctx.fillRect(-width/2, -height/2, width, height);,其中 width=50,height=50,然后就盡量不去動(dòng)它。那怎么畫出縮放和旋轉(zhuǎn)的效果,并且畫在點(diǎn) (100, 100) 的地方呢?就是用到之前說的變換坐標(biāo)系,簡單看下代碼:

ctx.save(); // 之前提到過了,你要修改 ctx 上的一些配置或者畫一個(gè)物體,最好先 save 一下,這是個(gè)好習(xí)慣
ctx.translate(100, 100); // 此時(shí)原點(diǎn)已經(jīng)變到了 (100, 100) 的地方
ctx.scale(2, 2); // 坐標(biāo)系放大兩倍
ctx.rotate(Util.degreesToRadians(45)); // 注意 canvas 中用的都是弧度(弧度 / 2 * Math.PI = 角度 / 360),所以需要簡單換算下
ctx.fillRect(-width/2, height/2, width, height); // 繪制矩形的方法固定不變,寬高一般也不會去修改
ctx.restore(); // 畫完之后還原 ctx 狀態(tài),這是個(gè)好習(xí)慣

再來看看第二個(gè)例子,在左下角畫一個(gè)邊長為 100 的等邊三角形△,我們要做的就是先把原點(diǎn)移到三角形的某個(gè)頂點(diǎn)上(這里我們當(dāng)然拿左下角的頂點(diǎn)啦),然后通過不斷旋轉(zhuǎn)坐標(biāo)系繪制三條邊,看下代碼????????:

ctx.save();
ctx.translate(0, 畫布高度); // 左下角變?yōu)?0, 0) 點(diǎn)了
ctx.rotate(Util.degreesToRadians(30)); // 準(zhǔn)備畫左邊這條邊
ctx.moveTo(0, 0);
ctx.lineTo(100, 0);
ctx.rotate(Util.degreesToRadians(120)); // 準(zhǔn)備畫右邊這條邊
ctx.lineTo(100, 0);
ctx.rotate(Util.degreesToRadians(120)); // 準(zhǔn)備畫下面這條邊
ctx.lineTo(100, 0);
ctx.restore();

大家可以在此基礎(chǔ)上畫一畫正多邊形,就能夠體會到旋轉(zhuǎn)的意思了。 至于第三個(gè)畫圓的例子,這里也簡單放下代碼:

ctx.save();
ctx.translate(100, 100);
ctx.scale(2, 2);
ctx.arc(0, 0, r, 0, 2 * Math.PI); // 畫圓的方法始終不變
ctx.fill();
ctx.restore();

我們不再把物體上面的變換用于物體自身,而是用于坐標(biāo)系,從而簡化了計(jì)算量和繪圖操作。

但可能還是不好看出來能抽象出什么(其實(shí)就只抽出了變換????),所以讓我們來看看代碼吧????????:

class FabricObject {
    /** 渲染物體的通用流程 */
    render(ctx: CanvasRenderingContext2D) {
        // 看不見的物體不繪制
        if (this.width === 0 || this.height === 0 || !this.visible) return;
         // 凡是要變換坐標(biāo)系或者設(shè)置畫筆屬性都需要用先用 save 保存和再用 restore 還原,避免影響到其他東西的繪制
        ctx.save();
        // 1、坐標(biāo)變換
        this.transform(ctx);
        // 2、繪制物體
        this._render(ctx);
        ctx.restore();
    }
    transform(ctx: CanvasRenderingContext2D) {
        ctx.translate(this.left, this.top);
        ctx.rotate(Util.degreesToRadians(this.angle));
        ctx.scale(this.scaleX, this.scaleY);
    }
    /** 具體由子類來實(shí)現(xiàn),因?yàn)檫@確實(shí)是每個(gè)子類物體所獨(dú)有的 */
    _render(ctx: CanvasRenderingContext2D) {}
}

從上面的代碼中可以看到物體的繪制被分成了兩步:transform_render
對于 transform 建議大家可以拿正多邊形和折線來找找感覺,本質(zhì)就是 n 條線段通過 translate 來不斷改變線段起始位置,通過 rotate 改變方向,通過 scale 來改變線段長度,而繪制期間線段自身的長度其實(shí)并沒有改變,然后畫之前在腦海里想一下每一條線段的效果,看看畫的是否與想的一致。記住核心思路(重要的事情說三遍????):

  • 我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。

  • 我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。

  • 我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。 另外關(guān)于 transform 還要注意的是:

  • 變換是會疊加的,比如我 ctx.scale(2) 了之后又 ctx.scale(2),那最終的結(jié)果就是 ctx.scale(4),所以你還需要學(xué)會如何變換回去。一般有兩種方法:一種是配合 save 和 restore 使用,另一種就是往反方向進(jìn)行變換。

  • 變換是有順序的,不同的順序最終繪制出來的效果也大不一樣,通常是 translate > rotate > scale,比較符合人的直覺。當(dāng)然你要用其他順序也是可以的,那重點(diǎn)是什么呢?重點(diǎn)是同一個(gè)庫或者引擎的內(nèi)部實(shí)現(xiàn)用的是同一種順序就行。

  • 矩陣:其實(shí)這三種變換和矩陣是可以相互轉(zhuǎn)換的,就是把 transform 里面的函數(shù)換個(gè)寫法而已,我們用矩陣的形式 matrix(a, b, c, d, tx, ty) 也能達(dá)到同樣的效果,但是矩陣更加強(qiáng)大并統(tǒng)一了寫法,而且除了三種基本的變換,還能達(dá)到其他效果,比如斜切 skew。關(guān)于矩陣的概念和寫法我們會在這個(gè)系列的最后幾個(gè)章節(jié)單獨(dú)講一下,目前我們可以暫且認(rèn)為這三種變換和矩陣是等價(jià)的。

scale 是沿著坐標(biāo)軸放大,并不一定是水平或豎直方向,假如物體旋轉(zhuǎn)了,就是沿著旋轉(zhuǎn)之后的坐標(biāo)軸方向放大,如下圖所示:

JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)

說完了 transform,我們再來看看 _render,這個(gè)就真沒啥共性了,需要由子類自己實(shí)現(xiàn)。

Rect 類的實(shí)現(xiàn)

接下來就趁熱打鐵,我們以一個(gè)最簡單也最常用的 Rect 矩形類為例子來看看子類又是怎么操作的,這里直接上代碼,因?yàn)榇_實(shí)簡單????????:

/** 矩形類 */
class Rect extends FabricObject {
    /** 矩形標(biāo)識 */
    public type: string = 'rect';
    /** 圓角 rx */
    public rx: number = 0;
    /** 圓角 ry */
    public ry: number = 0;
    constructor(options) {
        super(options);
        this._initStateProperties();
        this._initRxRy(options);
    }
    /** 一些共有的和獨(dú)有的屬性 */
    _initStateProperties() {
        this.stateProperties = this.stateProperties.concat(['rx', 'ry']);
    }
    /** 初始化圓角值 */
    _initRxRy(options) {
        this.rx = options.rx || 0;
        this.ry = options.ry || 0;
    }
    /** 單純的繪制一個(gè)普普通通的矩形 */
    _render(ctx: CanvasRenderingContext2D) {
        let rx = this.rx || 0,
            ry = this.ry || 0,
            x = -this.width / 2,
            y = -this.height / 2,
            w = this.width,
            h = this.height;
        // 繪制一個(gè)新的東西,大部分情況下都要開啟一個(gè)新路徑,要養(yǎng)成習(xí)慣
        ctx.beginPath();
        // 從左上角開始向右順時(shí)針畫一個(gè)矩形,這里就是單純的繪制一個(gè)規(guī)規(guī)矩矩的矩形
        // 不考慮旋轉(zhuǎn)縮放啥的,因?yàn)樾D(zhuǎn)縮放會在調(diào)用 _render 函數(shù)之前處理
        // 另外這里考慮了圓角的實(shí)現(xiàn),所以用到了貝塞爾曲線,不然你可以直接畫成四條線段,再懶一點(diǎn)可以直接調(diào)用原生方法 fillRect 和 strokeRect
        // 不過自己寫的話自由度更高,也方便擴(kuò)展
        ctx.moveTo(x + rx, y);
        ctx.lineTo(x + w - rx, y);
        ctx.bezierCurveTo(x + w, y, x + w, y + ry, x + w, y + ry);
        ctx.lineTo(x + w, y + h - ry);
        ctx.bezierCurveTo(x + w, y + h, x + w - rx, y + h, x + w - rx, y + h);
        ctx.lineTo(x + rx, y + h);
        ctx.bezierCurveTo(x, y + h, x, y + h - ry, x, y + h - ry);
        ctx.lineTo(x, y + ry);
        ctx.bezierCurveTo(x, y, x + rx, y, x + rx, y);
        ctx.closePath();
        if (this.fill) ctx.fill();
        if (this.stroke) ctx.stroke();
    }
}

現(xiàn)在我們已經(jīng)有了一個(gè)最基礎(chǔ)也最為重要的一個(gè)物體:矩形。于是就可以將它添加到畫布中,我們在上一章節(jié)的 Canvas 類中加一個(gè) add 方法,如下代碼所示????????:

class Canvas {
    /**
     * 添加元素
     * 目前的模式是調(diào)用 add 添加物體的時(shí)候就立馬渲染,如果一次性加入大量元素,就會做很多無用功
     * 所以可以優(yōu)化一下,就是先批量添加元素(需要加一個(gè)變量標(biāo)識),最后再統(tǒng)一渲染(手動(dòng)調(diào)用 renderAll 函數(shù)即可),這里先了解即可
    */
    add(...args): Canvas {
        this._objects.push(...args);
        this.renderAll();
        return this;
    }
    /** 在下層畫布上繪制所有物體 */
    renderAll(): Canvas {
        // 獲取下層畫布
        const ctx = this.contextContainer;
        // 清除畫布
        this.clearContext(ctx);
        // 簡單粗暴的遍歷渲染
        this._objects.forEach(object => {
            // render = transfrom + _render
            object.render(ctx);
        })
        return this;
    }
}

現(xiàn)在我們只需要傳入不同的參數(shù)就能在畫布中創(chuàng)建形形色色的矩形了,而子類里面的 _render 方法一般寫好了就行,很少會去動(dòng)它。

大家可以類比一下瀏覽器的盒模型,其實(shí)就是四四方方的矩形,然后用 css 中的 transfrom 做各種變換,也能達(dá)到各種效果,而元素的寬高大小并沒與改變。如果不理解為什么要拆成 transform 和 _render 兩部分,大家可以先記住,后面會體會到它的好。

到此,關(guān)于“JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

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

AI