欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS前端使用canvas實(shí)現(xiàn)擴(kuò)展物體類和事件派發(fā)

 更新時(shí)間:2022年08月03日 09:23:38   作者:尤水就下  
這篇文章主要為大家介紹了JS前端使用canvas實(shí)現(xiàn)擴(kuò)展物體類和事件派發(fā)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

雖然我們講了這么多個(gè)章節(jié),但其實(shí)目前為止就只有一個(gè) Rect 類能用,略顯單調(diào)。于是乎,為了讓整個(gè)畫布稍微生動(dòng)一些,這個(gè)章節(jié)我們來嘗試增加一個(gè)圖片類,如果你以后需要擴(kuò)展一個(gè)物體類,也是用同樣的方法。

另外有時(shí)候我們還希望在物體屬性改變時(shí)或者畫布創(chuàng)建后做一些額外的事情,這個(gè)時(shí)候事件系統(tǒng)就派上用場啦,也就是我們常說的發(fā)布訂閱,我覺的這是前端應(yīng)用最廣的設(shè)計(jì)模式?jīng)]有之一了??。

FabricImage 圖片類

話不多說,開擼走起??。先來看看 FabricImage 圖片類的實(shí)現(xiàn),我們可以想一下一個(gè)圖片類應(yīng)該具備什么樣的功能??,可以看看下面圖片類代碼的調(diào)用方式找找靈感????:

FabricImage.fromURL(
    'https://p26-passport.byteacctimg.com/img/user-avatar/7470b65342454dd6699a6cf772652260~300x300.image',
    (img) => { canvas.add(img) }, // 這里需要手動(dòng)回調(diào)添加物體
    { width: 200, height: 200, left: 300, top: 300 }
);
FabricImage.fromURL(
    './src/beidaihe.jpeg',
    (img) => { canvas.add(img) }, // 這里需要手動(dòng)回調(diào)添加物體
    { width: 200, height: 200, left: 600, top: 400 }
);

上面代碼展示了兩種最常用的圖片加載方式,一個(gè)是遠(yuǎn)程鏈接,一個(gè)是本地圖片,調(diào)用方式看起來有些特殊,不過我們先不管這個(gè),直接來實(shí)現(xiàn)它就行。既然要繪制圖片,那肯定要先加載好才能用,這也是圖片類特殊的地方,它是異步的,并且加載圖片的方法是通用的,所以我們把它寫在 Util 類里,來簡單看下加載圖片的代碼(也許你在面試中遇見過??):

class Util {
    static loadImage(url) {
        return new Promise((resolve, reject) => { // 方便鏈?zhǔn)秸{(diào)用,promise 這玩意多寫多熟悉就懂了
            const img = document.createElement('img');
            img.onload = () => { // 先進(jìn)行事件監(jiān)聽,要在請(qǐng)求圖片前
                img.onload = img.onerror = null;
                resolve(img);
            };
            img.onerror = () => {
                reject(new Error('Error loading ' + img.src));
            };
            img.src = url; // 這個(gè)才是真正去請(qǐng)求圖片
        });
    }
}

代碼不多也不難理解,那接下來就要看如何繪制了。在 canvas 中要想繪制圖片也不難,大體過程就是把圖片變成 img 標(biāo)簽,當(dāng)做參數(shù)傳給 ctx.drawImage 這個(gè)畫布專用繪制方法,稍微要注意點(diǎn)的就是圖片的寬高設(shè)置,我們會(huì)先取傳入?yún)?shù) options 中的寬高作為圖片的大小,沒傳參數(shù)的話再取圖片自身的寬高(因?yàn)榇藭r(shí)圖片已經(jīng)加載完成,所以可以取到圖片的信息),同樣的來簡單看下代碼實(shí)現(xiàn)????:

class FabricImage extends FabricObject { // 繼承基類是必須的
    public type: string = 'image'; // 類型標(biāo)識(shí)
    public _element: HTMLImageElement;
    /** 默認(rèn)通過 img 標(biāo)簽來繪制,因?yàn)樽罱K都是要通過該標(biāo)簽繪制的 */
    constructor(element: HTMLImageElement, options) {
        super(options);
        this._initElement(element, options);
    }
    _initElement(element: HTMLImageElement, options) {
        this._element = element;
        this.setOptions(options);
        this._setWidthHeight(options);
        return this;
    }
    /** 設(shè)置圖像大小 */
    _setWidthHeight(options) {
        this.width = 'width' in options ? options.width : this.getElement() ? this.getElement().width || 0 : 0;
        this.height = 'height' in options ? options.height : this.getElement() ? this.getElement().height || 0 : 0;
    }
    /** 核心:直接調(diào)用 drawImage 繪制圖像 */
    _render(ctx: CanvasRenderingContext2D) {
        const x = -this.width / 2;
        const y = -this.height / 2;
        const elementToDraw = this._element;
        elementToDraw && ctx.drawImage(elementToDraw, x, y, this.width, this.height);
    }
    getElement() {
        return this._element;
    }
    /** 如果是根據(jù) url 或者本地路徑加載圖像,本質(zhì)都是取加載圖片完成之后在轉(zhuǎn)成 img 標(biāo)簽 */
    static fromURL(url, callback, imgOptions) {
        Util.loadImage(url).then((img) => {
            callback && callback(new FabricImage(img as HTMLImageElement, imgOptions));
        });
    }
}

看完上面的代碼,你應(yīng)該理解了前面為什么要那樣調(diào)用,雖然看起來有點(diǎn)繁瑣??。然后。。。一個(gè)簡簡單單的 FabricImage 類就寫好啦。不過這里我再補(bǔ)充兩個(gè)小點(diǎn):

一個(gè)是我們可以將圖片素材緩存起來,這樣如果用到多張相同的圖片就不用重復(fù)發(fā)請(qǐng)求啦;

另一個(gè)就是 imageSmoothingEnabled 屬性,這個(gè)是 canvas 中用來設(shè)置圖片是否平滑的屬性,默認(rèn)值為 true,表示平滑,false 則表示圖片不平滑。比如將一張 50*50 的圖像放大 3 倍的時(shí)候,canvas 會(huì)默認(rèn)做一些抗鋸齒處理使之平滑,如果不需要的話可以將其設(shè)置成 false,也算是種優(yōu)化,具體可以看看 mdn 上這個(gè)具體例子,這里就作為知識(shí)點(diǎn)簡單了解下,當(dāng)然我也截了個(gè)示意圖意思一下(仔細(xì)看??,一定能看出差別的):

其實(shí)擴(kuò)展一個(gè)類還是非常簡單的,你只需要知道這個(gè)類會(huì)有哪些獨(dú)特的自有屬性,并搞定 _render() 方法即可??。

事件派發(fā)

因?yàn)檫@個(gè)章節(jié)內(nèi)容比較少,所以我就把事件派發(fā)的內(nèi)容也放在這里講解了??。

有時(shí)候我們希望在物體初始化前后、狀態(tài)改變前后、一些交互前后,能夠觸發(fā)相應(yīng)的事件來實(shí)現(xiàn)自己的需求,比如畫布被點(diǎn)擊了我想...,物體被移動(dòng)了我想...,這個(gè)就是典型的發(fā)布訂閱模式,前端應(yīng)用最廣泛的設(shè)計(jì)模式,沒有之一(當(dāng)然只是我覺得),比如:

  • html 中的 addEventListener
  • vue 中的 EventBus
  • 各種庫和插件暴露的一些鉤子函數(shù)(或者說是生命周期)

早前這玩意我也沒真正理解,總是看了就忘,因?yàn)榭偢杏X這東西很抽象,說不上來這到底是個(gè)什么東西,所以這里我希望把它具象化,以便于理解。發(fā)布訂閱它其實(shí)可以理解成一個(gè)簡單的對(duì)象,就像下面這樣:

// key 就是事件名,key 存儲(chǔ)的值就是一堆回調(diào)函數(shù)
const eventObj = {
    eventName1: [cb1, cb2, ... ],
    eventName2: [cb1, cb2, cb3, ... ],
    ...
    // 比如下面這些常見的事件名
    click: [cb1, cb2, ... ],
    created: [cb1, cb2, cb3, ... ],
    mounted: [cb1, cb2, ... ],
}

我們最終要構(gòu)造的就是這樣一個(gè)對(duì)象,eventObj 相當(dāng)于一個(gè)事件管理中心,當(dāng)我們觸發(fā)相應(yīng) eventName 的事件時(shí)(發(fā)布),就會(huì)找到 eventObj 里面 eventName 對(duì)應(yīng)的那個(gè)數(shù)組,然后將里面的回調(diào)函數(shù) cb 挨個(gè)遍歷執(zhí)行即可。那我們?cè)趺聪?eventObj 添加事件回調(diào)呢,很簡單就是找到 eventName 對(duì)應(yīng)的數(shù)組往里 push 就行(訂閱),當(dāng)然為了操作方便我們需要提供 eventObj.on、eventObj.off、eventObj.emit 等方法方便我們添加、觸發(fā)和刪除事件。

