官术网_书友最值得收藏!

實驗6 鳥巢

簡介

本實驗的目的是利用CanvasRenderingContext2D的一些API繪制一個鳥巢圖像,如圖1-20所示。

圖1-20 鳥巢

涉及的知識點和技巧包括:三次貝塞爾曲線的繪制,CanvasRenderingContext2D的translate和rotate等API。

橢圓繪制

在CanvasRenderingContext2D開放的一些曲線繪制API當中,有繪制線段、矩形和圓形的,沒有繪制橢圓的。所以需要自己去封裝一個繪制橢圓的方法,如:

        function drawEllipse(x, y, w, h) {
        //code here
                }

其中,x、y為橢圓的左上角的坐標(不是中心坐標),w和h分別為長軸和短軸。

在實現這個方法之前,很容易就可以想到下面幾種預選方案:

● 根據橢圓笛卡兒坐標系方程繪制;

● 根據橢圓極坐標方程繪制;

● 利用四條貝塞爾曲線繪制。

第一種和第二種方式都是基于點的,以不斷地連接橢圓上的所有點來擬合一個完整的橢圓,這樣會帶來大量的計算和重復調用CanvasRenderingContext2D的API。在Canvas性能優化中很重要的一點就是盡量少地調用CanvasRenderingContext2D的API。比如下面兩份代碼的對比,第二段代碼的性能遠遠優于第一段。

代碼一:

        for (var i=0; i < points.length-1; i++) {
        var p1=points[i];
        var p2=points[i+1];
                  context.beginPath();
                  context.moveTo(p1.x, p1.y);
                  context.lineTo(p2.x, p2.y);
                  context.stroke();
                }

代碼二:

                context.beginPath();
        for (var i=0; i < points.length-1; i++) {
        var p1=points[i];
        var p2=points[i+1];
                  context.moveTo(p1.x, p1.y);
                  context.lineTo(p2.x, p2.y);
                }
                context.stroke();

第三種,也是性能最好的一種,繪制過程只需調用4次CanvasRenderingContext2D. bezierCurveTo,這樣可以避免復雜的計算和大量重復調用CanvasRenderingContext2D的API。

所以采用第三種方式來繪制橢圓。代碼如下所示:

        function drawEllipse(x, y, w, h) {
        var k=0.55228475;
        var ox=(w / 2) * k;
        var oy=(h / 2) * k;
        var xe=x+w;
        var ye=y+h;
        var xm=x+w / 2;
        var ym=y+h / 2;
                  ctx.beginPath();
                  ctx.moveTo(x, ym);
                  ctx.bezierCurveTo(x, ym-oy, xm-ox, y, xm, y);
                  ctx.bezierCurveTo(xm+ox, y, xe, ym-oy, xe, ym);
                  ctx.bezierCurveTo(xe, ym+oy, xm+ox, ye, xm, ye);
                  ctx.bezierCurveTo(xm-ox, ye, x, ym+oy, x, ym);
                  ctx.stroke();
                }

三次貝塞爾曲線

上面通過繪制4段三次貝塞爾曲線來繪制一個橢圓。要知道以上代碼中系數k的由來,可推導如下:

如圖1-21所示,P0P1P2P3四個點在平面或在三維空間中定義了三次貝塞爾曲線。曲線起始于P0走向P1,并從P2的方向來到P3。一般不會經過P1P2,這兩個點只是在那里提供方向信息。P0P1之間的間距決定了曲線在轉而趨近 P3之前,走向 P2方向的“長度有多長”。

圖1-21 三次貝塞爾曲線

曲線的參數形式為:

B(t)=P0 (1-t)3+3P1 t(1-t)2+3P2 t2 (1-t)+P3 t3 , t ∈ [0,1]

貝塞爾曲線擬合弧如圖1-22所示。

圖1-22 貝塞爾曲線擬合弧

從圖1-22可看出,x0=0,x2=1,x3=1,x1就是需要求的k的值。根據對稱性,解出x1就等于得到y2的值。將這些值代入三次貝塞爾曲線,馬上能得到如下方程:

x=(3x1-2)t3+ (3-6x1)t2+3x1t

t=0.5時,曲線上的點落在四分之一圓的45°切點上,于是有

sin(π/4)=0.125×(3x1-2)+0.25×(3- 6x1)+0.5×(3x1)

解得x1的值為0.55228475,也就是k的值。

旋轉橢圓

繪制完橢圓,需要旋轉橢圓來形成鳥巢。這里的旋轉不是繞上面的drawEllipse的前兩個參數xy旋轉,而是繞橢圓的中心旋轉。所以僅僅使用CanvasRenderingContext2D.rotate是不夠的,因為CanvasRenderingContext2D.rotate是繞畫布的左上角(0,0)旋轉的。所以先要把(0,0)通過CanvasRenderingContext2D.translate到橢圓的中心,然后再drawEllipse (-a/2,-b/2,a, b)。

以上就是繞中心旋轉的核心。這里還可以推廣到任意圖形或者圖片(假設有約定的中心),如圖1-23所示。

圖1-23 Canvas坐標變換

