亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

JS前端使用canvas實(shí)現(xiàn)物體的點(diǎn)選示例

 更新時(shí)間:2022年08月02日 17:25:58   作者:尤水就下  
這篇文章主要為大家介紹了JS前端使用canvas實(shí)現(xiàn)物體的點(diǎn)選示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

上個(gè)章節(jié)中我們已經(jīng)給物體加上了被選中的效果,現(xiàn)在可以上點(diǎn)交互了,這個(gè)章節(jié)主要實(shí)現(xiàn)的就是物體的 hover 和 click 事件,當(dāng)鼠標(biāo) hover 到物體上時(shí),我們會(huì)改變鼠標(biāo)的樣式使其變成移動(dòng)的樣子;

當(dāng) hover 到控制點(diǎn)時(shí)則會(huì)變成對(duì)應(yīng)的操作樣式;

當(dāng) click 物體時(shí),會(huì)將物體變成激活態(tài),也就是展示邊框和控制點(diǎn)。話不多說(shuō),直接開(kāi)擼 ?? ?? ?? ??

hover 的實(shí)現(xiàn)

首先我們來(lái)處理鼠標(biāo)的 hover 事件,也就是 hover 到某個(gè)物體時(shí)把鼠標(biāo)變成移動(dòng)的樣式,如果是移到激活物體的控制點(diǎn)上就將鼠標(biāo)變成相應(yīng)的旋轉(zhuǎn)和縮放箭頭。具體要怎么做呢?

顯然 canvas 本身并不支持該功能,它就是一幅畫(huà),所有東西都被揉成可一團(tuán),所以我們是區(qū)分不了某個(gè)物體的。好在前面幾個(gè)章節(jié)中我們構(gòu)建了一個(gè) Canvas 類(lèi),把所有元素都放進(jìn)了 _objects 里面,現(xiàn)在只要從后往前遍歷 _objects 數(shù)組,找出與鼠標(biāo)有交集的第一個(gè)物體即可,找不到就是沒(méi)有選中任何物體則將鼠標(biāo)置為默認(rèn)樣式。之所以從后往前遍歷是因?yàn)槲覀兝L制是有順序的,越后面添加的物體會(huì)越后面繪制,因而層級(jí)也越高,會(huì)越先被點(diǎn)選,所以從后往前遍歷能提高效率,也符合視覺(jué)效果。

然后再提醒一下,我們物體都是有包圍盒的,所以每個(gè)物體都可以簡(jiǎn)化成一個(gè)矩形,于是要判斷鼠標(biāo)是否 hover 到某個(gè)物體上,就變成了判斷鼠標(biāo)是否 hover 到某個(gè)矩形上,更進(jìn)一步的就是判斷點(diǎn)是否在矩形內(nèi)部。

是不是好像有點(diǎn)碰撞檢測(cè)的味道呢??,只不過(guò)這里是點(diǎn)和矩形的碰撞。 顯然對(duì)于一個(gè)常規(guī)的沒(méi)有旋轉(zhuǎn)的矩形(top、left、width、height)和一個(gè)坐標(biāo)點(diǎn)(x, y),大家能很容易判斷出來(lái),就是 x >= left && x <= left + width && y >= top && y <= top + height 這樣簡(jiǎn)單判斷一下就行。那如果是個(gè)旋轉(zhuǎn)之后的矩形呢?誒。。。好像不怎么好搞??;