下面我們來看看具體實(shí)現(xiàn),這東西寫多了就是很簡單的一件事情,寫法也比較固定,寫好了之后也基本不用改,實(shí)在不行 copy 也行??:

/**
 * 發(fā)布訂閱,事件中心
 * 應(yīng)用場景:可以在特定的時(shí)間點(diǎn)觸發(fā)一系列事件(在本文主要就是渲染前后、初始化物體前后、物體狀態(tài)改變時(shí))
 */
export class EventCenter {
    private __eventListeners; // 就是上面說的 eventObj 那個(gè)對(duì)象
    /** 往某個(gè)事件里面添加回調(diào),找到事件名所對(duì)應(yīng)的數(shù)組往里push */
    on(eventName, handler) {
        if (!this.__eventListeners) {
            this.__eventListeners = {};
        }
        if (!this.__eventListeners[eventName]) {
            this.__eventListeners[eventName] = [];
        }
        this.__eventListeners[eventName].push(handler);
        return this;
    }
    /** 觸發(fā)某個(gè)事件回調(diào),找到事件名對(duì)應(yīng)的數(shù)組拿出來遍歷執(zhí)行 */
    emit(eventName, options = {}) {
        if (!this.__eventListeners) {
            return this;
        }
        let listenersForEvent = this.__eventListeners[eventName];
        if (!listenersForEvent) {
            return this;
        }
        for (let i = 0, len = listenersForEvent.length; i < len; i++) {
            listenersForEvent[i] && listenersForEvent[i].call(this, options);
        }
        this.__eventListeners[eventName] = listenersForEvent.filter((value) => value !== false);
        return this;
    }
    /** 刪除某個(gè)事件回調(diào) */
    off(eventName, handler) {
        if (!this.__eventListeners) {
            return this;
        }
        if (arguments.length === 0) {
            // 如果沒有參數(shù),就是解綁所有事件
            for (eventName in this.__eventListeners) {
                this._removeEventListener.call(this, eventName);
            }
        } else {
            // 解綁單個(gè)事件
            this._removeEventListener.call(this, eventName, handler);
        }
        return this;
    }
    _removeEventListener(eventName, handler) {
        if (!this.__eventListeners[eventName]) {
            return;
        }
        let eventListener = this.__eventListeners[eventName];
        // 注意:這里我們刪除監(jiān)聽一般都是置為 null 或者 false
        // 當(dāng)然也可以用 splice 刪除,不過 splice 會(huì)改變數(shù)組長度,這點(diǎn)要尤為注意
        if (handler) {
            eventListener[eventListener.indexOf(handler)] = false;
        } else {
            eventListener.fill(false);
        }
    }
}

希望這種模式大家能夠達(dá)到默寫的水平,對(duì)我們?nèi)蘸蟠a的理解也確實(shí)是很有幫助的。

然后接下來要做什么呢?很簡單,就是讓需要事件的類繼承至這個(gè)事件類就可以了,然后在有需要的地方觸發(fā)就行了,這里我們以畫布為例,看下下面的代碼你就知道這種套路了????(注意下面代碼中注釋的地方):

class Canvas extends EventCenter { // 繼承
    _initObject(obj: FabricObject) {
        obj.setupState();
        obj.setCoords();
        obj.canvas = this;
        this.emit('object:added', { target: obj }); // 畫布觸發(fā)添加物體時(shí)間
        obj.emit('added'); // 物體觸發(fā)被添加事件
    }
    renderAll() {
         this.emit('before:render');
         // 繪制所有物體...
         this.emit('after:render');
    }
    clear() {
        ...
        this.clearContext(this.contextContainer);
        this.clearContext(this.contextTop);
        this.emit('canvas:cleared'); // 觸發(fā)畫布清空事件
        this.renderAll();
        return this;
    }
    __onMouseMove(e: MouseEvent) {
        ...
        const target = this._currentTransform.target;
        if (this._currentTransform.action === 'rotate') { // 如果是旋轉(zhuǎn)物體
            this.emit('object:rotating', { target, e });
            target.emit('rotating', { e });
        } else if (this._currentTransform.action === 'scale') { // 如果是縮放物體
            this.emit('object:scaling', { target, e });
            target.emit('scaling', { e });
        } else { // 如果是拖拽物體
            this.emit('object:moving', { target, e });
            target.emit('moving', { e });
        }
        ...
        this.emit('mouse:move', { target, e });
        target && target.emit('mousemove', { e });
    }
    __onMouseUp(e: MouseEvent) {
        if (target.hasStateChanged()) { // 物體狀態(tài)改變了才派發(fā)事件
            this.emit('object:modified', { target });
            target.emit('modified');
        }
    }
}

