JavaScript圖形實例:Hilbert曲線

      德國數學家David Hilbert在1891年構造了一種曲線,首先把一個正方形等分成四個小正方形,依次從西北角的正方形中心出發往南到西南正方形中心,再往東到東南角的正方形中心,再往北到東北角正方形中心,這是一次迭代;如果對四個小正方形繼續上述過程,往下劃分,反覆進行,最終就得到一條可以填滿整個正方形的曲線,這就是Hibert曲線。其生成過程如圖1所示。

 

圖1  Hilbert曲線的生成

      Hilbert曲線可以採用遞歸過程實現,在遞歸處理時,連接中點的方式有4種,如圖2所示。

圖2  連接中心點的4種方式

      設正方形左上角的頂點坐標為(x1,y1),右下角頂點坐標為(x2,y2)。若將方式(3)的正方形左上角坐標置為(x2,y2),右下角坐標置為(x1,y1),則方式(3)等同於方式(1),相當於旋轉180°;同理,方式(4)等同於方式(2)。因此,4種連接中心點的方式可以看成(1)和(2)兩種。

      兩種連線方式的連線走向及下一次擴展的方式如圖3所示。

圖3  兩種連線方式走向及擴展

      其中,方式(1)的四個中心點坐標分別為:

      ①(x1+dx/4,y1+dy/4)           ②(x1+dx/4, y1+3*dy/4)

      ③ (x1+3*dx/4, y1+3*dy/4)     ④(x1+3*dx/4,y1+dy/4)   (dx=x2-1,dy=y2-y1)

      方式(2)的四個中心點坐標分別為:

      ①(x1+dx/4,y1+dy/4)           ②(x1+3*dx/4,y1+dy/4)

      ③ (x1+3*dx/4, y1+3*dy/4)     ④(x1+dx/4,  y1+3*dy/4)

      為此,引入一個標識變數s,s=1表示方式(1),s=-1表示方式(2),這樣兩種方式的中心點坐標可以統一表示為:

      ①(x1+dx/4,y1+dy/4)        ②(x1+(2-s)*dx/4,  y1+(2+s)*dy/4)

      ③(x1+3*dx/4, y1+3*dy/4)      ④(x1+(2+s)*dx/4,y1+(2-s)*dy/4)

      遞歸擴展時,方式(1)中4個小正方形的擴展方式分別是:方式(2)、方式(1)、方式(1)和方式(4)(注意:給定兩個頂點坐標順序調整後等同於方式(2));方式(2)中4個小正方形的擴展方式分別是:方式(1)、方式(2)、方式(2)和方式(3)。

編寫如下的HTML程式碼。

<!DOCTYPE html>

<head>

<title>Hilbert曲線</title>

</head>

<body>

<canvas id=”myCanvas” width=”500″ height=”500″ style=”border:3px double #996633;”>

</canvas>

<script type=”text/javascript”>

   var canvas = document.getElementById(‘myCanvas’);

   var ctx = canvas.getContext(‘2d’);

   var depth=5;

   ctx.lineWidth = 2;

   ctx.strokeStyle = “red”;

   ctx.beginPath();

   ctx.moveTo(50+400/Math.pow(2,depth+1),50+400/Math.pow(2,depth+1));

   drawShapes(depth,1,50,50,450,450);

   ctx.stroke(); 

   function drawShapes(n,s,x1,y1,x2,y2)

   { 

        dx = x2 – x1,

        dy = y2 – y1;

        if (n>1)

        {  

           if(s>0)

           {

               drawShapes(n-1,-1,x1,y1,(x1+x2)/2,(y1+y2)/2);

               drawShapes(n-1,1,x1,(y1+y2)/2,(x1+x2)/2,y2);

               drawShapes(n-1,1,(x1+x2)/2,(y1+y2)/2,x2,y2);

               drawShapes(n-1,-1,x2,(y1+y2)/2,(x1+x2)/2,y1);

           }

           else

           {

               drawShapes(n-1,1,x1,y1,(x1+x2)/2,(y1+y2)/2);

               drawShapes(n-1,-1,(x1+x2)/2,y1,x2,(y1+y2)/2);

               drawShapes(n-1,-1,(x1+x2)/2,(y1+y2)/2,x2,y2);

               drawShapes(n-1,1,(x1+x2)/2,y2,x1,(y1+y2)/2);

           }

        }

        if (n==1)

        {

            ctx.lineTo(x1+dx/4,y1+dy/4);

            ctx.lineTo(x1+(2-s)*dx/4,  y1+(2+s)*dy/4);

            ctx.lineTo(x1+3*dx/4, y1+3*dy/4);

            ctx.lineTo(x1+(2+s)*dx/4,y1+(2-s)*dy/4);

        }

   }

</script>

</body>

</html>

      在瀏覽器中打開包含這段HTML程式碼的html文件,可以看到在瀏覽器窗口中繪製出如圖4所示的Hilbert曲線。

圖4  遞歸深度maxdepth =5的Hilbert曲線

      上面的程式需要推出方式(一)和方式(二)的坐標統一形式,還需注意方式(3)和方式(4)與方式(一)和方式(二)的同一性。

      由於Hilbert曲線可以看成是4種方式進行組合,因此可以直接對4種方式編寫遞歸過程。編寫如下的HTML文件。

<!DOCTYPE html>

<head>

<title>Hilbert曲線</title>

</head>

<body>

<canvas id=”myCanvas” width=”500″ height=”500″ style=”border:3px double #996633;”>

</canvas>