又或者是個(gè)平行四邊形呢?em。。。好像也不怎么好搞??;那如果是任意多邊形呢?啊。。。這??。。。。 我們需要一種更加通用的方式來(lái)判斷點(diǎn)在多邊形內(nèi)部,這就是實(shí)打?qū)嵉臄?shù)學(xué)知識(shí)了。一般情況下,遇到了這種問(wèn)題可以去搜一下相關(guān)解法然后 copy 過(guò)來(lái),這里我會(huì)盡量解釋的明白一些(退后,此處要開(kāi)始裝13了)。

  • 我們知道一個(gè)多邊形其實(shí)是由多條線段組成的封閉圖形,相當(dāng)于這個(gè)多邊形將世界分成了里外兩個(gè)部分,一部分在封閉區(qū)域里面,一部分在封閉區(qū)域外面。
  • 現(xiàn)在假設(shè)我們?cè)谌我庖稽c(diǎn)(鼠標(biāo)坐標(biāo)點(diǎn)),我們可以沿著該點(diǎn)向 x 軸方向做一條射線,然后計(jì)算出射線與多邊形邊的交點(diǎn)個(gè)數(shù),如果交點(diǎn)為偶數(shù)個(gè),則說(shuō)明點(diǎn)在多邊形外部。
  • 如果交點(diǎn)為奇數(shù)個(gè),則說(shuō)明點(diǎn)在多邊形內(nèi)部。這個(gè)現(xiàn)象很有趣??,大家可以在紙上試著畫(huà)一下,隨便畫(huà)個(gè)多邊形都可以,看看是不是符合上面這個(gè)規(guī)律。

可能你畫(huà)了幾個(gè)多邊形發(fā)現(xiàn)這個(gè)方法確實(shí)是適用的,但是卻不明白為什么我們可以用奇偶數(shù)來(lái)判斷點(diǎn)是否在多邊形內(nèi)部呢?這里有個(gè)通俗易懂的解釋?zhuān)?/p>

  • 我們可以認(rèn)為在多邊形的每條邊上都有一個(gè)小門(mén),經(jīng)過(guò)一條邊就相當(dāng)于打開(kāi)了一扇小門(mén),假設(shè)我們?cè)诙噙呅瓮饷?,那么如果我們打開(kāi)過(guò)兩個(gè)小門(mén)(偶數(shù)),說(shuō)明我們進(jìn)去了又出來(lái)了(點(diǎn)在外面);
  • 如果我們只打開(kāi)了一個(gè)小門(mén),說(shuō)明我們出去了但沒(méi)回來(lái)(點(diǎn)在里面)。
  • 應(yīng)用到實(shí)際生活中就是當(dāng)你的小區(qū)被劃為疫情管控區(qū)的時(shí)候,這個(gè)管控區(qū)就相當(dāng)于是一個(gè)多邊形,你在小區(qū)里面(多邊形內(nèi))無(wú)聊了,想要出去溜達(dá),你就必須經(jīng)過(guò)一個(gè)大門(mén)(一條邊),才能到達(dá)管控區(qū)外面的世界(多邊形外)。哇??。。。這個(gè)比喻真的是恰到好處(自己都覺(jué)得棒??)。

當(dāng)然聰明的同學(xué)肯定也想到了這種方法好像會(huì)有一些問(wèn)題,比如:

1、點(diǎn)恰好在多邊形上

2、射線經(jīng)過(guò)多邊形的頂點(diǎn)

3、射線與多邊形的邊重合 確實(shí)是這樣,所以針對(duì)以上三種情況,我們還需要再加一些額外的判斷條件。

  • 1、對(duì)于第一點(diǎn):需要判斷點(diǎn)是否在多邊形的邊上,當(dāng)然這種臨界狀態(tài)你說(shuō)在里在外都可以
  • 2、對(duì)于第二點(diǎn):每個(gè)頂點(diǎn)肯定會(huì)有兩條邊與之相連,如果兩條邊在射線的同一側(cè),我們就算做兩個(gè)交點(diǎn);如果兩條邊分別在射線的兩邊,就算做一個(gè)交點(diǎn)??梢杂脴O限的思想去理解,當(dāng)兩條邊在同側(cè)的話,取一條無(wú)限靠近該射線的水平線,顯然新的水平線會(huì)和兩條邊都相交;而當(dāng)兩條邊在異側(cè)的話,同樣可以取一條無(wú)限靠近該射線的水平線,顯然新的水平線只會(huì)與其中一條邊相交(這個(gè)思想也是真妙啊??)。
  • 3、對(duì)于第三點(diǎn):和第二點(diǎn)思想差不多,采用極限思想,把這個(gè)重合的邊想象成一個(gè)點(diǎn)即可,然后也要分與重合邊相鄰的兩條邊在同側(cè)還是異側(cè)兩種情況。

