本文主要介紹了如何使用canvas繪制可移動網(wǎng)格的示例代碼,分享給大家,具體如下:
這個是真實項目中遇到的需求,我把它抽離出來,屏蔽了那些業(yè)務(wù)相關(guān)的東西,僅從代碼角度來考慮這個問題。首先網(wǎng)格大小可配置,每個頂點是可以移動的??吹竭@個問題,不知道各位是怎么去思考的。就先來說說我自己的思路。
首先需要有一個起點,這樣就能確定網(wǎng)格所在的位置,其次就是網(wǎng)格中的每個正方形(我們就按正方形來思考,這樣簡單一點)的邊長是多少,另外每個頂點移動的時候,邊也需要跟著移動。
所以其實要存儲的就只有兩類對象,一類就是線,另外就是頂點。
如何存儲頂點和線呢?這里用了一個庫fabric.js
,就比較容易的創(chuàng)建頂點和邊的對象,并且它也提供了移動邊的方法,但是問題也同時出現(xiàn)了:按照上面的顯示,一個點最多關(guān)聯(lián)4條邊,最少也關(guān)聯(lián)了2條邊,如何表示這種頂點和邊的關(guān)聯(lián)關(guān)系呢?
先想到就是使用數(shù)組來存儲頂點和線,然后再根據(jù)線中包含的頂點坐標(biāo)來判斷這個線是否和某個頂點相連,如果是的話,則將將其加入到頂點的關(guān)聯(lián)屬性中。后面當(dāng)移動頂點的時候,根據(jù)頂點拿到其關(guān)聯(lián)的線,去動態(tài)改變線的坐標(biāo),這樣就能實現(xiàn)上面的那種效果了。
下面根據(jù)以上分析,我們來實現(xiàn)代碼。首先需要存儲的對象有頂點、邊。然后根據(jù)起點坐標(biāo)以及每個小矩形的邊長,很容易就可以計算出所有的頂點坐標(biāo)。
function Grid({node, unit, row, col, matrix = []}) { // 存儲頂點 this.vertexes = []; // 存儲邊 this.lines = []; // 根據(jù)起點坐標(biāo)以及單位邊長計算 for (let i = 0; i <= row; i++) { for (let j = 0; j <= col; j++) { const newVertex = makeRect(node.x + i * unit, node.y + j * unit); this.vertexes.push(newVertex); } } // 添加頂點對象的事件監(jiān)聽器 this.addListener(); }
那么邊怎么計算呢,構(gòu)造邊的話,只需要兩個頂點就可以連成邊,因此我們可以選擇遍歷頂點來構(gòu)造邊,但是這樣的話會造成重復(fù)的邊,而我們只需要一條邊就可以了,不然移動的話,你會發(fā)現(xiàn)移動完,下面還會顯示一條重疊的邊。當(dāng)然其實最重要的原因就是效率問題,如果不去重的話,會導(dǎo)致計算的時間復(fù)雜度過高。
現(xiàn)在有兩種方法來解決,一種就是給頂點做標(biāo)記,當(dāng)前做線的兩端的頂點已經(jīng)標(biāo)記過了,那么就跳過當(dāng)前輪的遍歷。另外一種方法,就是可以根據(jù)網(wǎng)格這種特定的形狀來獲取邊,如下圖,按照兩種不同的顏色來計算水平的邊和垂直的邊。
這樣的話,水平方向,就每行兩兩構(gòu)成邊,垂直方向,就按照一定的間隔連接兩個頂點構(gòu)成邊。這里因為后面需要傳給算法的格式是二維數(shù)組,因此就使用了這個方法。
// ...省略了 // 構(gòu)造矩陣 this.matrix = []; let index = -1; for (let i = 0; i < this.vertexes.length; i++) { if (i % (col + 1) === 0) { index++; this.matrix[index] = []; } this.matrix[index].push(this.vertexes[i]); } // 根據(jù)矩陣添加邊 let idx = 0; for (let i = 0; i < this.matrix.length; i++) { for (let j = 0; j < this.matrix[i].length; j++) { // 交叉渲染邊,這樣能夠在可視區(qū)內(nèi)優(yōu)先展示 this.matrix[i][j+1] && this.makeLine(this.matrix[i][j], this.matrix[i][j+1]); this.vertexes[idx + col + 1] && this.makeLine(this.vertexes[idx], this.vertexes[idx + col + 1]); idx++; } }
后面就是找每個頂點關(guān)聯(lián)了幾條邊
for (let i = 0; i < this.vertexes.length; i++) { const vertex = this.vertexes[i]; // 根據(jù)頂點的坐標(biāo)是否是邊的兩端的開始或結(jié)束坐標(biāo)來判斷頂點是否與這條邊關(guān)聯(lián) const associateLines = this.lines.filter(item => { return (item.x1 === vertex.left && item.y1 === vertex.top) || (item.x2 === vertext.left && item.y2 === vertex.top); }); vertex.lines = associateLines; }
眼精的同學(xué)肯定一眼就看出來啦,這個時間復(fù)雜度太高了。所以雖然網(wǎng)格畫出來了,但是當(dāng)頂點數(shù)量過多的時候,計算時間太長,導(dǎo)致瀏覽器卡住了了差不多2s往上,當(dāng)水平方向有50個頂點,垂直方向有50個頂點,就能明顯看到瀏覽器的卡頓,此時如果有輸入框之類的交互UI,是無法做任何操作的,這肯定也是不行滴。
那么有什么方法能夠高效的找到頂點和邊之間的關(guān)聯(lián)呢?這里就不賣關(guān)子了,當(dāng)然可能還有其他更好的方法,但是筆者知識所限,只能到這啦。
解決辦法就是圖這種結(jié)構(gòu),因為圖的邊可以使用鄰接表或者是鄰接矩陣來存儲,這樣如果我存儲了一個頂點,那么與這個頂點關(guān)聯(lián)的邊其實就確定了,也就是說,我們在添加頂點的時候,就順便
解決了這種頂點的關(guān)聯(lián)問題,不再需要再次遍歷所有的邊來找關(guān)聯(lián)了。(這里就不詳細(xì)介紹圖這種數(shù)據(jù)結(jié)構(gòu)了,有興趣的同學(xué)可以自己查找資料,實際這里運用圖的地方也就是這個邊和頂點的關(guān)聯(lián)關(guān)系,其他什么圖的遍歷都沒有用到)
我們來改進一下我們的代碼。
function Grid({node, unit, row, col, matrix = []}) { this.vertexes = []; this.lines = []; this.edges = new Map(); this.addEdges = addEdges; this.addVertexes = addVertexes; }
這里添加了一個新的屬性edges
,來存儲頂點和邊的映射關(guān)系。其他的步驟和先前都是一樣的,只是更換了添加頂點和邊的方法,什么意思呢,看代碼其實明白了:
function Grid({node, unit, row, col, matrix = []}) { // ...省略 // 根據(jù)矩陣添加邊 let idx = 0; for (let i = 0; i < this.matrix.length; i++) { for (let j = 0; j < this.matrix[i].length; j++) { // 交叉渲染邊,這樣能夠在可視區(qū)內(nèi)優(yōu)先展示 this.matrix[i][j+1] && this.addEdges(this.matrix[i][j], this.matrix[i][j+1]); this.vertexes[idx + col + 1] && this.addEdges(this.vertexes[idx], this.vertexes[idx + col + 1]); idx++; } } // 將邊關(guān)聯(lián)到頂點 this.edges.forEach((value, key) => { key.lines = value; }); }
這里我們就將復(fù)雜度為O(mn)
的計算降低為了O(n)
,這里m
為lines
的長度,n
為vertexes
的長度。然后再來看下此時計算100*100的頂點數(shù),計算時間只有200ms
,已經(jīng)能夠滿足我的需求了。那么圖是如何實現(xiàn)這種關(guān)聯(lián)的呢,其實就是每次添加邊的時候,將邊的兩個頂點同時添加進關(guān)聯(lián)關(guān)系中,也就是Map
的結(jié)構(gòu)中。
function addEdges(v, w) { const line = makeLine({point1: v, point2: w}); // 頂點v關(guān)聯(lián)了邊line this.edges.get(v).push(line); // 頂點w也同時關(guān)聯(lián)了邊line this.edges.get(w).push(line); this.lines.push(line); } function addVertexes(v) { this.vertexes.push(v); // 給每個頂點設(shè)置一個Map結(jié)構(gòu) this.edges.set(v, []); }
這樣計算完所有的頂點之后,實際頂點關(guān)聯(lián)的邊也都確定了,最后只需要遍歷一下這些edges
就可以了。
完成了這些之后,開開心心的調(diào)用fabric
的api,將這些對象添加進canvas
中就可以了。
// fabric的API,添加fabric對象到畫布中 canvas.add(...this.vertexes); canvas.add(...this.lines);
好了,大功告成,可以交差了。運行頁面,打開一看,好家伙,計算速度是快了很多,但是渲染的速度慘不忍睹,30*30的頂點數(shù)量,頁面還是有卡頓的情況,這是怎么回事呢?
仔細(xì)想想,添加這么多的對象到畫布中,計算量確實是非常大的,但是這里我們也無法改變這種渲染消耗。于是想到了一個折中的方法,就是利用時間切片,簡單來說,就是利用requestAnimationFrame
這個API,將渲染任務(wù)分割為一個一個的片段,在瀏覽器空閑時去渲染,這樣就不會去阻塞其他瀏覽器的任務(wù)。這里就涉及了一些瀏覽器渲染的相關(guān)知識。
function renderIdleCallback(canvas) { // 任務(wù)切片 const points = this.points.slice(); const lines = this.lines.slice(); const task = () => { // 清理canvas的時候,中斷后面的渲染 if (this.interrupt) return; if (!points.length && !lines.length) return; let slicePoint = [], sliceLine = []; for (let i = 0; i < 10; i++) { if (points.length) { const top = points.shift(); slicePoint.push(top); } if (lines.length) { const top = lines.shift(); sliceLine.push(top); } } canvas.add(...slicePoint); canvas.add(...sliceLine); window.requestAnimationFrame(task); } task(); }
上面的代碼加入了一個標(biāo)識符來中斷渲染,因為存在這樣一種情況,本次網(wǎng)格還沒有渲染完,就被清理掉又重新渲染,那么就需要停止上次的渲染,重新開始新的渲染了。
好了,到這里也就結(jié)束了。由于筆者知識淺薄,只能做到這種滿足需求的優(yōu)化了,更極致的優(yōu)化就要看各位大佬指點。同時此次嘗試也是筆者第一次將所學(xué)的數(shù)據(jù)結(jié)構(gòu)、優(yōu)化手段結(jié)合到項目中,成就感還是非常多的,也是感受到數(shù)據(jù)結(jié)構(gòu)算法對于程序員的重要性,如果想要突破自己的技術(shù)瓶頸,那么這也是繞不開的一個點。
到此這篇關(guān)于如何使用canvas繪制可移動網(wǎng)格的示例代碼的文章就介紹到這了,更多相關(guān)canvas 可移動網(wǎng)格內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持腳本之家!
標(biāo)簽:遼源 永州 棗莊 青島 西藏 漯河 新疆 池州
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《如何使用canvas繪制可移動網(wǎng)格的示例代碼》,本文關(guān)鍵詞 如何,使用,canvas,繪制,可移動,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。