<script type=”text/javascript”>

   var canvas = document.getElementById(‘myCanvas’);

   var ctx = canvas.getContext(‘2d’);

   ctx.lineWidth = 2;

   ctx.strokeStyle = “red”;

   ctx.beginPath();

   var depth=5;    //  遞歸深度

   var h=400/Math.pow(2,depth);

   var x = 50+h;

   var y = 50+h;

   ctx.moveTo(x,y);

   One(depth);

   ctx.stroke(); 

   function One(n)   // 方式(1)的遞歸調用

   {

      if(n > 0)

      {

         Two(n-1);

         ctx.lineTo(x, y+h); y+=h;

         One(n-1);

         ctx.lineTo(x+h, y); x+=h;

         One(n-1);

         ctx.lineTo(x, y-h); y-=h;

         Four(n-1);

      }

   }

   function Two(n)   // 方式(2)的遞歸調用

   {

       if(n > 0)

       {

           One(n-1);

           ctx.lineTo(x+h, y); x+=h;

           Two(n-1);

           ctx.lineTo(x, y+h); y+=h;

           Two(n-1);

           ctx.lineTo(x-h, y); x-=h;

           Three(n-1);

       }

   }

   function Three(n)   // 方式(3)的遞歸調用

   {

       if(n > 0)

       {

           Four(n-1);

           ctx.lineTo(x, y-h);  y-=h;

           Three(n-1);

           ctx.lineTo(x-h, y);  x-=h;

           Three(n-1);

           ctx.lineTo(x, y+h);  y+=h;

           Two(n-1);

       }

   }

   function Four(n)  // 方式(4)的遞歸調用

   {

     if(n > 0)

     {

          Three(n-1);

          ctx.lineTo(x-h,y);      x-=h;

          Four(n-1);

          ctx.lineTo(x, y-h);     y-=h;

          Four(n-1);

          ctx.lineTo(x+h, y); x+=h;

          One(n-1);

     }

   }

</script>

</body>

</html>

      在瀏覽器中打開包含這段HTML程式碼的html文件,可以看到在瀏覽器窗口中繪製出如圖5所示的Hilbert曲線。

 

圖5  調用One(depth)時繪製的圖形

      將程式中的調用語句「One(depth)」改寫成「Two(depth)」,則在瀏覽器窗口中繪製出如圖6所示的Hilbert曲線。這個圖形可以看成是圖5向左旋轉90°得到的。實際上,由圖2可知,將方式(一)的圖形向左旋轉90°得到的就是方式(二)的圖形。

 

圖6  調用Two(depth)時繪製的圖形

      將程式中調用語句「One(depth)」改寫成「Three(depth)」,同時修改初始坐標為

「var x = 450-h;   var y = 450-h;」,則在瀏覽器窗口中繪製出如圖7所示的Hilbert曲線。

圖7  調用THree(depth)時繪製的圖形

       將程式中調用語句「One(depth)」改寫成「Four(depth);」,同時修改初始坐標為

「var x = 450-h;   var y = 450-h;」,則在瀏覽器窗口中繪製出如圖8所示的Hilbert曲線。

圖8  調用Four(depth)時繪製的圖形 

      將Hilbert曲線的生成過程進行動畫展示,編寫如下的HTML程式碼。

<!DOCTYPE>

<html>

<head>

<title>Hilbert曲線</title>

</head>

<body>

<canvas id=”myCanvas” width=”500″ height=”500″ style=”border:3px double #996633;”></canvas>

<script type=”text/javascript”>

   var canvas = document.getElementById(‘myCanvas’);

   var ctx = canvas.getContext(‘2d’);

   var depth=1;

   function drawShapes(n,s,x1,y1,x2,y2)

   { 

        dx = x2 – x1,

        dy = y2 – y1;

        if (n>1)

        {  

           if(s>0)

           {

               drawShapes(n-1,-1,x1,y1,(x1+x2)/2,(y1+y2)/2);

               drawShapes(n-1,1,x1,(y1+y2)/2,(x1+x2)/2,y2);

               drawShapes(n-1,1,(x1+x2)/2,(y1+y2)/2,x2,y2);

               drawShapes(n-1,-1,x2,(y1+y2)/2,(x1+x2)/2,y1);

           }

           else

           {

               drawShapes(n-1,1,x1,y1,(x1+x2)/2,(y1+y2)/2);

               drawShapes(n-1,-1,(x1+x2)/2,y1,x2,(y1+y2)/2);

               drawShapes(n-1,-1,(x1+x2)/2,(y1+y2)/2,x2,y2);

               drawShapes(n-1,1,(x1+x2)/2,y2,x1,(y1+y2)/2);

           }

        }

        if (n==1)

        {

            ctx.lineTo(x1+dx/4,y1+dy/4);

            ctx.lineTo(x1+(2-s)*dx/4,  y1+(2+s)*dy/4);

            ctx.lineTo(x1+3*dx/4, y1+3*dy/4);

            ctx.lineTo(x1+(2+s)*dx/4,y1+(2-s)*dy/4);

        }

   }

   function go()

   {

        ctx.clearRect(0,0,canvas.width,canvas.height); 

        ctx.lineWidth = 2;

        ctx.strokeStyle = “red”;

        ctx.beginPath();

        ctx.moveTo(50+400/Math.pow(2,depth+1),50+400/Math.pow(2,depth+1));

        drawShapes(depth,1,50,50,450,450);

        ctx.stroke(); 

        depth++;     

        if (depth>6)

        {

           depth=1;

       }

   }

   window.setInterval(‘go()’, 1000);

</script>

</body>

</html>

      在瀏覽器中打開包含這段HTML程式碼的html文件,可以看到在瀏覽器窗口中呈現出如圖9所示的Hilbert曲線動態生成效果。

 

圖9  Hilbert曲線動態生成