可能你還是不懂,所以這里畫(huà)了個(gè)示意圖,咱們看圖說(shuō)話:

其實(shí)上面所說(shuō)的方法有個(gè)專(zhuān)業(yè)的名字叫做射線檢測(cè)法,它其實(shí)可以 360° 任選方向的,只不過(guò)我們通常用水平線來(lái)算,這樣會(huì)比較簡(jiǎn)單點(diǎn)。

  • 另外射線檢測(cè)法還有一個(gè)最根本的原因就是射線的無(wú)窮遠(yuǎn)處一定在多邊形外,這樣我們才能根據(jù)交點(diǎn)的奇偶性來(lái)倒推位置關(guān)系。
  • 數(shù)學(xué)就是這么巧妙的和前端結(jié)合起來(lái)了,一些復(fù)雜的效果歸根到底還是數(shù)學(xué)的抽象。
  • 不過(guò)雖然知道了大概原理,我們也不一定能寫(xiě)出代碼來(lái)??,所以這里附上一些 fabric.js 中的核心代碼片段,有興趣的可以看看(有注釋的,放心食用??):
class Canvas {
    _initEvents() {
        // 首先肯定要添加事件監(jiān)聽(tīng)啦
        Util.addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove.bind(this));
    }
    _onMouseMove(e: MouseEvent) {
        // 如果是 hover 事件,我們只需要改變鼠標(biāo)樣式,并不會(huì)重新渲染
        const style = this.upperCanvasEl.style;
        // findTarget 的過(guò)程就是看鼠標(biāo)有沒(méi)有 hover 到某個(gè)物體上
        const target = this.findTarget(e);
        // 設(shè)置鼠標(biāo)樣式
        if (target) {
            this._setCursorFromEvent(e, target);
        } else {
            style.cursor = this.defaultCursor;
        }
    }
    /** 檢測(cè)是否有物體在鼠標(biāo)位置 */
    findTarget(e: MouseEvent): FabricObject {
        let target;
        // 從后往前遍歷所有物體,判斷鼠標(biāo)點(diǎn)是否在物體包圍盒內(nèi)
        for (let i = this._objects.length; i--; ) {
            const object = this._objects[i];
            if (object && this.containsPoint(e, object)) {
                target = object;
                break;
            }
        }
        if (target) return target;
    }
}
class FabricObject {
    /**
     * 射線檢測(cè)法:以鼠標(biāo)坐標(biāo)點(diǎn)為參照,水平向右做一條射線,求坐標(biāo)點(diǎn)與多邊形的交點(diǎn)個(gè)數(shù)
     * 如果和物體相交的個(gè)數(shù)為偶數(shù)點(diǎn)則點(diǎn)在物體外部;如果為奇數(shù)點(diǎn)則點(diǎn)在內(nèi)部
     * 在 fabric 中的點(diǎn)選多邊形其實(shí)就是點(diǎn)選矩形,所以針對(duì)矩形做了一些優(yōu)化
     */
    _findCrossPoints(ex: number, ey: number, lines): number {
        let b1, // 射線的斜率
            b2, // 邊的斜率
            a1,
            a2,
            xi, // 射線與邊的交點(diǎn) x
            // yi, // 射線與邊的交點(diǎn) y
            xcount = 0,
            iLine; // 當(dāng)前邊
        // 遍歷包圍盒的四條邊
        for (let lineKey in lines) {
            iLine = lines[lineKey];
            // 優(yōu)化1:如果邊的兩個(gè)端點(diǎn)的 y 值都小于鼠標(biāo)點(diǎn)的 y 值,則跳過(guò)
            if (iLine.o.y < ey && iLine.d.y < ey) continue;
            // 優(yōu)化2:如果邊的兩個(gè)端點(diǎn)的 y 值都大于等于鼠標(biāo)點(diǎn)的 y 值,則跳過(guò)
            if (iLine.o.y >= ey && iLine.d.y >= ey) continue;
            // 優(yōu)化3:如果邊是一條垂線
            if (iLine.o.x === iLine.d.x && iLine.o.x >= ex) {
                xi = iLine.o.x;
                // yi = ey;
            } else {
                // 執(zhí)行到這里就是一條普通斜線段了
                // 用 y=kx+b 簡(jiǎn)單算下射線與邊的交點(diǎn)即可
                b1 = 0;
                b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x);
                a1 = ey - b1 * ex;
                a2 = iLine.o.y - b2 * iLine.o.x;
                xi = -(a1 - a2) / (b1 - b2);
                // yi = a1 + b1 * xi;
            }
            // 只需要計(jì)數(shù) xi >= ex 的情況
            if (xi >= ex) {
                xcount += 1;
            }
            // 優(yōu)化4:因?yàn)?fabric 中的點(diǎn)選只需要用到矩形,所以根據(jù)矩形的特質(zhì),頂多只有兩個(gè)交點(diǎn),于是就可以提前結(jié)束循環(huán)
            if (xcount === 2) {
                break;
            }
        }
        return xcount;
    }
}

