您好,登錄后才能下訂單哦!
今天小編給大家分享一下JS前端如何使用canvas實現物體的點選的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
首先我們來處理鼠標的 hover 事件,也就是 hover 到某個物體時把鼠標變成移動的樣式,如果是移到激活物體的控制點上就將鼠標變成相應的旋轉和縮放箭頭。具體要怎么做呢?
顯然 canvas 本身并不支持該功能,它就是一幅畫,所有東西都被揉成可一團,所以我們是區分不了某個物體的。好在前面幾個章節中我們構建了一個 Canvas 類,把所有元素都放進了 _objects
里面,現在只要從后往前遍歷 _objects
數組,找出與鼠標有交集的第一個物體即可,找不到就是沒有選中任何物體則將鼠標置為默認樣式。之所以從后往前遍歷是因為我們繪制是有順序的,越后面添加的物體會越后面繪制,因而層級也越高,會越先被點選,所以從后往前遍歷能提高效率,也符合視覺效果。
然后再提醒一下,我們物體都是有包圍盒的,所以每個物體都可以簡化成一個矩形,于是要判斷鼠標是否 hover 到某個物體上,就變成了判斷鼠標是否 hover 到某個矩形上,更進一步的就是判斷點是否在矩形內部。
是不是好像有點碰撞檢測的味道呢????,只不過這里是點和矩形的碰撞。 顯然對于一個常規的沒有旋轉的矩形(top、left、width、height)和一個坐標點(x, y),大家能很容易判斷出來,就是 x >= left && x <= left + width && y >= top && y <= top + height 這樣簡單判斷一下就行。那如果是個旋轉之后的矩形呢?誒。。。好像不怎么好搞????;
又或者是個平行四邊形呢?em。。。好像也不怎么好搞????;那如果是任意多邊形呢?啊。。。這????。。。。 我們需要一種更加通用的方式來判斷點在多邊形內部,這就是實打實的數學知識了。一般情況下,遇到了這種問題可以去搜一下相關解法然后 copy 過來,這里我會盡量解釋的明白一些(退后,此處要開始裝13了)。
我們知道一個多邊形其實是由多條線段組成的封閉圖形,相當于這個多邊形將世界分成了里外兩個部分,一部分在封閉區域里面,一部分在封閉區域外面。
現在假設我們在任意一點(鼠標坐標點),我們可以沿著該點向 x 軸方向做一條射線,然后計算出射線與多邊形邊的交點個數,如果交點為偶數個,則說明點在多邊形外部。
如果交點為奇數個,則說明點在多邊形內部。這個現象很有趣????,大家可以在紙上試著畫一下,隨便畫個多邊形都可以,看看是不是符合上面這個規律。
可能你畫了幾個多邊形發現這個方法確實是適用的,但是卻不明白為什么我們可以用奇偶數來判斷點是否在多邊形內部呢?這里有個通俗易懂的解釋:
我們可以認為在多邊形的每條邊上都有一個小門,經過一條邊就相當于打開了一扇小門,假設我們在多邊形外面,那么如果我們打開過兩個小門(偶數),說明我們進去了又出來了(點在外面);
如果我們只打開了一個小門,說明我們出去了但沒回來(點在里面)。
應用到實際生活中就是當你的小區被劃為疫情管控區的時候,這個管控區就相當于是一個多邊形,你在小區里面(多邊形內)無聊了,想要出去溜達,你就必須經過一個大門(一條邊),才能到達管控區外面的世界(多邊形外)。哇????。。。這個比喻真的是恰到好處(自己都覺得棒????)。
當然聰明的同學肯定也想到了這種方法好像會有一些問題,比如:
1、點恰好在多邊形上
2、射線經過多邊形的頂點
3、射線與多邊形的邊重合 確實是這樣,所以針對以上三種情況,我們還需要再加一些額外的判斷條件。
1、對于第一點:需要判斷點是否在多邊形的邊上,當然這種臨界狀態你說在里在外都可以
2、對于第二點:每個頂點肯定會有兩條邊與之相連,如果兩條邊在射線的同一側,我們就算做兩個交點;如果兩條邊分別在射線的兩邊,就算做一個交點。可以用極限的思想去理解,當兩條邊在同側的話,取一條無限靠近該射線的水平線,顯然新的水平線會和兩條邊都相交;而當兩條邊在異側的話,同樣可以取一條無限靠近該射線的水平線,顯然新的水平線只會與其中一條邊相交(這個思想也是真妙啊????)。
3、對于第三點:和第二點思想差不多,采用極限思想,把這個重合的邊想象成一個點即可,然后也要分與重合邊相鄰的兩條邊在同側還是異側兩種情況。
可能你還是不懂,所以這里畫了個示意圖,咱們看圖說話:
其實上面所說的方法有個專業的名字叫做射線檢測法,它其實可以 360° 任選方向的,只不過我們通常用水平線來算,這樣會比較簡單點。
另外射線檢測法還有一個最根本的原因就是射線的無窮遠處一定在多邊形外,這樣我們才能根據交點的奇偶性來倒推位置關系。
數學就是這么巧妙的和前端結合起來了,一些復雜的效果歸根到底還是數學的抽象。
不過雖然知道了大概原理,我們也不一定能寫出代碼來????,所以這里附上一些 fabric.js 中的核心代碼片段,有興趣的可以看看(有注釋的,放心食用????):
class Canvas { _initEvents() { // 首先肯定要添加事件監聽啦 Util.addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove.bind(this)); } _onMouseMove(e: MouseEvent) { // 如果是 hover 事件,我們只需要改變鼠標樣式,并不會重新渲染 const style = this.upperCanvasEl.style; // findTarget 的過程就是看鼠標有沒有 hover 到某個物體上 const target = this.findTarget(e); // 設置鼠標樣式 if (target) { this._setCursorFromEvent(e, target); } else { style.cursor = this.defaultCursor; } } /** 檢測是否有物體在鼠標位置 */ findTarget(e: MouseEvent): FabricObject { let target; // 從后往前遍歷所有物體,判斷鼠標點是否在物體包圍盒內 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 { /** * 射線檢測法:以鼠標坐標點為參照,水平向右做一條射線,求坐標點與多邊形的交點個數 * 如果和物體相交的個數為偶數點則點在物體外部;如果為奇數點則點在內部 * 在 fabric 中的點選多邊形其實就是點選矩形,所以針對矩形做了一些優化 */ _findCrossPoints(ex: number, ey: number, lines): number { let b1, // 射線的斜率 b2, // 邊的斜率 a1, a2, xi, // 射線與邊的交點 x // yi, // 射線與邊的交點 y xcount = 0, iLine; // 當前邊 // 遍歷包圍盒的四條邊 for (let lineKey in lines) { iLine = lines[lineKey]; // 優化1:如果邊的兩個端點的 y 值都小于鼠標點的 y 值,則跳過 if (iLine.o.y < ey && iLine.d.y < ey) continue; // 優化2:如果邊的兩個端點的 y 值都大于等于鼠標點的 y 值,則跳過 if (iLine.o.y >= ey && iLine.d.y >= ey) continue; // 優化3:如果邊是一條垂線 if (iLine.o.x === iLine.d.x && iLine.o.x >= ex) { xi = iLine.o.x; // yi = ey; } else { // 執行到這里就是一條普通斜線段了 // 用 y=kx+b 簡單算下射線與邊的交點即可 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; } // 只需要計數 xi >= ex 的情況 if (xi >= ex) { xcount += 1; } // 優化4:因為 fabric 中的點選只需要用到矩形,所以根據矩形的特質,頂多只有兩個交點,于是就可以提前結束循環 if (xcount === 2) { break; } } return xcount; } }
至于物體周圍的幾個控制點呢,也是一樣的,它們也是個矩形,所以要判斷點是否在控制點內也是一樣的套路一樣的邏輯,這里就不展開了。
再來說說點選是怎么實現的,這個也很簡單,和 hover 的道理如出一轍,我們能夠獲取到 hover 時的物體,同樣也能夠獲取到點擊時的物體,都是判斷點是否在矩形內(你說巧不巧),然后將該物體的 active 屬性設置為 true,其他物體設置為 false 即可,這樣我們重新渲染的時候,物體會根據 active 屬性自動調用 drawBorders
和 drawControls
方法,看起來物體就被選中了,注意 hover 的時候不會導致重繪,只改變鼠標樣式;
點選會導致重繪并改變鼠標樣式。另外我們還可以對點選進行一些優化,比如記錄最近一個激活的物體,然后點選的時候先判斷鼠標點是否在最近一個激活物體的內部,如果在,就可以省去遍歷的過程了。
其實上面的講解我特意漏說了一個點,就是包圍盒和控制點的那個矩形是怎么來的,目前我們只是單純的畫出了邊框和控制點,但是并沒有記錄它們的寬高和位置,所以現在我們需要在初始化物體的時候進行一些簡單計算并用變量 oCoords 保存起來,就像這樣:
export interface Coords { /** 左上控制點 */ tl: Coord; /** 右上控制點 */ tr: Coord; /** 右下控制點 */ br: Coord; /** 左下控制點 */ bl: Coord; /** 左中控制點 */ ml: Coord; /** 上中控制點 */ mt: Coord; /** 右中控制點 */ mr: Coord; /** 下中控制點 */ mb: Coord; /** 上中旋轉控制點 */ mtr: Coord; } class Canvas { _initObject(obj: FabricObject) { obj.setCoords(); // 記錄控制點位置和大小,其實就是各個矩形的頂點坐標 obj.canvas = this; } }
具體計算方法比較繁瑣,我就不貼上來了,有興趣的可以去看看源碼,這里就簡單放個圖:
以上圖的矩形為例子,其實就是算出上圖矩形四個頂點的位置,寫的時候你只需要考慮一個點(比如圖中右上角的頂點)是怎么算的就行,其他點都是一樣的,相信你慢慢算一定可以算出來的????。
當然如果物體的某些屬性改變了,比如物體經過變換,記得需要及時更新 oCoords 的值。
其實判斷點是否在多邊形內部還有其他方法,比如:
用 canvas 自身的 api isPointInPath
將多邊形切割成多個三角形,然后判斷點是否在某個三角形內部
轉角累加法
面積法
... 這里我稍微說下另一種比較有意思的方法,如果不理解射線檢測法的同學,我們還能這么搞:假設矩形旋轉了一定角度,那我們將鼠標坐標點也旋轉一下,這樣旋轉后的坐標點就不就又和矩形是同一個水平垂直方向嗎,就像下圖這樣????????:
上述方法的核心要點就是將鼠標點換算成物體自身坐標系下的點(寫成矩陣的形式會比較方便點),然后再用原始的方法判斷即可,是不是看起來也挺方便的樣子。
現在我們來擴充下另外一個知識點,就是目前我們點選物體的時候,其實是點選包圍盒,當點到物體四周空白區域的時候,物體也是會被選中的,如果不想把空白區域也算在物體的點擊范圍內(比如 png 圖片),那該怎么做呢?
這個東西挺有意思的,可以停個幾秒種,思考一下下????。。。。 顯然我們要在上文所說的 findTarget 中做文章,除了判斷點是否在包圍盒內,還要進一步判斷點擊的是不是空白的地方,所謂空白,一定程度上可以理解成是透明的地方。
于是這就要用到前幾個章節提到過的第三個畫布 cacheCanvasEl 緩存畫布,在點擊到了包圍盒之后我們還需要把這個物體畫到這個緩存畫布上,然后用 getImageData 來獲取鼠標位置所在點的像素信息,當然我們允許有誤差,所以會取這個鼠標點周圍的一小塊正方形的像素信息,接著遍歷每個像素,如果找到一個像素中 rgba 的 a 的值 > 0 就說明至少有一個顏色存在,亦即不透明,退出循環,否則就是透明的,最后清除 getImageData 變量,清除緩沖層畫布即可。
是不是有種豁然開朗的感覺????,有了思路,代碼實現起來就比較簡單了:
class Canvas { /** * 用緩沖層判斷物體是否透明,目前默認都是不透明,可以加一些參數屬性,比如允許有幾個像素的誤差 * @param {FabricObject} target 物體 * @param {number} x 鼠標的 x 值 * @param {number} y 鼠標的 y 值 * @param {number} tolerance 允許鼠標的誤差范圍 * @returns */ _isTargetTransparent(target: FabricObject, x: number, y: number, tolerance: number = 0) { // 1、在緩沖層繪制物體 // 2、通過 getImageData 獲取鼠標位置的像素數據信息 // 3、遍歷像素數據,如果找到一個 rgba 中的 a 的值 > 0 就說明至少有一個顏色,亦即不透明,退出循環 // 4、清空 getImageData 變量,并清除緩沖層畫布 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) { // 只要看第四項透明度即可 let temp = imageData.data[i]; isTransparent = temp <= 0; if (isTransparent === false) break; // 找到一個顏色就停止 } imageData = null; this.clearContext(cacheContext); return isTransparent; } }
怎么樣,這個方法看起來還是有點意思的,而且通俗易懂。
當然了,這對不同物體可以有不同的檢測方法:
比如物體是一個幾何圖形,假設是正多邊形,同樣的,我們希望選中的是正多邊形,而不是正多邊形包圍盒所形成的的矩形,這時候只需要把點選物體包圍盒的邏輯改成點選正多邊形的邏輯即可,同樣采用的是射線檢測法(怎么又繞回來了????);
如果物體是條線段,就變成了點是否在線上的檢測;
如果是個圓,那就更簡單了,諸如此類。。。
此外還有一種空間換時間的取巧方法,就是在創建物體的時候在離屏 canvas 上多繪制一個和這個物體形狀大小一樣的純色物體,畫布上的物體都有各自的顏色并且唯一,然后做一個 { color: object } 的映射,之后我們點選的時候主要是通過點擊坐標獲取到對應離屏 canvas 上的純顏色,再根據映射取出對應的物體即可,這也是一種方法。
以上就是“JS前端如何使用canvas實現物體的點選”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。