因?yàn)?Canvas 類繼承了 EventCenter 這個(gè)類,所以畫布就有了訂閱和發(fā)布的功能,同樣的我們也可以讓 FabricObject 這個(gè)物體基類繼承 EventCenter,這樣每個(gè)物體也有了發(fā)布訂閱的功能。有同學(xué)可能會(huì)問,上面的代碼只看到了 emit 事件,怎么沒看到 on 和 off 事件呢?因?yàn)橹罢f了,庫或者插件一般只提供鉤子,上面 emit 的地方就可以稱作鉤子(怎么感覺有點(diǎn)像埋點(diǎn)??),而 on 和 off 事件則是我們開發(fā)時(shí)才需要寫的。

有同學(xué)可能還是會(huì)疑惑為什么要這樣,其實(shí)你把這個(gè)當(dāng)做一種好的寫法記住就行了,算是經(jīng)驗(yàn)總結(jié),寫多了就能慢慢體會(huì)到?;蛘呶覀兛梢灶惐认聻g覽器的事件監(jiān)聽,想想頁面中的元素是不是都可以有點(diǎn)擊和鼠標(biāo)移入移出事件,那頁面上的元素種類也很多,它又是怎么實(shí)現(xiàn)的呢?其實(shí)它們都也繼承于 EventTarget 類,所以就有了事件,怎么證明呢?我們可以在控制臺(tái)隨便打印一個(gè)元素看下(父級(jí)的)結(jié)果????:

不能說是很像,只能說是一毛一樣。而且一般情況下,如果有事件系統(tǒng),我們大多都會(huì)把它放在頂層供其他類繼承,可見這個(gè)類是很重要的,大家都想要它??。

這里還是再補(bǔ)充一個(gè)小點(diǎn)吧??:就是關(guān)于事件名的命名,舉上面代碼中的兩個(gè)例子,大概長這樣:

canvas:clearedobject:moving,為什么要加個(gè)冒號(hào)嘞,直接寫一個(gè)英文單詞不香嗎?這個(gè)其實(shí)要看你系統(tǒng)復(fù)不復(fù)雜,簡單的話用一個(gè)單詞就可以了,復(fù)雜的話一般會(huì)像這樣寫 主體:動(dòng)作,主要是為了方便區(qū)分,僅此而已(也只是我覺得),比如小程序里面的事件名就是這樣。

小結(jié)

本個(gè)章節(jié)我們主要講解了圖片類和事件系統(tǒng)的實(shí)現(xiàn),希望你能夠記住以下幾點(diǎn):

  • 圖片是異步的,加載完成之后需要將其變成 img 標(biāo)簽,再調(diào)用 ctx.drawImage 才能繪制到畫布上
  • 如果有事件系統(tǒng),我們大多都會(huì)把它放在頂層供其他類繼承,可見它在前端有多受歡迎

然后這里是簡版 fabric.js 的代碼鏈接,有興趣的可以看看,當(dāng)然啦更建議直接去看 fabric.js 的源碼。好啦,本次分享就到這里,下個(gè)章節(jié)會(huì)分享的是 canvas 中動(dòng)畫的實(shí)現(xiàn)??,又是這個(gè)系列最重要的章節(jié)之一

canvas ~ 開始真正的交互啦(七)??

canvas 中如何實(shí)現(xiàn)物體的框選(六)??

canvas 中如何實(shí)現(xiàn)物體的點(diǎn)選(五)??

canvas 中物體邊框和控制點(diǎn)的實(shí)現(xiàn)(四)??

實(shí)現(xiàn)一個(gè)輕量 fabric.js 系列三(物體基類)??

實(shí)現(xiàn)一個(gè)輕量 fabric.js 系列二(畫布初始化)??

實(shí)現(xiàn)一個(gè)輕量 fabric.js 系列一(摸透 canvas)??

以上就是JS前端使用canvas實(shí)現(xiàn)擴(kuò)展物體類和事件派發(fā)的詳細(xì)內(nèi)容,更多關(guān)于canvas擴(kuò)展物體類事件派發(fā)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論