至于物體周?chē)膸讉€(gè)控制點(diǎn)呢,也是一樣的,它們也是個(gè)矩形,所以要判斷點(diǎn)是否在控制點(diǎn)內(nèi)也是一樣的套路一樣的邏輯,這里就不展開(kāi)了。

click 的實(shí)現(xiàn)

再來(lái)說(shuō)說(shuō)點(diǎn)選是怎么實(shí)現(xiàn)的,這個(gè)也很簡(jiǎn)單,和 hover 的道理如出一轍,我們能夠獲取到 hover 時(shí)的物體,同樣也能夠獲取到點(diǎn)擊時(shí)的物體,都是判斷點(diǎn)是否在矩形內(nèi)(你說(shuō)巧不巧),然后將該物體的 active 屬性設(shè)置為 true,其他物體設(shè)置為 false 即可,這樣我們重新渲染的時(shí)候,物體會(huì)根據(jù) active 屬性自動(dòng)調(diào)用 drawBordersdrawControls 方法,看起來(lái)物體就被選中了,注意 hover 的時(shí)候不會(huì)導(dǎo)致重繪,只改變鼠標(biāo)樣式;

點(diǎn)選會(huì)導(dǎo)致重繪并改變鼠標(biāo)樣式。另外我們還可以對(duì)點(diǎn)選進(jìn)行一些優(yōu)化,比如記錄最近一個(gè)激活的物體,然后點(diǎn)選的時(shí)候先判斷鼠標(biāo)點(diǎn)是否在最近一個(gè)激活物體的內(nèi)部,如果在,就可以省去遍歷的過(guò)程了。

矩形的坐標(biāo)哪來(lái)的

其實(shí)上面的講解我特意漏說(shuō)了一個(gè)點(diǎn),就是包圍盒和控制點(diǎn)的那個(gè)矩形是怎么來(lái)的,目前我們只是單純的畫(huà)出了邊框和控制點(diǎn),但是并沒(méi)有記錄它們的寬高和位置,所以現(xiàn)在我們需要在初始化物體的時(shí)候進(jìn)行一些簡(jiǎn)單計(jì)算并用變量 oCoords 保存起來(lái),就像這樣:

export interface Coords {
    /** 左上控制點(diǎn) */
    tl: Coord;
    /** 右上控制點(diǎn) */
    tr: Coord;
    /** 右下控制點(diǎn) */
    br: Coord;
    /** 左下控制點(diǎn) */
    bl: Coord;
    /** 左中控制點(diǎn) */
    ml: Coord;
    /** 上中控制點(diǎn) */
    mt: Coord;
    /** 右中控制點(diǎn) */
    mr: Coord;
    /** 下中控制點(diǎn) */
    mb: Coord;
    /** 上中旋轉(zhuǎn)控制點(diǎn) */
    mtr: Coord;
}
class Canvas {
    _initObject(obj: FabricObject) {
        obj.setCoords(); // 記錄控制點(diǎn)位置和大小,其實(shí)就是各個(gè)矩形的頂點(diǎn)坐標(biāo)
        obj.canvas = this;
    }
}

