实验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所示,P0、P1、P2、P3四个点在平面或在三维空间中定义了三次贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2,这两个点只是在那里提供方向信息。P0和 P1之间的间距决定了曲线在转而趋近 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的前两个参数x,y旋转,而是绕椭圆的中心旋转。所以仅仅使用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使程序员可以使用线性的思维写程序,而不必关心回调,在一些线性混合深度嵌套的场景下,优势特别明显。