实验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使程序员可以使用线性的思维写程序,而不必关心回调,在一些线性混合深度嵌套的场景下,优势特别明显。