今天小編來(lái)和大家分享有關(guān)于html5中 canvas這方面的使用方法,下面是有關(guān)于:“怎么繪制個(gè)可以移動(dòng)的網(wǎng)格?”這方面的相關(guān)問(wèn)題分享!
具體如下:
效果
說(shuō)明
這個(gè)是真實(shí)項(xiàng)目中遇到的需求,我把它抽離出來(lái),屏蔽了那些業(yè)務(wù)相關(guān)的東西,僅從代碼角度來(lái)考慮這個(gè)問(wèn)題。首先網(wǎng)格大小可配置,每個(gè)頂點(diǎn)是可以移動(dòng)的。看到這個(gè)問(wèn)題,不知道各位是怎么去思考的。就先來(lái)說(shuō)說(shuō)我自己的思路。
分析
首先需要有一個(gè)起點(diǎn),這樣就能確定網(wǎng)格所在的位置,其次就是網(wǎng)格中的每個(gè)正方形(我們就按正方形來(lái)思考,這樣簡(jiǎn)單一點(diǎn))的邊長(zhǎng)是多少,另外每個(gè)頂點(diǎn)移動(dòng)的時(shí)候,邊也需要跟著移動(dòng)。
所以其實(shí)要存儲(chǔ)的就只有兩類對(duì)象,一類就是線,另外就是頂點(diǎn)。
如何存儲(chǔ)頂點(diǎn)和線呢?這里用了一個(gè)庫(kù)fabric.js
,就比較容易的創(chuàng)建頂點(diǎn)和邊的對(duì)象,并且它也提供了移動(dòng)邊的方法,但是問(wèn)題也同時(shí)出現(xiàn)了:按照上面的顯示,一個(gè)點(diǎn)最多關(guān)聯(lián)4條邊,最少也關(guān)聯(lián)了2條邊,如何表示這種頂點(diǎn)和邊的關(guān)聯(lián)關(guān)系呢?
先想到就是使用數(shù)組來(lái)存儲(chǔ)頂點(diǎn)和線,然后再根據(jù)線中包含的頂點(diǎn)坐標(biāo)來(lái)判斷這個(gè)線是否和某個(gè)頂點(diǎn)相連,如果是的話,則將將其加入到頂點(diǎn)的關(guān)聯(lián)屬性中。后面當(dāng)移動(dòng)頂點(diǎn)的時(shí)候,根據(jù)頂點(diǎn)拿到其關(guān)聯(lián)的線,去動(dòng)態(tài)改變線的坐標(biāo),這樣就能實(shí)現(xiàn)上面的那種效果了。
實(shí)現(xiàn)
下面根據(jù)以上分析,我們來(lái)實(shí)現(xiàn)代碼。首先需要存儲(chǔ)的對(duì)象有頂點(diǎn)、邊。然后根據(jù)起點(diǎn)坐標(biāo)以及每個(gè)小矩形的邊長(zhǎng),很容易就可以計(jì)算出所有的頂點(diǎn)坐標(biāo)。
function Grid({node, unit, row, col, matrix = []}) {
// 存儲(chǔ)頂點(diǎn)
this.vertexes = [];
// 存儲(chǔ)邊
this.lines = [];
// 根據(jù)起點(diǎn)坐標(biāo)以及單位邊長(zhǎng)計(jì)算
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);
}
}
// 添加頂點(diǎn)對(duì)象的事件監(jiān)聽(tīng)器
this.addListener();
}
那么邊怎么計(jì)算呢,構(gòu)造邊的話,只需要兩個(gè)頂點(diǎn)就可以連成邊,因此我們可以選擇遍歷頂點(diǎn)來(lái)構(gòu)造邊,但是這樣的話會(huì)造成重復(fù)的邊,而我們只需要一條邊就可以了,不然移動(dòng)的話,你會(huì)發(fā)現(xiàn)移動(dòng)完,下面還會(huì)顯示一條重疊的邊。當(dāng)然其實(shí)最重要的原因就是效率問(wèn)題,如果不去重的話,會(huì)導(dǎo)致計(jì)算的時(shí)間復(fù)雜度過(guò)高。
現(xiàn)在有兩種方法來(lái)解決,一種就是給頂點(diǎn)做標(biāo)記,當(dāng)前做線的兩端的頂點(diǎn)已經(jīng)標(biāo)記過(guò)了,那么就跳過(guò)當(dāng)前輪的遍歷。另外一種方法,就是可以根據(jù)網(wǎng)格這種特定的形狀來(lái)獲取邊,如下圖,按照兩種不同的顏色來(lái)計(jì)算水平的邊和垂直的邊。
這樣的話,水平方向,就每行兩兩構(gòu)成邊,垂直方向,就按照一定的間隔連接兩個(gè)頂點(diǎn)構(gòu)成邊。這里因?yàn)楹竺嫘枰獋鹘o算法的格式是二維數(shù)組,因此就使用了這個(gè)方法。
// ...省略了
// 構(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++;
}
}
后面就是找每個(gè)頂點(diǎn)關(guān)聯(lián)了幾條邊
for (let i = 0; i < this.vertexes.length; i++) {
const vertex = this.vertexes[i];
// 根據(jù)頂點(diǎn)的坐標(biāo)是否是邊的兩端的開(kāi)始或結(jié)束坐標(biāo)來(lái)判斷頂點(diǎn)是否與這條邊關(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é)肯定一眼就看出來(lái)啦,這個(gè)時(shí)間復(fù)雜度太高了。所以雖然網(wǎng)格畫出來(lái)了,但是當(dāng)頂點(diǎn)數(shù)量過(guò)多的時(shí)候,計(jì)算時(shí)間太長(zhǎng),導(dǎo)致瀏覽器卡住了了差不多2s往上,當(dāng)水平方向有50個(gè)頂點(diǎn),垂直方向有50個(gè)頂點(diǎn),就能明顯看到瀏覽器的卡頓,此時(shí)如果有輸入框之類的交互UI,是無(wú)法做任何操作的,這肯定也是不行滴。
改進(jìn)
那么有什么方法能夠高效的找到頂點(diǎn)和邊之間的關(guān)聯(lián)呢?這里就不賣關(guān)子了,當(dāng)然可能還有其他更好的方法,但是筆者知識(shí)所限,只能到這啦。
解決辦法就是圖這種結(jié)構(gòu),因?yàn)閳D的邊可以使用鄰接表或者是鄰接矩陣來(lái)存儲(chǔ),這樣如果我存儲(chǔ)了一個(gè)頂點(diǎn),那么與這個(gè)頂點(diǎn)關(guān)聯(lián)的邊其實(shí)就確定了,也就是說(shuō),我們?cè)谔砑禹旤c(diǎn)的時(shí)候,就順便
解決了這種頂點(diǎn)的關(guān)聯(lián)問(wèn)題,不再需要再次遍歷所有的邊來(lái)找關(guān)聯(lián)了。(這里就不詳細(xì)介紹圖這種數(shù)據(jù)結(jié)構(gòu)了,有興趣的同學(xué)可以自己查找資料,實(shí)際這里運(yùn)用圖的地方也就是這個(gè)邊和頂點(diǎn)的關(guān)聯(lián)關(guān)系,其他什么圖的遍歷都沒(méi)有用到)
我們來(lái)改進(jìn)一下我們的代碼。
function Grid({node, unit, row, col, matrix = []}) {
this.vertexes = [];
this.lines = [];
this.edges = new Map();
this.addEdges = addEdges;
this.addVertexes = addVertexes;
}
這里添加了一個(gè)新的屬性edges
,來(lái)存儲(chǔ)頂點(diǎn)和邊的映射關(guān)系。其他的步驟和先前都是一樣的,只是更換了添加頂點(diǎn)和邊的方法,什么意思呢,看代碼其實(shí)明白了:
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)到頂點(diǎn)
this.edges.forEach((value, key) => {
key.lines = value;
});
}
這里我們就將復(fù)雜度為O(mn)
的計(jì)算降低為了O(n)
,這里m
為lines
的長(zhǎng)度,n
為vertexes
的長(zhǎng)度。然后再來(lái)看下此時(shí)計(jì)算100*100的頂點(diǎn)數(shù),計(jì)算時(shí)間只有200ms
,已經(jīng)能夠滿足我的需求了。那么圖是如何實(shí)現(xiàn)這種關(guān)聯(lián)的呢,其實(shí)就是每次添加邊的時(shí)候,將邊的兩個(gè)頂點(diǎn)同時(shí)添加進(jìn)關(guān)聯(lián)關(guān)系中,也就是Map
的結(jié)構(gòu)中。
function addEdges(v, w) {
const line = makeLine({point1: v, point2: w});
// 頂點(diǎn)v關(guān)聯(lián)了邊line
this.edges.get(v).push(line);
// 頂點(diǎn)w也同時(shí)關(guān)聯(lián)了邊line
this.edges.get(w).push(line);
this.lines.push(line);
}
function addVertexes(v) {
this.vertexes.push(v);
// 給每個(gè)頂點(diǎn)設(shè)置一個(gè)Map結(jié)構(gòu)
this.edges.set(v, []);
}
這樣計(jì)算完所有的頂點(diǎn)之后,實(shí)際頂點(diǎn)關(guān)聯(lián)的邊也都確定了,最后只需要遍歷一下這些edges
就可以了。
完成了這些之后,開(kāi)開(kāi)心心的調(diào)用fabric
的api,將這些對(duì)象添加進(jìn)canvas
中就可以了。
// fabric的API,添加fabric對(duì)象到畫布中
canvas.add(...this.vertexes);
canvas.add(...this.lines);
好了,大功告成,可以交差了。運(yùn)行頁(yè)面,打開(kāi)一看,好家伙,計(jì)算速度是快了很多,但是渲染的速度慘不忍睹,30*30的頂點(diǎn)數(shù)量,頁(yè)面還是有卡頓的情況,這是怎么回事呢?
仔細(xì)想想,添加這么多的對(duì)象到畫布中,計(jì)算量確實(shí)是非常大的,但是這里我們也無(wú)法改變這種渲染消耗。于是想到了一個(gè)折中的方法,就是利用時(shí)間切片,簡(jiǎn)單來(lái)說(shuō),就是利用requestAnimationFrame
這個(gè)API,將渲染任務(wù)分割為一個(gè)一個(gè)的片段,在瀏覽器空閑時(shí)去渲染,這樣就不會(huì)去阻塞其他瀏覽器的任務(wù)。這里就涉及了一些瀏覽器渲染的相關(guān)知識(shí)。
function renderIdleCallback(canvas) {
// 任務(wù)切片
const points = this.points.slice();
const lines = this.lines.slice();
const task = () => {
// 清理canvas的時(shí)候,中斷后面的渲染
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();
}
上面的代碼加入了一個(gè)標(biāo)識(shí)符來(lái)中斷渲染,因?yàn)榇嬖谶@樣一種情況,本次網(wǎng)格還沒(méi)有渲染完,就被清理掉又重新渲染,那么就需要停止上次的渲染,重新開(kāi)始新的渲染了。
總結(jié)
好了,到這里也就結(jié)束了。由于筆者知識(shí)淺薄,只能做到這種滿足需求的優(yōu)化了,更極致的優(yōu)化就要看各位大佬指點(diǎn)。同時(shí)此次嘗試也是筆者第一次將所學(xué)的數(shù)據(jù)結(jié)構(gòu)、優(yōu)化手段結(jié)合到項(xiàng)目中,成就感還是非常多的,也是感受到數(shù)據(jù)結(jié)構(gòu)算法對(duì)于程序員的重要性,如果想要突破自己的技術(shù)瓶頸,那么這也是繞不開(kāi)的一個(gè)點(diǎn)。
那么以上就是小編和大家分享的有關(guān)于:“怎么繪制個(gè)可以移動(dòng)的網(wǎng)格?”這方面的相關(guān)內(nèi)容,想必通過(guò)這篇文章大家對(duì)于html5中的canvas這方面的使用也有了不少的了解吧!有感興趣的小伙伴們可以在W3Cschool中進(jìn)行學(xué)習(xí)和了解!