具體計(jì)算方法比較繁瑣,我就不貼上來(lái)了,有興趣的可以去看看源碼,這里就簡(jiǎn)單放個(gè)圖:

以上圖的矩形為例子,其實(shí)就是算出上圖矩形四個(gè)頂點(diǎn)的位置,寫(xiě)的時(shí)候你只需要考慮一個(gè)點(diǎn)(比如圖中右上角的頂點(diǎn))是怎么算的就行,其他點(diǎn)都是一樣的,相信你慢慢算一定可以算出來(lái)的??。

當(dāng)然如果物體的某些屬性改變了,比如物體經(jīng)過(guò)變換,記得需要及時(shí)更新 oCoords 的值。

點(diǎn)在多邊形內(nèi)的其他判斷方法

其實(shí)判斷點(diǎn)是否在多邊形內(nèi)部還有其他方法,比如:

  • 用 canvas 自身的 api isPointInPath
  • 將多邊形切割成多個(gè)三角形,然后判斷點(diǎn)是否在某個(gè)三角形內(nèi)部
  • 轉(zhuǎn)角累加法
  • 面積法

... 這里我稍微說(shuō)下另一種比較有意思的方法,如果不理解射線檢測(cè)法的同學(xué),我們還能這么搞:假設(shè)矩形旋轉(zhuǎn)了一定角度,那我們將鼠標(biāo)坐標(biāo)點(diǎn)也旋轉(zhuǎn)一下,這樣旋轉(zhuǎn)后的坐標(biāo)點(diǎn)就不就又和矩形是同一個(gè)水平垂直方向嗎,就像下圖這樣????:

上述方法的核心要點(diǎn)就是將鼠標(biāo)點(diǎn)換算成物體自身坐標(biāo)系下的點(diǎn)(寫(xiě)成矩陣的形式會(huì)比較方便點(diǎn)),然后再用原始的方法判斷即可,是不是看起來(lái)也挺方便的樣子。

穿透

現(xiàn)在我們來(lái)擴(kuò)充下另外一個(gè)知識(shí)點(diǎn),就是目前我們點(diǎn)選物體的時(shí)候,其實(shí)是點(diǎn)選包圍盒,當(dāng)點(diǎn)到物體四周空白區(qū)域的時(shí)候,物體也是會(huì)被選中的,如果不想把空白區(qū)域也算在物體的點(diǎn)擊范圍內(nèi)(比如 png 圖片),那該怎么做呢?

這個(gè)東西挺有意思的,可以停個(gè)幾秒種,思考一下下??。。。。 顯然我們要在上文所說(shuō)的 findTarget 中做文章,除了判斷點(diǎn)是否在包圍盒內(nèi),還要進(jìn)一步判斷點(diǎn)擊的是不是空白的地方,所謂空白,一定程度上可以理解成是透明的地方。

于是這就要用到前幾個(gè)章節(jié)提到過(guò)的第三個(gè)畫(huà)布 cacheCanvasEl 緩存畫(huà)布,在點(diǎn)擊到了包圍盒之后我們還需要把這個(gè)物體畫(huà)到這個(gè)緩存畫(huà)布上,然后用 getImageData 來(lái)獲取鼠標(biāo)位置所在點(diǎn)的像素信息,當(dāng)然我們?cè)试S有誤差,所以會(huì)取這個(gè)鼠標(biāo)點(diǎn)周?chē)囊恍K正方形的像素信息,接著遍歷每個(gè)像素,如果找到一個(gè)像素中 rgba 的 a 的值 > 0 就說(shuō)明至少有一個(gè)顏色存在,亦即不透明,退出循環(huán),否則就是透明的,最后清除 getImageData 變量,清除緩沖層畫(huà)布即可。

是不是有種豁然開(kāi)朗的感覺(jué)??,有了思路,代碼實(shí)現(xiàn)起來(lái)就比較簡(jiǎn)單了:

class Canvas {
    /**
     * 用緩沖層判斷物體是否透明,目前默認(rèn)都是不透明,可以加一些參數(shù)屬性,比如允許有幾個(gè)像素的誤差
     * @param {FabricObject} target 物體
     * @param {number} x 鼠標(biāo)的 x 值
     * @param {number} y 鼠標(biāo)的 y 值
     * @param {number} tolerance 允許鼠標(biāo)的誤差范圍
     * @returns
     */
    _isTargetTransparent(target: FabricObject, x: number, y: number, tolerance: number = 0) {
        // 1、在緩沖層繪制物體
        // 2、通過(guò) getImageData 獲取鼠標(biāo)位置的像素?cái)?shù)據(jù)信息
        // 3、遍歷像素?cái)?shù)據(jù),如果找到一個(gè) rgba 中的 a 的值 > 0 就說(shuō)明至少有一個(gè)顏色,亦即不透明,退出循環(huán)
        // 4、清空 getImageData 變量,并清除緩沖層畫(huà)布
        let cacheContext = this.contextCache;
        this._draw(cacheContext, target);
        if (tolerance > 0) { // 如果允許誤差
            if (x > tolerance) {
                x -= tolerance;
            } else {
                x = 0;
            }
            if (y > tolerance) {
                y -= tolerance;
            } else {
                y = 0;
            }
        }
        let isTransparent = true;
        let imageData = cacheContext.getImageData(x, y, tolerance * 2 || 1, tolerance * 2 || 1);
        for (let i = 3; i < imageData.data.length; i += 4) { // 只要看第四項(xiàng)透明度即可
            let temp = imageData.data[i];
            isTransparent = temp <= 0;
            if (isTransparent === false) break; // 找到一個(gè)顏色就停止
        }
        imageData = null;
        this.clearContext(cacheContext);
        return isTransparent;
    }
}

怎么樣,這個(gè)方法看起來(lái)還是有點(diǎn)意思的,而且通俗易懂。

當(dāng)然了,這對(duì)不同物體可以有不同的檢測(cè)方法:

比如物體是一個(gè)幾何圖形,假設(shè)是正多邊形,同樣的,我們希望選中的是正多邊形,而不是正多邊形包圍盒所形成的的矩形,這時(shí)候只需要把點(diǎn)選物體包圍盒的邏輯改成點(diǎn)選正多邊形的邏輯即可,同樣采用的是射線檢測(cè)法(怎么又繞回來(lái)了??);

如果物體是條線段,就變成了點(diǎn)是否在線上的檢測(cè);

如果是個(gè)圓,那就更簡(jiǎn)單了,諸如此類(lèi)。。。

此外還有一種空間換時(shí)間的取巧方法,就是在創(chuàng)建物體的時(shí)候在離屏 canvas 上多繪制一個(gè)和這個(gè)物體形狀大小一樣的純色物體,畫(huà)布上的物體都有各自的顏色并且唯一,然后做一個(gè) { color: object } 的映射,之后我們點(diǎn)選的時(shí)候主要是通過(guò)點(diǎn)擊坐標(biāo)獲取到對(duì)應(yīng)離屏 canvas 上的純顏色,再根據(jù)映射取出對(duì)應(yīng)的物體即可,這也是一種方法。

本章小結(jié)

這個(gè)章節(jié)我們主要實(shí)現(xiàn)了如何處理物體的 hover 和 click 事件,本質(zhì)其實(shí)就是如何如何判斷一個(gè)點(diǎn)在多邊形內(nèi)部,你可能聽(tīng)過(guò)一些方法,但不知道實(shí)際開(kāi)發(fā)時(shí)是怎么應(yīng)用上的,希望讀完本章你能記得射線檢測(cè)法的應(yīng)用,它的核心就是越過(guò)一條邊里外兩個(gè)世界就會(huì)互相交換。然后這里是簡(jiǎn)版 fabric.js 的代碼鏈接,有興趣的可以看看。好啦,本次分享就到這里,有什么問(wèn)題歡迎點(diǎn)贊評(píng)論留言,我們下期再見(jiàn),拜拜????

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

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

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

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

以上就是JS前端使用canvas實(shí)現(xiàn)物體的點(diǎn)選示例的詳細(xì)內(nèi)容,更多關(guān)于JS前端canvas物體點(diǎn)選的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論