完整的代碼:

        var canvas;
        var ctx;
        var px=0;
        var py=0;
        function init() {
                  canvas=document.getElementById("myCanvas2");
                  ctx=canvas.getContext("2d");
                  ctx.strokeStyle="#fff";
                  ctx.translate(70, 70);
                }
                init();
        var i=0;
        function drawEllipse(x, y, w, h) {
        var k=0.5522848;
        var ox=(w / 2) * k;
        var oy=(h / 2) * k;
        var xe=x+w;
        var ye=y+h;
        var xm=x+w / 2;
        var ym=y+h / 2;
                  ctx.beginPath();
                  ctx.moveTo(x, ym);
                  ctx.bezierCurveTo(x, ym-oy, xm-ox, y, xm, y);
                  ctx.bezierCurveTo(xm+ox, y, xe, ym-oy, xe, ym);
                  ctx.bezierCurveTo(xe, ym+oy, xm+ox, ye, xm, ye);
                  ctx.bezierCurveTo(xm-ox, ye, x, ym+oy, x, ym);
                  ctx.stroke();
                  ctx.translate(x+70, y+100);
                  px=-70;
                  py=-100;
                  ctx.rotate(10 * Math.PI * 2 / 360);
                }
        var ct;
        var drawAsync=eval(Jscex.compile("async", function (ct) {
        while (true) {
                      drawEllipse(px, py, 140, 200)
                      $await(Jscex.Async.sleep(200, ct));
                  }
                }))
        function start() {
                  ct=new Jscex.Async.CancellationToken();
                  drawAsync(ct).start();
                }
        function stop() {
                  ct.cancel();
                }

這里加入了Jscex最新的取消模型。當然也可以用下面幾種方式取消任務。

第一種:

        var xxxAsync=eval(Jscex.compile("async", function () {
        while (condition) {
                ....
                dosomething
                ....
                $await(Jscex.Async.sleep(1000));
            }
        }))

第二種:

        var xxxAsync=eval(Jscex.compile("async", function () {
        while (true) {
        if (condition) {
        //dosomething
        break;
                      }
        //dosomething
                      $await(Jscex.Async.sleep(1000));
                  }
                }))

第二種方式的好處是可以在if(condition)中做一些初始化設置。

因為break只能跳出當前循環,所以有些場景要使用一些技巧。如下面這種場景:

        var xxxAsync=eval(Jscex.compile("async", function () {
        while (true) {
        for (i in XXX) {
        if (condition) {
        //要在這里跳出最外層的循環。
                        }
                      }
        //dosomething
                      $await(Jscex.Async.sleep(1000));
                  }
                }))

其解決方案是:

        var xxxAsync=eval(Jscex.compile("async", function () {
        while (true) {
        for (i in XXX) {
        if (condition) {
        //要在這里跳出最外層的循環。
                            breakTag=true;
                        }
                      }
        if (breakTag) break;
        //dosomething
                      $await(Jscex.Async.sleep(1000));
                  }
                }))

另外一種復雜的場景如下所示:

        var countAsync1=eval(Jscex.compile("async", function () {
        while (true) {
        for (i in XXX) {
        if (condition) {
        //要在這里跳出最外層的循環
                        }
                      }
                      $await(Jscex.Async.sleep(1000));
                  }
                }))
        var countAsync2=eval(Jscex.compile("async", function () {
        while (true) {
        for (i in XXX) {
        if (condition) {
        //要在這里跳出最外層的循環。
                        }
                      }
                      $await(Jscex.Async.sleep(1000));
                  }
                }))
        var executeAsyncQueue=eval(Jscex.compile("async", function () {
        while (true) {
                      $await(countAsync1())
                      $await(countAsync2())
                      $await(Jscex.Async.sleep(1000));
                  }
                }))
                executeAsyncQueue().start();

其解決方案如下:

在if(condition)中設置breakTag=true,然后執行隊列時跳出整個循環:

        var executeAsyncQueue=eval(Jscex.compile("async", function () {
        while (true) {
                      $await(countAsync1())
                      $await(countAsync2())
        if (breakTag) break;
                      $await(Jscex.Async.sleep(1000));
                  }
                }))

最后一種場景依然利用breakTag跳出整個循環,如下所示:

        var xxAsync=eval(Jscex.compile("async", function () {
        while (true) {
        //dosomething
                      $await(xxxAsync())
        if (breakTag) break;
                      $await(Jscex.Async.sleep("1000"));
                  }
                }))
        var xxxAsync=eval(Jscex.compile("async", function () {
        if (condition) {
                      breakTag=true;
                  }
                  $await(Jscex.Async.sleep("1000"));
                }))

小結

通過本實驗了解了貝塞爾曲線的運用及其擬合橢圓的原理,然后分析了Jscex的各種取消方法和其自身的取消模型。通過這個取消模型,也可以體會到Jscex的優勢,Jscex使程序員可以使用線性的思維寫程序,而不必關心回調,在一些線性混合深度嵌套的場景下,優勢特別明顯。

主站蜘蛛池模板: 清新县| 光泽县| 夏河县| 宁安市| 屯留县| 广宁县| 安达市| 荃湾区| 台湾省| 沙洋县| 贡山| 永丰县| 南木林县| 汉沽区| 汨罗市| 平罗县| 宿松县| 丁青县| 莱州市| 康马县| 旬阳县| 平凉市| 胶州市| 梧州市| 南郑县| 和平区| 阿鲁科尔沁旗| 太白县| 疏附县| 同德县| 噶尔县| 湟源县| 大港区| 本溪| 财经| 信阳市| 怀化市| 石狮市| 阿拉善右旗| 长白| 聂拉木县|