1. ホーム
  2. Web制作
  3. html5

キャンバスを使った移動可能なグリッドの描画方法のサンプルコード

2022-01-11 12:55:55

この記事では、canvasを使った移動可能なグリッドの描画方法のサンプルコードを紹介し、以下のように共有します。

効果

画像

説明

これは実際のプロジェクトで遭遇した要件ですが、私はそれを抽象化し、ビジネス関連のものを遮断し、コードの観点のみから問題を考えました。まず、グリッドの大きさが設定可能で、各頂点が移動可能であること。この問題を見たとき、みなさんがどう考えているかはわかりません。まずは私自身の考えから。

分析

まず必要なのは、グリッドの位置がわかるように出発点を決めること、次にグリッドの各マスについて辺の長さをどうするか(マスで考えましょう、その方が単純です)、さらに各頂点が動くと辺も追随する必要があることです。

つまり、格納するオブジェクトは線と頂点の2種類だけなんです。

頂点と直線をどのように格納するのですか?ここでは、ライブラリ GW_CHILD GW_HWNDNEXT //int HandCount Defined global variable //HWND Handled[1024] //global variable defined //DebugInfo(AnsiString str) //custom string display function //recursive function void __fastcall TForm1::Gethh(HWND hendle) { HWND Group[1024]; HWND hh = GetWindow(hendle,GW_CHILD); int count = 0; if(hh ! = NULL) { int i=0; Group[i] = hh; do { Handled[HandCount++] = Group[i]; Group[i+1] = GetWindow(Group[i],GW_HWNDNEXT); DebugInfo("Group["+AnsiString(i)+"] Hendle is 0x"+IntToHex((__int32)Group[i],8)); count = i+1; }while(i++,NULL ! = Group[i]); } else To distinguish branch points, separate them with { Handled[HandCount++] = NULL; // To distinguish branch points, separate them by NULL DebugInfo(""); } for(int j=0;j Lines->Add("Handled["+AnsiString(i)+"] is 0x"+IntToHex((__int32)Handled[i],8)); } } //--------------------------------------------------------------------------- 頂点や辺のオブジェクトを作るのは比較的簡単で、辺を移動するメソッドも用意されていますが、同時に疑問が生じます。上の図のように、1つの点は最大4つの辺と、少なくとも2つの辺と関連していますが、この頂点と辺の関連性をどのように表現すればいいのでしょうか。

まず考えられるのは、配列を使って頂点と線を格納し、線に含まれる頂点の座標からその線が頂点とつながっているかどうかを判断し、つながっていればその頂点の関連性プロパティに追加することです。その後、頂点を移動させると、その頂点に関連付けられた線に従って、線の座標が動的に変更されるため、上記のような効果を得ることができるのです。

実装

以上の分析を踏まえて、コードを実装してみましょう。最初に格納する必要があるオブジェクトは、頂点と辺である。そして、各小矩形の開始座標と辺の長さをもとに、すべての頂点座標を簡単に計算することができる。

GW_CHILD GW_HWNDNEXT 
//int HandCount Defined global variable 
//HWND Handled[1024] //global variable defined 
//DebugInfo(AnsiString str) //custom string display function
//recursive function 
void __fastcall TForm1::Gethh(HWND hendle) 
{ 
        HWND Group[1024]; 
        HWND hh = GetWindow(hendle,GW_CHILD); 
        int count = 0; 
        if(hh ! = NULL) 
        { 
          int i=0;                    
          Group[i] = hh; 
          do 
          { 
            Handled[HandCount++] = Group[i]; 
            Group[i+1] = GetWindow(Group[i],GW_HWNDNEXT); 
            DebugInfo("Group["+AnsiString(i)+"] Hendle is 0x"+IntToHex((__int32)Group[i],8)); 
            count = i+1; 
          }while(i++,NULL ! = Group[i]); 
        } 
        else To distinguish branch points, separate them with 
        { 
          Handled[HandCount++] = NULL; // To distinguish branch points, separate them by NULL 
          DebugInfo("");        
        } 
        for(int j=0;j<count;j++) 
        { 
          Gethh(Group[j]); 
        } 
}
void __fastcall TForm1::Button6Click(TObject *Sender) //Example 
{ 
        HandCount=0; //initialize global variables 
        //HWND hh1 = FindWindowA("#32769 (???) ",NULL); //"Cypress USB Console"); 
        HWND hh1 = FindWindowA(NULL,"Cypress USB Console"); //get a window handle 
        Gethh(hh1); //call recursive function to get all handles 
        for(int i=0;i<HandCount;i++) 
        { 
          Report->Lines->Add("Handled["+AnsiString(i)+"] is 0x"+IntToHex((__int32)Handled[i],8)); 
        } 
} 
//---------------------------------------------------------------------------

では、どのようにエッジを計算するのでしょうか。エッジを構成する場合、エッジに接続する頂点は2つでよいので、頂点をトラバースしてエッジを構成する方法もありますが、そうするとエッジが重複してしまうので、エッジは1つでよく、そうしないと移動した後に、次のエッジも重複して表示されることがわかります。もちろん、一番重要な理由は、実は効率の問題で、重い言葉を並べないと、計算時間が長くなりすぎてしまうのです。

1つは、頂点をマークしておき、現在の行の両端の頂点はすでにマークされているので、現在の探索ラウンドをスキップする方法です。もうひとつは、以下のように、グリッドのような特定の形状に基づいてエッジを取得し、2つの異なる色に従って水平および垂直エッジを計算する方法です。

画像

このように、横方向には各行で2つずつ、縦方向には2つの頂点をある間隔で結ぶことで辺が形成されます。ここでは、アルゴリズムに渡す形式が2次元配列であるため、この方法を用いている。

// ... Omitted

// Construct the matrix
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]);
}

// Add edges based on the matrix
let idx = 0;
for (let i = 0; i < this.matrix.length; i++) {
    for (let j = 0; j < this.matrix[i].length; j++) {
        // Cross-render the edges so that they are displayed first in the visible area
        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++;
    }
}

後者は、各頂点に関連するエッジの数を求めるものである

for (let i = 0; i < this.vertexes.length; i++) {
  const vertex = this.vertexes[i];
  // determine if a vertex is associated with an edge based on whether its coordinates are the start or end coordinates of the two ends of the edge
  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;
}

一目でわかるように、時間的な複雑さが高すぎるのです。そのため、グリッドは描画されるものの、頂点の数が多すぎると計算時間が長くなり、結果的にブラウザが2s近く上がって動かなくなる、横方向に50頂点、縦方向に50頂点あると、明らかにブラウザが動かなくなる、この時、入力ボックスなどのインタラクティブUIがあると、何も操作ができない、これは確かに落ちないですね。

改善点

では、頂点と辺の関連を見つけるのに最も効率的な方法は何でしょうか?もちろん、他にもっと良い方法があるかもしれないが、筆者の知識には限りがあるので、この辺にしておくことにする。

解決策はグラフのような構造です。グラフの辺は隣接表や隣接行列を使って格納できるので、頂点を格納すれば、その頂点に関連する辺が実際に決まるので、頂点を追加するときに by the way は、この頂点の関連付けの問題を解決し、関連付けを見つけるために再びすべてのエッジをトラバースする必要がなくなりました。(グラフのデータ構造についてはここでは詳しく触れません。興味のある方はご自分で調べてみてください)

コードを改良しよう

{{コード

ここに新しい属性が追加されました function Grid({node, unit, row, col, matrix = []}) { this.vertexes = []; this.lines = []; this.edges = new Map(); this.addEdges = addEdges; this.addVertexes = addVertexes; } 頂点と辺のマッピングを保存します。他のステップは以前と同じで、頂点と辺の追加方法を置き換えるだけです。どういう意味か、実際にコードを見て理解してください。

プレ {コード

ここでは、複雑な edges に計算の function Grid({node, unit, row, col, matrix = []}) { // ... omit // Add edges based on the matrix let idx = 0; for (let i = 0; i < this.matrix.length; i++) { for (let j = 0; j < this.matrix[i].length; j++) { // Cross-render the edges so that they are displayed first in the visible area 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++; } } // Associate edges to vertices this.edges.forEach((value, key) => { key.lines = value; }); } ここで {コード は {コード の長さです。 O(mn) の長さです。 O(n) の長さになります。次に、この時点で100*100の頂点数の計算を見てみると、計算時間はたったの m で十分なんです。では、グラフはどのようにしてこの関連付けを実現しているのだろうか。実は、辺が追加されるたびに、その辺の2つの頂点が同時に関連付けに追加されている、つまり lines という構造になっています。

{{コード

すべての頂点が計算された後、実際の頂点に関連するエッジが決定され、これらをトラバースするだけである。 n で終わりです。

それができたら、楽しく電話をかけましょう。 vertexes api を使って、これらのオブジェクトを 200ms で完了です。

Map

さて、作業も終わり、そろそろ出番です。ページを実行して、開いて見て、いい人たちだ、計算速度はずっと速くなったが、レンダリング速度は悲惨だ、頂点の数は30 * 30、ページにはまだラグがある、どうなっているのだ?

考えてみれば、キャンバスにたくさんのオブジェクトを追加するのは確かに計算量が多いのですが、ここではこのレンダリング消費量も変えることができません。そこで、妥協案として、タイム スライシングを使用することが考えられました。 {コード {コード レンダリング タスクをセグメントに分割し、ブラウザがアイドル状態のときにレンダリングすることで、他のブラウザのタスクをブロックしないようにするAPIです。ここで、いくつかのブラウザのレンダリングが登場します。

{{コード

上記のコードでは、レンダリングを中断するための識別子を追加しています。現在のレンダリングが終了する前にグリッドのクリーンアップと再レンダリングを行う状況があるため、最後のレンダリングを停止して新しいレンダリングを開始する必要があるからです。

まとめ

さて、これでおしまいです。私の知識が浅いため、このような最適化はニーズに合わせてしかできず、もっと極端な最適化はお偉いさんの指導に依存することになります。同時に、この試みはまた、著者は、プロジェクトに組み合わせたデータ構造、最適化の手段を学ぶのは初めてですが、達成感はまだ非常にある、また、プログラマのためのデータ構造アルゴリズムの重要性を感じることです、あなたは彼らの技術的なボトルネックを突破したい場合、これはポイントも回避することはできませんです。

キャンバスを使用して移動可能なグリッドのサンプルコードを描画する方法については、この記事はここで紹介されている、より多くの関連キャンバス移動可能なグリッドの内容は、スクリプトハウスの以前の記事を検索するか、次の関連記事を参照してください続けて、私はあなたが将来的に多くのスクリプトハウスをサポートして願っています!.